claude-dev-env 2.27.0 → 2.28.1

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 (109) hide show
  1. package/.agents/skills/hitl/SKILL.md +45 -0
  2. package/.agents/skills-archived/pr-loop-cloud-transport/reference/identity-and-hooks.md +1 -1
  3. package/audit-rubrics/category_rubrics/category-o-docstring-vs-impl-drift.md +16 -29
  4. package/audit-rubrics/prompts/category-o-docstring-vs-impl-drift.md +2 -2
  5. package/bin/ever-shipped-skills.mjs +1 -0
  6. package/docs/CODE_RULES.md +3 -3
  7. package/docs/agent-spawn-protocol.md +1 -1
  8. package/docs/references/prose-style-enforcement.md +5 -9
  9. package/hooks/blocking/AGENTS.md +3 -13
  10. package/hooks/blocking/code_rules_docstrings.py +10 -2138
  11. package/hooks/blocking/code_rules_enforcer.py +0 -121
  12. package/hooks/blocking/code_rules_imports_logging.py +1 -236
  13. package/hooks/blocking/code_rules_shared.py +23 -0
  14. package/hooks/blocking/code_rules_test_layout.py +8 -8
  15. package/hooks/blocking/config/prose_style_enforcement_constants.py +4 -2
  16. package/hooks/blocking/config/test_prose_style_enforcement_constants.py +5 -1
  17. package/hooks/blocking/precommit_code_rules_gate.py +8 -43
  18. package/hooks/blocking/state_description_blocker.py +1 -7
  19. package/hooks/blocking/test_code_rules_enforcer_cap_meta.py +0 -1
  20. package/hooks/blocking/test_code_rules_enforcer_dispatch_wiring.py +0 -8
  21. package/hooks/blocking/test_code_rules_enforcer_module_docstring_roster.py +11 -112
  22. package/hooks/blocking/test_code_rules_enforcer_narrow_edit.py +0 -1
  23. package/hooks/blocking/test_code_rules_enforcer_split_entry_1.py +1 -18
  24. package/hooks/blocking/test_code_rules_shared.py +12 -0
  25. package/hooks/blocking/test_precommit_code_rules_gate.py +32 -179
  26. package/hooks/blocking/test_precommit_code_rules_gate_native_owner.py +0 -1
  27. package/hooks/blocking/test_state_description_blocker.py +6 -4
  28. package/hooks/blocking/test_stop_dispatcher.py +5 -29
  29. package/hooks/hooks_constants/AGENTS.md +1 -6
  30. package/hooks/hooks_constants/bash_pre_tool_use_dispatcher_constants.py +0 -1
  31. package/hooks/hooks_constants/code_rules_enforcer_constants.py +3 -0
  32. package/hooks/hooks_constants/messages.py +0 -2
  33. package/hooks/hooks_constants/precommit_code_rules_gate_constants.py +3 -17
  34. package/hooks/hooks_constants/stop_dispatcher_constants.py +0 -2
  35. package/hooks/hooks_constants/test_bash_pre_tool_use_dispatcher_constants.py +0 -1
  36. package/hooks/hooks_constants/test_code_rules_enforcer_constants.py +7 -0
  37. package/hooks/hooks_constants/test_messages.py +5 -3
  38. package/hooks/hooks_constants/test_stop_dispatcher_constants.py +0 -2
  39. package/package.json +1 -1
  40. package/rules/AGENTS.md +3 -6
  41. package/rules/claims-as-quotes.md +0 -10
  42. package/rules/code-standards.md +6 -6
  43. package/rules/explore-thoroughly.md +0 -1
  44. package/rules/failure-blast-radius.md +0 -8
  45. package/rules/falsify-before-green.md +0 -8
  46. package/rules/file-global-constants.md +2 -2
  47. package/rules/filesystem-search.md +1 -1
  48. package/rules/git-workflow.md +1 -9
  49. package/rules/hedging-claims.md +2 -6
  50. package/rules/long-horizon-autonomy.md +1 -1
  51. package/rules/measurement-denominators.md +0 -9
  52. package/rules/research-mode.md +0 -6
  53. package/rules/verify-before-asking.md +0 -5
  54. package/rules/verify-runtime-state.md +0 -5
  55. package/scripts/codex_compat_materializer.py +99 -24
  56. package/scripts/sync_to_cursor/rules.py +0 -10
  57. package/scripts/tests/test_codex_compat_materializer.py +70 -19
  58. package/scripts/tests/test_engine.py +0 -1
  59. package/scripts/tests/test_rules.py +0 -1
  60. package/scripts/tests/test_sync_to_cursor.py +0 -1
  61. package/system-prompts/software-engineer.xml +3 -3
  62. package/hooks/blocking/code_rules_dead_argparse_argument.py +0 -554
  63. package/hooks/blocking/code_rules_dead_config_field.py +0 -568
  64. package/hooks/blocking/code_rules_dead_dataclass_field.py +0 -348
  65. package/hooks/blocking/code_rules_dead_module_constant.py +0 -757
  66. package/hooks/blocking/code_rules_dead_split_branch.py +0 -225
  67. package/hooks/blocking/code_rules_mock_completeness.py +0 -295
  68. package/hooks/blocking/code_rules_scope_binding.py +0 -151
  69. package/hooks/blocking/code_rules_unused_imports.py +0 -197
  70. package/hooks/blocking/hedging_language_blocker.py +0 -221
  71. package/hooks/blocking/intent_only_ending_blocker.py +0 -148
  72. package/hooks/blocking/test_code_rules_enforcer_dead_argparse_argument.py +0 -534
  73. package/hooks/blocking/test_code_rules_enforcer_dead_config_field.py +0 -846
  74. package/hooks/blocking/test_code_rules_enforcer_dead_dataclass_field.py +0 -507
  75. package/hooks/blocking/test_code_rules_enforcer_dead_module_constant.py +0 -679
  76. package/hooks/blocking/test_code_rules_enforcer_dead_module_constant_alias.py +0 -133
  77. package/hooks/blocking/test_code_rules_enforcer_dead_module_constant_read_cap.py +0 -103
  78. package/hooks/blocking/test_code_rules_enforcer_dead_split_branch.py +0 -105
  79. package/hooks/blocking/test_code_rules_enforcer_docstring_args_span_scope.py +0 -425
  80. package/hooks/blocking/test_code_rules_enforcer_docstring_cardinal_family.py +0 -176
  81. package/hooks/blocking/test_code_rules_enforcer_docstring_delegation_summary.py +0 -385
  82. package/hooks/blocking/test_code_rules_enforcer_docstring_fallback_branch.py +0 -398
  83. package/hooks/blocking/test_code_rules_enforcer_docstring_field_runmode_outcome.py +0 -129
  84. package/hooks/blocking/test_code_rules_enforcer_docstring_inline_literal_claim.py +0 -93
  85. package/hooks/blocking/test_code_rules_enforcer_docstring_length_constant_superlative.py +0 -198
  86. package/hooks/blocking/test_code_rules_enforcer_docstring_mark_glyph_enumeration.py +0 -262
  87. package/hooks/blocking/test_code_rules_enforcer_docstring_no_consumer.py +0 -93
  88. package/hooks/blocking/test_code_rules_enforcer_docstring_no_network.py +0 -115
  89. package/hooks/blocking/test_code_rules_enforcer_docstring_raises_largezipfile.py +0 -226
  90. package/hooks/blocking/test_code_rules_enforcer_docstring_returns_plural_cardinality.py +0 -207
  91. package/hooks/blocking/test_code_rules_enforcer_docstring_step_dispatch.py +0 -262
  92. package/hooks/blocking/test_code_rules_enforcer_docstring_type_checking_gate.py +0 -164
  93. package/hooks/blocking/test_code_rules_enforcer_docstring_unguarded_payload.py +0 -188
  94. package/hooks/blocking/test_code_rules_enforcer_import_block_sort.py +0 -157
  95. package/hooks/blocking/test_code_rules_enforcer_split_mocks_1.py +0 -303
  96. package/hooks/blocking/test_code_rules_enforcer_split_mocks_2.py +0 -111
  97. package/hooks/blocking/test_code_rules_enforcer_unused_imports.py +0 -656
  98. package/hooks/blocking/test_hedging_language_blocker.py +0 -261
  99. package/hooks/blocking/test_intent_only_ending_blocker.py +0 -209
  100. package/hooks/hooks_constants/dead_argparse_argument_constants.py +0 -28
  101. package/hooks/hooks_constants/dead_config_field_constants.py +0 -39
  102. package/hooks/hooks_constants/dead_dataclass_field_constants.py +0 -25
  103. package/hooks/hooks_constants/dead_module_constant_constants.py +0 -30
  104. package/hooks/hooks_constants/hedging_uncertainty_constants.py +0 -42
  105. package/hooks/hooks_constants/test_dispatcher_constants_docstrings.py +0 -44
  106. package/rules/conservative-action.md +0 -17
  107. package/rules/context7.md +0 -8
  108. package/rules/few-words.md +0 -3
  109. package/rules/parallel-tools.md +0 -23
