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,385 @@
|
|
|
1
|
+
"""Banned identifier, banned noun-word, and banned function-prefix naming checks."""
|
|
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_path_utils import ( # noqa: E402
|
|
15
|
+
is_config_file,
|
|
16
|
+
)
|
|
17
|
+
from code_rules_shared import ( # noqa: E402
|
|
18
|
+
_collect_annotated_arguments,
|
|
19
|
+
_collect_target_names,
|
|
20
|
+
_scope_violations_to_changed_lines,
|
|
21
|
+
_walk_skipping_type_checking_blocks,
|
|
22
|
+
is_hook_infrastructure,
|
|
23
|
+
is_migration_file,
|
|
24
|
+
is_test_file,
|
|
25
|
+
is_workflow_registry_file,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
from hooks_constants.banned_identifiers_constants import ( # noqa: E402
|
|
29
|
+
ALL_BANNED_IDENTIFIERS,
|
|
30
|
+
ALL_BANNED_NOUN_WORDS,
|
|
31
|
+
BANNED_IDENTIFIER_MESSAGE_SUFFIX,
|
|
32
|
+
BANNED_IDENTIFIER_SKIP_ADVISORY,
|
|
33
|
+
BANNED_NOUN_WORD_MESSAGE_SUFFIX,
|
|
34
|
+
CAMEL_CASE_WORD_PATTERN,
|
|
35
|
+
MAX_BANNED_IDENTIFIER_ISSUES,
|
|
36
|
+
)
|
|
37
|
+
from hooks_constants.blocking_check_limits import ( # noqa: E402
|
|
38
|
+
ALL_BANNED_PREFIX_NAMES,
|
|
39
|
+
MAX_BANNED_PREFIX_ISSUES,
|
|
40
|
+
)
|
|
41
|
+
from hooks_constants.code_rules_enforcer_constants import ( # noqa: E402
|
|
42
|
+
ALL_SELF_AND_CLS_PARAMETER_NAMES,
|
|
43
|
+
BANNED_NOUN_SPAN_FRAGMENT_TEMPLATE,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _collect_banned_names_from_target(target: ast.expr) -> list[ast.Name]:
|
|
48
|
+
"""Return every banned ast.Name reachable through tuple/list unpacking or starred targets."""
|
|
49
|
+
return [
|
|
50
|
+
each_name_node
|
|
51
|
+
for each_name_node in _collect_target_names(target)
|
|
52
|
+
if each_name_node.id in ALL_BANNED_IDENTIFIERS
|
|
53
|
+
]
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _value_is_parse_args_namespace_call(value_node: ast.AST | None) -> bool:
|
|
57
|
+
if value_node is None:
|
|
58
|
+
return False
|
|
59
|
+
if not isinstance(value_node, ast.Call):
|
|
60
|
+
return False
|
|
61
|
+
callee = value_node.func
|
|
62
|
+
return isinstance(callee, ast.Attribute) and callee.attr == "parse_args"
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _without_parse_args_namespace_exemption(
|
|
66
|
+
all_banned_names: list[ast.Name], value_node: ast.AST | None
|
|
67
|
+
) -> list[ast.Name]:
|
|
68
|
+
if not _value_is_parse_args_namespace_call(value_node):
|
|
69
|
+
return all_banned_names
|
|
70
|
+
return [each_name for each_name in all_banned_names if each_name.id != "args"]
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _synthesize_alias_name_node(
|
|
74
|
+
bound_identifier: str, alias_node: ast.alias
|
|
75
|
+
) -> ast.Name:
|
|
76
|
+
synthetic_name = ast.Name(id=bound_identifier, ctx=ast.Store())
|
|
77
|
+
synthetic_name.lineno = alias_node.lineno
|
|
78
|
+
synthetic_name.col_offset = alias_node.col_offset
|
|
79
|
+
return synthetic_name
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _collect_banned_names_from_import(
|
|
83
|
+
import_statement: ast.Import | ast.ImportFrom,
|
|
84
|
+
) -> list[ast.Name]:
|
|
85
|
+
banned_alias_nodes: list[ast.Name] = []
|
|
86
|
+
for each_alias in import_statement.names:
|
|
87
|
+
bound_identifier = each_alias.asname or each_alias.name.split(".")[0]
|
|
88
|
+
if bound_identifier in ALL_BANNED_IDENTIFIERS:
|
|
89
|
+
banned_alias_nodes.append(
|
|
90
|
+
_synthesize_alias_name_node(bound_identifier, each_alias)
|
|
91
|
+
)
|
|
92
|
+
return banned_alias_nodes
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _collect_banned_names_from_node(node: ast.AST) -> list[ast.Name]:
|
|
96
|
+
"""Return banned ast.Name nodes introduced by a single binding construct."""
|
|
97
|
+
if isinstance(node, ast.Assign):
|
|
98
|
+
banned_names: list[ast.Name] = []
|
|
99
|
+
for each_target in node.targets:
|
|
100
|
+
banned_names.extend(_collect_banned_names_from_target(each_target))
|
|
101
|
+
return _without_parse_args_namespace_exemption(banned_names, node.value)
|
|
102
|
+
if isinstance(node, ast.AnnAssign):
|
|
103
|
+
banned_names = _collect_banned_names_from_target(node.target)
|
|
104
|
+
return _without_parse_args_namespace_exemption(banned_names, node.value)
|
|
105
|
+
if isinstance(node, (ast.For, ast.AsyncFor)):
|
|
106
|
+
return _collect_banned_names_from_target(node.target)
|
|
107
|
+
if isinstance(node, ast.comprehension):
|
|
108
|
+
return _collect_banned_names_from_target(node.target)
|
|
109
|
+
if isinstance(node, ast.withitem):
|
|
110
|
+
if node.optional_vars is None:
|
|
111
|
+
return []
|
|
112
|
+
return _collect_banned_names_from_target(node.optional_vars)
|
|
113
|
+
if isinstance(node, ast.NamedExpr):
|
|
114
|
+
banned_names = _collect_banned_names_from_target(node.target)
|
|
115
|
+
return _without_parse_args_namespace_exemption(banned_names, node.value)
|
|
116
|
+
if isinstance(node, (ast.Import, ast.ImportFrom)):
|
|
117
|
+
return _collect_banned_names_from_import(node)
|
|
118
|
+
return []
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def check_banned_identifiers(content: str, file_path: str) -> list[str]:
|
|
122
|
+
"""Flag assignments to identifiers banned by the project Naming rules."""
|
|
123
|
+
if is_test_file(file_path) or is_hook_infrastructure(file_path):
|
|
124
|
+
return []
|
|
125
|
+
|
|
126
|
+
try:
|
|
127
|
+
parsed_tree = ast.parse(content)
|
|
128
|
+
except SyntaxError:
|
|
129
|
+
print(f"{file_path}: {BANNED_IDENTIFIER_SKIP_ADVISORY}", file=sys.stderr)
|
|
130
|
+
return []
|
|
131
|
+
|
|
132
|
+
banned_name_nodes: list[ast.Name] = []
|
|
133
|
+
for each_node in ast.walk(parsed_tree):
|
|
134
|
+
banned_name_nodes.extend(_collect_banned_names_from_node(each_node))
|
|
135
|
+
|
|
136
|
+
banned_name_nodes.sort(key=lambda each_name: (each_name.lineno, each_name.col_offset))
|
|
137
|
+
|
|
138
|
+
issues: list[str] = []
|
|
139
|
+
for each_name in banned_name_nodes:
|
|
140
|
+
issues.append(
|
|
141
|
+
f"Line {each_name.lineno}: Banned identifier '{each_name.id}' - {BANNED_IDENTIFIER_MESSAGE_SUFFIX}"
|
|
142
|
+
)
|
|
143
|
+
if len(issues) >= MAX_BANNED_IDENTIFIER_ISSUES:
|
|
144
|
+
break
|
|
145
|
+
|
|
146
|
+
return issues
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _identifier_word_parts(identifier: str) -> list[str]:
|
|
150
|
+
"""Split an identifier into lowercase word parts.
|
|
151
|
+
|
|
152
|
+
Handles snake_case (split on ``_``), SCREAMING_SNAKE_CASE, and camelCase /
|
|
153
|
+
PascalCase (split on capital-letter boundaries). Returns a list of
|
|
154
|
+
lowercased word tokens for membership comparison against banned-noun
|
|
155
|
+
vocabularies.
|
|
156
|
+
|
|
157
|
+
Args:
|
|
158
|
+
identifier: A Python identifier (variable, parameter, class, or
|
|
159
|
+
function name).
|
|
160
|
+
|
|
161
|
+
Returns:
|
|
162
|
+
Word tokens in their original order, lowercased. Empty list when the
|
|
163
|
+
identifier carries no letter characters.
|
|
164
|
+
"""
|
|
165
|
+
all_words: list[str] = []
|
|
166
|
+
for each_snake_segment in identifier.split("_"):
|
|
167
|
+
if not each_snake_segment:
|
|
168
|
+
continue
|
|
169
|
+
camel_pieces = CAMEL_CASE_WORD_PATTERN.findall(each_snake_segment)
|
|
170
|
+
if camel_pieces:
|
|
171
|
+
for each_piece in camel_pieces:
|
|
172
|
+
all_words.append(each_piece.lower())
|
|
173
|
+
else:
|
|
174
|
+
all_words.append(each_snake_segment.lower())
|
|
175
|
+
return all_words
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def _find_banned_noun_word(identifier: str) -> str | None:
|
|
179
|
+
"""Return the first banned-noun word embedded in *identifier*, or None.
|
|
180
|
+
|
|
181
|
+
Args:
|
|
182
|
+
identifier: A Python identifier.
|
|
183
|
+
|
|
184
|
+
Returns:
|
|
185
|
+
The lowercased banned noun word that appears as a word part inside the
|
|
186
|
+
identifier (e.g., ``'result'`` for ``'HolidayPeakResult'``). Returns
|
|
187
|
+
``None`` when no banned noun word is present.
|
|
188
|
+
"""
|
|
189
|
+
for each_word in _identifier_word_parts(identifier):
|
|
190
|
+
if each_word in ALL_BANNED_NOUN_WORDS:
|
|
191
|
+
return each_word
|
|
192
|
+
return None
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def _is_dunder_name(identifier: str) -> bool:
|
|
196
|
+
return identifier.startswith("__") and identifier.endswith("__")
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def _collect_banned_noun_word_bindings(
|
|
200
|
+
parsed_tree: ast.AST,
|
|
201
|
+
) -> list[tuple[str, int, int, str]]:
|
|
202
|
+
"""Yield ``(identifier, lineno, col_offset, banned_word)`` for each binding.
|
|
203
|
+
|
|
204
|
+
Walks assignment targets, annotated assignments, function/method
|
|
205
|
+
parameters, function/method definitions, and class definitions. Skips
|
|
206
|
+
identifiers that already match ``ALL_BANNED_IDENTIFIERS`` exactly (those
|
|
207
|
+
are reported by ``check_banned_identifiers``) and dunder names.
|
|
208
|
+
"""
|
|
209
|
+
flagged_bindings: list[tuple[str, int, int, str]] = []
|
|
210
|
+
seen_keys: set[tuple[str, int, int]] = set()
|
|
211
|
+
|
|
212
|
+
def record(name: str, lineno: int, col_offset: int) -> None:
|
|
213
|
+
if name in ALL_BANNED_IDENTIFIERS:
|
|
214
|
+
return
|
|
215
|
+
if _is_dunder_name(name):
|
|
216
|
+
return
|
|
217
|
+
banned_word = _find_banned_noun_word(name)
|
|
218
|
+
if banned_word is None:
|
|
219
|
+
return
|
|
220
|
+
key = (name, lineno, col_offset)
|
|
221
|
+
if key in seen_keys:
|
|
222
|
+
return
|
|
223
|
+
seen_keys.add(key)
|
|
224
|
+
flagged_bindings.append((name, lineno, col_offset, banned_word))
|
|
225
|
+
|
|
226
|
+
for each_node in ast.walk(parsed_tree):
|
|
227
|
+
if isinstance(each_node, ast.Assign):
|
|
228
|
+
for each_target in each_node.targets:
|
|
229
|
+
for each_name_node in _collect_target_names(each_target):
|
|
230
|
+
record(each_name_node.id, each_name_node.lineno, each_name_node.col_offset)
|
|
231
|
+
elif isinstance(each_node, ast.AnnAssign):
|
|
232
|
+
for each_name_node in _collect_target_names(each_node.target):
|
|
233
|
+
record(each_name_node.id, each_name_node.lineno, each_name_node.col_offset)
|
|
234
|
+
elif isinstance(each_node, (ast.For, ast.AsyncFor)):
|
|
235
|
+
for each_name_node in _collect_target_names(each_node.target):
|
|
236
|
+
record(each_name_node.id, each_name_node.lineno, each_name_node.col_offset)
|
|
237
|
+
elif isinstance(each_node, ast.NamedExpr) and isinstance(each_node.target, ast.Name):
|
|
238
|
+
record(each_node.target.id, each_node.target.lineno, each_node.target.col_offset)
|
|
239
|
+
elif isinstance(each_node, ast.comprehension):
|
|
240
|
+
for each_name_node in _collect_target_names(each_node.target):
|
|
241
|
+
record(each_name_node.id, each_name_node.lineno, each_name_node.col_offset)
|
|
242
|
+
elif isinstance(each_node, ast.withitem) and each_node.optional_vars is not None:
|
|
243
|
+
for each_name_node in _collect_target_names(each_node.optional_vars):
|
|
244
|
+
record(each_name_node.id, each_name_node.lineno, each_name_node.col_offset)
|
|
245
|
+
elif isinstance(each_node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
246
|
+
record(each_node.name, each_node.lineno, each_node.col_offset)
|
|
247
|
+
for each_arg in _collect_annotated_arguments(each_node):
|
|
248
|
+
if each_arg.arg in ALL_SELF_AND_CLS_PARAMETER_NAMES:
|
|
249
|
+
continue
|
|
250
|
+
record(each_arg.arg, each_arg.lineno, each_arg.col_offset)
|
|
251
|
+
elif isinstance(each_node, ast.ClassDef):
|
|
252
|
+
record(each_node.name, each_node.lineno, each_node.col_offset)
|
|
253
|
+
elif isinstance(each_node, (ast.Import, ast.ImportFrom)):
|
|
254
|
+
for each_alias in each_node.names:
|
|
255
|
+
if each_alias.asname is None:
|
|
256
|
+
continue
|
|
257
|
+
record(each_alias.asname, each_node.lineno, each_node.col_offset)
|
|
258
|
+
|
|
259
|
+
flagged_bindings.sort(key=lambda binding: (binding[1], binding[2]))
|
|
260
|
+
return flagged_bindings
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def check_banned_noun_word_boundary(
|
|
264
|
+
content: str,
|
|
265
|
+
file_path: str,
|
|
266
|
+
all_changed_lines: set[int] | None = None,
|
|
267
|
+
defer_scope_to_caller: bool = False,
|
|
268
|
+
) -> list[str]:
|
|
269
|
+
"""Flag identifiers containing CODE_RULES naming-rule banned noun words.
|
|
270
|
+
|
|
271
|
+
Companion to ``check_banned_identifiers`` (exact-match cases only). This
|
|
272
|
+
check catches the wider pattern: a banned noun word from
|
|
273
|
+
``ALL_BANNED_NOUN_WORDS`` — the singular nouns ``result``, ``data``,
|
|
274
|
+
``output``, ``response``, ``value``, ``item``, ``temp`` plus the plural
|
|
275
|
+
forms ``results``, ``outputs``, ``responses``, ``values``, ``items`` —
|
|
276
|
+
appearing as a snake_case word part or camelCase word part inside a longer
|
|
277
|
+
identifier (``canned_results``, ``HolidayPeakResult``, ``OUTPUT_DIR``,
|
|
278
|
+
``cached_response``).
|
|
279
|
+
|
|
280
|
+
Skips test files, config files, hook infrastructure, workflow registries,
|
|
281
|
+
and migrations. Identifiers that exactly match ``ALL_BANNED_IDENTIFIERS``
|
|
282
|
+
are skipped because they are already reported by
|
|
283
|
+
``check_banned_identifiers``.
|
|
284
|
+
|
|
285
|
+
Scoping mirrors ``check_function_length`` and
|
|
286
|
+
``check_tests_use_isolated_filesystem_paths`` through the shared
|
|
287
|
+
``_scope_violations_to_changed_lines`` helper. A banned-noun binding is a
|
|
288
|
+
point fact about one identifier, so its enclosing unit is its own binding
|
|
289
|
+
line: each violation carries the binding line as a one-line ``range`` for
|
|
290
|
+
terminal diff scoping and a ``(binding span at line X, spanning 1 lines)``
|
|
291
|
+
message fragment the commit gate reconstructs through the same shared span
|
|
292
|
+
extractor registry the other two scoped checks use. Anchoring to the
|
|
293
|
+
binding line (rather than the whole enclosing function) matches the
|
|
294
|
+
companion exact-match ``check_banned_identifiers`` and keeps a pre-existing
|
|
295
|
+
binding out of scope when an unrelated line of its enclosing function is
|
|
296
|
+
edited. On a terminal Edit only violations whose binding line is among
|
|
297
|
+
``all_changed_lines`` are returned; on a new-file or full-file write every
|
|
298
|
+
violation is in scope; ``defer_scope_to_caller`` returns every violation so
|
|
299
|
+
the gate scopes by added line.
|
|
300
|
+
|
|
301
|
+
Args:
|
|
302
|
+
content: The reconstructed effective file content to analyze (the
|
|
303
|
+
whole post-edit file on an Edit, the whole file at the gate).
|
|
304
|
+
file_path: The path of the file being checked (used for exemption
|
|
305
|
+
routing).
|
|
306
|
+
all_changed_lines: Post-edit line numbers the current edit touched, or
|
|
307
|
+
None to treat the whole file as in scope. When provided, a violation
|
|
308
|
+
blocks only when its binding line is among the changed lines.
|
|
309
|
+
defer_scope_to_caller: When True, return every violation so the
|
|
310
|
+
commit/push gate's ``split_violations_by_scope`` can scope by added
|
|
311
|
+
line and report the in-scope set.
|
|
312
|
+
|
|
313
|
+
Returns:
|
|
314
|
+
Issue strings, each describing one offending binding. When
|
|
315
|
+
*defer_scope_to_caller* is True every binding is returned for the gate
|
|
316
|
+
to scope; otherwise every binding in scope is returned.
|
|
317
|
+
"""
|
|
318
|
+
if is_test_file(file_path) or is_hook_infrastructure(file_path):
|
|
319
|
+
return []
|
|
320
|
+
if is_config_file(file_path):
|
|
321
|
+
return []
|
|
322
|
+
if is_workflow_registry_file(file_path):
|
|
323
|
+
return []
|
|
324
|
+
if is_migration_file(file_path):
|
|
325
|
+
return []
|
|
326
|
+
|
|
327
|
+
try:
|
|
328
|
+
parsed_tree = ast.parse(content)
|
|
329
|
+
except SyntaxError:
|
|
330
|
+
return []
|
|
331
|
+
|
|
332
|
+
single_line_span = 1
|
|
333
|
+
all_violations_in_walk_order: list[tuple[range, str]] = []
|
|
334
|
+
for each_name, each_lineno, _, each_word in _collect_banned_noun_word_bindings(parsed_tree):
|
|
335
|
+
span_range = range(each_lineno, each_lineno + single_line_span)
|
|
336
|
+
span_fragment = BANNED_NOUN_SPAN_FRAGMENT_TEMPLATE.format(
|
|
337
|
+
definition_line=each_lineno, line_span=single_line_span
|
|
338
|
+
)
|
|
339
|
+
message = (
|
|
340
|
+
f"Line {each_lineno}: Identifier {each_name!r} {BANNED_NOUN_WORD_MESSAGE_SUFFIX} "
|
|
341
|
+
f"(word: {each_word!r}) {span_fragment}"
|
|
342
|
+
)
|
|
343
|
+
all_violations_in_walk_order.append((span_range, message))
|
|
344
|
+
return _scope_violations_to_changed_lines(
|
|
345
|
+
all_violations_in_walk_order,
|
|
346
|
+
all_changed_lines,
|
|
347
|
+
defer_scope_to_caller,
|
|
348
|
+
)
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
def check_banned_prefixes(content: str, file_path: str) -> list[str]:
|
|
352
|
+
"""Flag function and method names using generic banned prefixes.
|
|
353
|
+
|
|
354
|
+
Per CODE_RULES.md / AGENTS.md Naming, function names use specific verbs.
|
|
355
|
+
Generic prefixes ``handle_``, ``process_``, ``manage_``, ``do_`` are
|
|
356
|
+
placeholders that hide the actual responsibility and are flagged so the
|
|
357
|
+
author renames the function to a specific verb.
|
|
358
|
+
"""
|
|
359
|
+
if is_test_file(file_path) or is_hook_infrastructure(file_path) or is_config_file(file_path):
|
|
360
|
+
return []
|
|
361
|
+
|
|
362
|
+
try:
|
|
363
|
+
parsed_tree = ast.parse(content)
|
|
364
|
+
except SyntaxError:
|
|
365
|
+
return []
|
|
366
|
+
|
|
367
|
+
flagged_function_nodes: list[ast.FunctionDef | ast.AsyncFunctionDef] = []
|
|
368
|
+
for each_node in _walk_skipping_type_checking_blocks(parsed_tree):
|
|
369
|
+
if not isinstance(each_node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
370
|
+
continue
|
|
371
|
+
if any(each_node.name.startswith(each_prefix) for each_prefix in ALL_BANNED_PREFIX_NAMES):
|
|
372
|
+
flagged_function_nodes.append(each_node)
|
|
373
|
+
|
|
374
|
+
flagged_function_nodes.sort(key=lambda each_function: each_function.lineno)
|
|
375
|
+
|
|
376
|
+
issues: list[str] = []
|
|
377
|
+
for each_function in flagged_function_nodes:
|
|
378
|
+
issues.append(
|
|
379
|
+
f"Line {each_function.lineno}: Function '{each_function.name}' uses banned prefix - "
|
|
380
|
+
"rename to a specific verb (see CODE_RULES Naming section)"
|
|
381
|
+
)
|
|
382
|
+
if len(issues) >= MAX_BANNED_PREFIX_ISSUES:
|
|
383
|
+
break
|
|
384
|
+
|
|
385
|
+
return issues
|