claude-dev-env 1.58.0 → 1.59.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/CLAUDE.md +2 -2
- package/_shared/pr-loop/scripts/code_rules_gate.py +36 -3
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/code_rules_gate_constants.py +6 -0
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/reviews_disabled_constants.py +1 -0
- package/_shared/pr-loop/scripts/reviews_disabled.py +12 -0
- package/_shared/pr-loop/scripts/tests/test_code_rules_gate.py +265 -0
- package/_shared/pr-loop/scripts/tests/test_reviews_disabled.py +29 -0
- package/audit-rubrics/category_rubrics/category-o-docstring-vs-impl-drift.md +1 -1
- package/bin/install.mjs +100 -27
- package/bin/install.test.mjs +133 -1
- package/docs/CODE_RULES.md +3 -3
- package/hooks/blocking/code_rules_annotations_length.py +153 -0
- package/hooks/blocking/code_rules_dead_dataclass_field.py +319 -0
- package/hooks/blocking/code_rules_duplicate_body.py +287 -0
- package/hooks/blocking/code_rules_enforcer.py +175 -21
- package/hooks/blocking/code_rules_magic_values.py +98 -0
- package/hooks/blocking/code_rules_shared.py +41 -0
- package/hooks/blocking/destructive_command_blocker.py +1027 -12
- package/hooks/blocking/hook_prose_detector_consistency.py +150 -0
- package/hooks/blocking/subprocess_budget_completeness.py +380 -0
- package/hooks/blocking/test_code_rules_enforcer_annotations.py +225 -0
- package/hooks/blocking/test_code_rules_enforcer_cap_meta.py +1 -0
- package/hooks/blocking/test_code_rules_enforcer_dead_dataclass_field.py +467 -0
- package/hooks/blocking/test_code_rules_enforcer_duplicate_body.py +330 -0
- package/hooks/blocking/test_code_rules_enforcer_duplicate_body_hook_routing.py +179 -0
- package/hooks/blocking/test_code_rules_enforcer_magic_slice_bounds.py +133 -0
- package/hooks/blocking/test_destructive_command_blocker.py +622 -3
- package/hooks/blocking/test_hook_prose_detector_consistency.py +265 -0
- package/hooks/blocking/test_subprocess_budget_completeness.py +588 -0
- package/hooks/blocking/test_workflow_substitution_slot_blocker.py +242 -0
- package/hooks/blocking/workflow_substitution_slot_blocker.py +159 -0
- package/hooks/hooks.json +15 -0
- package/hooks/hooks_constants/code_rules_enforcer_constants.py +16 -0
- package/hooks/hooks_constants/dead_dataclass_field_constants.py +25 -0
- package/hooks/hooks_constants/destructive_command_segment_constants.py +178 -0
- package/hooks/hooks_constants/duplicate_function_body_constants.py +17 -0
- package/hooks/hooks_constants/hook_prose_detector_consistency_constants.py +30 -0
- package/hooks/hooks_constants/subprocess_budget_completeness_constants.py +5 -0
- package/hooks/hooks_constants/workflow_substitution_slot_blocker_constants.py +22 -0
- package/package.json +1 -1
- package/rules/docstring-prose-matches-implementation.md +43 -0
- package/rules/hook-prose-matches-detector.md +26 -0
- package/rules/no-inline-destructive-literals.md +11 -0
- package/rules/workflow-substitution-slots.md +7 -0
- package/skills/autoconverge/SKILL.md +13 -2
- package/skills/autoconverge/reference/convergence.md +7 -3
- package/skills/autoconverge/reference/stop-conditions.md +7 -2
- package/skills/autoconverge/workflow/converge.copilot-gate.test.mjs +265 -0
- package/skills/autoconverge/workflow/converge.mjs +106 -36
- package/skills/pr-converge/scripts/check_convergence.py +195 -64
- package/skills/pr-converge/scripts/test_check_convergence.py +173 -2
- package/skills/update/SKILL.md +37 -5
|
@@ -95,3 +95,228 @@ def test_should_skip_return_check_in_test_files() -> None:
|
|
|
95
95
|
assert issues == [], f"Test files must be exempt, got: {issues}"
|
|
96
96
|
|
|
97
97
|
|
|
98
|
+
def test_should_flag_unannotated_known_fixture_in_test_file() -> None:
|
|
99
|
+
source = "def test_board(tmp_path):\n assert tmp_path.exists()\n"
|
|
100
|
+
issues = code_rules_enforcer.check_known_pytest_fixture_annotations(
|
|
101
|
+
source, TEST_FILE_PATH
|
|
102
|
+
)
|
|
103
|
+
assert any(
|
|
104
|
+
"tmp_path" in each_issue and "Path" in each_issue for each_issue in issues
|
|
105
|
+
), f"Expected unannotated tmp_path fixture flagged, got: {issues}"
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def test_should_not_flag_annotated_known_fixture_in_test_file() -> None:
|
|
109
|
+
source = (
|
|
110
|
+
"from pathlib import Path\n"
|
|
111
|
+
"def test_board(tmp_path: Path) -> None:\n"
|
|
112
|
+
" assert tmp_path.exists()\n"
|
|
113
|
+
)
|
|
114
|
+
issues = code_rules_enforcer.check_known_pytest_fixture_annotations(
|
|
115
|
+
source, TEST_FILE_PATH
|
|
116
|
+
)
|
|
117
|
+
assert issues == [], f"Annotated tmp_path must not be flagged, got: {issues}"
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def test_should_not_flag_ordinary_test_parameter() -> None:
|
|
121
|
+
source = "def test_thing(some_value):\n assert some_value\n"
|
|
122
|
+
issues = code_rules_enforcer.check_known_pytest_fixture_annotations(
|
|
123
|
+
source, TEST_FILE_PATH
|
|
124
|
+
)
|
|
125
|
+
assert issues == [], (
|
|
126
|
+
f"Ordinary test params stay exempt; only known fixtures are checked, "
|
|
127
|
+
f"got: {issues}"
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def test_should_not_flag_known_fixture_name_outside_test_files() -> None:
|
|
132
|
+
source = "def build(monkeypatch):\n return monkeypatch\n"
|
|
133
|
+
issues = code_rules_enforcer.check_known_pytest_fixture_annotations(
|
|
134
|
+
source, PRODUCTION_FILE_PATH
|
|
135
|
+
)
|
|
136
|
+
assert issues == [], (
|
|
137
|
+
f"Non-test files are covered by the broad parameter check, not this one, "
|
|
138
|
+
f"got: {issues}"
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def test_should_flag_unannotated_monkeypatch_fixture() -> None:
|
|
143
|
+
source = "def test_env(monkeypatch):\n monkeypatch.setenv('A', 'B')\n"
|
|
144
|
+
issues = code_rules_enforcer.check_known_pytest_fixture_annotations(
|
|
145
|
+
source, TEST_FILE_PATH
|
|
146
|
+
)
|
|
147
|
+
assert any(
|
|
148
|
+
"monkeypatch" in each_issue and "MonkeyPatch" in each_issue
|
|
149
|
+
for each_issue in issues
|
|
150
|
+
), f"Expected unannotated monkeypatch fixture flagged, got: {issues}"
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def test_should_not_flag_known_fixture_in_non_test_helper() -> None:
|
|
154
|
+
source = "def render_view(request):\n return request.path\n"
|
|
155
|
+
issues = code_rules_enforcer.check_known_pytest_fixture_annotations(
|
|
156
|
+
source, TEST_FILE_PATH
|
|
157
|
+
)
|
|
158
|
+
assert issues == [], (
|
|
159
|
+
f"Ordinary helper (non-test, non-fixture) must not be flagged, got: {issues}"
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def test_should_flag_unannotated_fixture_in_decorated_fixture() -> None:
|
|
164
|
+
source = (
|
|
165
|
+
"import pytest\n"
|
|
166
|
+
"@pytest.fixture\n"
|
|
167
|
+
"def board(tmp_path):\n"
|
|
168
|
+
" return tmp_path\n"
|
|
169
|
+
)
|
|
170
|
+
issues = code_rules_enforcer.check_known_pytest_fixture_annotations(
|
|
171
|
+
source, TEST_FILE_PATH
|
|
172
|
+
)
|
|
173
|
+
assert any(
|
|
174
|
+
"tmp_path" in each_issue and "Path" in each_issue for each_issue in issues
|
|
175
|
+
), f"Expected unannotated tmp_path in @pytest.fixture-decorated function flagged, got: {issues}"
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def test_should_flag_known_fixture_with_wrong_annotation() -> None:
|
|
179
|
+
source = "def test_board(tmp_path: str):\n assert tmp_path\n"
|
|
180
|
+
issues = code_rules_enforcer.check_known_pytest_fixture_annotations(
|
|
181
|
+
source, TEST_FILE_PATH
|
|
182
|
+
)
|
|
183
|
+
assert any(
|
|
184
|
+
"tmp_path" in each_issue and "Path" in each_issue for each_issue in issues
|
|
185
|
+
), f"Expected wrongly annotated tmp_path: str flagged, got: {issues}"
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def test_should_flag_known_fixture_with_unrelated_annotation() -> None:
|
|
189
|
+
source = "def test_board(tmp_path: int):\n assert tmp_path\n"
|
|
190
|
+
issues = code_rules_enforcer.check_known_pytest_fixture_annotations(
|
|
191
|
+
source, TEST_FILE_PATH
|
|
192
|
+
)
|
|
193
|
+
assert any(
|
|
194
|
+
"tmp_path" in each_issue and "Path" in each_issue for each_issue in issues
|
|
195
|
+
), f"Expected wrongly annotated tmp_path: int flagged, got: {issues}"
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def test_should_not_flag_correctly_annotated_qualified_fixture() -> None:
|
|
199
|
+
source = (
|
|
200
|
+
"import pytest\n"
|
|
201
|
+
"def test_env(monkeypatch: pytest.MonkeyPatch) -> None:\n"
|
|
202
|
+
" monkeypatch.setenv('A', 'B')\n"
|
|
203
|
+
)
|
|
204
|
+
issues = code_rules_enforcer.check_known_pytest_fixture_annotations(
|
|
205
|
+
source, TEST_FILE_PATH
|
|
206
|
+
)
|
|
207
|
+
assert issues == [], (
|
|
208
|
+
f"Correctly annotated monkeypatch must not be flagged, got: {issues}"
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def test_should_not_flag_dotted_pathlib_path_fixture_annotation() -> None:
|
|
213
|
+
source = (
|
|
214
|
+
"import pathlib\n"
|
|
215
|
+
"def test_board(tmp_path: pathlib.Path) -> None:\n"
|
|
216
|
+
" assert tmp_path.exists()\n"
|
|
217
|
+
)
|
|
218
|
+
issues = code_rules_enforcer.check_known_pytest_fixture_annotations(
|
|
219
|
+
source, TEST_FILE_PATH
|
|
220
|
+
)
|
|
221
|
+
assert issues == [], (
|
|
222
|
+
f"tmp_path: pathlib.Path is an equally-correct spelling, got: {issues}"
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def test_should_not_flag_bare_tail_of_qualified_fixture_annotation() -> None:
|
|
227
|
+
source = (
|
|
228
|
+
"from pytest import MonkeyPatch\n"
|
|
229
|
+
"def test_env(monkeypatch: MonkeyPatch) -> None:\n"
|
|
230
|
+
" monkeypatch.setenv('A', 'B')\n"
|
|
231
|
+
)
|
|
232
|
+
issues = code_rules_enforcer.check_known_pytest_fixture_annotations(
|
|
233
|
+
source, TEST_FILE_PATH
|
|
234
|
+
)
|
|
235
|
+
assert issues == [], (
|
|
236
|
+
f"monkeypatch: MonkeyPatch matches the qualified expected tail, got: {issues}"
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def test_should_not_flag_forward_reference_fixture_annotation() -> None:
|
|
241
|
+
source = 'def test_board(tmp_path: "Path") -> None:\n assert tmp_path\n'
|
|
242
|
+
issues = code_rules_enforcer.check_known_pytest_fixture_annotations(
|
|
243
|
+
source, TEST_FILE_PATH
|
|
244
|
+
)
|
|
245
|
+
assert issues == [], (
|
|
246
|
+
f'A forward-ref "Path" annotation must not be flagged, got: {issues}'
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def test_should_not_flag_star_arg_fixture_name() -> None:
|
|
251
|
+
source = "def test_board(*tmp_path):\n assert tmp_path\n"
|
|
252
|
+
issues = code_rules_enforcer.check_known_pytest_fixture_annotations(
|
|
253
|
+
source, TEST_FILE_PATH
|
|
254
|
+
)
|
|
255
|
+
assert issues == [], (
|
|
256
|
+
f"A *vararg sharing a fixture name is not an injection site, got: {issues}"
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def test_should_not_flag_double_star_arg_fixture_name() -> None:
|
|
261
|
+
source = "def test_env(**monkeypatch):\n assert monkeypatch\n"
|
|
262
|
+
issues = code_rules_enforcer.check_known_pytest_fixture_annotations(
|
|
263
|
+
source, TEST_FILE_PATH
|
|
264
|
+
)
|
|
265
|
+
assert issues == [], (
|
|
266
|
+
f"A **kwarg sharing a fixture name is not an injection site, got: {issues}"
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def test_should_not_flag_positional_only_fixture_name() -> None:
|
|
271
|
+
source = "def test_board(tmp_path, /):\n pass\n"
|
|
272
|
+
issues = code_rules_enforcer.check_known_pytest_fixture_annotations(
|
|
273
|
+
source, TEST_FILE_PATH
|
|
274
|
+
)
|
|
275
|
+
assert issues == [], (
|
|
276
|
+
f"A positional-only param cannot receive a keyword-injected fixture, got: {issues}"
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def test_should_flag_keyword_only_fixture_name() -> None:
|
|
281
|
+
source = "def test_board(*, tmp_path):\n pass\n"
|
|
282
|
+
issues = code_rules_enforcer.check_known_pytest_fixture_annotations(
|
|
283
|
+
source, TEST_FILE_PATH
|
|
284
|
+
)
|
|
285
|
+
assert any(
|
|
286
|
+
"tmp_path" in each_issue and "Path" in each_issue for each_issue in issues
|
|
287
|
+
), f"A keyword-only fixture is still an injection site, got: {issues}"
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def test_should_not_flag_defaulted_fixture_name() -> None:
|
|
291
|
+
source = "def test_board(tmp_path=None):\n pass\n"
|
|
292
|
+
issues = code_rules_enforcer.check_known_pytest_fixture_annotations(
|
|
293
|
+
source, TEST_FILE_PATH
|
|
294
|
+
)
|
|
295
|
+
assert issues == [], (
|
|
296
|
+
f"A defaulted param is not fixture-injected and must not be flagged, got: {issues}"
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def test_should_not_flag_defaulted_keyword_only_fixture_name() -> None:
|
|
301
|
+
source = "def test_board(*, tmp_path=None):\n pass\n"
|
|
302
|
+
issues = code_rules_enforcer.check_known_pytest_fixture_annotations(
|
|
303
|
+
source, TEST_FILE_PATH
|
|
304
|
+
)
|
|
305
|
+
assert issues == [], (
|
|
306
|
+
f"A defaulted keyword-only param is not fixture-injected, got: {issues}"
|
|
307
|
+
)
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def test_should_flag_undefaulted_fixture_before_defaulted_one() -> None:
|
|
311
|
+
source = "def test_board(tmp_path, capsys=None):\n pass\n"
|
|
312
|
+
issues = code_rules_enforcer.check_known_pytest_fixture_annotations(
|
|
313
|
+
source, TEST_FILE_PATH
|
|
314
|
+
)
|
|
315
|
+
assert any(
|
|
316
|
+
"tmp_path" in each_issue and "Path" in each_issue for each_issue in issues
|
|
317
|
+
), f"An undefaulted leading fixture stays an injection site, got: {issues}"
|
|
318
|
+
assert not any("capsys" in each_issue for each_issue in issues), (
|
|
319
|
+
f"The trailing defaulted param must not be flagged, got: {issues}"
|
|
320
|
+
)
|
|
321
|
+
|
|
322
|
+
|
|
@@ -70,6 +70,7 @@ KNOWN_UNCAPPED_CHECKS_PENDING_REVIEW: frozenset[str] = frozenset(
|
|
|
70
70
|
"check_file_global_constants_use_count",
|
|
71
71
|
"check_imports_at_top",
|
|
72
72
|
"check_inline_literal_collections",
|
|
73
|
+
"check_known_pytest_fixture_annotations",
|
|
73
74
|
"check_library_print",
|
|
74
75
|
"check_loop_variable_naming",
|
|
75
76
|
"check_parameter_annotations",
|
|
@@ -0,0 +1,467 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import importlib.util
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
ENFORCER_PATH = Path(__file__).resolve().parent / "code_rules_enforcer.py"
|
|
7
|
+
specification = importlib.util.spec_from_file_location(
|
|
8
|
+
"code_rules_enforcer", ENFORCER_PATH
|
|
9
|
+
)
|
|
10
|
+
assert specification is not None and specification.loader is not None
|
|
11
|
+
code_rules_enforcer = importlib.util.module_from_spec(specification)
|
|
12
|
+
specification.loader.exec_module(code_rules_enforcer)
|
|
13
|
+
|
|
14
|
+
PRODUCTION_FILE_PATH = "packages/app/services/report.py"
|
|
15
|
+
TEST_FILE_PATH = "packages/app/services/test_report.py"
|
|
16
|
+
MIGRATION_FILE_PATH = "packages/app/migrations/0001_initial.py"
|
|
17
|
+
WORKFLOW_FILE_PATH = "packages/app/skills/thing/workflow/render_report.py"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _check(source: str, file_path: str) -> list[str]:
|
|
21
|
+
return code_rules_enforcer.check_dead_dataclass_fields(source, file_path)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def test_should_flag_dataclass_field_assigned_but_never_read() -> None:
|
|
25
|
+
source = (
|
|
26
|
+
"from dataclasses import dataclass\n"
|
|
27
|
+
"\n"
|
|
28
|
+
"@dataclass\n"
|
|
29
|
+
"class PrMetadata:\n"
|
|
30
|
+
" number: int\n"
|
|
31
|
+
" url: str\n"
|
|
32
|
+
"\n"
|
|
33
|
+
"def build() -> PrMetadata:\n"
|
|
34
|
+
" return PrMetadata(number=1, url='x')\n"
|
|
35
|
+
"\n"
|
|
36
|
+
"def render(metadata: PrMetadata) -> int:\n"
|
|
37
|
+
" return metadata.number\n"
|
|
38
|
+
)
|
|
39
|
+
issues = _check(source, PRODUCTION_FILE_PATH)
|
|
40
|
+
assert any(
|
|
41
|
+
"'url'" in each_issue and "PrMetadata" in each_issue for each_issue in issues
|
|
42
|
+
), f"Expected dead 'url' field flagged, got: {issues}"
|
|
43
|
+
assert not any(
|
|
44
|
+
"'number'" in each_issue for each_issue in issues
|
|
45
|
+
), f"Read field 'number' must not be flagged, got: {issues}"
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def test_should_not_flag_field_read_via_attribute_access() -> None:
|
|
49
|
+
source = (
|
|
50
|
+
"from dataclasses import dataclass\n"
|
|
51
|
+
"\n"
|
|
52
|
+
"@dataclass\n"
|
|
53
|
+
"class PrMetadata:\n"
|
|
54
|
+
" url: str\n"
|
|
55
|
+
"\n"
|
|
56
|
+
"def build() -> PrMetadata:\n"
|
|
57
|
+
" return PrMetadata(url='x')\n"
|
|
58
|
+
"\n"
|
|
59
|
+
"def render(metadata: PrMetadata) -> str:\n"
|
|
60
|
+
" return metadata.url\n"
|
|
61
|
+
)
|
|
62
|
+
assert _check(source, PRODUCTION_FILE_PATH) == []
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def test_should_not_flag_when_class_never_constructed_in_file() -> None:
|
|
66
|
+
source = (
|
|
67
|
+
"from dataclasses import dataclass\n"
|
|
68
|
+
"\n"
|
|
69
|
+
"@dataclass\n"
|
|
70
|
+
"class PublicConfig:\n"
|
|
71
|
+
" url: str\n"
|
|
72
|
+
" number: int\n"
|
|
73
|
+
)
|
|
74
|
+
assert _check(source, PRODUCTION_FILE_PATH) == []
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def test_should_not_flag_non_dataclass_class() -> None:
|
|
78
|
+
source = "class Plain:\n url: str\n\ndef build() -> Plain:\n return Plain()\n"
|
|
79
|
+
assert _check(source, PRODUCTION_FILE_PATH) == []
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def test_should_not_flag_classvar_field() -> None:
|
|
83
|
+
source = (
|
|
84
|
+
"from dataclasses import dataclass\n"
|
|
85
|
+
"from typing import ClassVar\n"
|
|
86
|
+
"\n"
|
|
87
|
+
"@dataclass\n"
|
|
88
|
+
"class Counter:\n"
|
|
89
|
+
" label: ClassVar[str] = 'count'\n"
|
|
90
|
+
" total: int\n"
|
|
91
|
+
"\n"
|
|
92
|
+
"def build() -> Counter:\n"
|
|
93
|
+
" return Counter(total=1)\n"
|
|
94
|
+
"\n"
|
|
95
|
+
"def read(counter: Counter) -> int:\n"
|
|
96
|
+
" return counter.total\n"
|
|
97
|
+
)
|
|
98
|
+
assert _check(source, PRODUCTION_FILE_PATH) == []
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def test_should_not_flag_field_read_via_literal_getattr() -> None:
|
|
102
|
+
source = (
|
|
103
|
+
"from dataclasses import dataclass\n"
|
|
104
|
+
"\n"
|
|
105
|
+
"@dataclass\n"
|
|
106
|
+
"class Row:\n"
|
|
107
|
+
" url: str\n"
|
|
108
|
+
"\n"
|
|
109
|
+
"def build() -> Row:\n"
|
|
110
|
+
" return Row(url='x')\n"
|
|
111
|
+
"\n"
|
|
112
|
+
"def read(row: Row) -> str:\n"
|
|
113
|
+
" return getattr(row, 'url')\n"
|
|
114
|
+
)
|
|
115
|
+
assert _check(source, PRODUCTION_FILE_PATH) == []
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def test_should_suppress_check_when_non_literal_getattr_present() -> None:
|
|
119
|
+
source = (
|
|
120
|
+
"from dataclasses import dataclass\n"
|
|
121
|
+
"\n"
|
|
122
|
+
"@dataclass\n"
|
|
123
|
+
"class Row:\n"
|
|
124
|
+
" url: str\n"
|
|
125
|
+
"\n"
|
|
126
|
+
"def build() -> Row:\n"
|
|
127
|
+
" return Row(url='x')\n"
|
|
128
|
+
"\n"
|
|
129
|
+
"def read(row: Row, field_name: str) -> str:\n"
|
|
130
|
+
" return getattr(row, field_name)\n"
|
|
131
|
+
)
|
|
132
|
+
assert _check(source, PRODUCTION_FILE_PATH) == []
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def test_should_handle_called_dataclass_decorator() -> None:
|
|
136
|
+
source = (
|
|
137
|
+
"from dataclasses import dataclass\n"
|
|
138
|
+
"\n"
|
|
139
|
+
"@dataclass(frozen=True)\n"
|
|
140
|
+
"class PrMetadata:\n"
|
|
141
|
+
" url: str\n"
|
|
142
|
+
"\n"
|
|
143
|
+
"def build() -> PrMetadata:\n"
|
|
144
|
+
" return PrMetadata(url='x')\n"
|
|
145
|
+
)
|
|
146
|
+
issues = _check(source, PRODUCTION_FILE_PATH)
|
|
147
|
+
assert any(
|
|
148
|
+
"'url'" in each_issue for each_issue in issues
|
|
149
|
+
), f"Expected dead field flagged on called-decorator dataclass, got: {issues}"
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def test_should_handle_dotted_dataclasses_decorator() -> None:
|
|
153
|
+
source = (
|
|
154
|
+
"import dataclasses\n"
|
|
155
|
+
"\n"
|
|
156
|
+
"@dataclasses.dataclass\n"
|
|
157
|
+
"class PrMetadata:\n"
|
|
158
|
+
" url: str\n"
|
|
159
|
+
"\n"
|
|
160
|
+
"def build() -> PrMetadata:\n"
|
|
161
|
+
" return PrMetadata(url='x')\n"
|
|
162
|
+
)
|
|
163
|
+
issues = _check(source, PRODUCTION_FILE_PATH)
|
|
164
|
+
assert any(
|
|
165
|
+
"'url'" in each_issue for each_issue in issues
|
|
166
|
+
), f"Expected dead field flagged on dotted-decorator dataclass, got: {issues}"
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def test_should_be_silent_for_test_files() -> None:
|
|
170
|
+
source = (
|
|
171
|
+
"from dataclasses import dataclass\n"
|
|
172
|
+
"\n"
|
|
173
|
+
"@dataclass\n"
|
|
174
|
+
"class PrMetadata:\n"
|
|
175
|
+
" url: str\n"
|
|
176
|
+
"\n"
|
|
177
|
+
"def build() -> PrMetadata:\n"
|
|
178
|
+
" return PrMetadata(url='x')\n"
|
|
179
|
+
)
|
|
180
|
+
assert _check(source, TEST_FILE_PATH) == []
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def test_should_be_silent_for_migration_files() -> None:
|
|
184
|
+
source = (
|
|
185
|
+
"from dataclasses import dataclass\n"
|
|
186
|
+
"\n"
|
|
187
|
+
"@dataclass\n"
|
|
188
|
+
"class PrMetadata:\n"
|
|
189
|
+
" url: str\n"
|
|
190
|
+
"\n"
|
|
191
|
+
"def build() -> PrMetadata:\n"
|
|
192
|
+
" return PrMetadata(url='x')\n"
|
|
193
|
+
)
|
|
194
|
+
assert _check(source, MIGRATION_FILE_PATH) == []
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def test_should_flag_dead_field_in_workflow_path() -> None:
|
|
198
|
+
source = (
|
|
199
|
+
"from dataclasses import dataclass\n"
|
|
200
|
+
"\n"
|
|
201
|
+
"@dataclass\n"
|
|
202
|
+
"class PrMetadata:\n"
|
|
203
|
+
" url: str\n"
|
|
204
|
+
"\n"
|
|
205
|
+
"def build() -> PrMetadata:\n"
|
|
206
|
+
" return PrMetadata(url='x')\n"
|
|
207
|
+
)
|
|
208
|
+
issues = _check(source, WORKFLOW_FILE_PATH)
|
|
209
|
+
assert any(
|
|
210
|
+
"'url'" in each_issue for each_issue in issues
|
|
211
|
+
), f"Workflow-path files are subject to dead-code detection, got: {issues}"
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def test_should_tolerate_syntax_error() -> None:
|
|
215
|
+
assert _check("@dataclass\nclass Broken(:\n", PRODUCTION_FILE_PATH) == []
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def test_validate_content_runs_dead_field_check() -> None:
|
|
219
|
+
source = (
|
|
220
|
+
"from dataclasses import dataclass\n"
|
|
221
|
+
"\n"
|
|
222
|
+
"@dataclass\n"
|
|
223
|
+
"class PrMetadata:\n"
|
|
224
|
+
" url: str\n"
|
|
225
|
+
"\n"
|
|
226
|
+
"def build() -> PrMetadata:\n"
|
|
227
|
+
" return PrMetadata(url='x')\n"
|
|
228
|
+
)
|
|
229
|
+
issues = code_rules_enforcer.validate_content(source, PRODUCTION_FILE_PATH)
|
|
230
|
+
assert any(
|
|
231
|
+
"'url'" in each_issue for each_issue in issues
|
|
232
|
+
), f"Expected the enforcer dispatch to surface the dead field, got: {issues}"
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def test_should_suppress_check_when_asdict_consumes_instance() -> None:
|
|
236
|
+
source = (
|
|
237
|
+
"from dataclasses import dataclass, asdict\n"
|
|
238
|
+
"\n"
|
|
239
|
+
"@dataclass\n"
|
|
240
|
+
"class Row:\n"
|
|
241
|
+
" url: str\n"
|
|
242
|
+
"\n"
|
|
243
|
+
"def build() -> dict:\n"
|
|
244
|
+
" return asdict(Row(url='x'))\n"
|
|
245
|
+
)
|
|
246
|
+
assert _check(source, PRODUCTION_FILE_PATH) == []
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def test_should_suppress_check_when_astuple_consumes_instance() -> None:
|
|
250
|
+
source = (
|
|
251
|
+
"from dataclasses import dataclass, astuple\n"
|
|
252
|
+
"\n"
|
|
253
|
+
"@dataclass\n"
|
|
254
|
+
"class Row:\n"
|
|
255
|
+
" url: str\n"
|
|
256
|
+
"\n"
|
|
257
|
+
"def build() -> tuple:\n"
|
|
258
|
+
" return astuple(Row(url='x'))\n"
|
|
259
|
+
)
|
|
260
|
+
assert _check(source, PRODUCTION_FILE_PATH) == []
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def test_should_suppress_check_when_vars_consumes_instance() -> None:
|
|
264
|
+
source = (
|
|
265
|
+
"from dataclasses import dataclass\n"
|
|
266
|
+
"\n"
|
|
267
|
+
"@dataclass\n"
|
|
268
|
+
"class Row:\n"
|
|
269
|
+
" url: str\n"
|
|
270
|
+
"\n"
|
|
271
|
+
"def build() -> dict:\n"
|
|
272
|
+
" return vars(Row(url='x'))\n"
|
|
273
|
+
)
|
|
274
|
+
assert _check(source, PRODUCTION_FILE_PATH) == []
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def test_should_suppress_check_when_replace_consumes_instance() -> None:
|
|
278
|
+
source = (
|
|
279
|
+
"from dataclasses import dataclass, replace\n"
|
|
280
|
+
"\n"
|
|
281
|
+
"@dataclass(frozen=True)\n"
|
|
282
|
+
"class Row:\n"
|
|
283
|
+
" url: str\n"
|
|
284
|
+
"\n"
|
|
285
|
+
"def build() -> Row:\n"
|
|
286
|
+
" return replace(Row(url='x'), url='y')\n"
|
|
287
|
+
)
|
|
288
|
+
assert _check(source, PRODUCTION_FILE_PATH) == []
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def test_should_suppress_check_when_dotted_replace_consumes_instance() -> None:
|
|
292
|
+
source = (
|
|
293
|
+
"import dataclasses\n"
|
|
294
|
+
"\n"
|
|
295
|
+
"@dataclasses.dataclass(frozen=True)\n"
|
|
296
|
+
"class Row:\n"
|
|
297
|
+
" url: str\n"
|
|
298
|
+
"\n"
|
|
299
|
+
"def build() -> Row:\n"
|
|
300
|
+
" return dataclasses.replace(Row(url='x'), url='y')\n"
|
|
301
|
+
)
|
|
302
|
+
assert _check(source, PRODUCTION_FILE_PATH) == []
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def test_should_suppress_check_when_instance_dict_consumes_instance() -> None:
|
|
306
|
+
source = (
|
|
307
|
+
"from dataclasses import dataclass\n"
|
|
308
|
+
"\n"
|
|
309
|
+
"@dataclass\n"
|
|
310
|
+
"class Row:\n"
|
|
311
|
+
" url: str\n"
|
|
312
|
+
"\n"
|
|
313
|
+
"def build() -> dict:\n"
|
|
314
|
+
" return Row(url='x').__dict__\n"
|
|
315
|
+
)
|
|
316
|
+
assert _check(source, PRODUCTION_FILE_PATH) == []
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def test_should_not_flag_field_read_via_multi_argument_attrgetter() -> None:
|
|
320
|
+
source = (
|
|
321
|
+
"from dataclasses import dataclass\n"
|
|
322
|
+
"from operator import attrgetter\n"
|
|
323
|
+
"\n"
|
|
324
|
+
"@dataclass\n"
|
|
325
|
+
"class Row:\n"
|
|
326
|
+
" first: int\n"
|
|
327
|
+
" second: int\n"
|
|
328
|
+
"\n"
|
|
329
|
+
"def build() -> tuple:\n"
|
|
330
|
+
" return attrgetter('first', 'second')(Row(first=1, second=2))\n"
|
|
331
|
+
)
|
|
332
|
+
assert _check(source, PRODUCTION_FILE_PATH) == []
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def test_should_suppress_check_when_two_instances_compared_for_equality() -> None:
|
|
336
|
+
source = (
|
|
337
|
+
"from dataclasses import dataclass\n"
|
|
338
|
+
"\n"
|
|
339
|
+
"@dataclass\n"
|
|
340
|
+
"class Row:\n"
|
|
341
|
+
" url: str\n"
|
|
342
|
+
"\n"
|
|
343
|
+
"def are_same(left: Row, right: Row) -> bool:\n"
|
|
344
|
+
" return Row(url='a') == Row(url='b')\n"
|
|
345
|
+
)
|
|
346
|
+
assert _check(source, PRODUCTION_FILE_PATH) == []
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
def test_should_suppress_check_when_ordered_instances_compared() -> None:
|
|
350
|
+
source = (
|
|
351
|
+
"from dataclasses import dataclass\n"
|
|
352
|
+
"\n"
|
|
353
|
+
"@dataclass(order=True)\n"
|
|
354
|
+
"class Row:\n"
|
|
355
|
+
" priority: int\n"
|
|
356
|
+
"\n"
|
|
357
|
+
"def is_earlier() -> bool:\n"
|
|
358
|
+
" return Row(priority=1) < Row(priority=2)\n"
|
|
359
|
+
)
|
|
360
|
+
assert _check(source, PRODUCTION_FILE_PATH) == []
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
def test_should_suppress_check_when_instances_placed_in_set() -> None:
|
|
364
|
+
source = (
|
|
365
|
+
"from dataclasses import dataclass\n"
|
|
366
|
+
"\n"
|
|
367
|
+
"@dataclass(frozen=True)\n"
|
|
368
|
+
"class Row:\n"
|
|
369
|
+
" url: str\n"
|
|
370
|
+
"\n"
|
|
371
|
+
"def unique_rows() -> set:\n"
|
|
372
|
+
" return {Row(url='a'), Row(url='b')}\n"
|
|
373
|
+
)
|
|
374
|
+
assert _check(source, PRODUCTION_FILE_PATH) == []
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
def test_should_suppress_check_when_whole_instance_stringified() -> None:
|
|
378
|
+
source = (
|
|
379
|
+
"from dataclasses import dataclass\n"
|
|
380
|
+
"\n"
|
|
381
|
+
"@dataclass\n"
|
|
382
|
+
"class Row:\n"
|
|
383
|
+
" url: str\n"
|
|
384
|
+
"\n"
|
|
385
|
+
"def describe() -> str:\n"
|
|
386
|
+
" return str(Row(url='x'))\n"
|
|
387
|
+
)
|
|
388
|
+
assert _check(source, PRODUCTION_FILE_PATH) == []
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
def test_should_not_flag_field_read_via_match_class_pattern() -> None:
|
|
392
|
+
source = (
|
|
393
|
+
"from dataclasses import dataclass\n"
|
|
394
|
+
"\n"
|
|
395
|
+
"@dataclass\n"
|
|
396
|
+
"class Row:\n"
|
|
397
|
+
" url: str\n"
|
|
398
|
+
"\n"
|
|
399
|
+
"def read(row: Row) -> str:\n"
|
|
400
|
+
" match row:\n"
|
|
401
|
+
" case Row(url=found):\n"
|
|
402
|
+
" return found\n"
|
|
403
|
+
" return ''\n"
|
|
404
|
+
"\n"
|
|
405
|
+
"def build() -> Row:\n"
|
|
406
|
+
" return Row(url='x')\n"
|
|
407
|
+
)
|
|
408
|
+
assert _check(source, PRODUCTION_FILE_PATH) == []
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
def test_should_suppress_check_when_fields_reflection_consumes_instance() -> None:
|
|
412
|
+
source = (
|
|
413
|
+
"import dataclasses\n"
|
|
414
|
+
"from dataclasses import dataclass\n"
|
|
415
|
+
"\n"
|
|
416
|
+
"@dataclass\n"
|
|
417
|
+
"class Row:\n"
|
|
418
|
+
" url: str\n"
|
|
419
|
+
"\n"
|
|
420
|
+
"def field_names() -> list:\n"
|
|
421
|
+
" return [each_field.name for each_field in dataclasses.fields(Row(url='x'))]\n"
|
|
422
|
+
)
|
|
423
|
+
assert _check(source, PRODUCTION_FILE_PATH) == []
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
def test_should_not_flag_field_read_via_augmented_assignment() -> None:
|
|
427
|
+
source = (
|
|
428
|
+
"from dataclasses import dataclass\n"
|
|
429
|
+
"\n"
|
|
430
|
+
"@dataclass\n"
|
|
431
|
+
"class Row:\n"
|
|
432
|
+
" counter: int\n"
|
|
433
|
+
"\n"
|
|
434
|
+
"def bump(row: Row) -> None:\n"
|
|
435
|
+
" row.counter += 1\n"
|
|
436
|
+
"\n"
|
|
437
|
+
"def build() -> Row:\n"
|
|
438
|
+
" return Row(counter=0)\n"
|
|
439
|
+
)
|
|
440
|
+
assert _check(source, PRODUCTION_FILE_PATH) == []
|
|
441
|
+
|
|
442
|
+
|
|
443
|
+
def test_should_evaluate_full_file_content_when_supplied() -> None:
|
|
444
|
+
fragment = " return metadata.number\n"
|
|
445
|
+
full_file = (
|
|
446
|
+
"from dataclasses import dataclass\n"
|
|
447
|
+
"\n"
|
|
448
|
+
"@dataclass\n"
|
|
449
|
+
"class PrMetadata:\n"
|
|
450
|
+
" number: int\n"
|
|
451
|
+
" url: str\n"
|
|
452
|
+
"\n"
|
|
453
|
+
"def build() -> PrMetadata:\n"
|
|
454
|
+
" return PrMetadata(number=1, url='x')\n"
|
|
455
|
+
"\n"
|
|
456
|
+
"def render(metadata: PrMetadata) -> int:\n"
|
|
457
|
+
" return metadata.number\n"
|
|
458
|
+
)
|
|
459
|
+
issues = code_rules_enforcer.check_dead_dataclass_fields(
|
|
460
|
+
fragment, PRODUCTION_FILE_PATH, full_file
|
|
461
|
+
)
|
|
462
|
+
assert any(
|
|
463
|
+
"'url'" in each_issue for each_issue in issues
|
|
464
|
+
), f"Expected the reconstructed whole-file content to govern, got: {issues}"
|
|
465
|
+
assert not any(
|
|
466
|
+
"'number'" in each_issue for each_issue in issues
|
|
467
|
+
), f"Read field 'number' must not be flagged, got: {issues}"
|