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,348 +0,0 @@
1
- """Dead dataclass-field check: a @dataclass field assigned but never read in the same file."""
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_migration_file,
16
- is_test_file,
17
- )
18
-
19
- from hooks_constants.dead_dataclass_field_constants import ( # noqa: E402
20
- ALL_DATACLASS_DECORATOR_NAMES,
21
- ALL_WHOLE_INSTANCE_STRINGIFY_NAMES,
22
- ATTRGETTER_FUNCTION_NAME,
23
- CLASSVAR_ANNOTATION_NAME,
24
- DEAD_DATACLASS_FIELD_GUIDANCE,
25
- GETATTR_FUNCTION_NAME,
26
- GETATTR_NAME_ARGUMENT_MINIMUM,
27
- MAX_DEAD_DATACLASS_FIELD_ISSUES,
28
- ALL_REFLECTIVE_FIELD_CONSUMER_NAMES,
29
- WHOLE_INSTANCE_DICT_ATTRIBUTE_NAME,
30
- )
31
-
32
-
33
- def _decorator_calls_dataclass(decorator_node: ast.expr) -> bool:
34
- """Return whether a decorator expression applies @dataclass (bare or called)."""
35
- target_node = (
36
- decorator_node.func if isinstance(decorator_node, ast.Call) else decorator_node
37
- )
38
- if isinstance(target_node, ast.Name):
39
- return target_node.id in ALL_DATACLASS_DECORATOR_NAMES
40
- if isinstance(target_node, ast.Attribute):
41
- return target_node.attr in ALL_DATACLASS_DECORATOR_NAMES
42
- return False
43
-
44
-
45
- def _is_dataclass(class_node: ast.ClassDef) -> bool:
46
- return any(
47
- _decorator_calls_dataclass(each_decorator)
48
- for each_decorator in class_node.decorator_list
49
- )
50
-
51
-
52
- def _annotation_is_classvar(annotation_node: ast.expr | None) -> bool:
53
- if annotation_node is None:
54
- return False
55
- if isinstance(annotation_node, ast.Name):
56
- return annotation_node.id == CLASSVAR_ANNOTATION_NAME
57
- if isinstance(annotation_node, ast.Attribute):
58
- return annotation_node.attr == CLASSVAR_ANNOTATION_NAME
59
- if isinstance(annotation_node, ast.Subscript):
60
- return _annotation_is_classvar(annotation_node.value)
61
- return False
62
-
63
-
64
- def _dataclass_field_definitions(class_node: ast.ClassDef) -> list[tuple[str, int]]:
65
- """Return (field_name, line) for each instance field declared in a dataclass body."""
66
- fields: list[tuple[str, int]] = []
67
- for each_statement in class_node.body:
68
- if not isinstance(each_statement, ast.AnnAssign):
69
- continue
70
- if not isinstance(each_statement.target, ast.Name):
71
- continue
72
- if _annotation_is_classvar(each_statement.annotation):
73
- continue
74
- fields.append((each_statement.target.id, each_statement.lineno))
75
- return fields
76
-
77
-
78
- def _string_constant_literal(node: ast.expr) -> str | None:
79
- if isinstance(node, ast.Constant) and isinstance(node.value, str):
80
- return node.value
81
- return None
82
-
83
-
84
- def _dynamic_access_names(tree: ast.Module) -> tuple[set[str], bool]:
85
- """Return literal attribute-name reads and whether the check must be suppressed.
86
-
87
- Walks every ``getattr(obj, "name")`` and ``operator.attrgetter("a", "b")``
88
- call, contributing each literal string name as a read attribute name —
89
- ``getattr`` names its attribute in the second positional argument while
90
- ``attrgetter`` names one attribute per positional argument. A non-literal
91
- name argument, or any reflective whole-instance consumer (``asdict``,
92
- ``astuple``, ``fields``, ``replace``, ``vars``) that reads every field at
93
- once, means a field cannot be proven unread, so the boolean signals the
94
- caller to suppress the check for the whole file.
95
- """
96
- literal_names: set[str] = set()
97
- should_suppress_check = False
98
- for each_node in ast.walk(tree):
99
- if not isinstance(each_node, ast.Call):
100
- continue
101
- function_node = each_node.func
102
- function_name = None
103
- if isinstance(function_node, ast.Name):
104
- function_name = function_node.id
105
- elif isinstance(function_node, ast.Attribute):
106
- function_name = function_node.attr
107
- if function_name in ALL_REFLECTIVE_FIELD_CONSUMER_NAMES:
108
- should_suppress_check = True
109
- continue
110
- if function_name not in {GETATTR_FUNCTION_NAME, ATTRGETTER_FUNCTION_NAME}:
111
- continue
112
- string_arguments = [
113
- argument
114
- for argument in each_node.args
115
- if not isinstance(argument, ast.Starred)
116
- ]
117
- if function_name == GETATTR_FUNCTION_NAME:
118
- name_arguments = (
119
- [string_arguments[1]]
120
- if len(string_arguments) >= GETATTR_NAME_ARGUMENT_MINIMUM
121
- else []
122
- )
123
- else:
124
- name_arguments = string_arguments
125
- if not name_arguments:
126
- should_suppress_check = True
127
- continue
128
- for each_name_argument in name_arguments:
129
- literal_name = _string_constant_literal(each_name_argument)
130
- if literal_name is None:
131
- should_suppress_check = True
132
- else:
133
- literal_names.add(literal_name)
134
- return literal_names, should_suppress_check
135
-
136
-
137
- def _augmented_assignment_attribute_names(tree: ast.Module) -> set[str]:
138
- """Return attribute names that an augmented assignment reads before writing.
139
-
140
- ``obj.field += value`` parses to an ``ast.Attribute`` target in Store
141
- context, yet ``+=`` reads the current attribute value before storing the
142
- result, so the target attribute counts as a read.
143
- """
144
- augmented_read_names: set[str] = set()
145
- for each_node in ast.walk(tree):
146
- if not isinstance(each_node, ast.AugAssign):
147
- continue
148
- if isinstance(each_node.target, ast.Attribute):
149
- augmented_read_names.add(each_node.target.attr)
150
- return augmented_read_names
151
-
152
-
153
- def _attribute_read_names(tree: ast.Module) -> tuple[set[str], bool]:
154
- """Return literal attribute-name reads and whether the check must be suppressed.
155
-
156
- Walks every attribute read (Load context) and every augmented-assignment
157
- target in the module, contributing each attribute name as a read name —
158
- ``obj.field += value`` reads ``field`` before writing it. A read of
159
- ``__dict__`` consumes every field of an instance at once, so it cannot prove
160
- any single field unread and the boolean signals the caller to suppress the
161
- check for the whole file.
162
- """
163
- read_names: set[str] = _augmented_assignment_attribute_names(tree)
164
- should_suppress_check = False
165
- for each_node in ast.walk(tree):
166
- if not isinstance(each_node, ast.Attribute) or not isinstance(
167
- each_node.ctx, ast.Load
168
- ):
169
- continue
170
- if each_node.attr == WHOLE_INSTANCE_DICT_ATTRIBUTE_NAME:
171
- should_suppress_check = True
172
- continue
173
- read_names.add(each_node.attr)
174
- return read_names, should_suppress_check
175
-
176
-
177
- def _match_pattern_attribute_names(tree: ast.Module) -> set[str]:
178
- """Return field names that a class pattern reads in a ``match`` statement.
179
-
180
- A class pattern ``case Row(url=found):`` reads the ``url`` field through
181
- ``ast.MatchClass.kwd_attrs``, so each keyword-pattern attribute name counts
182
- as a read name even though it never appears as an attribute access.
183
- """
184
- matched_names: set[str] = set()
185
- for each_node in ast.walk(tree):
186
- if isinstance(each_node, ast.MatchClass):
187
- matched_names.update(each_node.kwd_attrs)
188
- return matched_names
189
-
190
-
191
- def _exported_names(tree: ast.Module) -> set[str]:
192
- """Return names listed in a module-level ``__all__`` literal."""
193
- exported: set[str] = set()
194
- for each_node in tree.body:
195
- if not isinstance(each_node, ast.Assign):
196
- continue
197
- targets_all = any(
198
- isinstance(each_target, ast.Name) and each_target.id == "__all__"
199
- for each_target in each_node.targets
200
- )
201
- if not targets_all:
202
- continue
203
- if isinstance(each_node.value, (ast.List, ast.Tuple, ast.Set)):
204
- for each_element in each_node.value.elts:
205
- literal_name = _string_constant_literal(each_element)
206
- if literal_name is not None:
207
- exported.add(literal_name)
208
- return exported
209
-
210
-
211
- def _constructed_class_names(tree: ast.Module) -> set[str]:
212
- """Return names of classes instantiated by a direct call anywhere in the module."""
213
- constructed: set[str] = set()
214
- for each_node in ast.walk(tree):
215
- if isinstance(each_node, ast.Call) and isinstance(each_node.func, ast.Name):
216
- constructed.add(each_node.func.id)
217
- return constructed
218
-
219
-
220
- def _module_level_singleton_class_names(tree: ast.Module) -> set[str]:
221
- """Return class names constructed by a module-level assignment.
222
-
223
- A module-level assignment such as ``CURRENT_OS = CurrentOsConfig()`` binds a
224
- singleton that importer modules read across files, beyond this file's view.
225
- The in-file read scan cannot observe those cross-module reads, so a field on
226
- such a class is never provably dead from this file alone. A construction that
227
- sits inside a function or class body is not collected, so a dataclass built
228
- for local use stays in scope for the check.
229
- """
230
- singleton_class_names: set[str] = set()
231
- for each_statement in tree.body:
232
- if not isinstance(each_statement, (ast.Assign, ast.AnnAssign)):
233
- continue
234
- assigned_value = each_statement.value
235
- if assigned_value is None:
236
- continue
237
- for each_node in ast.walk(assigned_value):
238
- if isinstance(each_node, ast.Call) and isinstance(
239
- each_node.func, ast.Name
240
- ):
241
- singleton_class_names.add(each_node.func.id)
242
- return singleton_class_names
243
-
244
-
245
- def _is_whole_instance_stringify_call(node: ast.AST) -> bool:
246
- """Return whether a call stringifies a whole instance via ``str``/``repr``/``format``."""
247
- if not isinstance(node, ast.Call):
248
- return False
249
- function_node = node.func
250
- if isinstance(function_node, ast.Name):
251
- return function_node.id in ALL_WHOLE_INSTANCE_STRINGIFY_NAMES
252
- if isinstance(function_node, ast.Attribute):
253
- return function_node.attr in ALL_WHOLE_INSTANCE_STRINGIFY_NAMES
254
- return False
255
-
256
-
257
- def _uses_dataclass_dunder_field_reads(tree: ast.Module) -> bool:
258
- """Return whether the file relies on auto-generated dataclass dunders to read fields.
259
-
260
- ``@dataclass`` synthesizes ``__eq__`` (and ``__lt__``/``__hash__`` under
261
- ``order``/``frozen``) plus the always-present ``__repr__``, each of which
262
- reads every field without naming it as an attribute access. Comparing two
263
- instances, placing instances in a set or dict, formatted-string conversion,
264
- or stringifying a whole instance therefore reads fields the static scan
265
- cannot otherwise observe, so the check is suppressed for the whole file.
266
- """
267
- for each_node in ast.walk(tree):
268
- if isinstance(each_node, ast.Compare):
269
- return True
270
- if isinstance(each_node, (ast.Set, ast.SetComp, ast.Dict, ast.DictComp)):
271
- return True
272
- if isinstance(each_node, ast.FormattedValue):
273
- return True
274
- if _is_whole_instance_stringify_call(each_node):
275
- return True
276
- return False
277
-
278
-
279
- def check_dead_dataclass_fields(
280
- content: str, file_path: str, full_file_content: str | None = None
281
- ) -> list[str]:
282
- """Flag a @dataclass field that the same file constructs but never reads.
283
-
284
- A field is dead when its dataclass is instantiated by a call in the file
285
- (so the class is live) but never bound to a module-level singleton (whose
286
- fields importer modules read across files, beyond this file's view), the
287
- field name never appears as an attribute read, an augmented-assignment
288
- target, a class-pattern keyword, or a literal ``getattr``/``attrgetter``
289
- access anywhere in the file, and the file contains no non-literal dynamic
290
- access, reflective whole-instance consumer (``asdict``, ``astuple``,
291
- ``fields``, ``replace``, ``vars``), ``__dict__`` read, or auto-generated
292
- dataclass dunder field read (comparison, set/dict membership, or
293
- whole-instance stringification) that could read it indirectly. Whole-file
294
- analysis runs against ``full_file_content`` when supplied so an Edit
295
- fragment is judged against the reconstructed post-edit file.
296
-
297
- Args:
298
- content: The new content under validation (Edit fragment or whole file).
299
- file_path: The destination path, used for the test/registry exemptions.
300
- full_file_content: The reconstructed post-edit whole-file content for an
301
- Edit, or None for a Write where ``content`` is already the whole file.
302
-
303
- Returns:
304
- One violation message per dead dataclass field, capped at the configured
305
- maximum.
306
- """
307
- if is_test_file(file_path):
308
- return []
309
- if is_migration_file(file_path):
310
- return []
311
- effective_content = content if full_file_content is None else full_file_content
312
- try:
313
- tree = ast.parse(effective_content)
314
- except SyntaxError:
315
- return []
316
- if _uses_dataclass_dunder_field_reads(tree):
317
- return []
318
- dynamic_literal_names, dynamic_access_suppresses_check = _dynamic_access_names(tree)
319
- attribute_read_names, instance_dict_suppresses_check = _attribute_read_names(tree)
320
- if dynamic_access_suppresses_check or instance_dict_suppresses_check:
321
- return []
322
- read_names = (
323
- attribute_read_names
324
- | dynamic_literal_names
325
- | _match_pattern_attribute_names(tree)
326
- | _exported_names(tree)
327
- )
328
- constructed_class_names = _constructed_class_names(tree)
329
- singleton_class_names = _module_level_singleton_class_names(tree)
330
- issues: list[str] = []
331
- for each_node in ast.walk(tree):
332
- if not isinstance(each_node, ast.ClassDef) or not _is_dataclass(each_node):
333
- continue
334
- if each_node.name not in constructed_class_names:
335
- continue
336
- if each_node.name in singleton_class_names:
337
- continue
338
- for each_field_definition in _dataclass_field_definitions(each_node):
339
- field_name, field_line = each_field_definition
340
- if field_name in read_names:
341
- continue
342
- issues.append(
343
- f"Line {field_line}: dataclass field {field_name!r} on {each_node.name}"
344
- f" - {DEAD_DATACLASS_FIELD_GUIDANCE}"
345
- )
346
- if len(issues) >= MAX_DEAD_DATACLASS_FIELD_ISSUES:
347
- return issues
348
- return issues