claude-dev-env 1.50.0 → 1.50.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/hooks/blocking/_gh_body_arg_utils.py +67 -11
- package/hooks/blocking/_md_to_html_blocker_test_support.py +65 -0
- package/hooks/blocking/code_rules_annotations_length.py +167 -0
- package/hooks/blocking/code_rules_banned_identifiers.py +385 -0
- package/hooks/blocking/code_rules_boolean_mustcheck.py +350 -0
- package/hooks/blocking/code_rules_comments.py +337 -0
- package/hooks/blocking/code_rules_constants_config.py +252 -0
- package/hooks/blocking/code_rules_docstrings.py +308 -0
- package/hooks/blocking/code_rules_enforcer.py +98 -5765
- package/hooks/blocking/code_rules_imports_logging.py +276 -0
- package/hooks/blocking/code_rules_magic_values.py +180 -0
- package/hooks/blocking/code_rules_mock_completeness.py +295 -0
- package/hooks/blocking/code_rules_naming_collection.py +264 -0
- package/hooks/blocking/code_rules_optional_params.py +288 -0
- package/hooks/blocking/code_rules_paths_syspath.py +186 -0
- package/hooks/blocking/code_rules_probe_chains.py +305 -0
- package/hooks/blocking/code_rules_probe_detection.py +257 -0
- package/hooks/blocking/code_rules_probe_recording.py +225 -0
- package/hooks/blocking/code_rules_scope_binding.py +151 -0
- package/hooks/blocking/code_rules_shared.py +301 -0
- package/hooks/blocking/code_rules_string_magic.py +207 -0
- package/hooks/blocking/code_rules_test_assertions.py +226 -0
- package/hooks/blocking/code_rules_test_branching_except.py +181 -0
- package/hooks/blocking/code_rules_test_isolation.py +341 -0
- package/hooks/blocking/code_rules_type_escape.py +341 -0
- package/hooks/blocking/code_rules_typeddict_stub.py +305 -0
- package/hooks/blocking/code_rules_unused_imports.py +256 -0
- package/hooks/blocking/conftest.py +30 -0
- package/hooks/blocking/pr_description_body_audit.py +148 -0
- package/hooks/blocking/pr_description_command_parser.py +233 -0
- package/hooks/blocking/pr_description_enforcer.py +36 -825
- package/hooks/blocking/pr_description_pr_number.py +153 -0
- package/hooks/blocking/pr_description_readability.py +366 -0
- package/hooks/blocking/tdd_enforcer.py +31 -0
- package/hooks/blocking/test_code_rules_constants_config.py +26 -0
- package/hooks/blocking/test_code_rules_enforcer_banned_noun_word.py +5 -2
- package/hooks/blocking/test_code_rules_enforcer_cap_meta.py +0 -5
- package/hooks/blocking/test_code_rules_enforcer_comment_string_awareness.py +21 -15
- package/hooks/blocking/test_code_rules_enforcer_config_path.py +20 -16
- package/hooks/blocking/test_code_rules_enforcer_exempt_marker_chained.py +4 -2
- package/hooks/blocking/test_code_rules_enforcer_function_length.py +154 -18
- package/hooks/blocking/test_code_rules_enforcer_hardcoded_user_path.py +1 -2
- package/hooks/blocking/test_code_rules_enforcer_ignored_must_check_return.py +22 -12
- package/hooks/blocking/test_code_rules_enforcer_split_annotations_length.py +55 -0
- package/hooks/blocking/test_code_rules_enforcer_split_banned.py +170 -0
- package/hooks/blocking/test_code_rules_enforcer_split_comments.py +60 -0
- package/hooks/blocking/test_code_rules_enforcer_split_config_path.py +52 -0
- package/hooks/blocking/test_code_rules_enforcer_split_constants_config.py +236 -0
- package/hooks/blocking/test_code_rules_enforcer_split_entry_1.py +296 -0
- package/hooks/blocking/test_code_rules_enforcer_split_entry_2.py +238 -0
- package/hooks/blocking/test_code_rules_enforcer_split_isolation_1.py +271 -0
- package/hooks/blocking/test_code_rules_enforcer_split_isolation_2.py +283 -0
- package/hooks/blocking/test_code_rules_enforcer_split_isolation_3.py +268 -0
- package/hooks/blocking/test_code_rules_enforcer_split_isolation_4.py +85 -0
- package/hooks/blocking/test_code_rules_enforcer_split_mocks_1.py +303 -0
- package/hooks/blocking/test_code_rules_enforcer_split_mocks_2.py +111 -0
- package/hooks/blocking/test_code_rules_enforcer_split_mustcheck.py +87 -0
- package/hooks/blocking/test_code_rules_enforcer_split_naming.py +107 -0
- package/hooks/blocking/test_code_rules_enforcer_split_optional_params.py +325 -0
- package/hooks/blocking/test_code_rules_enforcer_split_paths_syspath.py +110 -0
- package/hooks/blocking/test_code_rules_enforcer_split_shared.py +44 -0
- package/hooks/blocking/test_code_rules_enforcer_split_string_magic.py +55 -0
- package/hooks/blocking/test_code_rules_enforcer_split_test_assertions.py +56 -0
- package/hooks/blocking/test_code_rules_enforcer_todo_markers.py +21 -15
- package/hooks/blocking/test_code_rules_paths_syspath.py +26 -0
- package/hooks/blocking/test_md_to_html_blocker_exemptions.py +368 -0
- package/hooks/blocking/test_md_to_html_blocker_extensions.py +157 -0
- package/hooks/blocking/test_md_to_html_blocker_path_resolution.py +336 -0
- package/hooks/blocking/test_pr_description_enforcer.py +13 -1499
- package/hooks/blocking/test_pr_description_enforcer_body_audit.py +247 -0
- package/hooks/blocking/test_pr_description_enforcer_body_rules.py +493 -0
- package/hooks/blocking/test_pr_description_enforcer_command_parser.py +366 -0
- package/hooks/blocking/test_pr_description_enforcer_pr_number.py +159 -0
- package/hooks/blocking/test_pr_description_enforcer_readability.py +443 -0
- package/hooks/blocking/test_tdd_enforcer.py +116 -0
- package/hooks/hooks_constants/blocking_check_limits.py +3 -0
- package/hooks/hooks_constants/code_rules_enforcer_constants.py +8 -0
- package/hooks/hooks_constants/pr_description_enforcer_constants.py +7 -0
- package/hooks/hooks_constants/sys_path_insert_constants.py +1 -0
- package/package.json +1 -1
- package/hooks/blocking/test_code_rules_enforcer.py +0 -2669
- package/hooks/blocking/test_md_to_html_blocker.py +0 -810
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
"""Attribute-chain and os.environ alias-resolution primitives for the test-isolation check."""
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
import sys
|
|
5
|
+
from collections.abc import Iterator
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
_blocking_directory = str(Path(__file__).resolve().parent)
|
|
9
|
+
_hooks_directory = str(Path(__file__).resolve().parent.parent)
|
|
10
|
+
if _blocking_directory not in sys.path:
|
|
11
|
+
sys.path.insert(0, _blocking_directory)
|
|
12
|
+
if _hooks_directory not in sys.path:
|
|
13
|
+
sys.path.insert(0, _hooks_directory)
|
|
14
|
+
|
|
15
|
+
from hooks_constants.code_rules_enforcer_constants import ( # noqa: E402
|
|
16
|
+
ALL_CANONICAL_DOTTED_NAMES_BY_BARE_IMPORT,
|
|
17
|
+
ALL_ENVIRONMENT_GETTER_DOTTED_NAMES,
|
|
18
|
+
ALL_PROBE_ALIASABLE_CANONICAL_PREFIXES,
|
|
19
|
+
ALL_PROBE_RELEVANT_MODULE_CANONICAL_NAMES,
|
|
20
|
+
ENVIRON_GET_METHOD_NAME,
|
|
21
|
+
OS_ENVIRON_DOTTED_NAME,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _dotted_call_attribute_chain(call_node: ast.Call) -> str | None:
|
|
26
|
+
"""Return the dotted name path of *call_node*'s callee, or None.
|
|
27
|
+
|
|
28
|
+
For ``pathlib.Path.home()`` returns ``"pathlib.Path.home"``; for
|
|
29
|
+
``Path.home()`` returns ``"Path.home"``; for ``tempfile.gettempdir()``
|
|
30
|
+
returns ``"tempfile.gettempdir"``. Returns ``None`` when the call target
|
|
31
|
+
is not a pure attribute chain rooted at an ``ast.Name`` (for example,
|
|
32
|
+
``obj.method()`` where ``obj`` is the result of another expression).
|
|
33
|
+
"""
|
|
34
|
+
chain_parts: list[str] = []
|
|
35
|
+
walker: ast.expr = call_node.func
|
|
36
|
+
while isinstance(walker, ast.Attribute):
|
|
37
|
+
chain_parts.append(walker.attr)
|
|
38
|
+
walker = walker.value
|
|
39
|
+
if not isinstance(walker, ast.Name):
|
|
40
|
+
return None
|
|
41
|
+
chain_parts.append(walker.id)
|
|
42
|
+
chain_parts.reverse()
|
|
43
|
+
return ".".join(chain_parts)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _record_probe_import_aliases(
|
|
47
|
+
import_node: ast.Import | ast.ImportFrom,
|
|
48
|
+
all_canonical_names_by_alias: dict[str, str],
|
|
49
|
+
) -> None:
|
|
50
|
+
"""Record the probe-relevant alias entries from a single import statement.
|
|
51
|
+
|
|
52
|
+
Module aliases are recorded only for the probe-relevant modules in
|
|
53
|
+
``ALL_PROBE_RELEVANT_MODULE_CANONICAL_NAMES``. Bare-imported names are
|
|
54
|
+
recorded only for the ``(module, name)`` pairs in
|
|
55
|
+
``ALL_CANONICAL_DOTTED_NAMES_BY_BARE_IMPORT``. Imports outside those sets are
|
|
56
|
+
ignored so unrelated bindings never rewrite a chain.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
import_node: A single ``ast.Import`` or ``ast.ImportFrom`` statement.
|
|
60
|
+
all_canonical_names_by_alias: The alias map to mutate in place with any
|
|
61
|
+
probe-relevant local-name to canonical-dotted-prefix entries.
|
|
62
|
+
"""
|
|
63
|
+
if isinstance(import_node, ast.Import):
|
|
64
|
+
for each_alias in import_node.names:
|
|
65
|
+
if each_alias.name not in ALL_PROBE_RELEVANT_MODULE_CANONICAL_NAMES:
|
|
66
|
+
continue
|
|
67
|
+
local_name = each_alias.asname or each_alias.name
|
|
68
|
+
all_canonical_names_by_alias[local_name] = each_alias.name
|
|
69
|
+
return
|
|
70
|
+
for each_alias in import_node.names:
|
|
71
|
+
canonical_dotted = ALL_CANONICAL_DOTTED_NAMES_BY_BARE_IMPORT.get(
|
|
72
|
+
(import_node.module or "", each_alias.name)
|
|
73
|
+
)
|
|
74
|
+
if canonical_dotted is None:
|
|
75
|
+
continue
|
|
76
|
+
local_name = each_alias.asname or each_alias.name
|
|
77
|
+
all_canonical_names_by_alias[local_name] = canonical_dotted
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _node_is_lexically_inside_function_or_class(
|
|
81
|
+
node: ast.AST, parent_by_child_id: dict[int, ast.AST],
|
|
82
|
+
) -> bool:
|
|
83
|
+
"""Return True when *node* is nested inside a function or class body.
|
|
84
|
+
|
|
85
|
+
Walks ancestors via *parent_by_child_id*. A node nested only inside
|
|
86
|
+
module-level ``try``/``if``/``with`` blocks has no enclosing function or
|
|
87
|
+
class and is module-scoped; a node inside a
|
|
88
|
+
``FunctionDef``/``AsyncFunctionDef``/``ClassDef`` body is scoped to that
|
|
89
|
+
enclosing definition and is not module-scoped. A class-body import binds
|
|
90
|
+
its alias only within the class namespace, so it must not enter the
|
|
91
|
+
module-wide alias map any more than a function-local import does.
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
node: The node whose lexical scope is being classified.
|
|
95
|
+
parent_by_child_id: Child-``id()``-to-parent map from
|
|
96
|
+
``_build_parent_map``.
|
|
97
|
+
|
|
98
|
+
Returns:
|
|
99
|
+
True when an enclosing
|
|
100
|
+
``FunctionDef``/``AsyncFunctionDef``/``ClassDef`` exists.
|
|
101
|
+
"""
|
|
102
|
+
current_ancestor = parent_by_child_id.get(id(node))
|
|
103
|
+
while current_ancestor is not None:
|
|
104
|
+
if isinstance(
|
|
105
|
+
current_ancestor,
|
|
106
|
+
(ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef),
|
|
107
|
+
):
|
|
108
|
+
return True
|
|
109
|
+
current_ancestor = parent_by_child_id.get(id(current_ancestor))
|
|
110
|
+
return False
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _canonical_probe_prefix_for_value(
|
|
114
|
+
node: ast.expr, all_canonical_names_by_alias: dict[str, str],
|
|
115
|
+
) -> str | None:
|
|
116
|
+
if isinstance(node, ast.Name):
|
|
117
|
+
candidate_prefix = all_canonical_names_by_alias.get(node.id, node.id)
|
|
118
|
+
elif isinstance(node, ast.Attribute):
|
|
119
|
+
attribute_chain = _dotted_attribute_chain(node)
|
|
120
|
+
if attribute_chain is None:
|
|
121
|
+
return None
|
|
122
|
+
candidate_prefix = _resolve_chain_through_aliases(
|
|
123
|
+
attribute_chain, all_canonical_names_by_alias
|
|
124
|
+
)
|
|
125
|
+
else:
|
|
126
|
+
return None
|
|
127
|
+
if candidate_prefix in ALL_PROBE_ALIASABLE_CANONICAL_PREFIXES:
|
|
128
|
+
return candidate_prefix
|
|
129
|
+
return None
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _attribute_chain_resolves_to_os_environ(
|
|
133
|
+
node: ast.expr, all_canonical_names_by_alias: dict[str, str],
|
|
134
|
+
) -> bool:
|
|
135
|
+
if not isinstance(node, ast.Attribute):
|
|
136
|
+
return False
|
|
137
|
+
chain = _dotted_attribute_chain(node)
|
|
138
|
+
if chain is None:
|
|
139
|
+
return False
|
|
140
|
+
canonical_chain = _resolve_chain_through_aliases(
|
|
141
|
+
chain, all_canonical_names_by_alias
|
|
142
|
+
)
|
|
143
|
+
return canonical_chain == OS_ENVIRON_DOTTED_NAME
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _dotted_attribute_chain(attribute_node: ast.Attribute) -> str | None:
|
|
147
|
+
chain_parts: list[str] = []
|
|
148
|
+
walker: ast.expr = attribute_node
|
|
149
|
+
while isinstance(walker, ast.Attribute):
|
|
150
|
+
chain_parts.append(walker.attr)
|
|
151
|
+
walker = walker.value
|
|
152
|
+
if not isinstance(walker, ast.Name):
|
|
153
|
+
return None
|
|
154
|
+
chain_parts.append(walker.id)
|
|
155
|
+
chain_parts.reverse()
|
|
156
|
+
return ".".join(chain_parts)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _resolve_chain_through_aliases(
|
|
160
|
+
chain: str, all_canonical_names_by_alias: dict[str, str],
|
|
161
|
+
) -> str:
|
|
162
|
+
"""Rewrite the leading segment of *chain* through the alias map.
|
|
163
|
+
|
|
164
|
+
Args:
|
|
165
|
+
chain: A dotted callee chain such as ``"P.home"``,
|
|
166
|
+
``"op.expanduser"``, or ``"o.path.expanduser"``.
|
|
167
|
+
all_canonical_names_by_alias: Local-binding-to-canonical-prefix
|
|
168
|
+
mapping from ``_build_alias_canonicalization_map``.
|
|
169
|
+
|
|
170
|
+
Returns:
|
|
171
|
+
The chain with its leading segment replaced by the canonical
|
|
172
|
+
(possibly multi-segment) prefix when a binding matches; otherwise
|
|
173
|
+
the chain unchanged.
|
|
174
|
+
"""
|
|
175
|
+
first_segment, separator, remainder = chain.partition(".")
|
|
176
|
+
canonical_prefix = all_canonical_names_by_alias.get(first_segment)
|
|
177
|
+
if canonical_prefix is None:
|
|
178
|
+
return chain
|
|
179
|
+
if not separator:
|
|
180
|
+
return canonical_prefix
|
|
181
|
+
return f"{canonical_prefix}{separator}{remainder}"
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def _environ_key_string_from_call(
|
|
185
|
+
call_node: ast.Call,
|
|
186
|
+
all_canonical_names_by_alias: dict[str, str],
|
|
187
|
+
all_environ_local_bindings: set[str],
|
|
188
|
+
) -> str | None:
|
|
189
|
+
if not _call_is_environment_getter(call_node, all_canonical_names_by_alias, all_environ_local_bindings):
|
|
190
|
+
return None
|
|
191
|
+
if not call_node.args:
|
|
192
|
+
return None
|
|
193
|
+
first_argument = call_node.args[0]
|
|
194
|
+
if isinstance(first_argument, ast.Constant) and isinstance(first_argument.value, str):
|
|
195
|
+
return first_argument.value
|
|
196
|
+
return None
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def _call_is_environment_getter(
|
|
200
|
+
call_node: ast.Call,
|
|
201
|
+
all_canonical_names_by_alias: dict[str, str],
|
|
202
|
+
all_environ_local_bindings: set[str],
|
|
203
|
+
) -> bool:
|
|
204
|
+
"""Return True when *call_node* reads an env var via a recognized getter.
|
|
205
|
+
|
|
206
|
+
Recognizes the canonical ``os.getenv(...)`` / ``os.environ.get(...)``
|
|
207
|
+
chains and the local-alias ``e.get(...)`` form where ``e`` is a name in
|
|
208
|
+
*all_environ_local_bindings* (a binding to ``os.environ`` collected from
|
|
209
|
+
the same test function).
|
|
210
|
+
|
|
211
|
+
Args:
|
|
212
|
+
call_node: The call to inspect.
|
|
213
|
+
all_canonical_names_by_alias: Import-alias map from
|
|
214
|
+
``_build_alias_canonicalization_map``.
|
|
215
|
+
all_environ_local_bindings: Local names bound to ``os.environ`` within
|
|
216
|
+
the test function being analyzed.
|
|
217
|
+
|
|
218
|
+
Returns:
|
|
219
|
+
True when the call is an environment getter whose key argument is
|
|
220
|
+
worth inspecting.
|
|
221
|
+
"""
|
|
222
|
+
if _call_targets_local_environ_get(call_node, all_environ_local_bindings):
|
|
223
|
+
return True
|
|
224
|
+
raw_chain = _dotted_call_attribute_chain(call_node)
|
|
225
|
+
if raw_chain is None:
|
|
226
|
+
return False
|
|
227
|
+
canonical_chain = _resolve_chain_through_aliases(raw_chain, all_canonical_names_by_alias)
|
|
228
|
+
return canonical_chain in ALL_ENVIRONMENT_GETTER_DOTTED_NAMES
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def _call_targets_local_environ_get(
|
|
232
|
+
call_node: ast.Call, all_environ_local_bindings: set[str],
|
|
233
|
+
) -> bool:
|
|
234
|
+
callee = call_node.func
|
|
235
|
+
if not isinstance(callee, ast.Attribute):
|
|
236
|
+
return False
|
|
237
|
+
if callee.attr != ENVIRON_GET_METHOD_NAME:
|
|
238
|
+
return False
|
|
239
|
+
receiver = callee.value
|
|
240
|
+
return isinstance(receiver, ast.Name) and receiver.id in all_environ_local_bindings
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def _environ_key_string_from_subscript(
|
|
244
|
+
subscript_node: ast.Subscript,
|
|
245
|
+
all_canonical_names_by_alias: dict[str, str],
|
|
246
|
+
all_environ_local_bindings: set[str],
|
|
247
|
+
) -> str | None:
|
|
248
|
+
if not _subscript_target_is_os_environ(
|
|
249
|
+
subscript_node.value, all_canonical_names_by_alias, all_environ_local_bindings
|
|
250
|
+
):
|
|
251
|
+
return None
|
|
252
|
+
key_node = subscript_node.slice
|
|
253
|
+
if isinstance(key_node, ast.Constant) and isinstance(key_node.value, str):
|
|
254
|
+
return key_node.value
|
|
255
|
+
return None
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def _subscript_target_is_os_environ(
|
|
259
|
+
target_node: ast.expr,
|
|
260
|
+
all_canonical_names_by_alias: dict[str, str],
|
|
261
|
+
all_environ_local_bindings: set[str],
|
|
262
|
+
) -> bool:
|
|
263
|
+
if isinstance(target_node, ast.Name):
|
|
264
|
+
if target_node.id in all_environ_local_bindings:
|
|
265
|
+
return True
|
|
266
|
+
return all_canonical_names_by_alias.get(target_node.id) == OS_ENVIRON_DOTTED_NAME
|
|
267
|
+
if isinstance(target_node, ast.Attribute):
|
|
268
|
+
return _attribute_chain_resolves_to_os_environ(target_node, all_canonical_names_by_alias)
|
|
269
|
+
return False
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def _children_to_descend_into(node: ast.AST) -> list[ast.AST]:
|
|
273
|
+
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.Lambda)):
|
|
274
|
+
return []
|
|
275
|
+
if isinstance(node, ast.ClassDef):
|
|
276
|
+
return list(node.body)
|
|
277
|
+
return list(ast.iter_child_nodes(node))
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def _descend_within_test_scope(
|
|
281
|
+
function_node: ast.FunctionDef | ast.AsyncFunctionDef,
|
|
282
|
+
) -> Iterator[ast.AST]:
|
|
283
|
+
"""Yield every descendant of *function_node* on the test's own runtime path.
|
|
284
|
+
|
|
285
|
+
Bounded traversal that shares ``_children_to_descend_into`` so every caller
|
|
286
|
+
treats the same nodes as in scope. Nested function definitions, methods, and
|
|
287
|
+
lambdas are scope boundaries — Python does not run a callable's body just
|
|
288
|
+
because the callable (or its enclosing class) is defined, so a binding or
|
|
289
|
+
probe inside one does not leak onto the test's runtime path. Nested
|
|
290
|
+
``ClassDef`` bodies stay in scope because their class-creation statements
|
|
291
|
+
(class attribute initializers) run as the ``class`` statement executes
|
|
292
|
+
during the test; descent stops at the methods declared in that class body.
|
|
293
|
+
|
|
294
|
+
Args:
|
|
295
|
+
function_node: The test function whose in-scope descendants to yield.
|
|
296
|
+
|
|
297
|
+
Yields:
|
|
298
|
+
Each descendant node within the test's bounded scope, in stack-pop
|
|
299
|
+
order.
|
|
300
|
+
"""
|
|
301
|
+
nodes_to_visit: list[ast.AST] = list(ast.iter_child_nodes(function_node))
|
|
302
|
+
while nodes_to_visit:
|
|
303
|
+
each_descendant = nodes_to_visit.pop()
|
|
304
|
+
yield each_descendant
|
|
305
|
+
nodes_to_visit.extend(_children_to_descend_into(each_descendant))
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
"""Home-directory and shared-temp value-reference detection for the test-isolation check."""
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
import sys
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
_blocking_directory = str(Path(__file__).resolve().parent)
|
|
8
|
+
_hooks_directory = str(Path(__file__).resolve().parent.parent)
|
|
9
|
+
if _blocking_directory not in sys.path:
|
|
10
|
+
sys.path.insert(0, _blocking_directory)
|
|
11
|
+
if _hooks_directory not in sys.path:
|
|
12
|
+
sys.path.insert(0, _hooks_directory)
|
|
13
|
+
|
|
14
|
+
from code_rules_probe_chains import ( # noqa: E402
|
|
15
|
+
_dotted_call_attribute_chain,
|
|
16
|
+
_environ_key_string_from_call,
|
|
17
|
+
_environ_key_string_from_subscript,
|
|
18
|
+
_resolve_chain_through_aliases,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
from hooks_constants.code_rules_enforcer_constants import ( # noqa: E402
|
|
22
|
+
ALL_HOME_DIRECTORY_ENV_VAR_NAMES,
|
|
23
|
+
ALL_PATHLIB_PATH_CONSTRUCTOR_CANONICAL_NAMES,
|
|
24
|
+
ALL_SHARED_TEMP_SOURCE_PROBE_DOTTED_NAMES,
|
|
25
|
+
ENVIRONMENT_VARIABLE_REFERENCE_PATTERN,
|
|
26
|
+
HOME_DIRECTORY_TILDE_PREFIX,
|
|
27
|
+
PATHLIB_EXPANDUSER_METHOD_NAME,
|
|
28
|
+
TEMPFILE_FACTORY_ISOLATION_DIRECTORY_KEYWORD,
|
|
29
|
+
WINDOWS_PERCENT_VARIABLE_REFERENCE_PATTERN,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _pathlib_path_construction_uses_home_tilde(
|
|
34
|
+
node: ast.expr, all_canonical_names_by_alias: dict[str, str],
|
|
35
|
+
) -> bool:
|
|
36
|
+
"""Return True for a ``pathlib.Path('~...')`` construction with a home tilde.
|
|
37
|
+
|
|
38
|
+
The node is a Path construction when its callee chain resolves (directly,
|
|
39
|
+
aliased, or fully qualified) to a member of
|
|
40
|
+
``ALL_PATHLIB_PATH_CONSTRUCTOR_CANONICAL_NAMES``. It uses the home tilde
|
|
41
|
+
when its first argument is a literal string beginning with ``~``. A
|
|
42
|
+
tilde-free or dynamic first argument expands no home directory and returns
|
|
43
|
+
False, mirroring ``_expanduser_argument_references_home``.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
node: The candidate ``Path(...)`` construction expression.
|
|
47
|
+
all_canonical_names_by_alias: Import-alias map from
|
|
48
|
+
``_build_alias_canonicalization_map``.
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
True when *node* constructs a ``pathlib.Path`` from a leading-tilde
|
|
52
|
+
literal string.
|
|
53
|
+
"""
|
|
54
|
+
if not isinstance(node, ast.Call):
|
|
55
|
+
return False
|
|
56
|
+
constructor_chain = _dotted_call_attribute_chain(node)
|
|
57
|
+
if constructor_chain is None:
|
|
58
|
+
return False
|
|
59
|
+
canonical_chain = _resolve_chain_through_aliases(
|
|
60
|
+
constructor_chain, all_canonical_names_by_alias
|
|
61
|
+
)
|
|
62
|
+
if canonical_chain not in ALL_PATHLIB_PATH_CONSTRUCTOR_CANONICAL_NAMES:
|
|
63
|
+
return False
|
|
64
|
+
return _expanduser_argument_references_home(node)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _expanduser_method_call_targets_pathlib_path(
|
|
68
|
+
call_node: ast.Call,
|
|
69
|
+
all_canonical_names_by_alias: dict[str, str],
|
|
70
|
+
all_path_local_bindings: set[str],
|
|
71
|
+
) -> bool:
|
|
72
|
+
"""Return True for a ``.expanduser()`` call on a home-tilde ``pathlib.Path``.
|
|
73
|
+
|
|
74
|
+
``Path.expanduser`` expands the ``~`` bound into the receiver Path, so the
|
|
75
|
+
call resolves the home directory only when that receiver carries a leading
|
|
76
|
+
tilde. The receiver carries a tilde when it is a ``pathlib.Path('~...')``
|
|
77
|
+
construction (directly, aliased, or fully qualified) or a local variable
|
|
78
|
+
previously bound to such a construction. A tilde-free or dynamic receiver
|
|
79
|
+
(``Path('/tmp/x').expanduser()`` / ``Path(some_path).expanduser()``)
|
|
80
|
+
expands no home directory and is not flagged, keeping the form symmetric
|
|
81
|
+
with ``os.path.expanduser`` argument inspection.
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
call_node: The call whose callee attribute is ``expanduser``.
|
|
85
|
+
all_canonical_names_by_alias: Import-alias map from
|
|
86
|
+
``_build_alias_canonicalization_map``.
|
|
87
|
+
all_path_local_bindings: Local names bound to a home-tilde
|
|
88
|
+
``pathlib.Path`` construction from
|
|
89
|
+
``_collect_pathlib_path_local_binding_names``.
|
|
90
|
+
|
|
91
|
+
Returns:
|
|
92
|
+
True when the ``expanduser`` receiver resolves to a home-tilde
|
|
93
|
+
``pathlib.Path``.
|
|
94
|
+
"""
|
|
95
|
+
callee = call_node.func
|
|
96
|
+
if not isinstance(callee, ast.Attribute):
|
|
97
|
+
return False
|
|
98
|
+
if callee.attr != PATHLIB_EXPANDUSER_METHOD_NAME:
|
|
99
|
+
return False
|
|
100
|
+
receiver = callee.value
|
|
101
|
+
if isinstance(receiver, ast.Name):
|
|
102
|
+
return receiver.id in all_path_local_bindings
|
|
103
|
+
return _pathlib_path_construction_uses_home_tilde(receiver, all_canonical_names_by_alias)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _expandvars_argument_references_home_or_temp(call_node: ast.Call) -> bool:
|
|
107
|
+
"""Return True when an ``expandvars`` call expands a home/temp env var.
|
|
108
|
+
|
|
109
|
+
Inspects the first string argument for dollar-style ``$NAME`` / ``${NAME}``
|
|
110
|
+
references and Windows percent-style ``%NAME%`` references, then reports
|
|
111
|
+
whether any referenced name is a home/temp env var. ``os.path.expandvars``
|
|
112
|
+
expands percent syntax on Windows, so both forms reach the same home/temp
|
|
113
|
+
env-var name set. A non-constant or absent argument is treated as not
|
|
114
|
+
referencing a home/temp variable, mirroring the conservative env-name
|
|
115
|
+
filtering applied to ``os.getenv``.
|
|
116
|
+
|
|
117
|
+
Args:
|
|
118
|
+
call_node: The ``os.path.expandvars(...)`` call node.
|
|
119
|
+
|
|
120
|
+
Returns:
|
|
121
|
+
True when at least one expanded variable name is in
|
|
122
|
+
``ALL_HOME_DIRECTORY_ENV_VAR_NAMES``.
|
|
123
|
+
"""
|
|
124
|
+
if not call_node.args:
|
|
125
|
+
return False
|
|
126
|
+
first_argument = call_node.args[0]
|
|
127
|
+
if not (
|
|
128
|
+
isinstance(first_argument, ast.Constant)
|
|
129
|
+
and isinstance(first_argument.value, str)
|
|
130
|
+
):
|
|
131
|
+
return False
|
|
132
|
+
dollar_style_names = ENVIRONMENT_VARIABLE_REFERENCE_PATTERN.findall(
|
|
133
|
+
first_argument.value
|
|
134
|
+
)
|
|
135
|
+
percent_style_names = WINDOWS_PERCENT_VARIABLE_REFERENCE_PATTERN.findall(
|
|
136
|
+
first_argument.value
|
|
137
|
+
)
|
|
138
|
+
all_referenced_names = dollar_style_names + percent_style_names
|
|
139
|
+
return any(
|
|
140
|
+
each_name in ALL_HOME_DIRECTORY_ENV_VAR_NAMES
|
|
141
|
+
for each_name in all_referenced_names
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _expanduser_argument_references_home(call_node: ast.Call) -> bool:
|
|
146
|
+
"""Return True when an ``expanduser`` call expands the home directory.
|
|
147
|
+
|
|
148
|
+
``os.path.expanduser`` only substitutes a leading ``~`` (``~`` alone or
|
|
149
|
+
``~user``); a string without a leading tilde is returned unchanged and
|
|
150
|
+
never touches HOME. A non-constant or absent argument is treated as not
|
|
151
|
+
referencing home, mirroring the conservative argument inspection applied
|
|
152
|
+
to ``expandvars``.
|
|
153
|
+
|
|
154
|
+
Args:
|
|
155
|
+
call_node: The ``os.path.expanduser(...)`` call node.
|
|
156
|
+
|
|
157
|
+
Returns:
|
|
158
|
+
True when the first string argument begins with the home-directory
|
|
159
|
+
tilde prefix.
|
|
160
|
+
"""
|
|
161
|
+
if not call_node.args:
|
|
162
|
+
return False
|
|
163
|
+
first_argument = call_node.args[0]
|
|
164
|
+
if not (
|
|
165
|
+
isinstance(first_argument, ast.Constant)
|
|
166
|
+
and isinstance(first_argument.value, str)
|
|
167
|
+
):
|
|
168
|
+
return False
|
|
169
|
+
return first_argument.value.startswith(HOME_DIRECTORY_TILDE_PREFIX)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def _tempfile_factory_call_is_isolated_by_dir(
|
|
173
|
+
call_node: ast.Call,
|
|
174
|
+
all_canonical_names_by_alias: dict[str, str],
|
|
175
|
+
all_environ_local_bindings: set[str],
|
|
176
|
+
) -> bool:
|
|
177
|
+
"""Return True when a tempfile factory's ``dir=`` sandboxes the allocation.
|
|
178
|
+
|
|
179
|
+
A ``dir=`` keyword sandboxes the allocation only when its value is a
|
|
180
|
+
plausibly isolated path (typically the pytest ``tmp_path`` fixture). A
|
|
181
|
+
``dir=`` value that resolves to the shared temp directory does not isolate
|
|
182
|
+
the call and is treated as absent:
|
|
183
|
+
|
|
184
|
+
- a constant ``None`` selects the default shared temp directory; and
|
|
185
|
+
- a shared-temp source — ``os.getenv('TMPDIR'|'TEMP'|'TMP')`` /
|
|
186
|
+
``os.environ['TMPDIR'|...]`` / ``os.environ.get('TMPDIR'|...)``, or
|
|
187
|
+
``tempfile.gettempdir()`` / ``tempfile.gettempprefix()`` — returns the
|
|
188
|
+
shared temp directory.
|
|
189
|
+
|
|
190
|
+
Only an explicit ``dir=`` keyword counts; a ``**kwargs`` ``dir`` cannot be
|
|
191
|
+
resolved statically and is treated as absent, mirroring the conservative
|
|
192
|
+
argument inspection applied to ``expandvars`` and ``expanduser``.
|
|
193
|
+
|
|
194
|
+
Args:
|
|
195
|
+
call_node: The tempfile factory call node.
|
|
196
|
+
all_canonical_names_by_alias: Import-alias map used to resolve aliased
|
|
197
|
+
shared-temp sources passed as the ``dir=`` value.
|
|
198
|
+
all_environ_local_bindings: Local names bound to ``os.environ`` within
|
|
199
|
+
the test function, used to recognize aliased ``os.environ`` reads.
|
|
200
|
+
|
|
201
|
+
Returns:
|
|
202
|
+
True when an explicit ``dir=`` keyword is present and its value is not
|
|
203
|
+
a recognized shared-temp source.
|
|
204
|
+
"""
|
|
205
|
+
for each_keyword in call_node.keywords:
|
|
206
|
+
if each_keyword.arg != TEMPFILE_FACTORY_ISOLATION_DIRECTORY_KEYWORD:
|
|
207
|
+
continue
|
|
208
|
+
return not _dir_value_resolves_to_shared_temp(
|
|
209
|
+
each_keyword.value,
|
|
210
|
+
all_canonical_names_by_alias,
|
|
211
|
+
all_environ_local_bindings,
|
|
212
|
+
)
|
|
213
|
+
return False
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def _dir_value_resolves_to_shared_temp(
|
|
217
|
+
dir_value: ast.expr,
|
|
218
|
+
all_canonical_names_by_alias: dict[str, str],
|
|
219
|
+
all_environ_local_bindings: set[str],
|
|
220
|
+
) -> bool:
|
|
221
|
+
"""Return True when a tempfile ``dir=`` value points at the shared temp dir.
|
|
222
|
+
|
|
223
|
+
Args:
|
|
224
|
+
dir_value: The expression supplied as the factory's ``dir=`` value.
|
|
225
|
+
all_canonical_names_by_alias: Import-alias map used to resolve aliased
|
|
226
|
+
``os.getenv`` / ``os.environ`` / ``tempfile`` references.
|
|
227
|
+
all_environ_local_bindings: Local names bound to ``os.environ`` within
|
|
228
|
+
the test function.
|
|
229
|
+
|
|
230
|
+
Returns:
|
|
231
|
+
True when the value is a constant ``None``, a call or subscript that
|
|
232
|
+
reads a home or temp environment variable named in
|
|
233
|
+
``ALL_HOME_DIRECTORY_ENV_VAR_NAMES``, or a call whose dotted chain is
|
|
234
|
+
a recognized shared-temp source in
|
|
235
|
+
``ALL_SHARED_TEMP_SOURCE_PROBE_DOTTED_NAMES``.
|
|
236
|
+
"""
|
|
237
|
+
if isinstance(dir_value, ast.Constant) and dir_value.value is None:
|
|
238
|
+
return True
|
|
239
|
+
if isinstance(dir_value, ast.Call):
|
|
240
|
+
environ_key = _environ_key_string_from_call(
|
|
241
|
+
dir_value, all_canonical_names_by_alias, all_environ_local_bindings
|
|
242
|
+
)
|
|
243
|
+
if environ_key in ALL_HOME_DIRECTORY_ENV_VAR_NAMES:
|
|
244
|
+
return True
|
|
245
|
+
raw_chain = _dotted_call_attribute_chain(dir_value)
|
|
246
|
+
if raw_chain is None:
|
|
247
|
+
return False
|
|
248
|
+
canonical_chain = _resolve_chain_through_aliases(
|
|
249
|
+
raw_chain, all_canonical_names_by_alias
|
|
250
|
+
)
|
|
251
|
+
return canonical_chain in ALL_SHARED_TEMP_SOURCE_PROBE_DOTTED_NAMES
|
|
252
|
+
if isinstance(dir_value, ast.Subscript):
|
|
253
|
+
environ_key = _environ_key_string_from_subscript(
|
|
254
|
+
dir_value, all_canonical_names_by_alias, all_environ_local_bindings
|
|
255
|
+
)
|
|
256
|
+
return environ_key in ALL_HOME_DIRECTORY_ENV_VAR_NAMES
|
|
257
|
+
return False
|