claude-dev-env 2.28.0 → 2.29.0

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 (212) hide show
  1. package/.agents/agents/AGENTS.md +0 -31
  2. package/.agents/agents/clean-coder.md +1 -1
  3. package/.agents/agents/test_agent_frontmatter.py +3 -1
  4. package/.agents/skills/AGENTS.md +0 -70
  5. package/.agents/skills/_shared/AGENTS.md +0 -44
  6. package/.agents/skills/_shared/advisor/AGENTS.md +0 -8
  7. package/.agents/skills/_shared/pr-loop/AGENTS.md +0 -57
  8. package/.agents/skills/_shared/pr-loop/prompts/AGENTS.md +0 -8
  9. package/.agents/skills/_shared/pr-loop/scripts/AGENTS.md +0 -34
  10. package/.agents/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/AGENTS.md +0 -23
  11. package/.agents/skills/autoconverge/AGENTS.md +0 -35
  12. package/.agents/skills/autoconverge/reference/AGENTS.md +0 -15
  13. package/.agents/skills/autoconverge/workflow/AGENTS.md +0 -25
  14. package/.agents/skills/autoconverge/workflow/autoconverge_report_constants/AGENTS.md +0 -15
  15. package/.agents/skills/everything-search/AGENTS.md +0 -16
  16. package/.agents/skills/fresh-branch/AGENTS.md +0 -13
  17. package/.agents/skills/grok-spawn/AGENTS.md +0 -27
  18. package/.agents/skills/orchestrator/.claude/CLAUDE.md +1 -0
  19. package/.agents/skills/orchestrator/AGENTS.md +1 -0
  20. package/.agents/skills/orchestrator/SKILL.md +44 -53
  21. package/.agents/skills/orchestrator/reference/.claude/CLAUDE.md +1 -0
  22. package/.agents/skills/orchestrator/reference/AGENTS.md +1 -0
  23. package/.agents/skills/orchestrator/reference/consult-the-orchestrator.md +70 -0
  24. package/.agents/skills/orchestrator/reference/executor-consult-block.md +62 -0
  25. package/.agents/skills/orchestrator/reference/host-detect.md +15 -0
  26. package/.agents/skills/orchestrator/test_orchestrator_skill_contract.py +62 -0
  27. package/.agents/skills/orchestrator-refresh/SKILL.md +16 -34
  28. package/.agents/skills/rebase/AGENTS.md +0 -31
  29. package/.agents/skills/session-log/AGENTS.md +0 -31
  30. package/.agents/skills/session-tidy/AGENTS.md +0 -35
  31. package/.agents/skills/skill-builder/AGENTS.md +0 -48
  32. package/.agents/skills/skill-builder/references/AGENTS.md +0 -24
  33. package/.agents/skills/skill-builder/templates/AGENTS.md +0 -13
  34. package/.agents/skills/skill-builder/workflows/AGENTS.md +0 -18
  35. package/.agents/skills/task-build/AGENTS.md +0 -28
  36. package/.agents/skills/update/AGENTS.md +0 -37
  37. package/.agents/skills-archived/AGENTS.md +0 -44
  38. package/.agents/skills-archived/anthropic-plan/AGENTS.md +0 -33
  39. package/.agents/skills-archived/anthropic-plan/scripts/AGENTS.md +0 -10
  40. package/.agents/skills-archived/anthropic-plan/scripts/anthropic_plan_scripts_constants/AGENTS.md +0 -15
  41. package/.agents/skills-archived/anthropic-plan/templates/AGENTS.md +0 -12
  42. package/.agents/skills-archived/anthropic-plan/workflow/AGENTS.md +0 -13
  43. package/.agents/skills-archived/auditing-claude-config/AGENTS.md +0 -20
  44. package/.agents/skills-archived/bugteam/AGENTS.md +0 -29
  45. package/.agents/skills-archived/bugteam/reference/AGENTS.md +0 -19
  46. package/.agents/skills-archived/bugteam/reference/obstacles/AGENTS.md +0 -23
  47. package/.agents/skills-archived/bugteam/scripts/AGENTS.md +0 -29
  48. package/.agents/skills-archived/bugteam/scripts/bugteam_scripts_constants/AGENTS.md +0 -17
  49. package/.agents/skills-archived/codex-review/AGENTS.md +0 -45
  50. package/.agents/skills-archived/codex-review/reference/AGENTS.md +0 -14
  51. package/.agents/skills-archived/codex-review/scripts/codex_review_scripts_constants/AGENTS.md +0 -17
  52. package/.agents/skills-archived/codex-review/test_skill_scaffold.py +13 -3
  53. package/.agents/skills-archived/copilot-review/AGENTS.md +0 -17
  54. package/.agents/skills-archived/pr-converge/AGENTS.md +0 -31
  55. package/.agents/skills-archived/pr-converge/pr_converge_skill_constants/AGENTS.md +0 -25
  56. package/.agents/skills-archived/pr-converge/reference/AGENTS.md +0 -27
  57. package/.agents/skills-archived/pr-converge/reference/obstacles/AGENTS.md +0 -22
  58. package/.agents/skills-archived/pr-converge/scripts/AGENTS.md +0 -45
  59. package/.agents/skills-archived/pr-converge/scripts/pr_converge_scripts_constants/AGENTS.md +0 -17
  60. package/.agents/skills-archived/pr-converge/workflows/AGENTS.md +0 -15
  61. package/.agents/skills-archived/pr-loop-cloud-transport/reference/identity-and-hooks.md +1 -1
  62. package/.agents/skills-archived/recall/AGENTS.md +0 -29
  63. package/.agents/skills-archived/remember/AGENTS.md +0 -30
  64. package/AGENTS.md +0 -112
  65. package/_shared/AGENTS.md +0 -16
  66. package/_shared/advisor/AGENTS.md +0 -21
  67. package/_shared/advisor/advisor-protocol.md +4 -1
  68. package/_shared/advisor/reference/consult-format.md +4 -2
  69. package/_shared/pr-loop/AGENTS.md +0 -27
  70. package/_shared/pr-loop/scripts/AGENTS.md +0 -49
  71. package/_shared/pr-loop/scripts/code_rules_gate_parts/AGENTS.md +0 -41
  72. package/_shared/pr-loop/scripts/codex_review_scripts_constants/AGENTS.md +0 -17
  73. package/_shared/pr-loop/scripts/pr_converge_scripts_constants/AGENTS.md +0 -17
  74. package/_shared/pr-loop/scripts/pr_converge_skill_constants/AGENTS.md +0 -25
  75. package/_shared/pr-loop/scripts/pr_loop_shared_constants/AGENTS.md +0 -25
  76. package/_shared/pr-loop/scripts/tests/AGENTS.md +0 -43
  77. package/_shared/process-tree/AGENTS.md +0 -40
  78. package/audit-rubrics/AGENTS.md +0 -42
  79. package/audit-rubrics/category_rubrics/AGENTS.md +0 -36
  80. package/audit-rubrics/category_rubrics/category-o-docstring-vs-impl-drift.md +16 -29
  81. package/audit-rubrics/prompts/AGENTS.md +0 -36
  82. package/audit-rubrics/prompts/category-o-docstring-vs-impl-drift.md +2 -2
  83. package/bin/AGENTS.md +0 -118
  84. package/bin/install.test.mjs +1 -1
  85. package/commands/AGENTS.md +0 -14
  86. package/docs/AGENTS.md +0 -31
  87. package/docs/CODE_RULES.md +3 -3
  88. package/docs/agent-spawn-protocol.md +1 -1
  89. package/docs/references/AGENTS.md +0 -16
  90. package/docs/references/prose-style-enforcement.md +5 -9
  91. package/hooks/AGENTS.md +0 -27
  92. package/hooks/advisory/AGENTS.md +0 -15
  93. package/hooks/blocking/AGENTS.md +0 -122
  94. package/hooks/blocking/claude_md_orphan_file_blocker_parts/AGENTS.md +0 -27
  95. package/hooks/blocking/code_rules_docstrings.py +10 -2138
  96. package/hooks/blocking/code_rules_enforcer.py +0 -121
  97. package/hooks/blocking/code_rules_imports_logging.py +1 -236
  98. package/hooks/blocking/code_rules_shared.py +23 -0
  99. package/hooks/blocking/code_rules_test_layout.py +8 -8
  100. package/hooks/blocking/config/AGENTS.md +0 -9
  101. package/hooks/blocking/config/prose_style_enforcement_constants.py +4 -2
  102. package/hooks/blocking/config/test_prose_style_enforcement_constants.py +5 -1
  103. package/hooks/blocking/inventory_intent_records/AGENTS.md +0 -25
  104. package/hooks/blocking/package_inventory_stale_blocker_parts/AGENTS.md +0 -25
  105. package/hooks/blocking/pii_prevention_blocker_parts/AGENTS.md +0 -23
  106. package/hooks/blocking/precommit_code_rules_gate.py +8 -43
  107. package/hooks/blocking/state_description_blocker.py +1 -7
  108. package/hooks/blocking/tdd_enforcer_parts/AGENTS.md +0 -29
  109. package/hooks/blocking/test_code_rules_enforcer_cap_meta.py +0 -1
  110. package/hooks/blocking/test_code_rules_enforcer_dispatch_wiring.py +0 -8
  111. package/hooks/blocking/test_code_rules_enforcer_module_docstring_roster.py +11 -112
  112. package/hooks/blocking/test_code_rules_enforcer_narrow_edit.py +0 -1
  113. package/hooks/blocking/test_code_rules_enforcer_split_entry_1.py +1 -18
  114. package/hooks/blocking/test_code_rules_shared.py +12 -0
  115. package/hooks/blocking/test_precommit_code_rules_gate.py +32 -179
  116. package/hooks/blocking/test_precommit_code_rules_gate_native_owner.py +0 -1
  117. package/hooks/blocking/test_state_description_blocker.py +6 -4
  118. package/hooks/blocking/test_stop_dispatcher.py +5 -29
  119. package/hooks/git-hooks/AGENTS.md +0 -31
  120. package/hooks/git-hooks/git_hooks_constants/AGENTS.md +0 -20
  121. package/hooks/hooks_constants/AGENTS.md +0 -104
  122. package/hooks/hooks_constants/bash_pre_tool_use_dispatcher_constants.py +0 -1
  123. package/hooks/hooks_constants/code_rules_enforcer_constants.py +3 -0
  124. package/hooks/hooks_constants/messages.py +0 -2
  125. package/hooks/hooks_constants/precommit_code_rules_gate_constants.py +3 -17
  126. package/hooks/hooks_constants/stop_dispatcher_constants.py +0 -2
  127. package/hooks/hooks_constants/test_bash_pre_tool_use_dispatcher_constants.py +0 -1
  128. package/hooks/hooks_constants/test_code_rules_enforcer_constants.py +7 -0
  129. package/hooks/hooks_constants/test_messages.py +5 -3
  130. package/hooks/hooks_constants/test_stop_dispatcher_constants.py +0 -2
  131. package/hooks/lifecycle/AGENTS.md +0 -17
  132. package/hooks/observability/AGENTS.md +0 -19
  133. package/hooks/session/AGENTS.md +0 -37
  134. package/hooks/validation/AGENTS.md +0 -19
  135. package/hooks/validators/AGENTS.md +0 -51
  136. package/hooks/workflow/AGENTS.md +0 -15
  137. package/output-styles/AGENTS.md +0 -14
  138. package/package.json +1 -1
  139. package/rules/AGENTS.md +0 -58
  140. package/rules/claims-as-quotes.md +0 -10
  141. package/rules/code-standards.md +6 -6
  142. package/rules/explore-thoroughly.md +0 -1
  143. package/rules/failure-blast-radius.md +0 -8
  144. package/rules/falsify-before-green.md +0 -8
  145. package/rules/file-global-constants.md +2 -2
  146. package/rules/filesystem-search.md +1 -1
  147. package/rules/git-workflow.md +1 -9
  148. package/rules/hedging-claims.md +2 -6
  149. package/rules/long-horizon-autonomy.md +1 -1
  150. package/rules/measurement-denominators.md +0 -9
  151. package/rules/research-mode.md +0 -6
  152. package/rules/verify-before-asking.md +0 -5
  153. package/rules/verify-runtime-state.md +0 -5
  154. package/scripts/AGENTS.md +0 -55
  155. package/scripts/codex_compat_materializer.py +0 -12
  156. package/scripts/dev_env_scripts_constants/AGENTS.md +0 -20
  157. package/scripts/sync_to_cursor/AGENTS.md +0 -22
  158. package/scripts/sync_to_cursor/rules.py +0 -10
  159. package/scripts/tests/AGENTS.md +0 -34
  160. package/scripts/tests/test_engine.py +0 -1
  161. package/scripts/tests/test_rules.py +0 -1
  162. package/scripts/tests/test_sync_to_cursor.py +0 -1
  163. package/system-prompts/AGENTS.md +0 -24
  164. package/system-prompts/software-engineer.xml +3 -3
  165. package/hooks/blocking/code_rules_dead_argparse_argument.py +0 -554
  166. package/hooks/blocking/code_rules_dead_config_field.py +0 -568
  167. package/hooks/blocking/code_rules_dead_dataclass_field.py +0 -348
  168. package/hooks/blocking/code_rules_dead_module_constant.py +0 -757
  169. package/hooks/blocking/code_rules_dead_split_branch.py +0 -225
  170. package/hooks/blocking/code_rules_mock_completeness.py +0 -295
  171. package/hooks/blocking/code_rules_scope_binding.py +0 -151
  172. package/hooks/blocking/code_rules_unused_imports.py +0 -197
  173. package/hooks/blocking/hedging_language_blocker.py +0 -221
  174. package/hooks/blocking/intent_only_ending_blocker.py +0 -148
  175. package/hooks/blocking/test_code_rules_enforcer_dead_argparse_argument.py +0 -534
  176. package/hooks/blocking/test_code_rules_enforcer_dead_config_field.py +0 -846
  177. package/hooks/blocking/test_code_rules_enforcer_dead_dataclass_field.py +0 -507
  178. package/hooks/blocking/test_code_rules_enforcer_dead_module_constant.py +0 -679
  179. package/hooks/blocking/test_code_rules_enforcer_dead_module_constant_alias.py +0 -133
  180. package/hooks/blocking/test_code_rules_enforcer_dead_module_constant_read_cap.py +0 -103
  181. package/hooks/blocking/test_code_rules_enforcer_dead_split_branch.py +0 -105
  182. package/hooks/blocking/test_code_rules_enforcer_docstring_args_span_scope.py +0 -425
  183. package/hooks/blocking/test_code_rules_enforcer_docstring_cardinal_family.py +0 -176
  184. package/hooks/blocking/test_code_rules_enforcer_docstring_delegation_summary.py +0 -385
  185. package/hooks/blocking/test_code_rules_enforcer_docstring_fallback_branch.py +0 -398
  186. package/hooks/blocking/test_code_rules_enforcer_docstring_field_runmode_outcome.py +0 -129
  187. package/hooks/blocking/test_code_rules_enforcer_docstring_inline_literal_claim.py +0 -93
  188. package/hooks/blocking/test_code_rules_enforcer_docstring_length_constant_superlative.py +0 -198
  189. package/hooks/blocking/test_code_rules_enforcer_docstring_mark_glyph_enumeration.py +0 -262
  190. package/hooks/blocking/test_code_rules_enforcer_docstring_no_consumer.py +0 -93
  191. package/hooks/blocking/test_code_rules_enforcer_docstring_no_network.py +0 -115
  192. package/hooks/blocking/test_code_rules_enforcer_docstring_raises_largezipfile.py +0 -226
  193. package/hooks/blocking/test_code_rules_enforcer_docstring_returns_plural_cardinality.py +0 -207
  194. package/hooks/blocking/test_code_rules_enforcer_docstring_step_dispatch.py +0 -262
  195. package/hooks/blocking/test_code_rules_enforcer_docstring_type_checking_gate.py +0 -164
  196. package/hooks/blocking/test_code_rules_enforcer_docstring_unguarded_payload.py +0 -188
  197. package/hooks/blocking/test_code_rules_enforcer_import_block_sort.py +0 -157
  198. package/hooks/blocking/test_code_rules_enforcer_split_mocks_1.py +0 -303
  199. package/hooks/blocking/test_code_rules_enforcer_split_mocks_2.py +0 -111
  200. package/hooks/blocking/test_code_rules_enforcer_unused_imports.py +0 -656
  201. package/hooks/blocking/test_hedging_language_blocker.py +0 -261
  202. package/hooks/blocking/test_intent_only_ending_blocker.py +0 -209
  203. package/hooks/hooks_constants/dead_argparse_argument_constants.py +0 -28
  204. package/hooks/hooks_constants/dead_config_field_constants.py +0 -39
  205. package/hooks/hooks_constants/dead_dataclass_field_constants.py +0 -25
  206. package/hooks/hooks_constants/dead_module_constant_constants.py +0 -30
  207. package/hooks/hooks_constants/hedging_uncertainty_constants.py +0 -42
  208. package/hooks/hooks_constants/test_dispatcher_constants_docstrings.py +0 -44
  209. package/rules/conservative-action.md +0 -17
  210. package/rules/context7.md +0 -8
  211. package/rules/few-words.md +0 -3
  212. package/rules/parallel-tools.md +0 -23
