claude-dev-env 1.44.0 → 1.45.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/CLAUDE.md +9 -0
  2. package/_shared/pr-loop/scripts/code_rules_gate.py +426 -85
  3. package/_shared/pr-loop/scripts/pr_loop_shared_constants/code_rules_gate_constants.py +20 -0
  4. package/_shared/pr-loop/scripts/tests/test_code_rules_gate.py +625 -21
  5. package/_shared/pr-loop/scripts/tests/test_code_rules_gate_constants.py +15 -0
  6. package/agents/clean-coder.md +7 -1
  7. package/agents/code-quality-agent.md +8 -5
  8. package/hooks/blocking/code_rules_enforcer.py +1562 -37
  9. package/hooks/blocking/open_questions_in_plans_blocker.py +249 -0
  10. package/hooks/blocking/test_code_rules_enforcer.py +1389 -0
  11. package/hooks/blocking/test_code_rules_enforcer_banned_noun_word.py +292 -0
  12. package/hooks/blocking/test_code_rules_enforcer_cap_meta.py +46 -8
  13. package/hooks/blocking/test_code_rules_enforcer_exempt_marker_chained.py +189 -0
  14. package/hooks/blocking/test_code_rules_enforcer_function_length.py +210 -0
  15. package/hooks/blocking/test_code_rules_enforcer_tests_isolate_home_temp.py +1512 -0
  16. package/hooks/blocking/test_code_rules_enforcer_unused_imports.py +9 -5
  17. package/hooks/blocking/test_open_questions_in_plans_blocker.py +790 -0
  18. package/hooks/hooks.json +10 -0
  19. package/hooks/hooks_constants/banned_identifiers_constants.py +19 -0
  20. package/hooks/hooks_constants/code_rules_enforcer_constants.py +129 -2
  21. package/hooks/hooks_constants/open_questions_in_plans_blocker_constants.py +35 -0
  22. package/hooks/hooks_constants/test_open_questions_in_plans_blocker_constants.py +125 -0
  23. package/package.json +1 -1
  24. package/skills/_shared/pr-loop/scripts/_path_resolver.py +34 -13
  25. package/skills/_shared/pr-loop/scripts/init_loop_state.py +1 -2
  26. package/skills/_shared/pr-loop/scripts/teardown_worktrees.py +1 -4
  27. package/skills/_shared/pr-loop/scripts/test__path_resolver.py +57 -0
  28. package/skills/_shared/pr-loop/scripts/test_init_loop_state.py +48 -0
  29. package/skills/_shared/pr-loop/scripts/test_teardown_worktrees.py +59 -0
  30. package/skills/bugteam/PROMPTS.md +48 -12
  31. package/skills/bugteam/reference/team-setup.md +4 -2
  32. package/skills/bugteam/scripts/bugteam_code_rules_gate.py +487 -76
  33. package/skills/bugteam/scripts/bugteam_scripts_constants/bugteam_code_rules_gate_constants.py +22 -1
  34. package/skills/bugteam/scripts/test_bugteam_code_rules_gate.py +597 -12
