claude-dev-env 1.50.1 → 1.50.3

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.
Files changed (91) hide show
  1. package/_shared/pr-loop/audit-contract.md +3 -3
  2. package/audit-rubrics/category_rubrics/category-e-dead-code.md +3 -2
  3. package/audit-rubrics/prompts/category-a-api-contracts.md +1 -1
  4. package/audit-rubrics/prompts/category-b-selector-engine-compat.md +2 -2
  5. package/audit-rubrics/prompts/category-c-resource-cleanup.md +2 -2
  6. package/audit-rubrics/prompts/category-d-scoping-and-ordering.md +2 -2
  7. package/audit-rubrics/prompts/category-e-dead-code.md +5 -4
  8. package/audit-rubrics/prompts/category-f-silent-failures.md +2 -2
  9. package/audit-rubrics/prompts/category-g-bounds-and-overflow.md +2 -2
  10. package/audit-rubrics/prompts/category-h-security-boundaries.md +2 -2
  11. package/audit-rubrics/prompts/category-i-concurrency.md +2 -2
  12. package/audit-rubrics/prompts/category-j-code-rules-compliance.md +2 -2
  13. package/audit-rubrics/prompts/category-k-codebase-conflicts.md +2 -2
  14. package/docs/CODE_RULES.md +1 -1
  15. package/hooks/blocking/code_rules_annotations_length.py +167 -0
  16. package/hooks/blocking/code_rules_banned_identifiers.py +385 -0
  17. package/hooks/blocking/code_rules_boolean_mustcheck.py +350 -0
  18. package/hooks/blocking/code_rules_comments.py +337 -0
  19. package/hooks/blocking/code_rules_constants_config.py +252 -0
  20. package/hooks/blocking/code_rules_docstrings.py +308 -0
  21. package/hooks/blocking/code_rules_enforcer.py +98 -5807
  22. package/hooks/blocking/code_rules_imports_logging.py +276 -0
  23. package/hooks/blocking/code_rules_magic_values.py +180 -0
  24. package/hooks/blocking/code_rules_mock_completeness.py +295 -0
  25. package/hooks/blocking/code_rules_naming_collection.py +264 -0
  26. package/hooks/blocking/code_rules_optional_params.py +288 -0
  27. package/hooks/blocking/code_rules_paths_syspath.py +186 -0
  28. package/hooks/blocking/code_rules_probe_chains.py +305 -0
  29. package/hooks/blocking/code_rules_probe_detection.py +257 -0
  30. package/hooks/blocking/code_rules_probe_recording.py +225 -0
  31. package/hooks/blocking/code_rules_scope_binding.py +151 -0
  32. package/hooks/blocking/code_rules_shared.py +301 -0
  33. package/hooks/blocking/code_rules_string_magic.py +207 -0
  34. package/hooks/blocking/code_rules_test_assertions.py +226 -0
  35. package/hooks/blocking/code_rules_test_branching_except.py +181 -0
  36. package/hooks/blocking/code_rules_test_isolation.py +341 -0
  37. package/hooks/blocking/code_rules_type_escape.py +341 -0
  38. package/hooks/blocking/code_rules_typeddict_stub.py +305 -0
  39. package/hooks/blocking/code_rules_unused_imports.py +256 -0
  40. package/hooks/blocking/tdd_enforcer.py +31 -0
  41. package/hooks/blocking/test_code_rules_constants_config.py +26 -0
  42. package/hooks/blocking/test_code_rules_enforcer_banned_noun_word.py +5 -2
  43. package/hooks/blocking/test_code_rules_enforcer_cap_meta.py +0 -5
  44. package/hooks/blocking/test_code_rules_enforcer_comment_string_awareness.py +21 -15
  45. package/hooks/blocking/test_code_rules_enforcer_config_path.py +20 -16
  46. package/hooks/blocking/test_code_rules_enforcer_exempt_marker_chained.py +4 -2
  47. package/hooks/blocking/test_code_rules_enforcer_function_length.py +18 -13
  48. package/hooks/blocking/test_code_rules_enforcer_hardcoded_user_path.py +1 -2
  49. package/hooks/blocking/test_code_rules_enforcer_ignored_must_check_return.py +22 -12
  50. package/hooks/blocking/test_code_rules_enforcer_split_annotations_length.py +55 -0
  51. package/hooks/blocking/test_code_rules_enforcer_split_banned.py +170 -0
  52. package/hooks/blocking/test_code_rules_enforcer_split_comments.py +60 -0
  53. package/hooks/blocking/test_code_rules_enforcer_split_config_path.py +52 -0
  54. package/hooks/blocking/test_code_rules_enforcer_split_constants_config.py +236 -0
  55. package/hooks/blocking/test_code_rules_enforcer_split_entry_1.py +296 -0
  56. package/hooks/blocking/test_code_rules_enforcer_split_entry_2.py +238 -0
  57. package/hooks/blocking/test_code_rules_enforcer_split_isolation_1.py +271 -0
  58. package/hooks/blocking/test_code_rules_enforcer_split_isolation_2.py +283 -0
  59. package/hooks/blocking/test_code_rules_enforcer_split_isolation_3.py +268 -0
  60. package/hooks/blocking/test_code_rules_enforcer_split_isolation_4.py +85 -0
  61. package/hooks/blocking/test_code_rules_enforcer_split_mocks_1.py +303 -0
  62. package/hooks/blocking/test_code_rules_enforcer_split_mocks_2.py +111 -0
  63. package/hooks/blocking/test_code_rules_enforcer_split_mustcheck.py +87 -0
  64. package/hooks/blocking/test_code_rules_enforcer_split_naming.py +107 -0
  65. package/hooks/blocking/test_code_rules_enforcer_split_optional_params.py +325 -0
  66. package/hooks/blocking/test_code_rules_enforcer_split_paths_syspath.py +110 -0
  67. package/hooks/blocking/test_code_rules_enforcer_split_shared.py +44 -0
  68. package/hooks/blocking/test_code_rules_enforcer_split_string_magic.py +55 -0
  69. package/hooks/blocking/test_code_rules_enforcer_split_test_assertions.py +56 -0
  70. package/hooks/blocking/test_code_rules_enforcer_todo_markers.py +21 -15
  71. package/hooks/blocking/test_code_rules_paths_syspath.py +26 -0
  72. package/hooks/blocking/test_tdd_enforcer.py +116 -0
  73. package/hooks/hooks_constants/blocking_check_limits.py +3 -0
  74. package/hooks/hooks_constants/code_rules_enforcer_constants.py +8 -0
  75. package/hooks/hooks_constants/sys_path_insert_constants.py +1 -0
  76. package/package.json +1 -1
  77. package/skills/_shared/pr-loop/scripts/build_audit_prompt.py +13 -7
  78. package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/path_resolver_constants.py +21 -11
  79. package/skills/_shared/pr-loop/scripts/test_build_audit_prompt.py +92 -0
  80. package/skills/bugteam/CONSTRAINTS.md +1 -1
  81. package/skills/bugteam/PROMPTS.md +20 -48
  82. package/skills/bugteam/SKILL.md +5 -5
  83. package/skills/bugteam/reference/audit-and-teammates.md +1 -1
  84. package/skills/bugteam/reference/audit-contract.md +4 -4
  85. package/skills/bugteam/reference/design-rationale.md +1 -1
  86. package/skills/findbugs/SKILL.md +21 -12
  87. package/skills/fixbugs/SKILL.md +1 -1
  88. package/skills/qbug/SKILL.md +5 -5
  89. package/skills/qbug/test_qbug_skill_audit_schema.py +13 -23
  90. package/skills/refine/SKILL.md +1 -1
  91. package/hooks/blocking/test_code_rules_enforcer.py +0 -2669
@@ -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