@@ -1,656 +0,0 @@
1
- """Tests for unused module-level import detection.
2
-
3
- Bot reviewers on PR #257 and PR #289 flagged FLAG_INCREMENTAL,
4
- ALL_REPOSITORY_ROOT_MARKER_FILENAMES, VENV_DIRECTORY_NAME and other
5
- imports that survived into a PR without ever being referenced.
6
-
7
- The detector is intentionally narrow: it only flags `from X import Y`
8
- or `import X` where Y/X is never referenced in the file body and the
9
- file does not declare `__all__`. A `TYPE_CHECKING` guard block no longer
10
- exempts the whole file — its guarded imports live nested inside the
11
- `if TYPE_CHECKING:` node so they are never flag candidates, while a
12
- genuinely dead top-level runtime import in the same file is still
13
- flagged. The narrow scope keeps false positives low.
14
- """
15
-
16
- from __future__ import annotations
17
-
18
- import importlib.util
19
- import pathlib
20
- import sys
21
-
22
-
23
- _HOOK_DIRECTORY = pathlib.Path(__file__).parent
24
- if str(_HOOK_DIRECTORY) not in sys.path:
25
- sys.path.insert(0, str(_HOOK_DIRECTORY))
26
-
27
- _hook_spec = importlib.util.spec_from_file_location(
28
- "code_rules_enforcer",
29
- _HOOK_DIRECTORY / "code_rules_enforcer.py",
30
- )
31
- assert _hook_spec is not None
32
- assert _hook_spec.loader is not None
33
- _hook_module = importlib.util.module_from_spec(_hook_spec)
34
- _hook_spec.loader.exec_module(_hook_module)
35
- check_unused_module_level_imports = _hook_module.check_unused_module_level_imports
36
- prior_and_post_edit_content = _hook_module.prior_and_post_edit_content
37
-
38
-
39
- PRODUCTION_FILE_PATH = "packages/app/services/loader.py"
40
- TEST_FILE_PATH = "packages/app/tests/test_loader.py"
41
-
42
-
43
- def test_should_flag_unused_from_import() -> None:
44
- source = (
45
- "from hooks_constants.preflight_constants import VENV_DIRECTORY_NAME\n"
46
- "\n"
47
- "def run() -> None:\n"
48
- " return None\n"
49
- )
50
- issues = check_unused_module_level_imports(source, PRODUCTION_FILE_PATH)
51
- assert any("VENV_DIRECTORY_NAME" in each_issue for each_issue in issues), (
52
- f"Expected VENV_DIRECTORY_NAME flagged, got: {issues}"
53
- )
54
-
55
-
56
- def test_should_not_flag_used_from_import() -> None:
57
- source = (
58
- "from hooks_constants.preflight_constants import VENV_DIRECTORY_NAME\n"
59
- "\n"
60
- "def run() -> str:\n"
61
- " return VENV_DIRECTORY_NAME\n"
62
- )
63
- issues = check_unused_module_level_imports(source, PRODUCTION_FILE_PATH)
64
- assert issues == [], f"Used import must not be flagged, got: {issues}"
65
-
66
-
67
- def test_should_flag_unused_plain_import() -> None:
68
- source = "import json\n\ndef run() -> None:\n return None\n"
69
- issues = check_unused_module_level_imports(source, PRODUCTION_FILE_PATH)
70
- assert any("json" in each_issue for each_issue in issues), (
71
- f"Expected unused 'import json' flagged, got: {issues}"
72
- )
73
-
74
-
75
- def test_should_not_flag_when_alias_is_used() -> None:
76
- source = "import json as _json\n\ndef run() -> str:\n return _json.dumps({})\n"
77
- issues = check_unused_module_level_imports(source, PRODUCTION_FILE_PATH)
78
- assert issues == [], (
79
- f"Aliased import referenced via alias must not flag, got: {issues}"
80
- )
81
-
82
-
83
- def test_should_skip_file_with_dunder_all() -> None:
84
- source = (
85
- "from hooks_constants.preflight_constants import VENV_DIRECTORY_NAME\n"
86
- "\n"
87
- "__all__ = ['something']\n"
88
- )
89
- issues = check_unused_module_level_imports(source, PRODUCTION_FILE_PATH)
90
- assert issues == [], (
91
- f"Files declaring __all__ may re-export — skip to avoid false positives, got: {issues}"
92
- )
93
-
94
-
95
- def test_should_skip_file_with_dunder_all_annotated_assignment() -> None:
96
- source = (
97
- "from hooks_constants.preflight_constants import VENV_DIRECTORY_NAME\n"
98
- "\n"
99
- '__all__: list[str] = ["VENV_DIRECTORY_NAME"]\n'
100
- )
101
- issues = check_unused_module_level_imports(source, PRODUCTION_FILE_PATH)
102
- assert issues == [], (
103
- "Annotated __all__ must skip unused-import scan like plain __all__, "
104
- f"got: {issues}"
105
- )
106
-
107
-
108
- def test_should_flag_dead_top_level_import_in_type_checking_file() -> None:
109
- source = (
110
- "from typing import TYPE_CHECKING\n"
111
- "from hooks_constants.constants import UNUSED_NAME\n"
112
- "\n"
113
- "if TYPE_CHECKING:\n"
114
- " from somewhere import OtherName\n"
115
- "\n"
116
- "def run() -> None:\n"
117
- " return None\n"
118
- )
119
- issues = check_unused_module_level_imports(source, PRODUCTION_FILE_PATH)
120
- assert any("UNUSED_NAME" in each_issue for each_issue in issues), (
121
- "A dead top-level runtime import must be flagged even when the file "
122
- f"also declares a TYPE_CHECKING block, got: {issues}"
123
- )
124
- assert not any("OtherName" in each_issue for each_issue in issues), (
125
- "A TYPE_CHECKING-guarded import is nested inside the if block and is "
126
- f"never a flag candidate, got: {issues}"
127
- )
128
-
129
-
130
- def test_should_not_flag_standard_type_checking_idiom() -> None:
131
- source = (
132
- "from typing import TYPE_CHECKING\n"
133
- "from hooks_constants.constants import RUNTIME_NAME\n"
134
- "\n"
135
- "if TYPE_CHECKING:\n"
136
- " from somewhere import TypeOnly\n"
137
- "\n"
138
- "def run(node: TypeOnly) -> None:\n"
139
- " RUNTIME_NAME()\n"
140
- )
141
- issues = check_unused_module_level_imports(source, PRODUCTION_FILE_PATH)
142
- assert issues == [], (
143
- "The standard TYPE_CHECKING idiom — TYPE_CHECKING referenced by the "
144
- "guard, the runtime import used in the body, and the guarded import "
145
- f"used in an annotation — must produce no findings, got: {issues}"
146
- )
147
-
148
-
149
- def test_should_flag_dead_import_beside_type_checking_string_annotation() -> None:
150
- source = (
151
- "from typing import TYPE_CHECKING\n"
152
- "from shared_utils.config import portal_specific_timeouts\n"
153
- "\n"
154
- "if TYPE_CHECKING:\n"
155
- " from shared_utils.base import BaseAutomation\n"
156
- "\n"
157
- 'async def run(automation: "BaseAutomation") -> None:\n'
158
- " return None\n"
159
- )
160
- issues = check_unused_module_level_imports(source, PRODUCTION_FILE_PATH)
161
- assert any("portal_specific_timeouts" in each_issue for each_issue in issues), (
162
- "A dead top-level import in a module that guards its annotation-only "
163
- f"import under TYPE_CHECKING must still be flagged, got: {issues}"
164
- )
165
- assert not any("BaseAutomation" in each_issue for each_issue in issues), (
166
- "The TYPE_CHECKING-guarded import used in a string annotation must not "
167
- f"be flagged, got: {issues}"
168
- )
169
-
170
-
171
- def test_should_flag_dead_import_in_workflow_handler_file() -> None:
172
- workflow_handler_path = (
173
- "shared_utils/web_automation/samsung_portal/workflow/binary/delete_handlers.py"
174
- )
175
- source = (
176
- "from typing import TYPE_CHECKING\n"
177
- "from shared_utils.config import portal_specific_timeouts\n"
178
- "\n"
179
- "if TYPE_CHECKING:\n"
180
- " from shared_utils.base import BaseAutomation\n"
181
- "\n"
182
- 'async def run(automation: "BaseAutomation") -> None:\n'
183
- " return None\n"
184
- )
185
- issues = check_unused_module_level_imports(source, workflow_handler_path)
186
- assert any("portal_specific_timeouts" in each_issue for each_issue in issues), (
187
- "A handler file under a workflow/ directory is not a state/module "
188
- "registry; a dead import in it must be flagged, not exempted by the "
189
- f"workflow path, got: {issues}"
190
- )
191
-
192
-
193
- def test_should_flag_unused_import_in_test_file() -> None:
194
- source = (
195
- "import asyncio\n"
196
- "\n"
197
- "def test_thing() -> None:\n"
198
- " assert True\n"
199
- )
200
- issues = check_unused_module_level_imports(source, TEST_FILE_PATH)
201
- assert any("asyncio" in each_issue for each_issue in issues), (
202
- f"Unused import in a test file must be flagged, got: {issues}"
203
- )
204
-
205
-
206
- def test_should_not_flag_used_import_in_test_file() -> None:
207
- source = (
208
- "import asyncio\n"
209
- "\n"
210
- "def test_thing() -> None:\n"
211
- " asyncio.run(main())\n"
212
- )
213
- issues = check_unused_module_level_imports(source, TEST_FILE_PATH)
214
- assert issues == [], f"Used import in a test file must not be flagged, got: {issues}"
215
-
216
-
217
- def test_should_handle_syntax_error_gracefully() -> None:
218
- source = "from config import (\n not python\n"
219
- issues = check_unused_module_level_imports(source, PRODUCTION_FILE_PATH)
220
- assert issues == [], f"Parse failure must return empty, got: {issues}"
221
-
222
-
223
- def test_should_include_line_number_in_issue() -> None:
224
- source = (
225
- "\n"
226
- "from hooks_constants.preflight_constants import VENV_DIRECTORY_NAME\n"
227
- "\n"
228
- "def run() -> None:\n"
229
- " return None\n"
230
- )
231
- issues = check_unused_module_level_imports(source, PRODUCTION_FILE_PATH)
232
- assert any("Line 2" in each_issue for each_issue in issues), (
233
- f"Expected line 2 reference, got: {issues}"
234
- )
235
-
236
-
237
- def test_should_flag_each_unused_in_multi_import() -> None:
238
- source = (
239
- "from hooks_constants.constants import USED_ONE, UNUSED_TWO\n"
240
- "\n"
241
- "def run() -> str:\n"
242
- " return USED_ONE\n"
243
- )
244
- issues = check_unused_module_level_imports(source, PRODUCTION_FILE_PATH)
245
- assert any("UNUSED_TWO" in each_issue for each_issue in issues), (
246
- f"Expected UNUSED_TWO flagged independently, got: {issues}"
247
- )
248
- assert not any("USED_ONE" in each_issue for each_issue in issues), (
249
- f"USED_ONE is referenced — must not flag, got: {issues}"
250
- )
251
-
252
-
253
- def test_should_not_flag_when_referenced_in_annotation() -> None:
254
- source = (
255
- "from typing import List\n\ndef run(xs: List[int]) -> None:\n return None\n"
256
- )
257
- issues = check_unused_module_level_imports(source, PRODUCTION_FILE_PATH)
258
- assert issues == [], (
259
- f"List used in annotation must count as a reference, got: {issues}"
260
- )
261
-
262
-
263
- def test_should_skip_noqa_marked_imports() -> None:
264
- source = (
265
- "from hooks_constants.constants import UNUSED_BUT_DELIBERATE # noqa: F401\n"
266
- "\n"
267
- "def run() -> None:\n"
268
- " return None\n"
269
- )
270
- issues = check_unused_module_level_imports(source, PRODUCTION_FILE_PATH)
271
- assert issues == [], (
272
- f"noqa-marked imports are deliberate side-effect imports, skip, got: {issues}"
273
- )
274
-
275
-
276
- def test_should_skip_noqa_on_from_keyword_line_for_multiline_import() -> None:
277
- source = (
278
- "from hooks_constants.constants import ( # noqa: F401\n"
279
- " SOME_CONSTANT,\n"
280
- " ANOTHER_CONSTANT,\n"
281
- ")\n"
282
- "\n"
283
- "def run() -> None:\n"
284
- " return None\n"
285
- )
286
- issues = check_unused_module_level_imports(source, PRODUCTION_FILE_PATH)
287
- assert issues == [], (
288
- f"noqa on the from-keyword line must suppress every alias in the block, got: {issues}"
289
- )
290
-
291
-
292
- def test_should_skip_future_annotations_import() -> None:
293
- source = (
294
- "from __future__ import annotations\n"
295
- "\n"
296
- "def run() -> None:\n"
297
- " return None\n"
298
- )
299
- issues = check_unused_module_level_imports(source, PRODUCTION_FILE_PATH)
300
- assert issues == [], (
301
- f"__future__ imports are behavior-changing side-effect imports whose "
302
- f"binding name is never referenced — skip, got: {issues}"
303
- )
304
-
305
-
306
- def test_should_skip_all_future_imports_regardless_of_name() -> None:
307
- source = (
308
- "from __future__ import annotations, division\n"
309
- "\n"
310
- "def run() -> None:\n"
311
- " return None\n"
312
- )
313
- issues = check_unused_module_level_imports(source, PRODUCTION_FILE_PATH)
314
- assert issues == [], (
315
- f"All __future__ imports must be skipped regardless of binding name, got: {issues}"
316
- )
317
-
318
-
319
- def test_should_skip_star_import() -> None:
320
- source = (
321
- "from os.path import *\n"
322
- "\n"
323
- "def run() -> str:\n"
324
- " return join('a', 'b')\n"
325
- )
326
- issues = check_unused_module_level_imports(source, PRODUCTION_FILE_PATH)
327
- assert issues == [], (
328
- f"Star imports cannot be meaningfully tracked - skip to avoid false positives, got: {issues}"
329
- )
330
-
331
-
332
- def test_should_flag_when_name_only_appears_in_comment() -> None:
333
- source = (
334
- "import json\n"
335
- "\n"
336
- "# json reserved for later\n"
337
- "def run() -> None:\n"
338
- " return None\n"
339
- )
340
- issues = check_unused_module_level_imports(source, PRODUCTION_FILE_PATH)
341
- assert any("json" in each_issue for each_issue in issues), (
342
- f"Mentions in comments must not count as references, got: {issues}"
343
- )
344
-
345
-
346
- def test_should_not_skip_when_type_checking_only_in_string_constant() -> None:
347
- source = (
348
- 'from hooks_constants.constants import UNUSED_NAME\n'
349
- '\n'
350
- 'HELP_TEXT = "See TYPE_CHECKING docs"\n'
351
- '\n'
352
- "def run() -> None:\n"
353
- " return None\n"
354
- )
355
- issues = check_unused_module_level_imports(source, PRODUCTION_FILE_PATH)
356
- assert any("UNUSED_NAME" in each_issue for each_issue in issues), (
357
- f"Substring TYPE_CHECKING in prose must not skip the scan, got: {issues}"
358
- )
359
-
360
-
361
- def test_should_flag_when_noqa_lists_only_non_f401_codes() -> None:
362
- source = (
363
- "from hooks_constants.constants import UNUSED # noqa: E402\n"
364
- "\n"
365
- "def run() -> None:\n"
366
- " return None\n"
367
- )
368
- issues = check_unused_module_level_imports(source, PRODUCTION_FILE_PATH)
369
- assert any("UNUSED" in each_issue for each_issue in issues), (
370
- f"E402-only noqa must not suppress unused-import findings, got: {issues}"
371
- )
372
-
373
-
374
- def test_should_skip_when_noqa_is_bare() -> None:
375
- source = (
376
- "from hooks_constants.constants import UNUSED # noqa\n"
377
- "\n"
378
- "def run() -> None:\n"
379
- " return None\n"
380
- )
381
- issues = check_unused_module_level_imports(source, PRODUCTION_FILE_PATH)
382
- assert issues == [], f"Bare noqa must suppress unused import, got: {issues}"
383
-
384
-
385
- def test_should_not_flag_imports_referenced_only_in_full_file_content() -> None:
386
- fragment = "from hooks_constants.constants import NEW_NAME\n"
387
- full_file = (
388
- "from hooks_constants.constants import NEW_NAME\n"
389
- "\n"
390
- "def existing_function() -> str:\n"
391
- " return NEW_NAME\n"
392
- )
393
- issues = check_unused_module_level_imports(
394
- fragment, PRODUCTION_FILE_PATH, full_file_content=full_file,
395
- )
396
- assert issues == [], (
397
- f"When the post-edit file references the import, it must not flag, got: {issues}"
398
- )
399
-
400
-
401
- def test_should_flag_imports_unused_in_full_file_content() -> None:
402
- fragment = "from hooks_constants.constants import TRULY_UNUSED\n"
403
- full_file = (
404
- "from hooks_constants.constants import TRULY_UNUSED\n"
405
- "\n"
406
- "def existing_function() -> None:\n"
407
- " return None\n"
408
- )
409
- issues = check_unused_module_level_imports(
410
- fragment, PRODUCTION_FILE_PATH, full_file_content=full_file,
411
- )
412
- assert any("TRULY_UNUSED" in each_issue for each_issue in issues), (
413
- f"Imports unused in the post-edit file must still flag, got: {issues}"
414
- )
415
-
416
-
417
- def test_should_only_flag_imports_in_fragment_not_full_file() -> None:
418
- fragment = "from hooks_constants.constants import FRAGMENT_IMPORT\n"
419
- full_file = (
420
- "from hooks_constants.other import PRE_EXISTING_UNUSED\n"
421
- "from hooks_constants.constants import FRAGMENT_IMPORT\n"
422
- "\n"
423
- "def existing_function() -> str:\n"
424
- " return FRAGMENT_IMPORT\n"
425
- )
426
- issues = check_unused_module_level_imports(
427
- fragment, PRODUCTION_FILE_PATH, full_file_content=full_file,
428
- )
429
- assert issues == [], (
430
- "Pre-existing imports outside the fragment must not be flagged on Edit; "
431
- f"got: {issues}"
432
- )
433
-
434
-
435
- def test_should_skip_when_full_file_declares_dunder_all() -> None:
436
- fragment = "from hooks_constants.constants import NEW_NAME\n"
437
- full_file = (
438
- "from hooks_constants.constants import NEW_NAME\n"
439
- "\n"
440
- "__all__ = ['NEW_NAME']\n"
441
- )
442
- issues = check_unused_module_level_imports(
443
- fragment, PRODUCTION_FILE_PATH, full_file_content=full_file,
444
- )
445
- assert issues == [], (
446
- "__all__ in the post-edit file must skip the scan even when the fragment "
447
- f"itself does not contain __all__, got: {issues}"
448
- )
449
-
450
-
451
- def test_should_flag_dead_fragment_import_when_full_file_uses_type_checking() -> None:
452
- fragment = "from hooks_constants.constants import NEW_NAME\n"
453
- full_file = (
454
- "from typing import TYPE_CHECKING\n"
455
- "from hooks_constants.constants import NEW_NAME\n"
456
- "\n"
457
- "if TYPE_CHECKING:\n"
458
- " from somewhere import OtherName\n"
459
- "\n"
460
- "def run() -> None:\n"
461
- " return None\n"
462
- )
463
- issues = check_unused_module_level_imports(
464
- fragment, PRODUCTION_FILE_PATH, full_file_content=full_file,
465
- )
466
- assert any("NEW_NAME" in each_issue for each_issue in issues), (
467
- "A TYPE_CHECKING block in the post-edit file must not exempt a dead "
468
- f"import added by the edit fragment, got: {issues}"
469
- )
470
-
471
-
472
- def test_should_fall_back_to_content_when_full_file_content_is_none() -> None:
473
- source = (
474
- "from hooks_constants.constants import VENV_DIRECTORY_NAME\n"
475
- "\n"
476
- "def run() -> None:\n"
477
- " return None\n"
478
- )
479
- issues = check_unused_module_level_imports(source, PRODUCTION_FILE_PATH, full_file_content=None)
480
- assert any("VENV_DIRECTORY_NAME" in each_issue for each_issue in issues), (
481
- f"Backward-compat: with full_file_content=None, behavior must match "
482
- f"the existing single-argument scan, got: {issues}"
483
- )
484
-
485
-
486
- def test_should_fall_back_when_full_file_content_has_syntax_error() -> None:
487
- fragment = "from hooks_constants.constants import NEW_NAME\n"
488
- full_file_with_syntax_error = "from config import (\n not python\n"
489
- issues = check_unused_module_level_imports(
490
- fragment, PRODUCTION_FILE_PATH, full_file_content=full_file_with_syntax_error,
491
- )
492
- assert issues == [], (
493
- "When the reconstructed post-edit content cannot be parsed, return empty "
494
- f"rather than raising, got: {issues}"
495
- )
496
-
497
-
498
- def test_reconstruct_post_edit_returns_replaced_content(tmp_path: pathlib.Path) -> None:
499
- target_file = tmp_path / "module.py"
500
- target_file.write_text("ALPHA = 1\nBETA = 2\n", encoding="utf-8")
501
- prior_content, post_edit = prior_and_post_edit_content(
502
- str(target_file), "BETA = 2", "BETA = 22\nGAMMA = 3",
503
- )
504
- assert prior_content == "ALPHA = 1\nBETA = 2\n"
505
- assert post_edit == "ALPHA = 1\nBETA = 22\nGAMMA = 3\n", (
506
- f"Helper must return the file body with the first occurrence replaced, got: {post_edit!r}"
507
- )
508
-
509
-
510
- def test_reconstruct_post_edit_returns_none_when_file_missing(tmp_path: pathlib.Path) -> None:
511
- missing_file = tmp_path / "does_not_exist.py"
512
- prior_content, post_edit = prior_and_post_edit_content(
513
- str(missing_file), "any_old", "any_new",
514
- )
515
- assert prior_content is None
516
- assert post_edit is None, (
517
- f"Missing file must yield None so the caller treats it as 'no full-file context', got: {post_edit!r}"
518
- )
519
-
520
-
521
- def test_reconstruct_post_edit_returns_none_when_old_string_absent(tmp_path: pathlib.Path) -> None:
522
- target_file = tmp_path / "module.py"
523
- target_file.write_text("ALPHA = 1\n", encoding="utf-8")
524
- prior_content, post_edit = prior_and_post_edit_content(
525
- str(target_file), "ZETA = 9", "OMEGA = 0",
526
- )
527
- assert prior_content is None
528
- assert post_edit is None, (
529
- f"Absent old_string means the Edit will not apply cleanly — return None, got: {post_edit!r}"
530
- )
531
-
532
-
533
- def test_reconstruct_post_edit_replaces_only_first_occurrence(tmp_path: pathlib.Path) -> None:
534
- target_file = tmp_path / "module.py"
535
- target_file.write_text("X = 1\nX = 1\n", encoding="utf-8")
536
- prior_content, post_edit = prior_and_post_edit_content(
537
- str(target_file), "X = 1", "X = 2",
538
- )
539
- assert prior_content == "X = 1\nX = 1\n"
540
- assert post_edit == "X = 2\nX = 1\n", (
541
- "Edit replaces only the first occurrence; helper must mirror that, got: "
542
- f"{post_edit!r}"
543
- )
544
-
545
-
546
- def test_should_flag_import_when_only_shadowed_local_name_is_loaded() -> None:
547
- source = (
548
- "import json\n"
549
- "\n"
550
- "def run(json: object) -> object:\n"
551
- " return json\n"
552
- )
553
- issues = check_unused_module_level_imports(source, PRODUCTION_FILE_PATH)
554
- assert any("json" in each_issue for each_issue in issues), (
555
- f"Local shadow bindings must not count as import references, got: {issues}"
556
- )
557
-
558
-
559
- def test_should_not_flag_guarded_import_under_imported_type_checking_alias() -> None:
560
- source = (
561
- "from typing import TYPE_CHECKING as IS_TYPE_CHECKING\n"
562
- "from hooks_constants.constants import UNUSED_NAME\n"
563
- "\n"
564
- "if IS_TYPE_CHECKING:\n"
565
- " from somewhere import OtherName\n"
566
- "\n"
567
- "def run() -> None:\n"
568
- " return None\n"
569
- )
570
- issues = check_unused_module_level_imports(source, PRODUCTION_FILE_PATH)
571
- assert not any("OtherName" in each_issue for each_issue in issues), (
572
- "An import guarded under an aliased TYPE_CHECKING is nested in the if "
573
- f"block and is never a flag candidate, got: {issues}"
574
- )
575
- assert any("UNUSED_NAME" in each_issue for each_issue in issues), (
576
- "A dead top-level import beside an aliased TYPE_CHECKING guard is still "
577
- f"flagged, got: {issues}"
578
- )
579
-
580
-
581
- def test_should_not_flag_guarded_import_under_module_type_checking_alias() -> None:
582
- source = (
583
- "import typing as t\n"
584
- "from hooks_constants.constants import UNUSED_NAME\n"
585
- "\n"
586
- "if t.TYPE_CHECKING:\n"
587
- " from somewhere import OtherName\n"
588
- "\n"
589
- "def run() -> None:\n"
590
- " return None\n"
591
- )
592
- issues = check_unused_module_level_imports(source, PRODUCTION_FILE_PATH)
593
- assert not any("OtherName" in each_issue for each_issue in issues), (
594
- "An import guarded under a module-qualified TYPE_CHECKING is nested in "
595
- f"the if block and is never a flag candidate, got: {issues}"
596
- )
597
- assert any("UNUSED_NAME" in each_issue for each_issue in issues), (
598
- "A dead top-level import beside a module-qualified TYPE_CHECKING guard "
599
- f"is still flagged, got: {issues}"
600
- )
601
-
602
-
603
- def test_should_not_flag_when_referenced_in_quoted_annotation() -> None:
604
- source = (
605
- "from typing import List\n"
606
- "\n"
607
- "def run(xs: \"List[int]\") -> None:\n"
608
- " return None\n"
609
- )
610
- issues = check_unused_module_level_imports(source, PRODUCTION_FILE_PATH)
611
- assert issues == [], (
612
- f"Quoted annotations must count as import references, got: {issues}"
613
- )
614
-
615
-
616
- def test_should_flag_when_noqa_only_appears_inside_string_literal() -> None:
617
- source = (
618
- "from hooks_constants.constants import UNUSED; MARKER = '# noqa: F401'\n"
619
- "\n"
620
- "def run() -> None:\n"
621
- " return None\n"
622
- )
623
- issues = check_unused_module_level_imports(source, PRODUCTION_FILE_PATH)
624
- assert any("UNUSED" in each_issue for each_issue in issues), (
625
- f"String literal noqa text must not suppress unused imports, got: {issues}"
626
- )
627
-
628
-
629
- def test_should_not_flag_when_class_body_binding_matches_import_used_in_method() -> None:
630
- source = (
631
- "import os\n"
632
- "\n"
633
- "class Foo:\n"
634
- " os = 'linux'\n"
635
- "\n"
636
- " def bar(self) -> str:\n"
637
- " return os.path.join('a', 'b')\n"
638
- )
639
- issues = check_unused_module_level_imports(source, PRODUCTION_FILE_PATH)
640
- assert issues == [], (
641
- f"Class body bindings must not shadow module-level imports inside methods, got: {issues}"
642
- )
643
-
644
-
645
- def test_should_not_flag_when_comprehension_variable_matches_import_used_after() -> None:
646
- source = (
647
- "import os\n"
648
- "\n"
649
- "def run() -> str:\n"
650
- " result = [x for os in [1, 2, 3]]\n"
651
- " return os.path.join('a', 'b')\n"
652
- )
653
- issues = check_unused_module_level_imports(source, PRODUCTION_FILE_PATH)
654
- assert issues == [], (
655
- f"Comprehension iteration variables must not shadow enclosing scope bindings, got: {issues}"
656
- )