@@ -0,0 +1,292 @@
1
+ """Tests for ``check_banned_noun_word_boundary``.
2
+
3
+ Pattern class: identifiers embedding a CODE_RULES §5 banned noun word
4
+ (``result``, ``data``, ``output``, ``response``, ``value``, ``item``,
5
+ ``temp``) as a snake_case word part or camelCase word part inside a longer
6
+ identifier, even when the exact-match check would let them through. Cited
7
+ SYNTHESIS evidence: pa#143 F8 (``OUTPUT`` constant), pa#144 F19
8
+ (``HolidayPeakResult`` class), pa#143 F13 (``canned_results`` collection),
9
+ pa#136 F35 (snake_case variants).
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import importlib.util
15
+ import pathlib
16
+ import sys
17
+
18
+ _HOOK_DIR = pathlib.Path(__file__).parent
19
+ if str(_HOOK_DIR) not in sys.path:
20
+ sys.path.insert(0, str(_HOOK_DIR))
21
+
22
+ hook_spec = importlib.util.spec_from_file_location(
23
+ "code_rules_enforcer",
24
+ _HOOK_DIR / "code_rules_enforcer.py",
25
+ )
26
+ assert hook_spec is not None
27
+ assert hook_spec.loader is not None
28
+ hook_module = importlib.util.module_from_spec(hook_spec)
29
+ hook_spec.loader.exec_module(hook_module)
30
+ check_banned_noun_word_boundary = hook_module.check_banned_noun_word_boundary
31
+ validate_content = hook_module.validate_content
32
+ _identifier_word_parts = hook_module._identifier_word_parts
33
+ _find_banned_noun_word = hook_module._find_banned_noun_word
34
+
35
+ PRODUCTION_FILE_PATH = "packages/app/services/customer_pipeline.py"
36
+ TEST_FILE_PATH = "packages/app/services/test_customer_pipeline.py"
37
+ CONFIG_FILE_PATH = "packages/app/config/constants.py"
38
+ HOOK_INFRASTRUCTURE_PATH = "/packages/claude-dev-env/hooks/blocking/example.py"
39
+
40
+
41
+ def test_should_split_snake_case_identifier_into_lowercase_words() -> None:
42
+ assert _identifier_word_parts("canned_results") == ["canned", "results"]
43
+
44
+
45
+ def test_should_split_camel_case_identifier_into_lowercase_words() -> None:
46
+ assert _identifier_word_parts("HolidayPeakResult") == ["holiday", "peak", "result"]
47
+
48
+
49
+ def test_should_split_screaming_snake_case_into_lowercase_words() -> None:
50
+ assert _identifier_word_parts("SAFE_OUTPUT_PATH") == ["safe", "output", "path"]
51
+
52
+
53
+ def test_should_treat_consecutive_capitals_as_acronym_word() -> None:
54
+ assert _identifier_word_parts("XMLParser") == ["xml", "parser"]
55
+
56
+
57
+ def test_should_return_no_banned_word_for_clean_identifier() -> None:
58
+ assert _find_banned_noun_word("customer_pipeline") is None
59
+
60
+
61
+ def test_should_return_banned_word_for_camel_case_with_result_suffix() -> None:
62
+ assert _find_banned_noun_word("HolidayPeakResult") == "result"
63
+
64
+
65
+ def test_should_return_banned_word_for_snake_case_with_results_plural() -> None:
66
+ assert _find_banned_noun_word("canned_results") == "results"
67
+
68
+
69
+ def test_should_return_banned_word_for_screaming_snake_with_output() -> None:
70
+ assert _find_banned_noun_word("SAFE_OUTPUT_PATH") == "output"
71
+
72
+
73
+ def test_should_flag_class_name_containing_result_word() -> None:
74
+ source = "class HolidayPeakResult:\n pass\n"
75
+ issues = check_banned_noun_word_boundary(source, PRODUCTION_FILE_PATH)
76
+ assert any("HolidayPeakResult" in each_issue for each_issue in issues)
77
+
78
+
79
+ def test_should_flag_module_level_constant_with_output_word() -> None:
80
+ source = "SAFE_OUTPUT_PATH = '/tmp/x'\n"
81
+ issues = check_banned_noun_word_boundary(source, PRODUCTION_FILE_PATH)
82
+ assert any("SAFE_OUTPUT_PATH" in each_issue for each_issue in issues)
83
+
84
+
85
+ def test_should_flag_local_variable_with_results_word() -> None:
86
+ source = (
87
+ "def aggregate() -> list[int]:\n canned_results = [1, 2, 3]\n return canned_results\n"
88
+ )
89
+ issues = check_banned_noun_word_boundary(source, PRODUCTION_FILE_PATH)
90
+ assert any("canned_results" in each_issue for each_issue in issues)
91
+
92
+
93
+ def test_should_flag_function_parameter_with_response_word() -> None:
94
+ source = (
95
+ "def handle(cached_response: dict[str, int]) -> int:\n return len(cached_response)\n"
96
+ )
97
+ issues = check_banned_noun_word_boundary(source, PRODUCTION_FILE_PATH)
98
+ assert any("cached_response" in each_issue for each_issue in issues)
99
+
100
+
101
+ def test_should_not_flag_exact_match_banned_identifier() -> None:
102
+ source = "result = compute()\n"
103
+ issues = check_banned_noun_word_boundary(source, PRODUCTION_FILE_PATH)
104
+ assert issues == []
105
+
106
+
107
+ def test_should_not_flag_dunder_method_with_banned_word() -> None:
108
+ source = "class Foo:\n def __init_data__(self) -> None: pass\n"
109
+ issues = check_banned_noun_word_boundary(source, PRODUCTION_FILE_PATH)
110
+ assert issues == []
111
+
112
+
113
+ def test_should_skip_test_files() -> None:
114
+ source = "class HolidayPeakResult:\n pass\n"
115
+ issues = check_banned_noun_word_boundary(source, TEST_FILE_PATH)
116
+ assert issues == []
117
+
118
+
119
+ def test_should_skip_config_files() -> None:
120
+ source = "OUTPUT_DIR = '/tmp'\n"
121
+ issues = check_banned_noun_word_boundary(source, CONFIG_FILE_PATH)
122
+ assert issues == []
123
+
124
+
125
+ def test_should_skip_hook_infrastructure() -> None:
126
+ source = "OUTPUT_DIR = '/tmp'\n"
127
+ issues = check_banned_noun_word_boundary(source, HOOK_INFRASTRUCTURE_PATH)
128
+ assert issues == []
129
+
130
+
131
+ def test_should_skip_when_source_does_not_parse() -> None:
132
+ source = "def broken(:\n"
133
+ issues = check_banned_noun_word_boundary(source, PRODUCTION_FILE_PATH)
134
+ assert issues == []
135
+
136
+
137
+ def test_should_flag_response_count_parameter_on_method() -> None:
138
+ source = "class Foo:\n def bar(self, response_count: int) -> None: pass\n"
139
+ issues = check_banned_noun_word_boundary(source, PRODUCTION_FILE_PATH)
140
+ assert any("response_count" in each_issue for each_issue in issues)
141
+
142
+
143
+ def test_should_include_banned_word_in_message() -> None:
144
+ source = "PEAK_OUTPUTS = []\n"
145
+ issues = check_banned_noun_word_boundary(source, PRODUCTION_FILE_PATH)
146
+ assert issues
147
+ assert "outputs" in issues[0]
148
+
149
+
150
+ def test_terminal_fragment_reports_every_banned_noun_uncapped() -> None:
151
+ """On the terminal (non-deferred) path the check analyzes *content* as the
152
+ edited fragment, where every binding is in scope — so every banned-noun
153
+ binding is reported with no ceiling on the count."""
154
+ binding_count = 5
155
+ source = "".join(
156
+ f"BINDING_{each_index}_RESULT_PATH = {each_index}\n"
157
+ for each_index in range(binding_count)
158
+ )
159
+ issues = check_banned_noun_word_boundary(source, PRODUCTION_FILE_PATH)
160
+ assert len(issues) == binding_count
161
+
162
+
163
+ def test_deferred_path_returns_every_banned_noun_uncapped() -> None:
164
+ """When the gate sets the deferral flag the check returns every violation so
165
+ ``split_violations_by_scope`` can scope by added line and report the in-scope
166
+ set."""
167
+ binding_count = 5
168
+ source = "".join(
169
+ f"BINDING_{each_index}_RESULT_PATH = {each_index}\n"
170
+ for each_index in range(binding_count)
171
+ )
172
+ issues = check_banned_noun_word_boundary(
173
+ source, PRODUCTION_FILE_PATH, defer_scope_to_caller=True
174
+ )
175
+ assert len(issues) == binding_count
176
+
177
+
178
+ def test_should_flag_function_definition_with_data_word_in_name() -> None:
179
+ source = "def fetch_data_table() -> None:\n pass\n"
180
+ issues = check_banned_noun_word_boundary(source, PRODUCTION_FILE_PATH)
181
+ assert any("fetch_data_table" in each_issue for each_issue in issues)
182
+
183
+
184
+ def test_should_flag_import_from_author_chosen_alias_with_banned_word() -> None:
185
+ source = "from models import HolidayPeak as holiday_peak_result\n"
186
+ issues = check_banned_noun_word_boundary(source, PRODUCTION_FILE_PATH)
187
+ assert any("holiday_peak_result" in each_issue for each_issue in issues)
188
+
189
+
190
+ def test_should_not_flag_non_aliased_upstream_import_with_banned_word() -> None:
191
+ source = "from typing import ItemsView\n"
192
+ issues = check_banned_noun_word_boundary(source, PRODUCTION_FILE_PATH)
193
+ assert issues == []
194
+
195
+
196
+ def test_should_flag_import_renamed_with_banned_word() -> None:
197
+ source = "import legacy_helper as cached_response\n"
198
+ issues = check_banned_noun_word_boundary(source, PRODUCTION_FILE_PATH)
199
+ assert any("cached_response" in each_issue for each_issue in issues)
200
+
201
+
202
+ def test_should_skip_star_import_with_no_named_binding() -> None:
203
+ source = "from models import *\n"
204
+ issues = check_banned_noun_word_boundary(source, PRODUCTION_FILE_PATH)
205
+ assert issues == []
206
+
207
+
208
+ def test_should_not_flag_non_aliased_dotted_import_with_clean_first_segment() -> None:
209
+ source = "import analytics.data_pipeline\n"
210
+ issues = check_banned_noun_word_boundary(source, PRODUCTION_FILE_PATH)
211
+ assert issues == []
212
+
213
+
214
+ def test_should_not_flag_non_aliased_dotted_import_with_banned_first_segment() -> None:
215
+ source = "import data_pipeline.analytics\n"
216
+ issues = check_banned_noun_word_boundary(source, PRODUCTION_FILE_PATH)
217
+ assert issues == []
218
+
219
+
220
+ def test_should_flag_aliased_dotted_import_with_banned_word_in_alias() -> None:
221
+ source = "import analytics.pipeline as data_table\n"
222
+ issues = check_banned_noun_word_boundary(source, PRODUCTION_FILE_PATH)
223
+ assert any("data_table" in each_issue for each_issue in issues)
224
+
225
+
226
+ def test_should_flag_with_as_binding_target_with_banned_word() -> None:
227
+ source = (
228
+ "def load_payload() -> str:\n"
229
+ " with open('payload.json') as data_result:\n"
230
+ " return data_result.read()\n"
231
+ )
232
+ issues = check_banned_noun_word_boundary(source, PRODUCTION_FILE_PATH)
233
+ assert any("data_result" in each_issue for each_issue in issues)
234
+
235
+
236
+ EDIT_FRAGMENT_WITHOUT_BANNED_NAME = (
237
+ "def compute_total() -> int:\n running_sum = 0\n return running_sum\n"
238
+ )
239
+ FULL_FILE_WITH_BANNED_NAME_OUTSIDE_FRAGMENT = (
240
+ "def compute_total() -> int:\n running_sum = 0\n return running_sum\n"
241
+ "\n"
242
+ "def aggregate() -> list[int]:\n"
243
+ " canned_results = [4, 5, 6]\n"
244
+ " return canned_results\n"
245
+ )
246
+
247
+
248
+ def test_edit_drops_untouched_out_of_scope_banned_noun() -> None:
249
+ """An Edit that touches none of the banned-noun bindings reports nothing —
250
+ the check routes through the reconstructed effective content and the edit's
251
+ changed lines, exactly like ``check_function_length``, so an untouched
252
+ binding outside the edit hunk stays out of scope."""
253
+ prior_tail = (
254
+ "def compute_total() -> int:\n running_sum = 0\n return 0\n"
255
+ )
256
+ edited_tail = EDIT_FRAGMENT_WITHOUT_BANNED_NAME
257
+ prior_full_file = FULL_FILE_WITH_BANNED_NAME_OUTSIDE_FRAGMENT.replace(
258
+ EDIT_FRAGMENT_WITHOUT_BANNED_NAME, prior_tail
259
+ )
260
+ post_edit_full_file = FULL_FILE_WITH_BANNED_NAME_OUTSIDE_FRAGMENT
261
+ noun_issues = validate_content(
262
+ edited_tail,
263
+ PRODUCTION_FILE_PATH,
264
+ old_content=prior_tail,
265
+ full_file_content=post_edit_full_file,
266
+ prior_full_file_content=prior_full_file,
267
+ )
268
+ assert not any("canned_results" in each_issue for each_issue in noun_issues), (
269
+ "an untouched banned-noun binding outside the edit hunk must stay out of "
270
+ f"scope on a diff-scoped Edit; got {noun_issues!r}"
271
+ )
272
+
273
+
274
+ def test_edit_still_flags_banned_word_inside_changed_lines() -> None:
275
+ """An Edit whose changed lines introduce a banned-noun binding reports it,
276
+ using the reconstructed effective content and the edit's changed lines."""
277
+ edited_tail = (
278
+ "def aggregate() -> list[int]:\n"
279
+ " canned_results = [4, 5, 6]\n"
280
+ " return canned_results\n"
281
+ )
282
+ prior_tail = "def aggregate() -> list[int]:\n return []\n"
283
+ prior_full_file = EDIT_FRAGMENT_WITHOUT_BANNED_NAME + "\n" + prior_tail
284
+ post_edit_full_file = EDIT_FRAGMENT_WITHOUT_BANNED_NAME + "\n" + edited_tail
285
+ noun_issues = validate_content(
286
+ edited_tail,
287
+ PRODUCTION_FILE_PATH,
288
+ old_content=prior_tail,
289
+ full_file_content=post_edit_full_file,
290
+ prior_full_file_content=prior_full_file,
291
+ )
292
+ assert any("canned_results" in each_issue for each_issue in noun_issues)
@@ -8,11 +8,18 @@ payload when a single file has many violations of the same kind.
8
8
  The convention is: every check_* function should either apply an
