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.
- package/.agents/agents/AGENTS.md +0 -31
- package/.agents/agents/clean-coder.md +1 -1
- package/.agents/agents/test_agent_frontmatter.py +3 -1
- package/.agents/skills/AGENTS.md +0 -70
- package/.agents/skills/_shared/AGENTS.md +0 -44
- package/.agents/skills/_shared/advisor/AGENTS.md +0 -8
- package/.agents/skills/_shared/pr-loop/AGENTS.md +0 -57
- package/.agents/skills/_shared/pr-loop/prompts/AGENTS.md +0 -8
- package/.agents/skills/_shared/pr-loop/scripts/AGENTS.md +0 -34
- package/.agents/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/AGENTS.md +0 -23
- package/.agents/skills/autoconverge/AGENTS.md +0 -35
- package/.agents/skills/autoconverge/reference/AGENTS.md +0 -15
- package/.agents/skills/autoconverge/workflow/AGENTS.md +0 -25
- package/.agents/skills/autoconverge/workflow/autoconverge_report_constants/AGENTS.md +0 -15
- package/.agents/skills/everything-search/AGENTS.md +0 -16
- package/.agents/skills/fresh-branch/AGENTS.md +0 -13
- package/.agents/skills/grok-spawn/AGENTS.md +0 -27
- package/.agents/skills/orchestrator/.claude/CLAUDE.md +1 -0
- package/.agents/skills/orchestrator/AGENTS.md +1 -0
- package/.agents/skills/orchestrator/SKILL.md +44 -53
- package/.agents/skills/orchestrator/reference/.claude/CLAUDE.md +1 -0
- package/.agents/skills/orchestrator/reference/AGENTS.md +1 -0
- package/.agents/skills/orchestrator/reference/consult-the-orchestrator.md +70 -0
- package/.agents/skills/orchestrator/reference/executor-consult-block.md +62 -0
- package/.agents/skills/orchestrator/reference/host-detect.md +15 -0
- package/.agents/skills/orchestrator/test_orchestrator_skill_contract.py +62 -0
- package/.agents/skills/orchestrator-refresh/SKILL.md +16 -34
- package/.agents/skills/rebase/AGENTS.md +0 -31
- package/.agents/skills/session-log/AGENTS.md +0 -31
- package/.agents/skills/session-tidy/AGENTS.md +0 -35
- package/.agents/skills/skill-builder/AGENTS.md +0 -48
- package/.agents/skills/skill-builder/references/AGENTS.md +0 -24
- package/.agents/skills/skill-builder/templates/AGENTS.md +0 -13
- package/.agents/skills/skill-builder/workflows/AGENTS.md +0 -18
- package/.agents/skills/task-build/AGENTS.md +0 -28
- package/.agents/skills/update/AGENTS.md +0 -37
- package/.agents/skills-archived/AGENTS.md +0 -44
- package/.agents/skills-archived/anthropic-plan/AGENTS.md +0 -33
- package/.agents/skills-archived/anthropic-plan/scripts/AGENTS.md +0 -10
- package/.agents/skills-archived/anthropic-plan/scripts/anthropic_plan_scripts_constants/AGENTS.md +0 -15
- package/.agents/skills-archived/anthropic-plan/templates/AGENTS.md +0 -12
- package/.agents/skills-archived/anthropic-plan/workflow/AGENTS.md +0 -13
- package/.agents/skills-archived/auditing-claude-config/AGENTS.md +0 -20
- package/.agents/skills-archived/bugteam/AGENTS.md +0 -29
- package/.agents/skills-archived/bugteam/reference/AGENTS.md +0 -19
- package/.agents/skills-archived/bugteam/reference/obstacles/AGENTS.md +0 -23
- package/.agents/skills-archived/bugteam/scripts/AGENTS.md +0 -29
- package/.agents/skills-archived/bugteam/scripts/bugteam_scripts_constants/AGENTS.md +0 -17
- package/.agents/skills-archived/codex-review/AGENTS.md +0 -45
- package/.agents/skills-archived/codex-review/reference/AGENTS.md +0 -14
- package/.agents/skills-archived/codex-review/scripts/codex_review_scripts_constants/AGENTS.md +0 -17
- package/.agents/skills-archived/codex-review/test_skill_scaffold.py +13 -3
- package/.agents/skills-archived/copilot-review/AGENTS.md +0 -17
- package/.agents/skills-archived/pr-converge/AGENTS.md +0 -31
- package/.agents/skills-archived/pr-converge/pr_converge_skill_constants/AGENTS.md +0 -25
- package/.agents/skills-archived/pr-converge/reference/AGENTS.md +0 -27
- package/.agents/skills-archived/pr-converge/reference/obstacles/AGENTS.md +0 -22
- package/.agents/skills-archived/pr-converge/scripts/AGENTS.md +0 -45
- package/.agents/skills-archived/pr-converge/scripts/pr_converge_scripts_constants/AGENTS.md +0 -17
- package/.agents/skills-archived/pr-converge/workflows/AGENTS.md +0 -15
- package/.agents/skills-archived/pr-loop-cloud-transport/reference/identity-and-hooks.md +1 -1
- package/.agents/skills-archived/recall/AGENTS.md +0 -29
- package/.agents/skills-archived/remember/AGENTS.md +0 -30
- package/AGENTS.md +0 -112
- package/_shared/AGENTS.md +0 -16
- package/_shared/advisor/AGENTS.md +0 -21
- package/_shared/advisor/advisor-protocol.md +4 -1
- package/_shared/advisor/reference/consult-format.md +4 -2
- package/_shared/pr-loop/AGENTS.md +0 -27
- package/_shared/pr-loop/scripts/AGENTS.md +0 -49
- package/_shared/pr-loop/scripts/code_rules_gate_parts/AGENTS.md +0 -41
- package/_shared/pr-loop/scripts/codex_review_scripts_constants/AGENTS.md +0 -17
- package/_shared/pr-loop/scripts/pr_converge_scripts_constants/AGENTS.md +0 -17
- package/_shared/pr-loop/scripts/pr_converge_skill_constants/AGENTS.md +0 -25
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/AGENTS.md +0 -25
- package/_shared/pr-loop/scripts/tests/AGENTS.md +0 -43
- package/_shared/process-tree/AGENTS.md +0 -40
- package/audit-rubrics/AGENTS.md +0 -42
- package/audit-rubrics/category_rubrics/AGENTS.md +0 -36
- package/audit-rubrics/category_rubrics/category-o-docstring-vs-impl-drift.md +16 -29
- package/audit-rubrics/prompts/AGENTS.md +0 -36
- package/audit-rubrics/prompts/category-o-docstring-vs-impl-drift.md +2 -2
- package/bin/AGENTS.md +0 -118
- package/bin/install.test.mjs +1 -1
- package/commands/AGENTS.md +0 -14
- package/docs/AGENTS.md +0 -31
- package/docs/CODE_RULES.md +3 -3
- package/docs/agent-spawn-protocol.md +1 -1
- package/docs/references/AGENTS.md +0 -16
- package/docs/references/prose-style-enforcement.md +5 -9
- package/hooks/AGENTS.md +0 -27
- package/hooks/advisory/AGENTS.md +0 -15
- package/hooks/blocking/AGENTS.md +0 -122
- package/hooks/blocking/claude_md_orphan_file_blocker_parts/AGENTS.md +0 -27
- package/hooks/blocking/code_rules_docstrings.py +10 -2138
- package/hooks/blocking/code_rules_enforcer.py +0 -121
- package/hooks/blocking/code_rules_imports_logging.py +1 -236
- package/hooks/blocking/code_rules_shared.py +23 -0
- package/hooks/blocking/code_rules_test_layout.py +8 -8
- package/hooks/blocking/config/AGENTS.md +0 -9
- package/hooks/blocking/config/prose_style_enforcement_constants.py +4 -2
- package/hooks/blocking/config/test_prose_style_enforcement_constants.py +5 -1
- package/hooks/blocking/inventory_intent_records/AGENTS.md +0 -25
- package/hooks/blocking/package_inventory_stale_blocker_parts/AGENTS.md +0 -25
- package/hooks/blocking/pii_prevention_blocker_parts/AGENTS.md +0 -23
- package/hooks/blocking/precommit_code_rules_gate.py +8 -43
- package/hooks/blocking/state_description_blocker.py +1 -7
- package/hooks/blocking/tdd_enforcer_parts/AGENTS.md +0 -29
- package/hooks/blocking/test_code_rules_enforcer_cap_meta.py +0 -1
- package/hooks/blocking/test_code_rules_enforcer_dispatch_wiring.py +0 -8
- package/hooks/blocking/test_code_rules_enforcer_module_docstring_roster.py +11 -112
- package/hooks/blocking/test_code_rules_enforcer_narrow_edit.py +0 -1
- package/hooks/blocking/test_code_rules_enforcer_split_entry_1.py +1 -18
- package/hooks/blocking/test_code_rules_shared.py +12 -0
- package/hooks/blocking/test_precommit_code_rules_gate.py +32 -179
- package/hooks/blocking/test_precommit_code_rules_gate_native_owner.py +0 -1
- package/hooks/blocking/test_state_description_blocker.py +6 -4
- package/hooks/blocking/test_stop_dispatcher.py +5 -29
- package/hooks/git-hooks/AGENTS.md +0 -31
- package/hooks/git-hooks/git_hooks_constants/AGENTS.md +0 -20
- package/hooks/hooks_constants/AGENTS.md +0 -104
- package/hooks/hooks_constants/bash_pre_tool_use_dispatcher_constants.py +0 -1
- package/hooks/hooks_constants/code_rules_enforcer_constants.py +3 -0
- package/hooks/hooks_constants/messages.py +0 -2
- package/hooks/hooks_constants/precommit_code_rules_gate_constants.py +3 -17
- package/hooks/hooks_constants/stop_dispatcher_constants.py +0 -2
- package/hooks/hooks_constants/test_bash_pre_tool_use_dispatcher_constants.py +0 -1
- package/hooks/hooks_constants/test_code_rules_enforcer_constants.py +7 -0
- package/hooks/hooks_constants/test_messages.py +5 -3
- package/hooks/hooks_constants/test_stop_dispatcher_constants.py +0 -2
- package/hooks/lifecycle/AGENTS.md +0 -17
- package/hooks/observability/AGENTS.md +0 -19
- package/hooks/session/AGENTS.md +0 -37
- package/hooks/validation/AGENTS.md +0 -19
- package/hooks/validators/AGENTS.md +0 -51
- package/hooks/workflow/AGENTS.md +0 -15
- package/output-styles/AGENTS.md +0 -14
- package/package.json +1 -1
- package/rules/AGENTS.md +0 -58
- package/rules/claims-as-quotes.md +0 -10
- package/rules/code-standards.md +6 -6
- package/rules/explore-thoroughly.md +0 -1
- package/rules/failure-blast-radius.md +0 -8
- package/rules/falsify-before-green.md +0 -8
- package/rules/file-global-constants.md +2 -2
- package/rules/filesystem-search.md +1 -1
- package/rules/git-workflow.md +1 -9
- package/rules/hedging-claims.md +2 -6
- package/rules/long-horizon-autonomy.md +1 -1
- package/rules/measurement-denominators.md +0 -9
- package/rules/research-mode.md +0 -6
- package/rules/verify-before-asking.md +0 -5
- package/rules/verify-runtime-state.md +0 -5
- package/scripts/AGENTS.md +0 -55
- package/scripts/codex_compat_materializer.py +0 -12
- package/scripts/dev_env_scripts_constants/AGENTS.md +0 -20
- package/scripts/sync_to_cursor/AGENTS.md +0 -22
- package/scripts/sync_to_cursor/rules.py +0 -10
- package/scripts/tests/AGENTS.md +0 -34
- package/scripts/tests/test_engine.py +0 -1
- package/scripts/tests/test_rules.py +0 -1
- package/scripts/tests/test_sync_to_cursor.py +0 -1
- package/system-prompts/AGENTS.md +0 -24
- package/system-prompts/software-engineer.xml +3 -3
- package/hooks/blocking/code_rules_dead_argparse_argument.py +0 -554
- package/hooks/blocking/code_rules_dead_config_field.py +0 -568
- package/hooks/blocking/code_rules_dead_dataclass_field.py +0 -348
- package/hooks/blocking/code_rules_dead_module_constant.py +0 -757
- package/hooks/blocking/code_rules_dead_split_branch.py +0 -225
- package/hooks/blocking/code_rules_mock_completeness.py +0 -295
- package/hooks/blocking/code_rules_scope_binding.py +0 -151
- package/hooks/blocking/code_rules_unused_imports.py +0 -197
- package/hooks/blocking/hedging_language_blocker.py +0 -221
- package/hooks/blocking/intent_only_ending_blocker.py +0 -148
- package/hooks/blocking/test_code_rules_enforcer_dead_argparse_argument.py +0 -534
- package/hooks/blocking/test_code_rules_enforcer_dead_config_field.py +0 -846
- package/hooks/blocking/test_code_rules_enforcer_dead_dataclass_field.py +0 -507
- package/hooks/blocking/test_code_rules_enforcer_dead_module_constant.py +0 -679
- package/hooks/blocking/test_code_rules_enforcer_dead_module_constant_alias.py +0 -133
- package/hooks/blocking/test_code_rules_enforcer_dead_module_constant_read_cap.py +0 -103
- package/hooks/blocking/test_code_rules_enforcer_dead_split_branch.py +0 -105
- package/hooks/blocking/test_code_rules_enforcer_docstring_args_span_scope.py +0 -425
- package/hooks/blocking/test_code_rules_enforcer_docstring_cardinal_family.py +0 -176
- package/hooks/blocking/test_code_rules_enforcer_docstring_delegation_summary.py +0 -385
- package/hooks/blocking/test_code_rules_enforcer_docstring_fallback_branch.py +0 -398
- package/hooks/blocking/test_code_rules_enforcer_docstring_field_runmode_outcome.py +0 -129
- package/hooks/blocking/test_code_rules_enforcer_docstring_inline_literal_claim.py +0 -93
- package/hooks/blocking/test_code_rules_enforcer_docstring_length_constant_superlative.py +0 -198
- package/hooks/blocking/test_code_rules_enforcer_docstring_mark_glyph_enumeration.py +0 -262
- package/hooks/blocking/test_code_rules_enforcer_docstring_no_consumer.py +0 -93
- package/hooks/blocking/test_code_rules_enforcer_docstring_no_network.py +0 -115
- package/hooks/blocking/test_code_rules_enforcer_docstring_raises_largezipfile.py +0 -226
- package/hooks/blocking/test_code_rules_enforcer_docstring_returns_plural_cardinality.py +0 -207
- package/hooks/blocking/test_code_rules_enforcer_docstring_step_dispatch.py +0 -262
- package/hooks/blocking/test_code_rules_enforcer_docstring_type_checking_gate.py +0 -164
- package/hooks/blocking/test_code_rules_enforcer_docstring_unguarded_payload.py +0 -188
- package/hooks/blocking/test_code_rules_enforcer_import_block_sort.py +0 -157
- package/hooks/blocking/test_code_rules_enforcer_split_mocks_1.py +0 -303
- package/hooks/blocking/test_code_rules_enforcer_split_mocks_2.py +0 -111
- package/hooks/blocking/test_code_rules_enforcer_unused_imports.py +0 -656
- package/hooks/blocking/test_hedging_language_blocker.py +0 -261
- package/hooks/blocking/test_intent_only_ending_blocker.py +0 -209
- package/hooks/hooks_constants/dead_argparse_argument_constants.py +0 -28
- package/hooks/hooks_constants/dead_config_field_constants.py +0 -39
- package/hooks/hooks_constants/dead_dataclass_field_constants.py +0 -25
- package/hooks/hooks_constants/dead_module_constant_constants.py +0 -30
- package/hooks/hooks_constants/hedging_uncertainty_constants.py +0 -42
- package/hooks/hooks_constants/test_dispatcher_constants_docstrings.py +0 -44
- package/rules/conservative-action.md +0 -17
- package/rules/context7.md +0 -8
- package/rules/few-words.md +0 -3
- 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
|