@@ -1,225 +0,0 @@
1
- """Dead-conditional-branch check for a truthiness test on an always-non-empty split."""
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
- _walk_skipping_nested_function_defs,
20
- is_test_file,
21
- )
22
-
23
- from hooks_constants.code_rules_enforcer_constants import ( # noqa: E402
24
- ALL_ALWAYS_NONEMPTY_SPLIT_METHOD_NAMES,
25
- DEAD_SPLIT_BRANCH_MESSAGE_SUFFIX,
26
- MAX_DEAD_SPLIT_BRANCH_ISSUES,
27
- )
28
-
29
-
30
- def _is_separator_split_call(call_node: ast.Call) -> bool:
31
- """Return True for an ``x.split(sep)`` / ``x.rsplit(sep)`` with a non-empty separator.
32
-
33
- ``str.split`` and ``bytes.split`` return a list holding at least one element
34
- whenever a separator argument is supplied, so the result is always truthy.
35
- Requiring a non-empty string or bytes literal separator keeps the guarantee
36
- airtight: ``split()`` with no argument can return an empty list, and an
37
- empty separator raises at runtime. The receiver is restricted to a bare
38
- ``Name`` or a string/bytes literal so an intermediate attribute chain such
39
- as a pandas ``s.str.split(",")`` — whose result is a Series, not a list —
40
- is not treated as an always-non-empty split.
41
-
42
- Args:
43
- call_node: The call expression on the right-hand side of an assignment.
44
-
45
- Returns:
46
- True when the call is a separator-bearing split that never returns an
47
- empty list.
48
- """
49
- function_node = call_node.func
50
- if not isinstance(function_node, ast.Attribute):
51
- return False
52
- if function_node.attr not in ALL_ALWAYS_NONEMPTY_SPLIT_METHOD_NAMES:
53
- return False
54
- receiver_node = function_node.value
55
- if isinstance(receiver_node, ast.Constant):
56
- if not isinstance(receiver_node.value, (str, bytes)):
57
- return False
58
- elif not isinstance(receiver_node, ast.Name):
59
- return False
60
- if not call_node.args:
61
- return False
62
- first_argument = call_node.args[0]
63
- if not isinstance(first_argument, ast.Constant):
64
- return False
65
- separator_value = first_argument.value
66
- if not isinstance(separator_value, (str, bytes)):
67
- return False
68
- return len(separator_value) > 0
69
-
70
-
71
- def _separator_split_target_names(
72
- function_node: ast.FunctionDef | ast.AsyncFunctionDef,
73
- ) -> set[str]:
74
- """Return names bound exactly once in the function from a separator split.
75
-
76
- A name qualifies when its single binding in the function body is an
77
- assignment whose value is a separator-bearing ``split`` / ``rsplit`` call,
78
- so the bound list is always truthy. A name bound more than once, or also a
79
- parameter, is excluded because a later rebinding could make it falsy.
80
-
81
- Args:
82
- function_node: The function whose body is inspected.
83
-
84
- Returns:
85
- The set of always-truthy split-result names safe to reason about.
86
- """
87
- all_store_counts: dict[str, int] = {}
88
- all_split_names: set[str] = set()
89
- for each_statement in function_node.body:
90
- for each_node in _walk_skipping_nested_function_defs(each_statement):
91
- if isinstance(each_node, ast.Name) and isinstance(each_node.ctx, ast.Store):
92
- all_store_counts[each_node.id] = (
93
- all_store_counts.get(each_node.id, 0) + 1
94
- )
95
- if not isinstance(each_node, ast.Assign):
96
- continue
97
- if len(each_node.targets) != 1:
98
- continue
99
- assignment_target = each_node.targets[0]
100
- if not isinstance(assignment_target, ast.Name):
101
- continue
102
- if isinstance(each_node.value, ast.Call) and _is_separator_split_call(
103
- each_node.value
104
- ):
105
- all_split_names.add(assignment_target.id)
106
- all_parameter_names = {
107
- each_argument.arg
108
- for each_argument in _collect_annotated_arguments(function_node)
109
- }
110
- return {
111
- each_name
112
- for each_name in all_split_names
113
- if all_store_counts.get(each_name, 0) == 1
114
- and each_name not in all_parameter_names
115
- }
116
-
117
-
118
- def _truthiness_target(
119
- test_node: ast.expr, all_candidate_names: set[str]
120
- ) -> str | None:
121
- """Return the candidate name a bare ``Name`` truthiness test reads, else None."""
122
- if isinstance(test_node, ast.Name) and test_node.id in all_candidate_names:
123
- return test_node.id
124
- return None
125
-
126
-
127
- def _negated_truthiness_target(
128
- test_node: ast.expr, all_candidate_names: set[str]
129
- ) -> str | None:
130
- """Return the candidate name a ``not Name`` test reads, else None."""
131
- if not isinstance(test_node, ast.UnaryOp) or not isinstance(test_node.op, ast.Not):
132
- return None
133
- operand = test_node.operand
134
- if isinstance(operand, ast.Name) and operand.id in all_candidate_names:
135
- return operand.id
136
- return None
137
-
138
-
139
- def _branch_finding_for_node(
140
- node: ast.AST, all_candidate_names: set[str]
141
- ) -> tuple[int, str] | None:
142
- """Return ``(line, name)`` when a node carries a dead branch, else None.
143
-
144
- A conditional expression always carries an else arm, so any truthiness test
145
- on a candidate name is a dead branch. An ``if`` statement only carries a
146
- dead else when it has one, while an ``if not name`` statement always has a
147
- dead body.
148
- """
149
- if isinstance(node, ast.IfExp):
150
- truthy_name = _truthiness_target(node.test, all_candidate_names)
151
- if truthy_name is not None:
152
- return node.lineno, truthy_name
153
- return _negated_branch_finding(node.test, node.lineno, all_candidate_names)
154
- if isinstance(node, ast.If):
155
- truthy_name = _truthiness_target(node.test, all_candidate_names)
156
- if truthy_name is not None and node.orelse:
157
- return node.lineno, truthy_name
158
- return _negated_branch_finding(node.test, node.lineno, all_candidate_names)
159
- return None
160
-
161
-
162
- def _negated_branch_finding(
163
- test_node: ast.expr, line_number: int, all_candidate_names: set[str]
164
- ) -> tuple[int, str] | None:
165
- negated_name = _negated_truthiness_target(test_node, all_candidate_names)
166
- if negated_name is not None:
167
- return line_number, negated_name
168
- return None
169
-
170
-
171
- def _dead_branch_findings(
172
- function_node: ast.FunctionDef | ast.AsyncFunctionDef,
173
- all_candidate_names: set[str],
174
- ) -> list[tuple[int, str]]:
175
- """Return every ``(line, name)`` dead-branch finding in the function body."""
176
- all_findings: list[tuple[int, str]] = []
177
- for each_statement in function_node.body:
178
- for each_node in _walk_skipping_nested_function_defs(each_statement):
179
- finding = _branch_finding_for_node(each_node, all_candidate_names)
180
- if finding is not None:
181
- all_findings.append(finding)
182
- return all_findings
183
-
184
-
185
- def check_dead_split_truthiness_branch(content: str, file_path: str) -> list[str]:
186
- """Flag a conditional whose branch is unreachable after a separator split.
187
-
188
- ``str.split(sep)`` and ``bytes.split(sep)`` with a separator always return a
189
- list holding at least one element, so a value bound from such a call is
190
- always truthy. A conditional that tests that value's truthiness carries a
191
- dead branch: ``parts[0] if parts else fallback`` never reaches ``fallback``,
192
- and ``if not parts:`` never runs its body. Both the conditional-expression
193
- form and the ``if`` / ``if not`` statement form are reported, scoped to a
194
- value bound exactly once in the function from a separator split. Config
195
- files and test files are exempt.
196
-
197
- Args:
198
- content: The source text to inspect.
199
- file_path: The path the source will be written to, used for exemptions.
200
-
201
- Returns:
202
- One issue per dead conditional branch found, capped at the module limit.
203
- """
204
- if is_test_file(file_path) or is_config_file(file_path):
205
- return []
206
- try:
207
- tree = ast.parse(content)
208
- except SyntaxError:
209
- return []
210
- issues: list[str] = []
211
- for each_function_node in ast.walk(tree):
212
- if not isinstance(each_function_node, (ast.FunctionDef, ast.AsyncFunctionDef)):
213
- continue
214
- all_candidate_names = _separator_split_target_names(each_function_node)
215
- if not all_candidate_names:
216
- continue
217
- for each_line, each_name in _dead_branch_findings(
218
- each_function_node, all_candidate_names
219
- ):
220
- issues.append(
221
- f"Line {each_line}: {each_name!r} {DEAD_SPLIT_BRANCH_MESSAGE_SUFFIX}"
222
- )
223
- if len(issues) >= MAX_DEAD_SPLIT_BRANCH_ISSUES:
224
- return issues
225
- return issues
@@ -1,295 +0,0 @@
1
- """Incomplete-mock test-quality check and its scope-shadowing helpers."""
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_shared import ( # noqa: E402
15
- is_test_file,
16
- )
17
-
18
- from hooks_constants.code_rules_enforcer_constants import ( # noqa: E402
19
- ALL_BUILTIN_DICT_METHOD_NAMES,
20
- )
21
-
22
-
23
- def _collect_mock_dict_keys(assign_value: ast.expr) -> set[str] | None:
24
- """Return the string key set for a dict literal, or None if not a dict literal."""
25
- if not isinstance(assign_value, ast.Dict):
26
- return None
27
- key_names: set[str] = set()
28
- for each_key in assign_value.keys:
29
- if isinstance(each_key, ast.Constant) and isinstance(each_key.value, str):
30
- key_names.add(each_key.value)
31
- return key_names
32
-
33
-
34
- def _target_binds_name(target_node: ast.AST, variable_name: str) -> bool:
35
- """Return True when an assignment target binds variable_name.
36
-
37
- Handles the recursive assignment target shapes Python permits:
38
- a bare ``Name``, a ``Tuple`` or ``List`` of targets (including
39
- nested ones), and a ``Starred`` wrapper around any of the above.
40
- """
41
- if isinstance(target_node, ast.Name):
42
- return target_node.id == variable_name
43
- if isinstance(target_node, (ast.Tuple, ast.List)):
44
- return any(_target_binds_name(each_element, variable_name) for each_element in target_node.elts)
45
- if isinstance(target_node, ast.Starred):
46
- return _target_binds_name(target_node.value, variable_name)
47
- return False
48
-
49
-
50
- def _function_arguments_bind_name(
51
- arguments_node: ast.arguments,
52
- variable_name: str,
53
- ) -> bool:
54
- """Return True when any parameter slot declares variable_name."""
55
- all_positional_arguments = list(arguments_node.posonlyargs) + list(arguments_node.args)
56
- for each_argument in all_positional_arguments + list(arguments_node.kwonlyargs):
57
- if each_argument.arg == variable_name:
58
- return True
59
- if arguments_node.vararg is not None and arguments_node.vararg.arg == variable_name:
60
- return True
61
- if arguments_node.kwarg is not None and arguments_node.kwarg.arg == variable_name:
62
- return True
63
- return False
64
-
65
-
66
- def _node_binds_name(node: ast.AST, variable_name: str) -> bool:
67
- """Return True when a single AST node binds variable_name in its enclosing scope."""
68
- if isinstance(node, ast.Assign):
69
- return any(_target_binds_name(each_target, variable_name) for each_target in node.targets)
70
- if isinstance(node, ast.AnnAssign):
71
- return _target_binds_name(node.target, variable_name)
72
- if isinstance(node, ast.AugAssign):
73
- return _target_binds_name(node.target, variable_name)
74
- if isinstance(node, (ast.For, ast.AsyncFor)):
75
- return _target_binds_name(node.target, variable_name)
76
- if isinstance(node, (ast.With, ast.AsyncWith)):
77
- for each_item in node.items:
78
- optional_target = each_item.optional_vars
79
- if optional_target is not None and _target_binds_name(optional_target, variable_name):
80
- return True
81
- return False
82
- if isinstance(node, ast.ExceptHandler):
83
- return node.name == variable_name
84
- if isinstance(node, ast.NamedExpr):
85
- return _target_binds_name(node.target, variable_name)
86
- if isinstance(node, (ast.Import, ast.ImportFrom)):
87
- for each_alias in node.names:
88
- bound_name = each_alias.asname if each_alias.asname is not None else each_alias.name.split(".")[0]
89
- if bound_name == variable_name:
90
- return True
91
- return False
92
- if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)):
93
- return node.name == variable_name
94
- return False
95
-
96
-
97
- def _body_binds_name_recursively(all_body_statements: list[ast.stmt], variable_name: str) -> bool:
98
- """Return True when any node reachable within all_body_statements binds variable_name.
99
-
100
- Walks the body using a stack, descending into control-flow constructs
101
- (if/for/while/try/with) but treating nested function, async-function,
102
- class, and lambda definitions as opaque: their bodies belong to a
103
- different scope and do not affect bindings in the enclosing one.
104
- Function/class definitions themselves still bind their own name in
105
- the enclosing scope, which is handled by _node_binds_name.
106
- """
107
- nodes_to_visit: list[ast.AST] = list(all_body_statements)
108
- while nodes_to_visit:
109
- current_node = nodes_to_visit.pop()
110
- if _node_binds_name(current_node, variable_name):
111
- return True
112
- if isinstance(current_node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef, ast.Lambda)):
113
- continue
114
- nodes_to_visit.extend(ast.iter_child_nodes(current_node))
115
- return False
116
-
117
-
118
- def _scope_shadows_name(
119
- scope_node: ast.FunctionDef | ast.AsyncFunctionDef | ast.ClassDef,
120
- variable_name: str,
121
- ) -> bool:
122
- """Return True when scope_node locally binds variable_name.
123
-
124
- Detects every binding form Python treats as a local assignment:
125
- plain ``Assign``, annotated ``AnnAssign``, augmented ``AugAssign``,
126
- ``for`` targets, ``with`` as-targets, ``except`` handler names,
127
- walrus ``NamedExpr`` targets, ``import`` and ``from`` bindings
128
- (base name or ``as`` alias), nested function/class definitions
129
- (whose own name binds locally), and function parameters for
130
- ``FunctionDef`` / ``AsyncFunctionDef`` scopes. Bindings are
131
- detected at any nesting depth inside control-flow constructs;
132
- nested function, async-function, class, and lambda bodies are
133
- treated as opaque because their contents live in a different scope.
134
- """
135
- if isinstance(scope_node, (ast.FunctionDef, ast.AsyncFunctionDef)):
136
- if _function_arguments_bind_name(scope_node.args, variable_name):
137
- return True
138
- return _body_binds_name_recursively(list(scope_node.body), variable_name)
139
-
140
-
141
- def _walk_scope_skipping_shadowed(
142
- scope_node: ast.AST,
143
- variable_name: str,
144
- ) -> list[ast.AST]:
145
- """Walk all nodes in a scope, skipping nested function/class bodies that shadow variable_name."""
146
- collected: list[ast.AST] = []
147
- nodes_to_visit: list[ast.AST] = [scope_node]
148
- while nodes_to_visit:
149
- current = nodes_to_visit.pop()
150
- collected.append(current)
151
- for each_child in ast.iter_child_nodes(current):
152
- if (
153
- isinstance(each_child, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef))
154
- and each_child is not scope_node
155
- and _scope_shadows_name(each_child, variable_name)
156
- ):
157
- continue
158
- nodes_to_visit.append(each_child)
159
- return collected
160
-
161
-
162
- def _collect_mock_field_accesses_in_scope(
163
- scope_node: ast.AST,
164
- mock_name: str,
165
- ) -> list[tuple[str, int]]:
166
- """Return (field_name, line_number) for attribute or subscript accesses on mock_name within a scope.
167
-
168
- Skips nested function/class bodies that locally redefine the same mock
169
- variable to avoid false positives from name shadowing.
170
- """
171
- accesses: list[tuple[str, int]] = []
172
- for each_node in _walk_scope_skipping_shadowed(scope_node, mock_name):
173
- if isinstance(each_node, ast.Attribute):
174
- if isinstance(each_node.value, ast.Name) and each_node.value.id == mock_name:
175
- if isinstance(each_node.ctx, ast.Load):
176
- if each_node.attr in ALL_BUILTIN_DICT_METHOD_NAMES:
177
- continue
178
- accesses.append((each_node.attr, each_node.lineno))
179
- elif isinstance(each_node, ast.Subscript):
180
- if isinstance(each_node.value, ast.Name) and each_node.value.id == mock_name:
181
- if isinstance(each_node.ctx, ast.Load):
182
- slice_node = each_node.slice
183
- if isinstance(slice_node, ast.Constant) and isinstance(slice_node.value, str):
184
- accesses.append((slice_node.value, each_node.lineno))
185
- return accesses
186
-
187
-
188
- def _collect_mock_attribute_assignments_in_scope(
189
- scope_node: ast.AST,
190
- mock_name: str,
191
- ) -> set[str]:
192
- """Return field names assigned on a mock variable within a scope.
193
-
194
- Collects both attribute assignments (mock_x.field = ...) and subscript
195
- assignments with constant string keys (mock_x['field'] = ...).
196
-
197
- Skips nested function/class bodies that locally redefine the same mock
198
- variable, mirroring _collect_mock_field_accesses_in_scope so an outer
199
- mock's known-fields set cannot absorb assignments made on a shadowed
200
- inner mock of the same name.
201
- """
202
- assigned_fields: set[str] = set()
203
- for each_node in _walk_scope_skipping_shadowed(scope_node, mock_name):
204
- if not isinstance(each_node, ast.Assign):
205
- continue
206
- for each_target in each_node.targets:
207
- if (
208
- isinstance(each_target, ast.Attribute)
209
- and isinstance(each_target.value, ast.Name)
210
- and each_target.value.id == mock_name
211
- ):
212
- assigned_fields.add(each_target.attr)
213
- elif (
214
- isinstance(each_target, ast.Subscript)
215
- and isinstance(each_target.value, ast.Name)
216
- and each_target.value.id == mock_name
217
- and isinstance(each_target.slice, ast.Constant)
218
- and isinstance(each_target.slice.value, str)
219
- ):
220
- assigned_fields.add(each_target.slice.value)
221
- return assigned_fields
222
-
223
-
224
- def _collect_scoped_mock_definitions(
225
- module_tree: ast.Module,
226
- ) -> list[tuple[int, str, set[str], int, ast.AST]]:
227
- """Return (scope_id, mock_name, declared_keys, definition_line, scope_node) for each mock.
228
-
229
- Keyed by (scope_node id, variable_name) so the same mock name in two different
230
- test functions is tracked independently. Scope is the enclosing function node,
231
- or the module node for module-level assignments.
232
- """
233
- scope_definitions: list[tuple[int, str, set[str], int, ast.AST]] = []
234
- for each_scope in ast.walk(module_tree):
235
- if not isinstance(each_scope, (ast.FunctionDef, ast.AsyncFunctionDef, ast.Module)):
236
- continue
237
- scope_body = each_scope.body
238
- for each_stmt in scope_body:
239
- if not isinstance(each_stmt, ast.Assign):
240
- continue
241
- for each_target in each_stmt.targets:
242
- if not isinstance(each_target, ast.Name):
243
- continue
244
- target_name = each_target.id
245
- if not (target_name.startswith("mock_") or target_name.startswith("MOCK_")):
246
- continue
247
- mock_keys = _collect_mock_dict_keys(each_stmt.value)
248
- if mock_keys is not None:
249
- scope_definitions.append(
250
- (id(each_scope), target_name, mock_keys, each_stmt.lineno, each_scope)
251
- )
252
- elif isinstance(each_stmt.value, ast.Call):
253
- scope_definitions.append(
254
- (id(each_scope), target_name, set(), each_stmt.lineno, each_scope)
255
- )
256
- return scope_definitions
257
-
258
-
259
- def check_incomplete_mocks(content: str, file_path: str) -> None:
260
- """Emit stderr advisories when a mock dict/object is missing fields that are accessed.
261
-
262
- Scans test files for variables named mock_* or MOCK_* whose value is a dict
263
- literal. Each mock definition is keyed by (scope_node_id, variable_name) so
264
- the same name in different test functions is checked independently. Advisories
265
- are deduplicated per (mock_name, field_name) pair within each scope.
266
-
267
- This is advisory-only (no return value, no blocking).
268
- """
269
- if not is_test_file(file_path):
270
- return
271
-
272
- try:
273
- module_tree = ast.parse(content)
274
- except SyntaxError:
275
- return
276
-
277
- all_scoped_definitions = _collect_scoped_mock_definitions(module_tree)
278
-
279
- for each_scope_id, each_mock_name, each_declared_keys, each_definition_line, each_scope_node in all_scoped_definitions: # noqa: B007
280
- assigned_attributes = _collect_mock_attribute_assignments_in_scope(each_scope_node, each_mock_name)
281
- all_known_fields = each_declared_keys | assigned_attributes
282
- field_accesses = _collect_mock_field_accesses_in_scope(each_scope_node, each_mock_name)
283
- already_advised: set[tuple[str, str]] = set()
284
- for each_accessed_field, each_access_line in field_accesses:
285
- if each_accessed_field in all_known_fields:
286
- continue
287
- advisory_key = (each_mock_name, each_accessed_field)
288
- if advisory_key in already_advised:
289
- continue
290
- already_advised.add(advisory_key)
291
- print(
292
- f"[CODE_RULES advisory] Line {each_definition_line}: mock {each_mock_name}"
293
- f" missing field {each_accessed_field} accessed at line {each_access_line}",
294
- file=sys.stderr,
295
- )
@@ -1,151 +0,0 @@
1
- """Lexical scope-binding collectors used by the unused-import check."""
2
-
3
- import ast
4
-
5
- from hooks_constants.stuttering_import_binding_constants import (
6
- WILDCARD_IMPORT_SENTINEL,
7
- )
8
-
9
-
10
- def _attribute_root_name_if_loaded(attribute_node: ast.Attribute) -> ast.Name | None:
11
- current: ast.expr = attribute_node
12
- while isinstance(current, ast.Attribute):
13
- current = current.value
14
- if isinstance(current, ast.Name) and isinstance(current.ctx, ast.Load):
15
- return current
16
- return None
17
-
18
-
19
- class _ScopeBindingCollector(ast.NodeVisitor):
20
- def __init__(self) -> None:
21
- self.binding_names: set[str] = set()
22
- self.global_names: set[str] = set()
23
-
24
- def collect_arguments(self, arguments: ast.arguments) -> None:
25
- for each_argument in (
26
- arguments.posonlyargs
27
- + arguments.args
28
- + arguments.kwonlyargs
29
- ):
30
- self.binding_names.add(each_argument.arg)
31
- if arguments.vararg is not None:
32
- self.binding_names.add(arguments.vararg.arg)
33
- if arguments.kwarg is not None:
34
- self.binding_names.add(arguments.kwarg.arg)
35
-
36
- def visit_Global(self, node: ast.Global) -> None:
37
- self.global_names.update(node.names)
38
-
39
- def visit_Nonlocal(self, node: ast.Nonlocal) -> None:
40
- self.binding_names.update(node.names)
41
-
42
- def visit_FunctionDef(self, node: ast.FunctionDef) -> None:
43
- self.binding_names.add(node.name)
44
-
45
- def visit_AsyncFunctionDef(self, node: ast.AsyncFunctionDef) -> None:
46
- self.binding_names.add(node.name)
47
-
48
- def visit_ClassDef(self, node: ast.ClassDef) -> None:
49
- self.binding_names.add(node.name)
50
-
51
- def visit_Lambda(self, node: ast.Lambda) -> None:
52
- return None
53
-
54
- def visit_Name(self, node: ast.Name) -> None:
55
- if isinstance(node.ctx, ast.Store):
56
- self.binding_names.add(node.id)
57
-
58
- def visit_Import(self, node: ast.Import) -> None:
59
- for each_alias in node.names:
60
- self.binding_names.add(each_alias.asname or each_alias.name.split(".")[0])
61
-
62
- def visit_ImportFrom(self, node: ast.ImportFrom) -> None:
63
- for each_alias in node.names:
64
- if each_alias.name != WILDCARD_IMPORT_SENTINEL:
65
- self.binding_names.add(each_alias.asname or each_alias.name)
66
-
67
- def visit_ListComp(self, node: ast.ListComp) -> None:
68
- return None
69
-
70
- def visit_SetComp(self, node: ast.SetComp) -> None:
71
- return None
72
-
73
- def visit_DictComp(self, node: ast.DictComp) -> None:
74
- return None
75
-
76
- def visit_GeneratorExp(self, node: ast.GeneratorExp) -> None:
77
- return None
78
-
79
- def visit_ExceptHandler(self, node: ast.ExceptHandler) -> None:
80
- if node.name is not None:
81
- self.binding_names.add(node.name)
82
- self.generic_visit(node)
83
-
84
-
85
- def _scope_binding_names(scope_node: ast.AST) -> tuple[set[str], set[str]]:
86
- collector = _ScopeBindingCollector()
87
- if isinstance(scope_node, (ast.FunctionDef, ast.AsyncFunctionDef)):
88
- collector.collect_arguments(scope_node.args)
89
- for each_statement in scope_node.body:
90
- collector.visit(each_statement)
91
- elif isinstance(scope_node, ast.Lambda):
92
- collector.collect_arguments(scope_node.args)
93
- collector.visit(scope_node.body)
94
- elif isinstance(scope_node, ast.ClassDef):
95
- for each_statement in scope_node.body:
96
- collector.visit(each_statement)
97
- return collector.binding_names, collector.global_names
98
-
99
-
100
- def _load_name_is_shadowed(
101
- load_node: ast.AST,
102
- name: str,
103
- parent_by_node_id: dict[int, ast.AST],
104
- ) -> bool:
105
- current = parent_by_node_id.get(id(load_node))
106
- has_passed_function_scope = False
107
- while current is not None:
108
- if isinstance(current, (ast.FunctionDef, ast.AsyncFunctionDef, ast.Lambda)):
109
- has_passed_function_scope = True
110
- binding_names, global_names = _scope_binding_names(current)
111
- if name in global_names:
112
- return False
113
- if name in binding_names:
114
- return True
115
- elif isinstance(current, ast.ClassDef) and not has_passed_function_scope:
116
- # Class body bindings are order-dependent (name resolution is
117
- # dynamic, unlike function locals). A load before an assignment
118
- # still resolves to the module-level name, so conservatively
119
- # skip class-body shadow detection to avoid false positives.
120
- pass
121
- current = parent_by_node_id.get(id(current))
122
- return False
123
-
124
-
125
- def _names_from_annotation_text(annotation_text: str) -> set[str]:
126
- try:
127
- annotation_tree = ast.parse(annotation_text, mode="eval")
128
- except SyntaxError:
129
- return set()
130
- referenced_names: set[str] = set()
131
- for each_node in ast.walk(annotation_tree):
132
- if isinstance(each_node, ast.Name):
133
- referenced_names.add(each_node.id)
134
- elif isinstance(each_node, ast.Attribute):
135
- root_name = _attribute_root_name_if_loaded(each_node)
136
- if root_name is not None:
137
- referenced_names.add(root_name.id)
138
- return referenced_names
139
-
140
-
141
- def _collect_string_annotation_names(tree: ast.Module) -> set[str]:
142
- referenced_names: set[str] = set()
143
- for each_node in ast.walk(tree):
144
- annotation = None
145
- if isinstance(each_node, ast.arg):
146
- annotation = each_node.annotation
147
- elif isinstance(each_node, (ast.AnnAssign, ast.FunctionDef, ast.AsyncFunctionDef)):
148
- annotation = each_node.annotation if isinstance(each_node, ast.AnnAssign) else each_node.returns
149
- if isinstance(annotation, ast.Constant) and isinstance(annotation.value, str):
150
- referenced_names.update(_names_from_annotation_text(annotation.value))
151
- return referenced_names