claude-dev-env 2.32.0 → 2.33.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.
- package/.agents/agents/clean-coder.md +8 -7
- package/.agents/agents/code-quality-agent.md +4 -2
- package/.agents/agents/pr-description-writer.md +8 -3
- package/.agents/agents/reference/pr-description-illustrative-voice.md +6 -1
- package/.agents/agents/test_agent_frontmatter.py +0 -9
- package/.agents/agents/test_pr_description_writer_style.py +24 -1
- package/.agents/skills/e-simplify/SKILL.md +1 -15
- package/.agents/skills/grok-spawn/reference/worker-briefs.md +4 -0
- package/.agents/skills/pr-refinement/SKILL.md +2 -2
- package/.agents/skills/pr-shared-extraction/SKILL.md +2 -2
- package/.agents/skills/pr-title-description/SKILL.md +6 -2
- package/.agents/skills/review-tier/SKILL.md +1 -1
- package/.agents/skills/review-tier/reference/tier-policy.json +2 -2
- package/.agents/skills/review-tier/scripts/review_tier.py +31 -4
- package/.agents/skills/review-tier/scripts/test_generation.py +7 -19
- package/.agents/skills/review-tier/scripts/test_review_tier.py +11 -11
- package/AGENTS.md +83 -0
- package/_shared/pr-loop/code-rules-gate.md +5 -2
- package/_shared/pr-loop/scripts/code_rules_gate.py +7 -3
- package/_shared/pr-loop/scripts/code_rules_gate_parts/gate_arguments.py +13 -1
- package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_gate_arguments.py +7 -0
- package/_shared/pr-loop/scripts/tests/test_code_rules_gate.py +47 -1
- package/audit-rubrics/audit-categories.json +2 -2
- package/audit-rubrics/category_rubrics/category-e-dead-code.md +1 -1
- package/audit-rubrics/category_rubrics/category-j-code-rules-compliance.md +5 -3
- package/audit-rubrics/category_rubrics/category-l-behavior-equivalence.md +6 -3
- package/audit-rubrics/prompts/category-e-dead-code.md +7 -7
- package/audit-rubrics/prompts/category-j-code-rules-compliance.md +25 -19
- package/audit-rubrics/prompts/category-l-behavior-equivalence.md +28 -25
- package/docs/CODE_RULES.md +7 -8
- package/docs/REACT_PATTERNS.md +1 -1
- package/hooks/blocking/code_rules_comments.py +118 -59
- package/hooks/blocking/code_rules_enforcer.py +19 -7
- package/hooks/blocking/code_rules_type_escape.py +16 -23
- package/hooks/blocking/javascript_comment_scanner.py +99 -0
- package/hooks/blocking/test_code_rules_enforcer_any_type_ignore.py +24 -4
- package/hooks/blocking/test_code_rules_enforcer_cap_meta.py +0 -1
- package/hooks/blocking/test_code_rules_enforcer_comment_string_awareness.py +190 -3
- package/hooks/blocking/test_code_rules_enforcer_javascript_comments.py +44 -0
- package/hooks/blocking/test_code_rules_enforcer_join_separator_magic.py +0 -1
- package/hooks/git-hooks/pull_request_handoff.py +1 -1
- package/hooks/git-hooks/test_post_commit.py +8 -0
- package/hooks/hooks_constants/stale_comment_reference_blocker_constants.py +3 -4
- package/hooks/hooks_constants/test_unused_module_import_constants.py +9 -1
- package/hooks/hooks_constants/unused_module_import_constants.py +1 -1
- package/hooks/validators/comment_checks.py +2 -2
- package/hooks/validators/fast_save_validators.py +0 -9
- package/hooks/validators/git_checks.py +0 -84
- package/hooks/validators/run_all_validators.py +2 -9
- package/hooks/validators/test_fast_save_validators.py +2 -8
- package/hooks/validators/test_git_checks.py +0 -249
- package/hooks/validators/test_todo_checks.py +1 -0
- package/hooks/validators/todo_checks.py +1 -1
- package/package.json +1 -1
- package/rules/git-workflow.md +0 -8
- package/scripts/check.ps1 +35 -1
- package/scripts/codex_compat_materializer.py +1 -0
- package/scripts/tests/test_rules.py +128 -0
- package/system-prompts/software-engineer.xml +6 -19
- package/.agents/skills-archived/split-pr/SKILL.md +0 -85
- package/.agents/skills-archived/split-pr/reference/path-layers.md +0 -16
- package/.agents/skills-archived/split-pr/reference/proposal-format.md +0 -15
- package/.agents/skills-archived/split-pr/reference/split-further-loop.md +0 -10
- package/.agents/skills-archived/split-pr/reference/splitting-principles.md +0 -26
- package/.agents/skills-archived/split-pr/scripts/analyze_pr.py +0 -279
- package/.agents/skills-archived/split-pr/scripts/categorize_files.py +0 -106
- package/.agents/skills-archived/split-pr/scripts/config/__init__.py +0 -1
- package/.agents/skills-archived/split-pr/scripts/config/dependency_constants.py +0 -14
- package/.agents/skills-archived/split-pr/scripts/config/git_operations_constants.py +0 -36
- package/.agents/skills-archived/split-pr/scripts/config/packing_constants.py +0 -61
- package/.agents/skills-archived/split-pr/scripts/config/plan_constants.py +0 -49
- package/.agents/skills-archived/split-pr/scripts/config/split_pr_constants.py +0 -110
- package/.agents/skills-archived/split-pr/scripts/execute_split_slices.py +0 -82
- package/.agents/skills-archived/split-pr/scripts/pack_files_into_slices.py +0 -212
- package/.agents/skills-archived/split-pr/scripts/split_pr_dependency_graph.py +0 -70
- package/.agents/skills-archived/split-pr/scripts/split_pr_git_operations.py +0 -184
- package/.agents/skills-archived/split-pr/scripts/split_pr_layer_order.py +0 -58
- package/.agents/skills-archived/split-pr/scripts/split_pr_paginate.py +0 -119
- package/.agents/skills-archived/split-pr/scripts/split_pr_process_runner.py +0 -52
- package/.agents/skills-archived/split-pr/scripts/split_pr_script_types.py +0 -126
- package/.agents/skills-archived/split-pr/scripts/split_pr_title.py +0 -41
- package/.agents/skills-archived/split-pr/scripts/test_analyze_pr.py +0 -228
- package/.agents/skills-archived/split-pr/scripts/test_categorize_files.py +0 -55
- package/.agents/skills-archived/split-pr/scripts/test_categorize_files_packing.py +0 -59
- package/.agents/skills-archived/split-pr/scripts/test_execute_split_slices.py +0 -99
- package/.agents/skills-archived/split-pr/scripts/test_split_pr_dependency_graph.py +0 -47
- package/.agents/skills-archived/split-pr/scripts/test_split_pr_git_operations.py +0 -125
- package/.agents/skills-archived/split-pr/scripts/test_split_pr_layer_order.py +0 -36
- package/.agents/skills-archived/split-pr/scripts/test_split_pr_paginate.py +0 -65
- package/.agents/skills-archived/split-pr/scripts/test_split_pr_script_types.py +0 -73
- package/.agents/skills-archived/split-pr/scripts/test_split_pr_title.py +0 -28
- package/.agents/skills-archived/split-pr/scripts/test_verify_dependency_graph.py +0 -46
- package/.agents/skills-archived/split-pr/scripts/test_verify_plan.py +0 -56
- package/.agents/skills-archived/split-pr/scripts/test_verify_plan_contract.py +0 -50
- package/.agents/skills-archived/split-pr/scripts/test_verify_plan_path_normalization.py +0 -45
- package/.agents/skills-archived/split-pr/scripts/verify_dependency_graph.py +0 -111
- package/.agents/skills-archived/split-pr/scripts/verify_plan.py +0 -139
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
"""Comment-presence and comment-change checks for Python and JavaScript sources."""
|
|
2
2
|
|
|
3
|
+
import difflib
|
|
4
|
+
import importlib
|
|
3
5
|
import io
|
|
4
6
|
import sys
|
|
5
7
|
import tokenize
|
|
8
|
+
from collections import Counter
|
|
6
9
|
from collections.abc import Iterator
|
|
7
10
|
from pathlib import Path
|
|
11
|
+
from typing import cast
|
|
8
12
|
|
|
9
13
|
_blocking_directory = str(Path(__file__).resolve().parent)
|
|
10
14
|
_hooks_directory = str(Path(__file__).resolve().parent.parent)
|
|
@@ -20,7 +24,6 @@ from code_rules_shared import ( # noqa: E402
|
|
|
20
24
|
from hooks_constants.code_rules_enforcer_constants import ( # noqa: E402
|
|
21
25
|
ALL_FREE_FORM_EXEMPT_COMMENT_BODIES,
|
|
22
26
|
ALL_JAVASCRIPT_EXEMPT_COMMENT_PREFIXES,
|
|
23
|
-
ALL_JAVASCRIPT_EXEMPT_INLINE_COMMENT_PREFIXES,
|
|
24
27
|
ALL_JAVASCRIPT_EXTENSIONS,
|
|
25
28
|
ALL_PYTHON_EXTENSIONS,
|
|
26
29
|
ALL_PYTHON_TOKENIZE_FAILURE_EXCEPTIONS,
|
|
@@ -29,6 +32,10 @@ from hooks_constants.code_rules_enforcer_constants import ( # noqa: E402
|
|
|
29
32
|
CHAINED_INLINE_COMMENT_PATTERN,
|
|
30
33
|
MAX_COMMENT_ISSUES,
|
|
31
34
|
)
|
|
35
|
+
_javascript_comment_scanner = importlib.import_module("javascript_comment_scanner")
|
|
36
|
+
extract_javascript_comment_occurrences = (
|
|
37
|
+
_javascript_comment_scanner.extract_javascript_comment_occurrences
|
|
38
|
+
)
|
|
32
39
|
|
|
33
40
|
|
|
34
41
|
def check_comments_python(content: str) -> list[str]:
|
|
@@ -93,7 +100,9 @@ def check_comments_javascript(content: str) -> list[str]:
|
|
|
93
100
|
return issues
|
|
94
101
|
|
|
95
102
|
|
|
96
|
-
def extract_comment_texts(
|
|
103
|
+
def extract_comment_texts(
|
|
104
|
+
content: str, file_path: str, include_directive_comments: bool = False
|
|
105
|
+
) -> tuple[set[str], set[str]]:
|
|
97
106
|
"""Extract normalized comment text strings from content for comparison.
|
|
98
107
|
|
|
99
108
|
Returns:
|
|
@@ -108,46 +117,58 @@ def extract_comment_texts(content: str, file_path: str) -> tuple[set[str], set[s
|
|
|
108
117
|
return inline_comments, standalone_comments
|
|
109
118
|
|
|
110
119
|
if extension in ALL_PYTHON_EXTENSIONS:
|
|
111
|
-
inline_comments, standalone_comments, _ = _extract_python_comment_sets(
|
|
120
|
+
inline_comments, standalone_comments, _ = _extract_python_comment_sets(
|
|
121
|
+
content, include_directive_comments
|
|
122
|
+
)
|
|
112
123
|
return inline_comments, standalone_comments
|
|
113
124
|
|
|
114
|
-
lines = content.split("\n")
|
|
115
|
-
|
|
116
125
|
if extension in ALL_JAVASCRIPT_EXTENSIONS:
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
if stripped.startswith("//"):
|
|
132
|
-
if not stripped.startswith(ALL_JAVASCRIPT_EXEMPT_COMMENT_PREFIXES):
|
|
133
|
-
standalone_comments.add(stripped)
|
|
134
|
-
elif "//" in each_line:
|
|
135
|
-
before_slash = each_line[:each_line.index("//")]
|
|
136
|
-
if before_slash.strip():
|
|
137
|
-
comment_start = stripped.index("//")
|
|
138
|
-
comment_text = stripped[comment_start + 2 :].strip()
|
|
139
|
-
if not comment_text.startswith(ALL_JAVASCRIPT_EXEMPT_INLINE_COMMENT_PREFIXES):
|
|
140
|
-
inline_comments.add(stripped[comment_start:])
|
|
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
|
+
}
|
|
141
140
|
|
|
142
141
|
return inline_comments, standalone_comments
|
|
143
142
|
|
|
144
143
|
|
|
144
|
+
def _python_comment_occurrences(
|
|
145
|
+
content: str,
|
|
146
|
+
) -> tuple[list[tuple[str, int, bool]], bool]:
|
|
147
|
+
"""Return Python comments with exact lines and a tokenize status."""
|
|
148
|
+
source_lines = content.split("\n")
|
|
149
|
+
try:
|
|
150
|
+
all_tokens = list(_python_tokens(content))
|
|
151
|
+
except ALL_PYTHON_TOKENIZE_FAILURE_EXCEPTIONS:
|
|
152
|
+
return [], False
|
|
153
|
+
all_occurrences: list[tuple[str, int, bool]] = []
|
|
154
|
+
for each_token in all_tokens:
|
|
155
|
+
if each_token.type != tokenize.COMMENT:
|
|
156
|
+
continue
|
|
157
|
+
if each_token.string.startswith("#!") and each_token.start == (1, 0):
|
|
158
|
+
continue
|
|
159
|
+
source_line = source_lines[each_token.start[0] - 1]
|
|
160
|
+
is_inline = bool(source_line[: each_token.start[1]].strip())
|
|
161
|
+
all_occurrences.append(
|
|
162
|
+
(each_token.string.strip(), each_token.start[0], is_inline)
|
|
163
|
+
)
|
|
164
|
+
return all_occurrences, True
|
|
165
|
+
|
|
166
|
+
|
|
145
167
|
def check_comment_changes(old_content: str, new_content: str, file_path: str) -> list[str]:
|
|
146
168
|
"""Check for comment additions or removals between old and new content.
|
|
147
169
|
|
|
148
|
-
Inline
|
|
149
|
-
|
|
150
|
-
Existing comments being removed: NUDGE (print advisory) — comment preservation is advisory.
|
|
170
|
+
Inline and standalone comments are blocking findings when added.
|
|
171
|
+
Existing comments can be removed when the touched code no longer needs them.
|
|
151
172
|
|
|
152
173
|
When the file is Python and either *old_content* or *new_content* cannot
|
|
153
174
|
be tokenized (common for mid-edit Edit fragments), the comparison is
|
|
@@ -161,38 +182,71 @@ def check_comment_changes(old_content: str, new_content: str, file_path: str) ->
|
|
|
161
182
|
|
|
162
183
|
extension = get_file_extension(file_path)
|
|
163
184
|
if extension in ALL_PYTHON_EXTENSIONS:
|
|
164
|
-
|
|
165
|
-
|
|
185
|
+
old_occurrences, old_tokenize_ok = _python_comment_occurrences(old_content)
|
|
186
|
+
new_occurrences, new_tokenize_ok = _python_comment_occurrences(new_content)
|
|
166
187
|
if not (old_tokenize_ok and new_tokenize_ok):
|
|
167
188
|
return issues
|
|
168
189
|
else:
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
if not code_was_removed:
|
|
190
|
-
sample = next(iter(removed_comments))
|
|
191
|
-
print(f"[CODE_RULES advisory] Existing comment removed: {sample[:60]} - comment preservation is advisory", file=sys.stderr)
|
|
190
|
+
old_occurrences = extract_javascript_comment_occurrences(old_content, True)
|
|
191
|
+
new_occurrences = extract_javascript_comment_occurrences(new_content, True)
|
|
192
|
+
|
|
193
|
+
old_occurrence_counts = Counter(
|
|
194
|
+
(each_text, is_inline)
|
|
195
|
+
for each_text, _line_number, is_inline in old_occurrences
|
|
196
|
+
)
|
|
197
|
+
seen_occurrence_counts: Counter[tuple[str, bool]] = Counter()
|
|
198
|
+
for each_text, each_line_number, each_is_inline in new_occurrences:
|
|
199
|
+
occurrence_key = (each_text, each_is_inline)
|
|
200
|
+
if seen_occurrence_counts[occurrence_key] >= old_occurrence_counts[occurrence_key]:
|
|
201
|
+
comment_kind = "Inline" if each_is_inline else "Standalone"
|
|
202
|
+
issues.append(
|
|
203
|
+
f"Line {each_line_number}: {comment_kind} comment added: "
|
|
204
|
+
f"{each_text[:60]} - refactor to self-documenting code"
|
|
205
|
+
)
|
|
206
|
+
seen_occurrence_counts[occurrence_key] += 1
|
|
207
|
+
if len(issues) >= MAX_COMMENT_ISSUES:
|
|
208
|
+
break
|
|
209
|
+
issues.extend(_retained_comment_issues(old_content, new_content, file_path))
|
|
192
210
|
|
|
193
211
|
return issues
|
|
194
212
|
|
|
195
213
|
|
|
214
|
+
def _changed_line_numbers(old_content: str, new_content: str) -> set[int]:
|
|
215
|
+
matcher = difflib.SequenceMatcher(
|
|
216
|
+
None, old_content.splitlines(), new_content.splitlines(), autojunk=False
|
|
217
|
+
)
|
|
218
|
+
return {
|
|
219
|
+
each_line_number
|
|
220
|
+
for each_tag, _, _, each_new_start, each_new_end in matcher.get_opcodes()
|
|
221
|
+
if each_tag != "equal"
|
|
222
|
+
for each_line_number in range(each_new_start + 1, each_new_end + 1)
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def _retained_comment_issues(
|
|
227
|
+
old_content: str, new_content: str, file_path: str
|
|
228
|
+
) -> list[str]:
|
|
229
|
+
old_inline, old_standalone = extract_comment_texts(old_content, file_path, True)
|
|
230
|
+
new_inline, new_standalone = extract_comment_texts(new_content, file_path, True)
|
|
231
|
+
shared_inline = set(Counter(old_inline) & Counter(new_inline))
|
|
232
|
+
shared_standalone = old_standalone & new_standalone
|
|
233
|
+
if not shared_inline and not shared_standalone:
|
|
234
|
+
return []
|
|
235
|
+
changed_lines = _changed_line_numbers(old_content, new_content)
|
|
236
|
+
new_lines = new_content.splitlines()
|
|
237
|
+
issues: list[str] = []
|
|
238
|
+
for each_line_number in sorted(changed_lines):
|
|
239
|
+
inline_comment = next((each_comment for each_comment in shared_inline if each_comment in new_lines[each_line_number - 1]), None)
|
|
240
|
+
if inline_comment is not None:
|
|
241
|
+
issues.append(f"Line {each_line_number}: Inline comment retained on changed code: {inline_comment} - remove the comment")
|
|
242
|
+
preceding_line_number = each_line_number - 1
|
|
243
|
+
if preceding_line_number > 0 and new_lines[preceding_line_number - 1].strip() in shared_standalone:
|
|
244
|
+
issues.append(f"Line {each_line_number}: Standalone comment retained on changed code at line {preceding_line_number}: {new_lines[preceding_line_number - 1].strip()} - remove the comment")
|
|
245
|
+
if len(issues) >= MAX_COMMENT_ISSUES:
|
|
246
|
+
break
|
|
247
|
+
return issues[:MAX_COMMENT_ISSUES]
|
|
248
|
+
|
|
249
|
+
|
|
196
250
|
def _python_tokens(source: str) -> Iterator[tokenize.TokenInfo]:
|
|
197
251
|
"""Yield Python tokens from *source* one at a time.
|
|
198
252
|
|
|
@@ -304,7 +358,9 @@ def _starts_with_bounded_token_anchored_directive(directive_body: str) -> bool:
|
|
|
304
358
|
return False
|
|
305
359
|
|
|
306
360
|
|
|
307
|
-
def _extract_python_comment_sets(
|
|
361
|
+
def _extract_python_comment_sets(
|
|
362
|
+
content: str, include_directive_comments: bool = False
|
|
363
|
+
) -> tuple[set[str], set[str], bool]:
|
|
308
364
|
"""Return (inline_comments, standalone_comments, tokenize_succeeded).
|
|
309
365
|
|
|
310
366
|
Streams *content* once via ``_python_tokens``. A tokenize failure
|
|
@@ -321,7 +377,10 @@ def _extract_python_comment_sets(content: str) -> tuple[set[str], set[str], bool
|
|
|
321
377
|
for each_token in _python_tokens(content):
|
|
322
378
|
if each_token.type != tokenize.COMMENT:
|
|
323
379
|
continue
|
|
324
|
-
|
|
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
|
+
):
|
|
325
384
|
continue
|
|
326
385
|
line_number = each_token.start[0]
|
|
327
386
|
column_offset = each_token.start[1]
|
|
@@ -196,6 +196,8 @@ from hooks_constants.subprocess_budget_completeness_content import ( # noqa: E4
|
|
|
196
196
|
existing_file_content,
|
|
197
197
|
)
|
|
198
198
|
|
|
199
|
+
__all__ = ("is_test_file",)
|
|
200
|
+
|
|
199
201
|
|
|
200
202
|
def _multiedit_post_edit_view(
|
|
201
203
|
file_path: str, all_tool_input: dict[str, object]
|
|
@@ -234,6 +236,7 @@ class _ValidationContext(NamedTuple):
|
|
|
234
236
|
defer_scope_to_caller: bool
|
|
235
237
|
sibling_directory: Path | None
|
|
236
238
|
phase: str
|
|
239
|
+
include_comment_policy: bool = True
|
|
237
240
|
|
|
238
241
|
|
|
239
242
|
def _validated_phase(phase: str) -> str:
|
|
@@ -325,7 +328,7 @@ def _python_comment_and_logging_issues(context: _ValidationContext) -> list[str]
|
|
|
325
328
|
context.file_path,
|
|
326
329
|
)
|
|
327
330
|
all_issues: list[str] = []
|
|
328
|
-
if
|
|
331
|
+
if context.phase == FULL_GATE_PHASE and context.include_comment_policy:
|
|
329
332
|
all_issues.extend(check_comment_changes(old_content, content, file_path))
|
|
330
333
|
all_issues.extend(check_imports_at_top(content))
|
|
331
334
|
all_issues.extend(check_logging_fstrings(content))
|
|
@@ -366,7 +369,15 @@ def _python_duplicate_body_and_banned_issues(context: _ValidationContext) -> lis
|
|
|
366
369
|
all_issues = check_same_file_inline_duplicate_body(
|
|
367
370
|
effective_content, file_path, changed, defer
|
|
368
371
|
)
|
|
369
|
-
all_issues.extend(
|
|
372
|
+
all_issues.extend(
|
|
373
|
+
check_type_escape_hatches(
|
|
374
|
+
effective_content,
|
|
375
|
+
file_path,
|
|
376
|
+
include_type_ignore_comments=(
|
|
377
|
+
context.phase == FULL_GATE_PHASE and context.include_comment_policy
|
|
378
|
+
),
|
|
379
|
+
)
|
|
380
|
+
)
|
|
370
381
|
all_issues.extend(
|
|
371
382
|
check_banned_identifiers(effective_content, file_path, changed, defer)
|
|
372
383
|
)
|
|
@@ -576,7 +587,7 @@ def _javascript_comment_and_naming_issues(context: _ValidationContext) -> list[s
|
|
|
576
587
|
context.file_path,
|
|
577
588
|
)
|
|
578
589
|
all_issues: list[str] = []
|
|
579
|
-
if
|
|
590
|
+
if context.phase == FULL_GATE_PHASE and context.include_comment_policy:
|
|
580
591
|
all_issues.extend(check_comment_changes(old_content, content, file_path))
|
|
581
592
|
all_issues.extend(check_e2e_test_naming(content, file_path))
|
|
582
593
|
all_issues.extend(
|
|
@@ -642,6 +653,7 @@ def validate_content_for_phase(
|
|
|
642
653
|
sibling_directory: Path | None = None,
|
|
643
654
|
*,
|
|
644
655
|
phase: str,
|
|
656
|
+
include_comment_policy: bool = True,
|
|
645
657
|
) -> list[str]:
|
|
646
658
|
"""Run all applicable validators on content for one named validation phase."""
|
|
647
659
|
validated_phase = _validated_phase(phase)
|
|
@@ -649,10 +661,7 @@ def validate_content_for_phase(
|
|
|
649
661
|
effective_content, all_changed_lines = _effective_content_and_changed_lines(
|
|
650
662
|
content, full_file_content, prior_full_file_content
|
|
651
663
|
)
|
|
652
|
-
context = _ValidationContext(
|
|
653
|
-
content, old_content, effective_content, file_path, all_changed_lines,
|
|
654
|
-
defer_scope_to_caller, sibling_directory, validated_phase,
|
|
655
|
-
)
|
|
664
|
+
context = _ValidationContext(content, old_content, effective_content, file_path, all_changed_lines, defer_scope_to_caller, sibling_directory, validated_phase, include_comment_policy)
|
|
656
665
|
all_issues: list[str] = []
|
|
657
666
|
if extension in ALL_PYTHON_EXTENSIONS:
|
|
658
667
|
all_issues = _python_extension_issues(context)
|
|
@@ -693,6 +702,8 @@ def validate_content_for_full_gate(
|
|
|
693
702
|
prior_full_file_content: str = "",
|
|
694
703
|
defer_scope_to_caller: bool = False,
|
|
695
704
|
sibling_directory: Path | None = None,
|
|
705
|
+
*,
|
|
706
|
+
include_comment_policy: bool = True,
|
|
696
707
|
) -> list[str]:
|
|
697
708
|
"""Run the full-gate phase, including the six checks that read sibling files."""
|
|
698
709
|
return validate_content_for_phase(
|
|
@@ -704,6 +715,7 @@ def validate_content_for_full_gate(
|
|
|
704
715
|
defer_scope_to_caller,
|
|
705
716
|
sibling_directory,
|
|
706
717
|
phase=FULL_GATE_PHASE,
|
|
718
|
+
include_comment_policy=include_comment_policy,
|
|
707
719
|
)
|
|
708
720
|
|
|
709
721
|
|
|
@@ -79,23 +79,13 @@ def _find_any_annotation_lines(source: str) -> list[int]:
|
|
|
79
79
|
return offending_line_numbers
|
|
80
80
|
|
|
81
81
|
|
|
82
|
-
def
|
|
83
|
-
"""Return line numbers of
|
|
82
|
+
def _find_type_ignore_lines(source: str) -> list[int]:
|
|
83
|
+
"""Return line numbers of all type-ignore comments."""
|
|
84
84
|
ignore_pattern = re.compile(r"#\s*type:\s*ignore(?:\[[^\]]*\])?(.*)$")
|
|
85
|
-
minimum_justification_characters = len("xxxxx")
|
|
86
85
|
offending_line_numbers: list[int] = []
|
|
87
86
|
for each_comment_token in _comment_tokens(source):
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
continue
|
|
91
|
-
line_number = each_comment_token.start[0]
|
|
92
|
-
trailing_text = matched.group(1).strip()
|
|
93
|
-
if not trailing_text.startswith("#"):
|
|
94
|
-
offending_line_numbers.append(line_number)
|
|
95
|
-
continue
|
|
96
|
-
justification_text = trailing_text.lstrip("#").strip()
|
|
97
|
-
if len(justification_text) < minimum_justification_characters:
|
|
98
|
-
offending_line_numbers.append(line_number)
|
|
87
|
+
if ignore_pattern.search(each_comment_token.string):
|
|
88
|
+
offending_line_numbers.append(each_comment_token.start[0])
|
|
99
89
|
return offending_line_numbers
|
|
100
90
|
|
|
101
91
|
|
|
@@ -625,12 +615,20 @@ def _find_object_annotated_parameter_lines(source: str) -> list[tuple[int, str]]
|
|
|
625
615
|
return offending_parameters
|
|
626
616
|
|
|
627
617
|
|
|
628
|
-
def check_type_escape_hatches(
|
|
629
|
-
|
|
618
|
+
def check_type_escape_hatches(
|
|
619
|
+
content: str, file_path: str, include_type_ignore_comments: bool = True
|
|
620
|
+
) -> list[str]:
|
|
621
|
+
"""Flag type escape hatches and every type-ignore comment."""
|
|
622
|
+
issues: list[str] = []
|
|
623
|
+
type_ignore_issues: list[str] = []
|
|
624
|
+
if include_type_ignore_comments:
|
|
625
|
+
for each_ignore_line in _find_type_ignore_lines(content):
|
|
626
|
+
type_ignore_issues.append(
|
|
627
|
+
f"Line {each_ignore_line}: # type: ignore is not allowed - remove it and use a typed boundary or real type"
|
|
628
|
+
)
|
|
630
629
|
if is_test_file(file_path) or is_hook_infrastructure(file_path):
|
|
631
|
-
return
|
|
630
|
+
return type_ignore_issues
|
|
632
631
|
|
|
633
|
-
issues: list[str] = []
|
|
634
632
|
is_any_exempt = _file_path_matches_any_exemption(file_path)
|
|
635
633
|
|
|
636
634
|
if not is_any_exempt:
|
|
@@ -670,11 +668,6 @@ def check_type_escape_hatches(content: str, file_path: str) -> list[str]:
|
|
|
670
668
|
)
|
|
671
669
|
issues.extend(object_parameter_issues[:MAX_TYPE_ESCAPE_HATCH_ISSUES])
|
|
672
670
|
|
|
673
|
-
type_ignore_issues: list[str] = []
|
|
674
|
-
for each_ignore_line in _find_unjustified_type_ignore_lines(content):
|
|
675
|
-
type_ignore_issues.append(
|
|
676
|
-
f"Line {each_ignore_line}: Unjustified # type: ignore - add trailing '# reason' explaining why"
|
|
677
|
-
)
|
|
678
671
|
issues.extend(type_ignore_issues[:MAX_TYPE_ESCAPE_HATCH_ISSUES])
|
|
679
672
|
|
|
680
673
|
return issues
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"""Extract JavaScript comments without matching comment-like strings."""
|
|
2
|
+
|
|
3
|
+
import io
|
|
4
|
+
import re
|
|
5
|
+
import tokenize
|
|
6
|
+
from typing import NamedTuple
|
|
7
|
+
|
|
8
|
+
from hooks_constants.code_rules_enforcer_constants import (
|
|
9
|
+
ALL_JAVASCRIPT_EXEMPT_COMMENT_PREFIXES,
|
|
10
|
+
ALL_JAVASCRIPT_EXEMPT_INLINE_COMMENT_PREFIXES,
|
|
11
|
+
ALL_PYTHON_TOKENIZE_FAILURE_EXCEPTIONS,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class JavaScriptCommentOccurrence(NamedTuple):
|
|
16
|
+
"""One JavaScript comment with its source line and placement."""
|
|
17
|
+
|
|
18
|
+
text: str
|
|
19
|
+
line_number: int
|
|
20
|
+
is_inline: bool
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _mask_javascript_strings(content: str) -> str:
|
|
24
|
+
"""Replace quoted JavaScript strings while preserving source positions."""
|
|
25
|
+
string_pattern = r'''("(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|`(?:\\.|[^`\\])*`)'''
|
|
26
|
+
return re.sub(
|
|
27
|
+
string_pattern,
|
|
28
|
+
lambda each_match: "".join(
|
|
29
|
+
"\n" if each_character == "\n" else " "
|
|
30
|
+
for each_character in each_match.group()
|
|
31
|
+
),
|
|
32
|
+
content,
|
|
33
|
+
flags=re.DOTALL,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _is_allowed_comment(
|
|
38
|
+
comment_text: str, is_inline: bool, include_directive_comments: bool
|
|
39
|
+
) -> bool:
|
|
40
|
+
"""Return whether a comment is excluded from the policy comparison."""
|
|
41
|
+
if comment_text.startswith("/**"):
|
|
42
|
+
return True
|
|
43
|
+
if include_directive_comments:
|
|
44
|
+
return False
|
|
45
|
+
prefixes = (
|
|
46
|
+
ALL_JAVASCRIPT_EXEMPT_INLINE_COMMENT_PREFIXES
|
|
47
|
+
if is_inline
|
|
48
|
+
else ALL_JAVASCRIPT_EXEMPT_COMMENT_PREFIXES
|
|
49
|
+
)
|
|
50
|
+
comment_body = (
|
|
51
|
+
comment_text.removeprefix("//").lstrip()
|
|
52
|
+
if comment_text.startswith("//")
|
|
53
|
+
else comment_text
|
|
54
|
+
)
|
|
55
|
+
return comment_body.startswith(prefixes)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def extract_javascript_comment_occurrences(
|
|
59
|
+
content: str, include_directive_comments: bool
|
|
60
|
+
) -> list[JavaScriptCommentOccurrence]:
|
|
61
|
+
"""Return real JavaScript comment occurrences with exact line numbers."""
|
|
62
|
+
masked_content = _mask_javascript_strings(content)
|
|
63
|
+
all_occurrences: list[JavaScriptCommentOccurrence] = []
|
|
64
|
+
for each_match in re.finditer(r"//[^\n]*|/\*[\s\S]*?\*/", masked_content):
|
|
65
|
+
comment_text = content[each_match.start() : each_match.end()].strip()
|
|
66
|
+
line_start = masked_content.rfind("\n", 0, each_match.start()) + 1
|
|
67
|
+
is_inline = bool(masked_content[line_start : each_match.start()].strip())
|
|
68
|
+
if _is_allowed_comment(comment_text, is_inline, include_directive_comments):
|
|
69
|
+
continue
|
|
70
|
+
line_number = content.count("\n", 0, each_match.start()) + 1
|
|
71
|
+
all_occurrences.append(
|
|
72
|
+
JavaScriptCommentOccurrence(comment_text, line_number, is_inline)
|
|
73
|
+
)
|
|
74
|
+
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
|
|
@@ -56,6 +56,7 @@ def test_should_flag_bare_type_ignore() -> None:
|
|
|
56
56
|
source = "x = 1 # type: ignore\n"
|
|
57
57
|
issues = check_type_escape_hatches(source, PRODUCTION_FILE_PATH)
|
|
58
58
|
assert any("type: ignore" in issue for issue in issues)
|
|
59
|
+
assert any("remove it and use a typed boundary or real type" in issue for issue in issues)
|
|
59
60
|
|
|
60
61
|
|
|
61
62
|
def test_should_flag_coded_type_ignore_without_justification() -> None:
|
|
@@ -64,16 +65,35 @@ def test_should_flag_coded_type_ignore_without_justification() -> None:
|
|
|
64
65
|
assert any("type: ignore" in issue for issue in issues)
|
|
65
66
|
|
|
66
67
|
|
|
67
|
-
def
|
|
68
|
+
def test_should_flag_justified_type_ignore() -> None:
|
|
68
69
|
source = "x = 1 # type: ignore[misc] # stubs missing in foo library\n"
|
|
69
70
|
issues = check_type_escape_hatches(source, PRODUCTION_FILE_PATH)
|
|
70
|
-
assert
|
|
71
|
+
assert any("type: ignore" in issue for issue in issues)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def test_should_flag_justified_type_ignore_in_test_files() -> None:
|
|
75
|
+
source = "x = 1 # type: ignore[misc] # stubs missing in foo library\n"
|
|
76
|
+
issues = check_type_escape_hatches(source, TEST_FILE_PATH)
|
|
77
|
+
assert any("type: ignore" in issue for issue in issues)
|
|
71
78
|
|
|
72
79
|
|
|
73
|
-
def
|
|
80
|
+
def test_should_flag_type_ignore_in_test_files() -> None:
|
|
74
81
|
source = "def foo(x: Any) -> Any:\n y: Any = 1 # type: ignore\n return y\n"
|
|
75
82
|
issues = check_type_escape_hatches(source, TEST_FILE_PATH)
|
|
76
|
-
assert
|
|
83
|
+
assert any("type: ignore" in issue for issue in issues)
|
|
84
|
+
assert not any("Any annotation" in issue for issue in issues)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def test_edit_lane_does_not_report_type_ignore_comment_policy() -> None:
|
|
88
|
+
source = "total = 1 # type: ignore[misc]\n"
|
|
89
|
+
issues = hook_module.validate_content_for_edit_lane(source, TEST_FILE_PATH)
|
|
90
|
+
assert not any("type: ignore" in issue for issue in issues)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def test_edit_lane_does_not_report_production_type_ignore_comment_policy() -> None:
|
|
94
|
+
source = "total = 1 # type: ignore[misc]\n"
|
|
95
|
+
issues = hook_module.validate_content_for_edit_lane(source, PRODUCTION_FILE_PATH)
|
|
96
|
+
assert not any("type: ignore" in issue for issue in issues)
|
|
77
97
|
|
|
78
98
|
|
|
79
99
|
def test_should_flag_any_on_positional_only_parameter() -> None:
|
|
@@ -68,7 +68,6 @@ KNOWN_UNCAPPED_CHECKS_PENDING_REVIEW: frozenset[str] = frozenset(
|
|
|
68
68
|
{
|
|
69
69
|
"check_boolean_naming",
|
|
70
70
|
"check_collection_prefix",
|
|
71
|
-
"check_comment_changes",
|
|
72
71
|
"check_constant_equality_tests",
|
|
73
72
|
"check_constants_outside_config",
|
|
74
73
|
"check_existence_check_tests",
|