9
9
  explicit cap (the meta-test treats a function as capped when its source
10
10
  contains a ``MAX_`` constant name or uses ``itertools.islice`` for bounded
11
- iteration), or be explicitly listed below as a known-uncapped check
12
- along with the reason, or appear in VOID_ADVISORY_CHECK_FUNCTION_NAMES
13
- when the function is annotated ``-> None`` and never contributes issues to the
14
- blocking payload (stderr-only advisories). New check_* functions added to the
15
- module without consideration will trip this test.
11
+ iteration), or appear in DIFF_SCOPED_CHECK_FUNCTION_NAMES when its blocking
12
+ payload is scoped by the diff: on a terminal Edit (``all_changed_lines`` is a
13
+ set) only violations whose span intersects the edit's changed lines block, so
14
+ untouched code cannot spam the payload; on a new-file or full-file write
15
+ (``all_changed_lines is None``) every violation is reported because the author
16
+ wrote the whole file. The scoping bounds the Edit payload to the change the
17
+ author is making rather than imposing a fixed ceiling. A function may instead
18
+ be explicitly listed in KNOWN_UNCAPPED_CHECKS_PENDING_REVIEW along with the
19
+ reason, or appear in VOID_ADVISORY_CHECK_FUNCTION_NAMES when the function is
20
+ annotated ``-> None`` and never contributes issues to the blocking payload
21
+ (stderr-only advisories). New check_* functions added to the module without
22
+ consideration will trip this test.
16
23
  """
17
24
 
18
25
  from __future__ import annotations
@@ -43,6 +50,14 @@ VOID_ADVISORY_CHECK_FUNCTION_NAMES: frozenset[str] = frozenset(
43
50
  }
44
51
  )
45
52
 
53
+ DIFF_SCOPED_CHECK_FUNCTION_NAMES: frozenset[str] = frozenset(
54
+ {
55
+ "check_banned_noun_word_boundary",
56
+ "check_function_length",
57
+ "check_tests_use_isolated_filesystem_paths",
58
+ }
59
+ )
60
+
46
61
  KNOWN_UNCAPPED_CHECKS_PENDING_REVIEW: frozenset[str] = frozenset(
47
62
  {
48
63
  "check_boolean_naming",
@@ -102,13 +117,16 @@ def test_every_check_function_either_caps_or_is_explicitly_pending() -> None:
102
117
  uncapped_check_names
103
118
  - KNOWN_UNCAPPED_CHECKS_PENDING_REVIEW
104
119
  - VOID_ADVISORY_CHECK_FUNCTION_NAMES
120
+ - DIFF_SCOPED_CHECK_FUNCTION_NAMES
105
121
  )
106
122
  assert unexpected_uncapped == set(), (
107
123
  f"New check_* functions added without a cap and not on the pending-review list: "
108
124
  f"{sorted(unexpected_uncapped)}. Either add a MAX_* cap or islice-bounded loop in "
109
- f"source, or explicitly add the function to KNOWN_UNCAPPED_CHECKS_PENDING_REVIEW with "
110
- f"a reason in the test header docstring, or list it in VOID_ADVISORY_CHECK_FUNCTION_NAMES "
111
- f"when it is annotated -> None and emits only stderr guidance."
125
+ f"source, or add the function to DIFF_SCOPED_CHECK_FUNCTION_NAMES when its blocking "
126
+ f"payload is bounded by diff scoping, or explicitly add it to "
127
+ f"KNOWN_UNCAPPED_CHECKS_PENDING_REVIEW with a reason in the test header docstring, or "
128
+ f"list it in VOID_ADVISORY_CHECK_FUNCTION_NAMES when it is annotated -> None and emits "
129
+ f"only stderr guidance."
112
130
  )
113
131
 
114
132
 
@@ -157,6 +175,26 @@ def test_void_advisory_checks_are_registered_and_disjoint() -> None:
157
175
  )
158
176
 
159
177
 
178
+ def test_diff_scoped_checks_are_registered_and_disjoint() -> None:
179
+ all_check_names = set(_all_check_function_names())
180
+ assert DIFF_SCOPED_CHECK_FUNCTION_NAMES <= all_check_names, (
181
+ f"DIFF_SCOPED_CHECK_FUNCTION_NAMES references missing names: "
182
+ f"{sorted(DIFF_SCOPED_CHECK_FUNCTION_NAMES - all_check_names)}"
183
+ )
184
+ for each_name in DIFF_SCOPED_CHECK_FUNCTION_NAMES:
185
+ function_source = inspect.getsource(getattr(_hook_module, each_name))
186
+ assert "all_changed_lines" in function_source or "defer_scope_to_caller" in function_source, (
187
+ f"DIFF_SCOPED_CHECK_FUNCTION_NAMES must list only checks bounded by diff scoping. "
188
+ f"{each_name!r} references neither all_changed_lines nor defer_scope_to_caller."
189
+ )
190
+ pending_overlap = DIFF_SCOPED_CHECK_FUNCTION_NAMES & KNOWN_UNCAPPED_CHECKS_PENDING_REVIEW
191
+ void_overlap = DIFF_SCOPED_CHECK_FUNCTION_NAMES & VOID_ADVISORY_CHECK_FUNCTION_NAMES
192
+ assert pending_overlap == set() and void_overlap == set(), (
193
+ f"Diff-scoped checks must not also appear on the pending or void-advisory lists: "
194
+ f"pending {sorted(pending_overlap)}, void {sorted(void_overlap)}"
195
+ )
196
+
197
+
160
198
  def test_already_capped_checks_stay_capped() -> None:
161
199
  capped_baseline: frozenset[str] = frozenset(
162
200
  {
@@ -0,0 +1,189 @@
1
+ """Tests for token-anchored exempt-marker behavior in `_is_exempt_python_comment`.
2
+
3
+ A bare ``#`` appearing after a token-anchored exempt marker (``noqa``,
4
+ ``pylint:``, ``pragma:``) marks the trailing text as a separate inline comment
5
+ that the no-new-comments rule must catch. The chained ``#`` triggers detection
6
+ whether or not it carries surrounding whitespace — glued directly to the
7
+ directive (``# noqa: F401#note``), lacking a trailing space (``# noqa: F401
8
+ #prose``), or padded on both sides (``# noqa: F401 # prose``) all fire.
9
+ Free-form markers (``type:``, ``TODO``, ``FIXME``, ``HACK``, ``XXX``) keep their
10
+ permissive behavior because ``# type:`` participates in the justification
11
+ convention enforced by ``check_type_escape_hatches`` and the TODO-family markers
12
+ carry annotation text by convention.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import importlib.util
18
+ import pathlib
19
+ import sys
20
+ import tokenize
21
+
22
+ _HOOK_DIR = pathlib.Path(__file__).parent
23
+ if str(_HOOK_DIR) not in sys.path:
24
+ sys.path.insert(0, str(_HOOK_DIR))
25
+
26
+ hook_spec = importlib.util.spec_from_file_location(
27
+ "code_rules_enforcer",
28
+ _HOOK_DIR / "code_rules_enforcer.py",
29
+ )
30
+ assert hook_spec is not None
31
+ assert hook_spec.loader is not None
32
+ hook_module = importlib.util.module_from_spec(hook_spec)
33
+ hook_spec.loader.exec_module(hook_module)
34
+ _is_exempt_python_comment = hook_module._is_exempt_python_comment
35
+ check_comments_python = hook_module.check_comments_python
36
+
37
+
38
+ FIXTURE_INLINE_COMMENT_LINE = 5
39
+ FIXTURE_INLINE_COMMENT_COLUMN = 4
40
+
41
+
42
+ def _build_comment_token(comment_text: str) -> tokenize.TokenInfo:
43
+ return tokenize.TokenInfo(
44
+ type=tokenize.COMMENT,
45
+ string=comment_text,
46
+ start=(FIXTURE_INLINE_COMMENT_LINE, FIXTURE_INLINE_COMMENT_COLUMN),
47
+ end=(FIXTURE_INLINE_COMMENT_LINE, FIXTURE_INLINE_COMMENT_COLUMN + len(comment_text)),
48
+ line=comment_text + "\n",
49
+ )
50
+
51
+
52
+ def test_should_exempt_bare_noqa_directive() -> None:
53
+ token = _build_comment_token("# noqa: F401")
54
+ assert _is_exempt_python_comment(token) is True
55
+
56
+
57
+ def test_should_flag_noqa_with_chained_inline_comment() -> None:
58
+ token = _build_comment_token("# noqa: F401 # imported for re-export")
59
+ assert _is_exempt_python_comment(token) is False
60
+
61
+
62
+ def test_should_flag_noqa_prefixed_prose_lacking_token_boundary() -> None:
63
+ token = _build_comment_token("# noqa-but-not-really: explanation")
64
+ assert _is_exempt_python_comment(token) is False
65
+
66
+
67
+ def test_should_exempt_bare_noqa_without_code() -> None:
68
+ token = _build_comment_token("# noqa")
69
+ assert _is_exempt_python_comment(token) is True
70
+
71
+
72
+ def test_should_exempt_noqa_followed_by_whitespace_then_code() -> None:
73
+ token = _build_comment_token("# noqa F401")
74
+ assert _is_exempt_python_comment(token) is True
75
+
76
+
77
+ def test_should_flag_noqa_with_chained_hash_glued_directly_to_directive() -> None:
78
+ token = _build_comment_token("# noqa: F401#note")
79
+ assert _is_exempt_python_comment(token) is False
80
+
81
+
82
+ def test_should_flag_noqa_with_chained_hash_lacking_trailing_space() -> None:
83
+ token = _build_comment_token("# noqa: F401 #prose")
84
+ assert _is_exempt_python_comment(token) is False
85
+
86
+
87
+ def test_should_flag_pragma_with_chained_hash_glued_directly_to_directive() -> None:
88
+ token = _build_comment_token("# pragma: no cover#why")
89
+ assert _is_exempt_python_comment(token) is False
90
+
91
+
92
+ def test_should_flag_pylint_with_chained_hash_lacking_trailing_space() -> None:
93
+ token = _build_comment_token("# pylint: disable=line-too-long #prose")
94
+ assert _is_exempt_python_comment(token) is False
95
+
96
+
97
+ def test_should_exempt_bare_pylint_directive() -> None:
98
+ token = _build_comment_token("# pylint: disable=line-too-long")
99
+ assert _is_exempt_python_comment(token) is True
100
+
101
+
102
+ def test_should_flag_pylint_with_chained_inline_comment() -> None:
103
+ token = _build_comment_token("# pylint: disable=line-too-long # see SO answer")
104
+ assert _is_exempt_python_comment(token) is False
105
+
106
+
107
+ def test_should_exempt_pylint_directive_without_space_after_colon() -> None:
108
+ token = _build_comment_token("# pylint:disable=unused-import")
109
+ assert _is_exempt_python_comment(token) is True
110
+
111
+
112
+ def test_should_exempt_pragma_directive_without_space_after_colon() -> None:
113
+ token = _build_comment_token("# pragma:no-cover")
114
+ assert _is_exempt_python_comment(token) is True
115
+
116
+
117
+ def test_should_exempt_type_ignore_without_space_after_colon() -> None:
118
+ token = _build_comment_token("# type:ignore")
119
+ assert _is_exempt_python_comment(token) is True
120
+
121
+
122
+ def test_should_flag_noqa_glued_prose_lacking_real_boundary() -> None:
123
+ token = _build_comment_token("# noqaFOO")
124
+ assert _is_exempt_python_comment(token) is False
125
+
126
+
127
+ def test_should_exempt_bare_pragma_directive() -> None:
128
+ token = _build_comment_token("# pragma: no cover")
129
+ assert _is_exempt_python_comment(token) is True
130
+
131
+
132
+ def test_should_flag_pragma_with_chained_inline_comment() -> None:
133
+ token = _build_comment_token("# pragma: no cover # only exercised in nightly")
134
+ assert _is_exempt_python_comment(token) is False
135
+
136
+
137
+ def test_should_still_exempt_type_ignore_with_trailing_justification() -> None:
138
+ token = _build_comment_token("# type: ignore[misc] # stubs missing in foo library")
139
+ assert _is_exempt_python_comment(token) is True
140
+
141
+
142
+ def test_should_exempt_bare_type_ignore_directive() -> None:
143
+ token = _build_comment_token("# type: ignore")
144
+ assert _is_exempt_python_comment(token) is True
145
+
146
+
147
+ def test_should_exempt_todo_with_trailing_prose() -> None:
148
+ token = _build_comment_token("# TODO: rename after deprecation period")
149
+ assert _is_exempt_python_comment(token) is True
150
+
151
+
152
+ def test_should_exempt_fixme_with_trailing_prose() -> None:
153
+ token = _build_comment_token("# FIXME(jdoe): retry budget feels wrong")
154
+ assert _is_exempt_python_comment(token) is True
155
+
156
+
157
+ def test_should_exempt_shebang_at_line_one_column_zero() -> None:
158
+ token = tokenize.TokenInfo(
159
+ type=tokenize.COMMENT,
160
+ string="#!/usr/bin/env python3",
161
+ start=(1, 0),
162
+ end=(1, 22),
163
+ line="#!/usr/bin/env python3\n",
164
+ )
165
+ assert _is_exempt_python_comment(token) is True
166
+
167
+
168
+ def test_should_flag_shebang_lookalike_off_line_one() -> None:
169
+ token = tokenize.TokenInfo(
170
+ type=tokenize.COMMENT,
171
+ string="#!/usr/bin/env python3",
172
+ start=(42, 0),
173
+ end=(42, 22),
174
+ line="#!/usr/bin/env python3\n",
175
+ )
176
+ assert _is_exempt_python_comment(token) is False
177
+
178
+
179
+ def test_check_comments_python_flags_chained_noqa_inline_comment() -> None:
180
+ source = "x = 1 # noqa: F401 # this is just a description\n"
181
+ issues = check_comments_python(source)
182
+ assert issues, "chained noqa inline comment must be flagged"
183
+ assert "Line 1" in issues[0]
184
+
185
+
186
+ def test_check_comments_python_does_not_flag_justified_type_ignore() -> None:
187
+ source = "x = some_value() # type: ignore[arg-type] # third-party stub gap\n"
188
+ issues = check_comments_python(source)
189
+ assert issues == []