claude-dev-env 2.33.0 → 2.33.2
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/_shared/pr-loop/scripts/code_rules_gate.py +11 -1
- package/_shared/pr-loop/scripts/test_code_rules_gate.py +5 -8
- package/_shared/pr-loop/scripts/tests/test_code_rules_gate.py +17 -0
- package/audit-rubrics/prompts/category-e-dead-code.md +1 -1
- package/docs/REACT_PATTERNS.md +1 -1
- package/hooks/blocking/code_rules_comments.py +91 -92
- package/hooks/blocking/code_rules_enforcer.py +3 -3
- package/hooks/blocking/javascript_comment_scanner.py +0 -28
- package/hooks/blocking/test_code_rules_enforcer_comment_string_awareness.py +16 -11
- package/hooks/blocking/test_code_rules_enforcer_javascript_comments.py +33 -3
- package/hooks/blocking/test_pre_tool_use_dispatcher.py +3 -5
- package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +0 -4
- package/hooks/hooks_constants/test_pre_tool_use_dispatcher_constants.py +0 -1
- package/package.json +1 -1
- package/scripts/check.ps1 +11 -2
- package/scripts/tests/test_rules.py +17 -0
|
@@ -251,6 +251,16 @@ def _report_empty_file_set() -> int:
|
|
|
251
251
|
return EMPTY_FILE_SET_EXIT_CODE
|
|
252
252
|
|
|
253
253
|
|
|
254
|
+
def _report_empty_diff() -> int:
|
|
255
|
+
"""Print inspected 0 and return 0 when git has no changed files.
|
|
256
|
+
|
|
257
|
+
HEAD equals the chosen base and nothing is untracked. That is a clean
|
|
258
|
+
tree, so the gate stays quiet.
|
|
259
|
+
"""
|
|
260
|
+
sys.stderr.write(INSPECTED_COUNT_MESSAGE.format(inspected_count=0) + "\n")
|
|
261
|
+
return 0
|
|
262
|
+
|
|
263
|
+
|
|
254
264
|
def _run_explicit_paths_mode(
|
|
255
265
|
validate_content: enforcer_loading.ValidateContentCallable,
|
|
256
266
|
arguments: argparse.Namespace,
|
|
@@ -327,7 +337,7 @@ def _run_diff_mode(
|
|
|
327
337
|
+ paths_from_git_untracked(repository_root)
|
|
328
338
|
)
|
|
329
339
|
if not all_candidate_paths:
|
|
330
|
-
return
|
|
340
|
+
return _report_empty_diff()
|
|
331
341
|
file_paths = filter_paths_under_prefixes(
|
|
332
342
|
all_candidate_paths, repository_root, arguments.only_under
|
|
333
343
|
)
|
|
@@ -96,26 +96,23 @@ def temporary_git_repository(tmp_path: Path) -> Path:
|
|
|
96
96
|
return repository_root
|
|
97
97
|
|
|
98
98
|
|
|
99
|
-
def
|
|
99
|
+
def test_diff_mode_with_empty_file_set_exits_zero_and_says_so(
|
|
100
100
|
temporary_git_repository: Path,
|
|
101
101
|
monkeypatch: pytest.MonkeyPatch,
|
|
102
102
|
capsys: pytest.CaptureFixture[str],
|
|
103
103
|
) -> None:
|
|
104
|
-
"""HEAD
|
|
104
|
+
"""HEAD equals base and nothing is untracked, so there is no diff.
|
|
105
105
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
Issue #62's contract: that run exits non-zero and says so, because a
|
|
109
|
-
silent pass over zero files is trusted like a real pass.
|
|
106
|
+
Report inspected 0 and exit 0. A bad merge base or a wrong directory
|
|
107
|
+
can also look empty, but a clean tree is the case this test covers.
|
|
110
108
|
"""
|
|
111
109
|
monkeypatch.chdir(temporary_git_repository)
|
|
112
110
|
|
|
113
111
|
exit_code = gate_module.main(["--base", "HEAD"])
|
|
114
112
|
|
|
115
113
|
captured = capsys.readouterr()
|
|
116
|
-
assert exit_code
|
|
114
|
+
assert exit_code == 0
|
|
117
115
|
assert "inspected 0 file(s)" in captured.err
|
|
118
|
-
assert "empty" in captured.err.lower()
|
|
119
116
|
|
|
120
117
|
|
|
121
118
|
def test_diff_mode_changes_outside_only_under_prefixes_exit_clean_and_report(
|
|
@@ -2949,6 +2949,23 @@ def test_diff_mode_untracked_png_does_not_flood_stderr(
|
|
|
2949
2949
|
assert "untracked_icon.png" not in captured.err
|
|
2950
2950
|
|
|
2951
2951
|
|
|
2952
|
+
def test_diff_mode_empty_diff_exits_zero(
|
|
2953
|
+
temporary_git_repository: Path,
|
|
2954
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
2955
|
+
capsys: pytest.CaptureFixture[str],
|
|
2956
|
+
) -> None:
|
|
2957
|
+
write_file(temporary_git_repository / "seed.txt", "seed\n")
|
|
2958
|
+
commit_all_files(temporary_git_repository, "seed")
|
|
2959
|
+
monkeypatch.chdir(temporary_git_repository)
|
|
2960
|
+
|
|
2961
|
+
exit_code = gate_module.main(["--base", "HEAD"])
|
|
2962
|
+
|
|
2963
|
+
captured = capsys.readouterr()
|
|
2964
|
+
assert exit_code == 0
|
|
2965
|
+
assert "inspected 0 file(s)" in captured.err
|
|
2966
|
+
assert gate_module.EMPTY_FILE_SET_MESSAGE not in captured.err
|
|
2967
|
+
|
|
2968
|
+
|
|
2952
2969
|
def test_diff_mode_image_only_surface_exits_zero_not_empty_file_set(
|
|
2953
2970
|
temporary_git_repository: Path,
|
|
2954
2971
|
monkeypatch: pytest.MonkeyPatch,
|
|
@@ -20,7 +20,7 @@ Inline the artifact under this section using the section types defined in the ch
|
|
|
20
20
|
**E1. New imports without references**
|
|
21
21
|
- Walk every `import` line introduced or modified by the artifact. For each, locate at least one body reference in the same file (function call, attribute access, type annotation, decorator, default-argument expression).
|
|
22
22
|
- Confirm `__all__` re-exports: if the file declares `__all__`, an import that appears only in `__all__` still counts as referenced; if no `__all__` is declared, the exemption is inert and must be stated as such.
|
|
23
|
-
- Confirm
|
|
23
|
+
- Confirm changed `# noqa` directives are removed or resolved. Leave directives on untouched lines outside the change scope.
|
|
24
24
|
- Confirm `TYPE_CHECKING` blocks: imports inside `if TYPE_CHECKING:` are referenced only by string-form annotations or runtime `typing.get_type_hints` consumers; verify at least one such consumer or mark the exemption inert.
|
|
25
25
|
- Constants-only or re-export-only files (no imports of their own) — state explicitly that there is nothing to sweep.
|
|
26
26
|
|
package/docs/REACT_PATTERNS.md
CHANGED
|
@@ -104,7 +104,7 @@ components/
|
|
|
104
104
|
|
|
105
105
|
- **Complete type annotations** - All props, state, and return types defined
|
|
106
106
|
- **No `any` types** - Use generics, union types, or `unknown` with type guards
|
|
107
|
-
- **
|
|
107
|
+
- **Resolve `@ts-ignore` and `@ts-expect-error`** instead of adding or justifying them
|
|
108
108
|
- **Functional components only** - No class components (except error boundaries)
|
|
109
109
|
- **Custom hooks for logic** - Separate business logic from UI
|
|
110
110
|
- **Props interfaces** - Define explicit Props type for every component
|
|
@@ -8,7 +8,6 @@ import tokenize
|
|
|
8
8
|
from collections import Counter
|
|
9
9
|
from collections.abc import Iterator
|
|
10
10
|
from pathlib import Path
|
|
11
|
-
from typing import cast
|
|
12
11
|
|
|
13
12
|
_blocking_directory = str(Path(__file__).resolve().parent)
|
|
14
13
|
_hooks_directory = str(Path(__file__).resolve().parent.parent)
|
|
@@ -100,49 +99,41 @@ def check_comments_javascript(content: str) -> list[str]:
|
|
|
100
99
|
return issues
|
|
101
100
|
|
|
102
101
|
|
|
103
|
-
def
|
|
102
|
+
def _comment_occurrences(
|
|
104
103
|
content: str, file_path: str, include_directive_comments: bool = False
|
|
105
|
-
) -> tuple[
|
|
106
|
-
"""
|
|
104
|
+
) -> tuple[list[tuple[str, int, bool]], bool]:
|
|
105
|
+
"""List comments as text, line number, and inline vs standalone.
|
|
107
106
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
Standalone comments are lines where the entire line is a comment.
|
|
107
|
+
Python tokenize failure returns an empty list and False. JavaScript
|
|
108
|
+
returns the shared scanner's list and True. Other extensions return
|
|
109
|
+
an empty list and True.
|
|
112
110
|
"""
|
|
113
111
|
extension = get_file_extension(file_path)
|
|
114
|
-
inline_comments: set[str] = set()
|
|
115
|
-
standalone_comments: set[str] = set()
|
|
116
|
-
if not content:
|
|
117
|
-
return inline_comments, standalone_comments
|
|
118
|
-
|
|
119
112
|
if extension in ALL_PYTHON_EXTENSIONS:
|
|
120
|
-
|
|
113
|
+
return _python_comment_occurrences(content, include_directive_comments)
|
|
114
|
+
if extension in ALL_JAVASCRIPT_EXTENSIONS:
|
|
115
|
+
all_occurrences = extract_javascript_comment_occurrences(
|
|
121
116
|
content, include_directive_comments
|
|
122
117
|
)
|
|
123
|
-
return
|
|
118
|
+
return [tuple(each_occurrence) for each_occurrence in all_occurrences], True
|
|
119
|
+
return [], True
|
|
124
120
|
|
|
125
|
-
if extension in ALL_JAVASCRIPT_EXTENSIONS:
|
|
126
|
-
all_occurrences: list[tuple[str, int, bool]] = cast(
|
|
127
|
-
list[tuple[str, int, bool]],
|
|
128
|
-
extract_javascript_comment_occurrences(content, include_directive_comments),
|
|
129
|
-
)
|
|
130
|
-
inline_comments = {
|
|
131
|
-
each_text
|
|
132
|
-
for each_text, _each_line_number, is_inline in all_occurrences
|
|
133
|
-
if is_inline
|
|
134
|
-
}
|
|
135
|
-
standalone_comments = {
|
|
136
|
-
each_text
|
|
137
|
-
for each_text, _each_line_number, is_inline in all_occurrences
|
|
138
|
-
if not is_inline
|
|
139
|
-
}
|
|
140
121
|
|
|
141
|
-
|
|
122
|
+
def extract_comment_texts(
|
|
123
|
+
content: str, file_path: str, include_directive_comments: bool = False
|
|
124
|
+
) -> tuple[set[str], set[str]]:
|
|
125
|
+
"""Extract normalized comment text strings from content for comparison."""
|
|
126
|
+
all_occurrences, _ = _comment_occurrences(
|
|
127
|
+
content, file_path, include_directive_comments
|
|
128
|
+
)
|
|
129
|
+
return (
|
|
130
|
+
{each_text for each_text, _each_line, each_is_inline in all_occurrences if each_is_inline},
|
|
131
|
+
{each_text for each_text, _each_line, each_is_inline in all_occurrences if not each_is_inline},
|
|
132
|
+
)
|
|
142
133
|
|
|
143
134
|
|
|
144
135
|
def _python_comment_occurrences(
|
|
145
|
-
content: str,
|
|
136
|
+
content: str, include_directive_comments: bool = True
|
|
146
137
|
) -> tuple[list[tuple[str, int, bool]], bool]:
|
|
147
138
|
"""Return Python comments with exact lines and a tokenize status."""
|
|
148
139
|
source_lines = content.split("\n")
|
|
@@ -156,6 +147,8 @@ def _python_comment_occurrences(
|
|
|
156
147
|
continue
|
|
157
148
|
if each_token.string.startswith("#!") and each_token.start == (1, 0):
|
|
158
149
|
continue
|
|
150
|
+
if not include_directive_comments and _is_exempt_python_comment(each_token):
|
|
151
|
+
continue
|
|
159
152
|
source_line = source_lines[each_token.start[0] - 1]
|
|
160
153
|
is_inline = bool(source_line[: each_token.start[1]].strip())
|
|
161
154
|
all_occurrences.append(
|
|
@@ -182,8 +175,8 @@ def check_comment_changes(old_content: str, new_content: str, file_path: str) ->
|
|
|
182
175
|
|
|
183
176
|
extension = get_file_extension(file_path)
|
|
184
177
|
if extension in ALL_PYTHON_EXTENSIONS:
|
|
185
|
-
old_occurrences, old_tokenize_ok = _python_comment_occurrences(old_content)
|
|
186
|
-
new_occurrences, new_tokenize_ok = _python_comment_occurrences(new_content)
|
|
178
|
+
old_occurrences, old_tokenize_ok = _python_comment_occurrences(old_content, True)
|
|
179
|
+
new_occurrences, new_tokenize_ok = _python_comment_occurrences(new_content, True)
|
|
187
180
|
if not (old_tokenize_ok and new_tokenize_ok):
|
|
188
181
|
return issues
|
|
189
182
|
else:
|
|
@@ -223,25 +216,70 @@ def _changed_line_numbers(old_content: str, new_content: str) -> set[int]:
|
|
|
223
216
|
}
|
|
224
217
|
|
|
225
218
|
|
|
219
|
+
def _deleted_line_numbers(old_content: str, new_content: str) -> set[int]:
|
|
220
|
+
"""Old-file line numbers that a delete or replace covers."""
|
|
221
|
+
matcher = difflib.SequenceMatcher(
|
|
222
|
+
None, old_content.splitlines(), new_content.splitlines(), autojunk=False
|
|
223
|
+
)
|
|
224
|
+
return {
|
|
225
|
+
each_line_number
|
|
226
|
+
for each_tag, each_old_start, each_old_end, _, _ in matcher.get_opcodes()
|
|
227
|
+
if each_tag in {"delete", "replace"}
|
|
228
|
+
for each_line_number in range(each_old_start + 1, each_old_end + 1)
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def _comment_lines_by_key(
|
|
233
|
+
all_occurrences: list[tuple[str, int, bool]],
|
|
234
|
+
) -> dict[tuple[str, bool], list[int]]:
|
|
235
|
+
"""Group line numbers by comment text and inline vs standalone."""
|
|
236
|
+
lines_by_key: dict[tuple[str, bool], list[int]] = {}
|
|
237
|
+
for each_text, each_line_number, each_is_inline in all_occurrences:
|
|
238
|
+
lines_by_key.setdefault((each_text, each_is_inline), []).append(each_line_number)
|
|
239
|
+
return lines_by_key
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def _is_attached_to_changed_code(
|
|
243
|
+
each_line_number: int,
|
|
244
|
+
each_is_inline: bool,
|
|
245
|
+
old_line_number: int,
|
|
246
|
+
all_changed_lines: set[int],
|
|
247
|
+
all_deleted_lines: set[int],
|
|
248
|
+
) -> bool:
|
|
249
|
+
"""Check if this leftover comment still sits on a changed line.
|
|
250
|
+
|
|
251
|
+
Inline comments count when their own line changed. Standalone comments
|
|
252
|
+
count when the next line changed, or when the next line in the old file
|
|
253
|
+
was deleted.
|
|
254
|
+
"""
|
|
255
|
+
if each_is_inline:
|
|
256
|
+
return each_line_number in all_changed_lines
|
|
257
|
+
return each_line_number + 1 in all_changed_lines or old_line_number + 1 in all_deleted_lines
|
|
258
|
+
|
|
259
|
+
|
|
226
260
|
def _retained_comment_issues(
|
|
227
261
|
old_content: str, new_content: str, file_path: str
|
|
228
262
|
) -> list[str]:
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
shared_standalone = old_standalone & new_standalone
|
|
233
|
-
if not shared_inline and not shared_standalone:
|
|
263
|
+
old_occurrences, old_tokenize_ok = _comment_occurrences(old_content, file_path, True)
|
|
264
|
+
new_occurrences, new_tokenize_ok = _comment_occurrences(new_content, file_path, True)
|
|
265
|
+
if not (old_tokenize_ok and new_tokenize_ok):
|
|
234
266
|
return []
|
|
235
|
-
|
|
236
|
-
|
|
267
|
+
all_changed_lines = _changed_line_numbers(old_content, new_content)
|
|
268
|
+
all_deleted_lines = _deleted_line_numbers(old_content, new_content)
|
|
269
|
+
old_line_by_key = _comment_lines_by_key(old_occurrences)
|
|
237
270
|
issues: list[str] = []
|
|
238
|
-
for each_line_number in
|
|
239
|
-
|
|
240
|
-
if
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
if
|
|
244
|
-
|
|
271
|
+
for each_text, each_line_number, each_is_inline in new_occurrences:
|
|
272
|
+
matching_old_lines = old_line_by_key.get((each_text, each_is_inline), [])
|
|
273
|
+
if not matching_old_lines:
|
|
274
|
+
continue
|
|
275
|
+
old_line_number = matching_old_lines.pop(0)
|
|
276
|
+
if not _is_attached_to_changed_code(each_line_number, each_is_inline, old_line_number, all_changed_lines, all_deleted_lines):
|
|
277
|
+
continue
|
|
278
|
+
comment_kind = "Inline" if each_is_inline else "Standalone"
|
|
279
|
+
issues.append(
|
|
280
|
+
f"Line {each_line_number}: {comment_kind} comment still on the changed lines: "
|
|
281
|
+
f"{each_text} - remove the comment"
|
|
282
|
+
)
|
|
245
283
|
if len(issues) >= MAX_COMMENT_ISSUES:
|
|
246
284
|
break
|
|
247
285
|
return issues[:MAX_COMMENT_ISSUES]
|
|
@@ -263,12 +301,11 @@ def _python_tokens(source: str) -> Iterator[tokenize.TokenInfo]:
|
|
|
263
301
|
def _comment_tokens(source: str) -> Iterator[tokenize.TokenInfo]:
|
|
264
302
|
"""Yield COMMENT tokens from *source* one at a time.
|
|
265
303
|
|
|
266
|
-
Streams from ``_python_tokens`` so consumers that early-exit
|
|
267
|
-
``check_comments_python``
|
|
268
|
-
materializing the entire token list.
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
that need it (``_extract_python_comment_sets``) bypass this helper.
|
|
304
|
+
Streams from ``_python_tokens`` so consumers that early-exit, such as
|
|
305
|
+
``check_comments_python`` capping at ``MAX_COMMENT_ISSUES``, avoid
|
|
306
|
+
materializing the entire token list. Tokenize failure stops the
|
|
307
|
+
stream with no extra flag. Callers that need to know tokenize failed
|
|
308
|
+
use ``_python_comment_occurrences``.
|
|
272
309
|
"""
|
|
273
310
|
try:
|
|
274
311
|
for each_token in _python_tokens(source):
|
|
@@ -356,41 +393,3 @@ def _starts_with_bounded_token_anchored_directive(directive_body: str) -> bool:
|
|
|
356
393
|
if next_character in ALL_TOKEN_ANCHORED_DIRECTIVE_BOUNDARY_CHARACTERS:
|
|
357
394
|
return True
|
|
358
395
|
return False
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
def _extract_python_comment_sets(
|
|
362
|
-
content: str, include_directive_comments: bool = False
|
|
363
|
-
) -> tuple[set[str], set[str], bool]:
|
|
364
|
-
"""Return (inline_comments, standalone_comments, tokenize_succeeded).
|
|
365
|
-
|
|
366
|
-
Streams *content* once via ``_python_tokens``. A tokenize failure
|
|
367
|
-
(mid-edit fragment, syntax error) returns empty sets and ``False``
|
|
368
|
-
so callers can treat the situation as indeterminate rather than as
|
|
369
|
-
"no comments present". Inline vs standalone is decided by inspecting
|
|
370
|
-
the column offset of each ``COMMENT`` token against its source
|
|
371
|
-
line: an all-whitespace prefix means standalone.
|
|
372
|
-
"""
|
|
373
|
-
inline_comments: set[str] = set()
|
|
374
|
-
standalone_comments: set[str] = set()
|
|
375
|
-
lines = content.split("\n")
|
|
376
|
-
try:
|
|
377
|
-
for each_token in _python_tokens(content):
|
|
378
|
-
if each_token.type != tokenize.COMMENT:
|
|
379
|
-
continue
|
|
380
|
-
is_shebang = each_token.string.startswith("#!") and each_token.start == (1, 0)
|
|
381
|
-
if is_shebang or (
|
|
382
|
-
not include_directive_comments and _is_exempt_python_comment(each_token)
|
|
383
|
-
):
|
|
384
|
-
continue
|
|
385
|
-
line_number = each_token.start[0]
|
|
386
|
-
column_offset = each_token.start[1]
|
|
387
|
-
source_line = lines[line_number - 1] if line_number - 1 < len(lines) else ""
|
|
388
|
-
text_before_comment = source_line[:column_offset]
|
|
389
|
-
normalized_comment_text = each_token.string.strip()
|
|
390
|
-
if not text_before_comment.strip():
|
|
391
|
-
standalone_comments.add(normalized_comment_text)
|
|
392
|
-
else:
|
|
393
|
-
inline_comments.add(normalized_comment_text)
|
|
394
|
-
except ALL_PYTHON_TOKENIZE_FAILURE_EXCEPTIONS:
|
|
395
|
-
return set(), set(), False
|
|
396
|
-
return inline_comments, standalone_comments, True
|
|
@@ -236,7 +236,7 @@ class _ValidationContext(NamedTuple):
|
|
|
236
236
|
defer_scope_to_caller: bool
|
|
237
237
|
sibling_directory: Path | None
|
|
238
238
|
phase: str
|
|
239
|
-
include_comment_policy: bool =
|
|
239
|
+
include_comment_policy: bool = False
|
|
240
240
|
|
|
241
241
|
|
|
242
242
|
def _validated_phase(phase: str) -> str:
|
|
@@ -653,7 +653,7 @@ def validate_content_for_phase(
|
|
|
653
653
|
sibling_directory: Path | None = None,
|
|
654
654
|
*,
|
|
655
655
|
phase: str,
|
|
656
|
-
include_comment_policy: bool =
|
|
656
|
+
include_comment_policy: bool = False,
|
|
657
657
|
) -> list[str]:
|
|
658
658
|
"""Run all applicable validators on content for one named validation phase."""
|
|
659
659
|
validated_phase = _validated_phase(phase)
|
|
@@ -703,7 +703,7 @@ def validate_content_for_full_gate(
|
|
|
703
703
|
defer_scope_to_caller: bool = False,
|
|
704
704
|
sibling_directory: Path | None = None,
|
|
705
705
|
*,
|
|
706
|
-
include_comment_policy: bool =
|
|
706
|
+
include_comment_policy: bool = False,
|
|
707
707
|
) -> list[str]:
|
|
708
708
|
"""Run the full-gate phase, including the six checks that read sibling files."""
|
|
709
709
|
return validate_content_for_phase(
|
|
@@ -1,14 +1,11 @@
|
|
|
1
1
|
"""Extract JavaScript comments without matching comment-like strings."""
|
|
2
2
|
|
|
3
|
-
import io
|
|
4
3
|
import re
|
|
5
|
-
import tokenize
|
|
6
4
|
from typing import NamedTuple
|
|
7
5
|
|
|
8
6
|
from hooks_constants.code_rules_enforcer_constants import (
|
|
9
7
|
ALL_JAVASCRIPT_EXEMPT_COMMENT_PREFIXES,
|
|
10
8
|
ALL_JAVASCRIPT_EXEMPT_INLINE_COMMENT_PREFIXES,
|
|
11
|
-
ALL_PYTHON_TOKENIZE_FAILURE_EXCEPTIONS,
|
|
12
9
|
)
|
|
13
10
|
|
|
14
11
|
|
|
@@ -72,28 +69,3 @@ def extract_javascript_comment_occurrences(
|
|
|
72
69
|
JavaScriptCommentOccurrence(comment_text, line_number, is_inline)
|
|
73
70
|
)
|
|
74
71
|
return all_occurrences
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
def extract_python_comment_occurrences(
|
|
78
|
-
content: str,
|
|
79
|
-
) -> tuple[list[JavaScriptCommentOccurrence], bool]:
|
|
80
|
-
"""Return Python comments with exact lines and a tokenize status."""
|
|
81
|
-
source_lines = content.split("\n")
|
|
82
|
-
try:
|
|
83
|
-
all_tokens = list(tokenize.generate_tokens(io.StringIO(content).readline))
|
|
84
|
-
except ALL_PYTHON_TOKENIZE_FAILURE_EXCEPTIONS:
|
|
85
|
-
return [], False
|
|
86
|
-
all_occurrences: list[JavaScriptCommentOccurrence] = []
|
|
87
|
-
for each_token in all_tokens:
|
|
88
|
-
if each_token.type != tokenize.COMMENT:
|
|
89
|
-
continue
|
|
90
|
-
if each_token.string.startswith("#!") and each_token.start == (1, 0):
|
|
91
|
-
continue
|
|
92
|
-
source_line = source_lines[each_token.start[0] - 1]
|
|
93
|
-
is_inline = bool(source_line[: each_token.start[1]].strip())
|
|
94
|
-
all_occurrences.append(
|
|
95
|
-
JavaScriptCommentOccurrence(
|
|
96
|
-
each_token.string.strip(), each_token.start[0], is_inline
|
|
97
|
-
)
|
|
98
|
-
)
|
|
99
|
-
return all_occurrences, True
|
|
@@ -208,7 +208,12 @@ def test_full_gate_blocks_python_inline_comment_in_test_file() -> None:
|
|
|
208
208
|
sibling_directory=None,
|
|
209
209
|
phase="edit",
|
|
210
210
|
)
|
|
211
|
-
issues = code_rules_enforcer_module.validate_content_for_full_gate(
|
|
211
|
+
issues = code_rules_enforcer_module.validate_content_for_full_gate(
|
|
212
|
+
context.content,
|
|
213
|
+
context.file_path,
|
|
214
|
+
context.old_content,
|
|
215
|
+
include_comment_policy=True,
|
|
216
|
+
)
|
|
212
217
|
assert any("Inline comment added" in each_issue for each_issue in issues)
|
|
213
218
|
|
|
214
219
|
|
|
@@ -223,7 +228,7 @@ def test_full_gate_blocks_javascript_inline_comment_in_test_file() -> None:
|
|
|
223
228
|
sibling_directory=None,
|
|
224
229
|
phase="edit",
|
|
225
230
|
)
|
|
226
|
-
issues = code_rules_enforcer_module.validate_content_for_full_gate(context.content, context.file_path, context.old_content)
|
|
231
|
+
issues = code_rules_enforcer_module.validate_content_for_full_gate(context.content, context.file_path, context.old_content, include_comment_policy=True)
|
|
227
232
|
assert any("Inline comment added" in each_issue for each_issue in issues)
|
|
228
233
|
|
|
229
234
|
|
|
@@ -238,7 +243,7 @@ def test_full_gate_blocks_javascript_inline_block_comment_in_test_file() -> None
|
|
|
238
243
|
sibling_directory=None,
|
|
239
244
|
phase="edit",
|
|
240
245
|
)
|
|
241
|
-
issues = code_rules_enforcer_module.validate_content_for_full_gate(context.content, context.file_path, context.old_content)
|
|
246
|
+
issues = code_rules_enforcer_module.validate_content_for_full_gate(context.content, context.file_path, context.old_content, include_comment_policy=True)
|
|
242
247
|
assert any("Inline comment added" in each_issue for each_issue in issues)
|
|
243
248
|
|
|
244
249
|
|
|
@@ -253,7 +258,7 @@ def test_full_gate_blocks_javascript_directive_comment_in_test_file() -> None:
|
|
|
253
258
|
sibling_directory=None,
|
|
254
259
|
phase="edit",
|
|
255
260
|
)
|
|
256
|
-
issues = code_rules_enforcer_module.validate_content_for_full_gate(context.content, context.file_path, context.old_content)
|
|
261
|
+
issues = code_rules_enforcer_module.validate_content_for_full_gate(context.content, context.file_path, context.old_content, include_comment_policy=True)
|
|
257
262
|
assert any("Inline comment added" in each_issue for each_issue in issues)
|
|
258
263
|
|
|
259
264
|
|
|
@@ -268,7 +273,7 @@ def test_full_gate_blocks_python_directive_comment_in_test_file() -> None:
|
|
|
268
273
|
sibling_directory=None,
|
|
269
274
|
phase="edit",
|
|
270
275
|
)
|
|
271
|
-
issues = code_rules_enforcer_module.validate_content_for_full_gate(context.content, context.file_path, context.old_content)
|
|
276
|
+
issues = code_rules_enforcer_module.validate_content_for_full_gate(context.content, context.file_path, context.old_content, include_comment_policy=True)
|
|
272
277
|
assert any("comment added" in each_issue.lower() for each_issue in issues)
|
|
273
278
|
|
|
274
279
|
|
|
@@ -286,7 +291,7 @@ def test_full_gate_blocks_changed_marker_comments_in_test_file(each_marker: str)
|
|
|
286
291
|
sibling_directory=None,
|
|
287
292
|
phase="edit",
|
|
288
293
|
)
|
|
289
|
-
issues = code_rules_enforcer_module.validate_content_for_full_gate(context.content, context.file_path, context.old_content)
|
|
294
|
+
issues = code_rules_enforcer_module.validate_content_for_full_gate(context.content, context.file_path, context.old_content, include_comment_policy=True)
|
|
290
295
|
assert any("comment added" in each_issue.lower() for each_issue in issues)
|
|
291
296
|
|
|
292
297
|
|
|
@@ -301,7 +306,7 @@ def test_full_gate_blocks_standalone_comment_in_test_file() -> None:
|
|
|
301
306
|
sibling_directory=None,
|
|
302
307
|
phase="edit",
|
|
303
308
|
)
|
|
304
|
-
issues = code_rules_enforcer_module.validate_content_for_full_gate(context.content, context.file_path, context.old_content)
|
|
309
|
+
issues = code_rules_enforcer_module.validate_content_for_full_gate(context.content, context.file_path, context.old_content, include_comment_policy=True)
|
|
305
310
|
assert any("Standalone comment added" in each_issue for each_issue in issues)
|
|
306
311
|
|
|
307
312
|
|
|
@@ -316,7 +321,7 @@ def test_full_gate_preserves_shebang_and_docstring_in_test_file() -> None:
|
|
|
316
321
|
sibling_directory=None,
|
|
317
322
|
phase="edit",
|
|
318
323
|
)
|
|
319
|
-
issues = code_rules_enforcer_module.validate_content_for_full_gate(context.content, context.file_path, context.old_content)
|
|
324
|
+
issues = code_rules_enforcer_module.validate_content_for_full_gate(context.content, context.file_path, context.old_content, include_comment_policy=True)
|
|
320
325
|
assert not any("comment added" in each_issue.lower() for each_issue in issues)
|
|
321
326
|
|
|
322
327
|
|
|
@@ -333,7 +338,7 @@ def test_full_gate_allows_existing_comment_removal_without_advisory(
|
|
|
333
338
|
sibling_directory=None,
|
|
334
339
|
phase="edit",
|
|
335
340
|
)
|
|
336
|
-
issues = code_rules_enforcer_module.validate_content_for_full_gate(context.content, context.file_path, context.old_content)
|
|
341
|
+
issues = code_rules_enforcer_module.validate_content_for_full_gate(context.content, context.file_path, context.old_content, include_comment_policy=True)
|
|
337
342
|
assert not any("Existing comment removed" in each_issue for each_issue in issues)
|
|
338
343
|
assert capsys.readouterr().err == ""
|
|
339
344
|
|
|
@@ -373,11 +378,11 @@ def test_check_comment_changes_blocks_retained_comments_on_changed_code(
|
|
|
373
378
|
file_path: str, old_content: str, new_content: str
|
|
374
379
|
) -> None:
|
|
375
380
|
issues = code_rules_enforcer.check_comment_changes(old_content, new_content, file_path)
|
|
376
|
-
assert any("
|
|
381
|
+
assert any("still on the changed lines" in each_issue for each_issue in issues)
|
|
377
382
|
|
|
378
383
|
|
|
379
384
|
def test_check_comment_changes_accepts_distant_untouched_comment() -> None:
|
|
380
385
|
old_content = "# TODO #999: distant note\n\ntotal = 1\n"
|
|
381
386
|
new_content = "# TODO #999: distant note\n\ntotal = 2\n"
|
|
382
387
|
issues = code_rules_enforcer.check_comment_changes(old_content, new_content, "totals.py")
|
|
383
|
-
assert not any("
|
|
388
|
+
assert not any("still on the changed lines" in each_issue for each_issue in issues)
|
|
@@ -13,9 +13,9 @@ if _BLOCKING_DIRECTORY not in sys.path:
|
|
|
13
13
|
if _HOOKS_DIRECTORY not in sys.path:
|
|
14
14
|
sys.path.insert(0, _HOOKS_DIRECTORY)
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
)
|
|
16
|
+
comment_rules_module = importlib.import_module("code_rules_comments")
|
|
17
|
+
check_comment_changes = comment_rules_module.check_comment_changes
|
|
18
|
+
code_rules_enforcer_module = importlib.import_module("code_rules_enforcer")
|
|
19
19
|
|
|
20
20
|
|
|
21
21
|
def test_check_comment_changes_ignores_javascript_url_string() -> None:
|
|
@@ -42,3 +42,33 @@ def test_check_comment_changes_reports_duplicate_identical_comment_occurrence()
|
|
|
42
42
|
)
|
|
43
43
|
|
|
44
44
|
assert any("Line 2: Inline comment added" in each_issue for each_issue in issues)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def test_check_comment_changes_does_not_match_comment_text_inside_changed_string() -> None:
|
|
48
|
+
issues = check_comment_changes(
|
|
49
|
+
"# note\n\ntotal = 1\n",
|
|
50
|
+
'"# note"\ntotal = 2\n',
|
|
51
|
+
"totals.py",
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
assert not any("still on the changed lines" in each_issue for each_issue in issues)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def test_check_comment_changes_flags_comment_attached_to_deleted_code() -> None:
|
|
58
|
+
issues = check_comment_changes(
|
|
59
|
+
"# attached\nold_total = 1\nnew_total = 2\n",
|
|
60
|
+
"# attached\nnew_total = 2\n",
|
|
61
|
+
"totals.py",
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
assert any("Standalone comment still on the changed lines" in each_issue for each_issue in issues)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def test_full_gate_skips_comment_policy_by_default() -> None:
|
|
68
|
+
issues = code_rules_enforcer_module.validate_content_for_full_gate(
|
|
69
|
+
"total = 1 # added\n",
|
|
70
|
+
"totals.py",
|
|
71
|
+
"total = 1\n",
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
assert not any("comment" in each_issue.lower() for each_issue in issues)
|
|
@@ -723,14 +723,12 @@ def test_dispatcher_edit_applies_both_groups() -> None:
|
|
|
723
723
|
"""
|
|
724
724
|
all_edit_entries = _applicable_entries_for_tool(EDIT_TOOL_NAME)
|
|
725
725
|
all_edit_script_paths = {each_entry.script_relative_path for each_entry in all_edit_entries}
|
|
726
|
-
assert "blocking/stale_comment_reference_blocker.py" in all_edit_script_paths
|
|
727
|
-
"stale_comment_reference_blocker belongs in the Edit applicable set"
|
|
728
|
-
)
|
|
726
|
+
assert "blocking/stale_comment_reference_blocker.py" not in all_edit_script_paths
|
|
729
727
|
assert "advisory/refactor_guard.py" in all_edit_script_paths, (
|
|
730
728
|
"refactor_guard is Edit-scoped and hosted, so it belongs in the Edit applicable set"
|
|
731
729
|
)
|
|
732
|
-
assert len(all_edit_entries) ==
|
|
733
|
-
f"expected
|
|
730
|
+
assert len(all_edit_entries) == 20, (
|
|
731
|
+
f"expected 20 Edit entries, got {len(all_edit_entries)}"
|
|
734
732
|
)
|
|
735
733
|
|
|
736
734
|
|
|
@@ -130,10 +130,6 @@ ALL_HOSTED_HOOK_ENTRIES: tuple[HostedHookEntry, ...] = (
|
|
|
130
130
|
applicable_tool_names=ALL_WRITE_EDIT_MULTI_EDIT_TOOL_NAMES,
|
|
131
131
|
native_module_name=STATE_DESCRIPTION_BLOCKER_MODULE_NAME,
|
|
132
132
|
),
|
|
133
|
-
HostedHookEntry(
|
|
134
|
-
script_relative_path="blocking/stale_comment_reference_blocker.py",
|
|
135
|
-
applicable_tool_names=frozenset({EDIT_TOOL_NAME, MULTI_EDIT_TOOL_NAME}),
|
|
136
|
-
),
|
|
137
133
|
HostedHookEntry(
|
|
138
134
|
script_relative_path="blocking/subprocess_budget_completeness.py",
|
|
139
135
|
applicable_tool_names=ALL_WRITE_EDIT_MULTI_EDIT_TOOL_NAMES,
|
|
@@ -165,7 +165,6 @@ _ALL_DEFERRED_LINT_SCRIPT_PATHS = (
|
|
|
165
165
|
"blocking/duplicate_rmtree_helper_blocker.py",
|
|
166
166
|
"validation/hook_format_validator.py",
|
|
167
167
|
"blocking/hook_prose_detector_consistency.py",
|
|
168
|
-
"blocking/stale_comment_reference_blocker.py",
|
|
169
168
|
"blocking/subprocess_budget_completeness.py",
|
|
170
169
|
"blocking/windows_rmtree_blocker.py",
|
|
171
170
|
"advisory/refactor_guard.py",
|
package/package.json
CHANGED
package/scripts/check.ps1
CHANGED
|
@@ -61,13 +61,22 @@ function Resolve-CommentPolicyBase {
|
|
|
61
61
|
}
|
|
62
62
|
throw "Comment-policy baseline '$ExplicitBase' does not resolve in the repository."
|
|
63
63
|
}
|
|
64
|
-
|
|
64
|
+
|
|
65
|
+
$headCommit = & git -C $repositoryRoot rev-parse --verify --quiet 'HEAD^{commit}' 2>$null
|
|
66
|
+
$ciBases = @($env:GITHUB_BASE_SHA, $env:GITHUB_EVENT_BEFORE)
|
|
67
|
+
foreach ($candidateBase in @($ciBases + 'origin/main' + 'HEAD^1')) {
|
|
68
|
+
if (-not $candidateBase) {
|
|
69
|
+
continue
|
|
70
|
+
}
|
|
65
71
|
$resolvedCandidateBase = & git -C $repositoryRoot rev-parse --verify --quiet "${candidateBase}^{commit}" 2>$null
|
|
66
72
|
if ($LASTEXITCODE -eq 0 -and $resolvedCandidateBase) {
|
|
73
|
+
if ($candidateBase -eq 'origin/main' -and $resolvedCandidateBase.Trim() -eq $headCommit.Trim()) {
|
|
74
|
+
continue
|
|
75
|
+
}
|
|
67
76
|
return $candidateBase
|
|
68
77
|
}
|
|
69
78
|
}
|
|
70
|
-
throw 'Comment-policy baseline does not resolve to origin/main or HEAD^1.'
|
|
79
|
+
throw 'Comment-policy baseline does not resolve to a CI base, origin/main, or HEAD^1.'
|
|
71
80
|
}
|
|
72
81
|
|
|
73
82
|
$commentPolicyBase = Resolve-CommentPolicyBase -ExplicitBase $CommentPolicyBase
|
|
@@ -187,3 +187,20 @@ def test_comment_policy_uses_changed_comment_names_and_type_directives() -> None
|
|
|
187
187
|
assert "production and test code" in code_quality
|
|
188
188
|
assert "comments tied to touched code are removed" in gate
|
|
189
189
|
assert "comments tied to untouched code remain unchanged" in gate
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def test_comment_policy_removes_directive_justification_guidance() -> None:
|
|
193
|
+
react_patterns = (
|
|
194
|
+
_PACKAGE_ROOT / "docs" / "REACT_PATTERNS.md"
|
|
195
|
+
).read_text(encoding="utf-8").lower()
|
|
196
|
+
category_e_prompt = (
|
|
197
|
+
_PACKAGE_ROOT
|
|
198
|
+
/ "audit-rubrics"
|
|
199
|
+
/ "prompts"
|
|
200
|
+
/ "category-e-dead-code.md"
|
|
201
|
+
).read_text(encoding="utf-8").lower()
|
|
202
|
+
|
|
203
|
+
assert "resolve `@ts-ignore` and `@ts-expect-error`" in react_patterns
|
|
204
|
+
assert "without clear justification" not in react_patterns
|
|
205
|
+
assert "changed `# noqa` directives are removed or resolved" in category_e_prompt
|
|
206
|
+
assert "every `# noqa` on an import line must be justified" not in category_e_prompt
|