claude-dev-env 1.75.0 → 1.76.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/_shared/pr-loop/scripts/code_rules_gate.py +60 -5
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/CLAUDE.md +1 -0
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/inline_duplicate_body_span_constants.py +22 -0
- package/_shared/pr-loop/scripts/tests/test_code_rules_gate.py +147 -3
- package/docs/CODE_RULES.md +1 -1
- package/hooks/blocking/CLAUDE.md +2 -2
- package/hooks/blocking/claude_md_orphan_file_blocker.py +170 -20
- package/hooks/blocking/code_rules_duplicate_body.py +378 -26
- package/hooks/blocking/code_rules_enforcer.py +36 -5
- package/hooks/blocking/code_rules_imports_logging.py +679 -1
- package/hooks/blocking/code_rules_shared.py +8 -5
- package/hooks/blocking/code_rules_test_assertions.py +6 -7
- package/hooks/blocking/test_claude_md_orphan_file_blocker.py +484 -0
- package/hooks/blocking/test_code_rules_enforcer_cap_meta.py +1 -0
- package/hooks/blocking/test_code_rules_enforcer_import_block_sort.py +157 -0
- package/hooks/blocking/test_code_rules_enforcer_same_file_inline_duplicate.py +466 -0
- package/hooks/blocking/test_code_rules_enforcer_split_test_assertions.py +11 -9
- package/hooks/blocking/test_code_rules_js_resume_task_enumeration.py +758 -0
- package/hooks/blocking/test_code_rules_logging_printf_tokens.py +134 -0
- package/hooks/blocking/test_verification_verdict_store.py +66 -1
- package/hooks/blocking/test_verifier_verdict_minter.py +64 -5
- package/hooks/blocking/verification_verdict_store.py +19 -5
- package/hooks/blocking/verifier_verdict_minter.py +18 -15
- package/hooks/hooks_constants/blocking_check_limits.py +30 -1
- package/hooks/hooks_constants/claude_md_orphan_file_blocker_constants.py +52 -24
- package/hooks/hooks_constants/code_rules_enforcer_constants.py +41 -1
- package/hooks/hooks_constants/duplicate_function_body_constants.py +21 -5
- package/package.json +1 -1
- package/rules/claude-md-orphan-file.md +7 -8
- package/rules/docstring-prose-matches-implementation.md +1 -0
- package/rules/package-inventory-stale-entry.md +8 -0
- package/skills/anthropic-plan/CLAUDE.md +1 -1
- package/skills/anthropic-plan/SKILL.md +15 -2
- package/skills/autoconverge/workflow/converge.contract.test.mjs +12 -19
- package/skills/autoconverge/workflow/converge.fix-recovery.test.mjs +71 -0
- package/skills/autoconverge/workflow/converge.mjs +86 -110
- package/skills/bugteam/scripts/bugteam_code_rules_gate.py +58 -4
- package/skills/bugteam/scripts/bugteam_scripts_constants/bugteam_code_rules_gate_constants.py +9 -0
- package/skills/bugteam/scripts/test_bugteam_code_rules_gate.py +42 -0
|
@@ -1,33 +1,44 @@
|
|
|
1
|
-
"""
|
|
1
|
+
"""Duplicate top-level function body detection.
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
structurally identical to a top-level function
|
|
5
|
-
``.py`` module in the same directory.
|
|
6
|
-
|
|
7
|
-
|
|
3
|
+
``check_duplicate_function_body_across_files`` flags a top-level function in the
|
|
4
|
+
file being written whose body is structurally identical to a top-level function
|
|
5
|
+
already defined in a sibling ``.py`` module in the same directory.
|
|
6
|
+
``check_same_file_inline_duplicate_body`` flags a top-level function whose body
|
|
7
|
+
appears verbatim as a contiguous statement block inside another function in the
|
|
8
|
+
same file — the inlined-block copy the cross-file whole-function comparison
|
|
9
|
+
misses. Both catch the Reuse-before-create / DRY violation where a block of logic
|
|
10
|
+
is copied instead of called from one shared home, so a fix that lands in one copy
|
|
11
|
+
leaves the other carrying the bug.
|
|
8
12
|
|
|
9
|
-
The
|
|
13
|
+
The scans are deliberately conservative to keep false positives near zero:
|
|
10
14
|
|
|
11
15
|
- Only module-scope ``def`` / ``async def`` bodies are compared (the copied-helper
|
|
12
16
|
case), never methods nested in a class.
|
|
13
|
-
-
|
|
14
|
-
docstring dropped,
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
17
|
+
- The cross-file scan compares whole bodies by their normalized AST structure
|
|
18
|
+
with the leading docstring dropped, keeping identifier names, so a match
|
|
19
|
+
requires the body statements — local variable names included — to be
|
|
20
|
+
structurally identical; it ignores the parameter list, decorators, and whether
|
|
21
|
+
the function is ``async``, and a body must hold at least
|
|
22
|
+
``MINIMUM_DUPLICATE_BODY_STATEMENTS`` statements.
|
|
23
|
+
- The same-file inline scan trims a helper's leading ``assert`` preconditions and
|
|
24
|
+
drops its docstring, then seeks the remaining window verbatim inside another
|
|
25
|
+
function's reachable statement blocks. String-literal constants are canonicalized
|
|
26
|
+
before comparison, so two blocks that differ only in a logging or error message
|
|
27
|
+
still collide, while the window must hold a substantial compound statement
|
|
28
|
+
(``try``, ``for``, ``while``, ``with``) and sit inside a strictly larger,
|
|
29
|
+
non-twin enclosing body. A bare ``if`` guard, a flat run, and a structural-twin
|
|
30
|
+
peer helper never flag.
|
|
21
31
|
- Test files and ``__init__.py`` re-export surfaces never participate, on either
|
|
22
32
|
the writing side or the sibling side.
|
|
23
33
|
|
|
24
|
-
Unlike most code-rules checks,
|
|
25
|
-
copied-
|
|
26
|
-
directory itself, so gating
|
|
34
|
+
Unlike most code-rules checks, these run on hook-infrastructure files: the
|
|
35
|
+
copied-block violation they target appears often in the ``blocking/`` hook
|
|
36
|
+
directory itself, so gating them behind the hook-infrastructure exemption would
|
|
27
37
|
leave the exact violation class unguarded. The enforcer entry points route a
|
|
28
|
-
hook ``.py`` target to
|
|
29
|
-
|
|
30
|
-
``blocking/`` directory still blocks a copied sibling helper
|
|
38
|
+
hook ``.py`` target to both checks even though the full code-rules verdict stays
|
|
39
|
+
off hook infrastructure, so a Write or pre-check against a file under the
|
|
40
|
+
``blocking/`` directory still blocks a copied sibling helper and an inlined
|
|
41
|
+
helper body.
|
|
31
42
|
|
|
32
43
|
``advise_cross_skill_duplicate_helper`` is the non-blocking companion for a
|
|
33
44
|
different layout: a helper copied between two skills' ``scripts`` directories.
|
|
@@ -40,6 +51,7 @@ folders; within one skill the blocking check above already covers the copy.
|
|
|
40
51
|
"""
|
|
41
52
|
|
|
42
53
|
import ast
|
|
54
|
+
import copy
|
|
43
55
|
import sys
|
|
44
56
|
from pathlib import Path
|
|
45
57
|
|
|
@@ -63,12 +75,39 @@ from hooks_constants.duplicate_function_body_constants import ( # noqa: E402
|
|
|
63
75
|
MAX_CROSS_SKILL_ADVISORY_ISSUES,
|
|
64
76
|
MAX_DUPLICATE_BODY_ISSUES,
|
|
65
77
|
MINIMUM_DUPLICATE_BODY_STATEMENTS,
|
|
78
|
+
MINIMUM_INLINE_DUPLICATE_BODY_STATEMENTS,
|
|
66
79
|
PYTHON_SOURCE_SUFFIX,
|
|
80
|
+
SAME_FILE_INLINE_DUPLICATE_GUIDANCE,
|
|
81
|
+
SAME_FILE_INLINE_DUPLICATE_SPAN_SUFFIX_TEMPLATE,
|
|
67
82
|
SKILL_SCRIPTS_DIRECTORY_NAME,
|
|
68
83
|
SKILLS_DIRECTORY_NAME,
|
|
69
84
|
)
|
|
70
85
|
|
|
71
86
|
|
|
87
|
+
def _body_statements_without_docstring(
|
|
88
|
+
function_node: ast.FunctionDef | ast.AsyncFunctionDef,
|
|
89
|
+
) -> list[ast.stmt]:
|
|
90
|
+
"""Return the function's top-level body statements with a leading docstring dropped.
|
|
91
|
+
|
|
92
|
+
A function whose first statement is a bare string-literal expression carries a
|
|
93
|
+
docstring; that statement is omitted so two copies that differ only in their
|
|
94
|
+
docstring compare equal. A function with no leading docstring returns its body
|
|
95
|
+
unchanged.
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
function_node: The module-scope function whose body to read.
|
|
99
|
+
|
|
100
|
+
Returns:
|
|
101
|
+
The top-level body statements, excluding a leading docstring expression.
|
|
102
|
+
"""
|
|
103
|
+
body_statements = list(function_node.body)
|
|
104
|
+
if body_statements and isinstance(body_statements[0], ast.Expr):
|
|
105
|
+
first_value = body_statements[0].value
|
|
106
|
+
if isinstance(first_value, ast.Constant) and isinstance(first_value.value, str):
|
|
107
|
+
return body_statements[1:]
|
|
108
|
+
return body_statements
|
|
109
|
+
|
|
110
|
+
|
|
72
111
|
def _normalized_body_signature(function_node: ast.FunctionDef | ast.AsyncFunctionDef) -> str | None:
|
|
73
112
|
"""Return a position-independent structural fingerprint of the function body.
|
|
74
113
|
|
|
@@ -84,11 +123,7 @@ def _normalized_body_signature(function_node: ast.FunctionDef | ast.AsyncFunctio
|
|
|
84
123
|
A normalized AST dump of the body statements, or None when the body is
|
|
85
124
|
too small to compare.
|
|
86
125
|
"""
|
|
87
|
-
body_statements =
|
|
88
|
-
if body_statements and isinstance(body_statements[0], ast.Expr):
|
|
89
|
-
first_value = body_statements[0].value
|
|
90
|
-
if isinstance(first_value, ast.Constant) and isinstance(first_value.value, str):
|
|
91
|
-
body_statements = body_statements[1:]
|
|
126
|
+
body_statements = _body_statements_without_docstring(function_node)
|
|
92
127
|
if len(body_statements) < MINIMUM_DUPLICATE_BODY_STATEMENTS:
|
|
93
128
|
return None
|
|
94
129
|
return "\n".join(
|
|
@@ -301,6 +336,323 @@ def check_duplicate_function_body_across_files(
|
|
|
301
336
|
)
|
|
302
337
|
|
|
303
338
|
|
|
339
|
+
class _StringConstantCanonicalizer(ast.NodeTransformer):
|
|
340
|
+
"""Rewrite every string-literal constant to one placeholder.
|
|
341
|
+
|
|
342
|
+
Two copied blocks most often differ only in a message string — a logging or
|
|
343
|
+
error suffix the author tweaked while leaving the call, selector, and control
|
|
344
|
+
structure identical. Canonicalizing string constants before dumping lets such
|
|
345
|
+
blocks collide while a different call target, selector, or numeric constant
|
|
346
|
+
still keeps two blocks distinct. Non-string constants (numbers, ``None``,
|
|
347
|
+
booleans) are left untouched so a genuine value difference stays visible.
|
|
348
|
+
"""
|
|
349
|
+
|
|
350
|
+
def visit_Constant(self, node: ast.Constant) -> ast.Constant:
|
|
351
|
+
"""Replace a string-valued constant with a fixed placeholder string.
|
|
352
|
+
|
|
353
|
+
Args:
|
|
354
|
+
node: The constant node under transformation.
|
|
355
|
+
|
|
356
|
+
Returns:
|
|
357
|
+
A placeholder constant for a string value, or the node unchanged for
|
|
358
|
+
any non-string constant.
|
|
359
|
+
"""
|
|
360
|
+
if isinstance(node.value, str):
|
|
361
|
+
return ast.copy_location(ast.Constant(value=""), node)
|
|
362
|
+
return node
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
def _normalized_statement_dump(statement: ast.stmt) -> str:
|
|
366
|
+
"""Return the normalized AST dump of one statement.
|
|
367
|
+
|
|
368
|
+
Canonicalizes every string-literal constant to one placeholder before
|
|
369
|
+
dumping, so two statements that differ only in a message string still produce
|
|
370
|
+
the same dump. The call structure, selectors, exception handlers, and numeric
|
|
371
|
+
constants are preserved, so a genuine logic difference still keeps two
|
|
372
|
+
statements distinct.
|
|
373
|
+
|
|
374
|
+
Args:
|
|
375
|
+
statement: The statement node to fingerprint.
|
|
376
|
+
|
|
377
|
+
Returns:
|
|
378
|
+
The annotate-fields-suppressed AST dump of the statement after string
|
|
379
|
+
constants are canonicalized.
|
|
380
|
+
"""
|
|
381
|
+
canonical_statement = _StringConstantCanonicalizer().visit(
|
|
382
|
+
copy.deepcopy(statement)
|
|
383
|
+
)
|
|
384
|
+
return ast.dump(canonical_statement, annotate_fields=False)
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
def _statement_blocks_in_function(
|
|
388
|
+
function_node: ast.FunctionDef | ast.AsyncFunctionDef,
|
|
389
|
+
) -> list[list[ast.stmt]]:
|
|
390
|
+
"""Return every statement list reachable inside a function body.
|
|
391
|
+
|
|
392
|
+
A copied helper body can sit at the function's top level or nested inside a
|
|
393
|
+
branch, loop, or context block, so the inline-duplicate scan walks each nested
|
|
394
|
+
statement list as its own window source. The function's own immediate body is
|
|
395
|
+
the first block; every nested block (``If`` arms, ``For``/``While`` bodies,
|
|
396
|
+
``With``/``Try`` bodies and handlers) follows.
|
|
397
|
+
|
|
398
|
+
Args:
|
|
399
|
+
function_node: The module-scope function whose blocks to collect.
|
|
400
|
+
|
|
401
|
+
Returns:
|
|
402
|
+
A list of statement lists, one per reachable block in the function.
|
|
403
|
+
"""
|
|
404
|
+
all_blocks: list[list[ast.stmt]] = []
|
|
405
|
+
for each_node in ast.walk(function_node):
|
|
406
|
+
for each_field_name in ("body", "orelse", "finalbody"):
|
|
407
|
+
block = getattr(each_node, each_field_name, None)
|
|
408
|
+
if isinstance(block, list) and block and isinstance(block[0], ast.stmt):
|
|
409
|
+
all_blocks.append(block)
|
|
410
|
+
for each_handler in getattr(each_node, "handlers", []) or []:
|
|
411
|
+
if isinstance(each_handler, ast.ExceptHandler):
|
|
412
|
+
all_blocks.append(each_handler.body)
|
|
413
|
+
return all_blocks
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
def _total_reachable_statement_count(
|
|
417
|
+
function_node: ast.FunctionDef | ast.AsyncFunctionDef,
|
|
418
|
+
) -> int:
|
|
419
|
+
"""Return the count of every statement reachable inside the function body.
|
|
420
|
+
|
|
421
|
+
Walks the function's immediate body and every nested block — ``If`` arms,
|
|
422
|
+
loop and context bodies, ``Try`` bodies and handlers and ``finalbody`` — so a
|
|
423
|
+
duplicated window wrapped inside a single top-level compound (a ``try``/
|
|
424
|
+
``finally`` cleanup or one ``if`` guard) still counts the statements the window
|
|
425
|
+
occupies plus the statements around it. The leading docstring is excluded from
|
|
426
|
+
the immediate body so a docstring does not inflate the count.
|
|
427
|
+
|
|
428
|
+
Args:
|
|
429
|
+
function_node: The function whose reachable statements to count.
|
|
430
|
+
|
|
431
|
+
Returns:
|
|
432
|
+
The number of statements reachable in the function, excluding a leading
|
|
433
|
+
docstring expression.
|
|
434
|
+
"""
|
|
435
|
+
has_leading_docstring = len(_body_statements_without_docstring(function_node)) < len(
|
|
436
|
+
function_node.body
|
|
437
|
+
)
|
|
438
|
+
total_statement_count = sum(
|
|
439
|
+
1 for each_node in ast.walk(function_node) if isinstance(each_node, ast.stmt)
|
|
440
|
+
)
|
|
441
|
+
if has_leading_docstring:
|
|
442
|
+
return total_statement_count - 1
|
|
443
|
+
return total_statement_count
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
def _normalized_body_dump_multiset(
|
|
447
|
+
function_node: ast.FunctionDef | ast.AsyncFunctionDef,
|
|
448
|
+
) -> list[str]:
|
|
449
|
+
"""Return the function's docstring-stripped statement dumps, sorted.
|
|
450
|
+
|
|
451
|
+
Used to test whether two functions are structural twins — same statements in
|
|
452
|
+
any arrangement under string normalization. Sorting makes the comparison
|
|
453
|
+
order-independent, so two peer helpers that read different inputs but share
|
|
454
|
+
the same statement shapes compare equal and are left to the cross-file check.
|
|
455
|
+
|
|
456
|
+
Args:
|
|
457
|
+
function_node: The function whose body to fingerprint.
|
|
458
|
+
|
|
459
|
+
Returns:
|
|
460
|
+
The sorted per-statement normalized dumps of the docstring-stripped body.
|
|
461
|
+
"""
|
|
462
|
+
body_statements = _body_statements_without_docstring(function_node)
|
|
463
|
+
return sorted(_normalized_statement_dump(each) for each in body_statements)
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
def _function_inlines_window(
|
|
467
|
+
helper_node: ast.FunctionDef | ast.AsyncFunctionDef,
|
|
468
|
+
enclosing_node: ast.FunctionDef | ast.AsyncFunctionDef,
|
|
469
|
+
all_helper_window_dumps: list[str],
|
|
470
|
+
) -> bool:
|
|
471
|
+
"""Return whether a function inlines a helper window inside a larger body.
|
|
472
|
+
|
|
473
|
+
Slides a window the length of ``all_helper_window_dumps`` over each reachable
|
|
474
|
+
statement block in the enclosing function; a window whose per-statement dumps
|
|
475
|
+
match the helper's, in order, is the inlined copy. Two guards keep the report
|
|
476
|
+
to the genuine "helper inlined inside a larger function" shape:
|
|
477
|
+
|
|
478
|
+
- The enclosing function carries more total reachable statements than the
|
|
479
|
+
window, so a copy that fills a whole peer function is not reported here. The
|
|
480
|
+
count spans every nested block, so a window wrapped inside one top-level
|
|
481
|
+
compound — a ``try``/``finally`` cleanup, an ``except`` handler, or a single
|
|
482
|
+
``if`` guard — still clears the guard and is scanned.
|
|
483
|
+
- The two functions are not structural twins — their docstring-stripped
|
|
484
|
+
statement multisets differ — so two peer helpers that share a statement
|
|
485
|
+
shape but read different inputs are left to the cross-file whole-function
|
|
486
|
+
check ``check_duplicate_function_body_across_files`` rather than reported as
|
|
487
|
+
an inline duplicate.
|
|
488
|
+
|
|
489
|
+
Args:
|
|
490
|
+
helper_node: The candidate helper whose window is sought.
|
|
491
|
+
enclosing_node: The candidate enclosing function to scan.
|
|
492
|
+
all_helper_window_dumps: The helper's substantive-block per-statement dumps.
|
|
493
|
+
|
|
494
|
+
Returns:
|
|
495
|
+
True when some block in the enclosing function contains the helper window
|
|
496
|
+
verbatim as a contiguous run inside a strictly larger, non-twin body.
|
|
497
|
+
"""
|
|
498
|
+
window_length = len(all_helper_window_dumps)
|
|
499
|
+
if _total_reachable_statement_count(enclosing_node) <= window_length:
|
|
500
|
+
return False
|
|
501
|
+
if _normalized_body_dump_multiset(helper_node) == _normalized_body_dump_multiset(
|
|
502
|
+
enclosing_node
|
|
503
|
+
):
|
|
504
|
+
return False
|
|
505
|
+
for each_block in _statement_blocks_in_function(enclosing_node):
|
|
506
|
+
if len(each_block) < window_length:
|
|
507
|
+
continue
|
|
508
|
+
block_dumps = [_normalized_statement_dump(each) for each in each_block]
|
|
509
|
+
for each_start_index in range(len(block_dumps) - window_length + 1):
|
|
510
|
+
window = block_dumps[each_start_index : each_start_index + window_length]
|
|
511
|
+
if window == all_helper_window_dumps:
|
|
512
|
+
return True
|
|
513
|
+
return False
|
|
514
|
+
|
|
515
|
+
|
|
516
|
+
def _helper_match_window_dumps(
|
|
517
|
+
function_node: ast.FunctionDef | ast.AsyncFunctionDef,
|
|
518
|
+
) -> list[str] | None:
|
|
519
|
+
"""Return the helper's substantive block as per-statement dumps, or None.
|
|
520
|
+
|
|
521
|
+
Drops a leading docstring the same way ``_normalized_body_signature`` does,
|
|
522
|
+
then trims leading ``assert`` precondition guards so the match window begins at
|
|
523
|
+
the helper's first non-assert statement. A helper commonly wraps a copied block
|
|
524
|
+
in its own ``assert`` precondition, so trimming those lets the helper's
|
|
525
|
+
substantive block match the same block inlined elsewhere without the guard.
|
|
526
|
+
Only ``assert`` is trimmed — a leading assignment or call carries data the
|
|
527
|
+
duplicate must share, so trimming it would expose a generic tail and match
|
|
528
|
+
unrelated peer helpers.
|
|
529
|
+
|
|
530
|
+
The window must hold at least ``MINIMUM_INLINE_DUPLICATE_BODY_STATEMENTS``
|
|
531
|
+
statements and must contain a substantial compound statement — a ``try``,
|
|
532
|
+
``for``, ``while``, or ``with`` block. A run of flat statements, and a bare
|
|
533
|
+
``if`` guard (an idiomatic strict-vs-optional validator pair where one wraps a
|
|
534
|
+
``None`` check around the other's ``if ...: raise`` narrow), are too common to
|
|
535
|
+
be a meaningful inline duplicate and never flag, while a duplicated
|
|
536
|
+
``try``/``except`` or loop block — the substantial control structure worth a
|
|
537
|
+
shared helper — does.
|
|
538
|
+
|
|
539
|
+
Args:
|
|
540
|
+
function_node: The module-scope function to treat as a candidate helper.
|
|
541
|
+
|
|
542
|
+
Returns:
|
|
543
|
+
The per-statement normalized dumps of the helper's substantive block, or
|
|
544
|
+
None when it has no compound statement or is shorter than the minimum.
|
|
545
|
+
"""
|
|
546
|
+
body_statements = _body_statements_without_docstring(function_node)
|
|
547
|
+
first_non_assert_index = next(
|
|
548
|
+
(
|
|
549
|
+
each_index
|
|
550
|
+
for each_index, each_statement in enumerate(body_statements)
|
|
551
|
+
if not isinstance(each_statement, ast.Assert)
|
|
552
|
+
),
|
|
553
|
+
None,
|
|
554
|
+
)
|
|
555
|
+
if first_non_assert_index is None:
|
|
556
|
+
return None
|
|
557
|
+
match_window = body_statements[first_non_assert_index:]
|
|
558
|
+
if len(match_window) < MINIMUM_INLINE_DUPLICATE_BODY_STATEMENTS:
|
|
559
|
+
return None
|
|
560
|
+
substantial_compound_statement_types = (ast.Try, ast.For, ast.While, ast.With)
|
|
561
|
+
has_substantial_compound = any(
|
|
562
|
+
isinstance(each, substantial_compound_statement_types)
|
|
563
|
+
for each in match_window
|
|
564
|
+
)
|
|
565
|
+
if not has_substantial_compound:
|
|
566
|
+
return None
|
|
567
|
+
return [_normalized_statement_dump(each) for each in match_window]
|
|
568
|
+
|
|
569
|
+
|
|
570
|
+
def check_same_file_inline_duplicate_body(
|
|
571
|
+
content: str,
|
|
572
|
+
file_path: str,
|
|
573
|
+
all_changed_lines: set[int] | None = None,
|
|
574
|
+
defer_scope_to_caller: bool = False,
|
|
575
|
+
) -> list[str]:
|
|
576
|
+
"""Flag a module-scope helper whose body is inlined inside another function.
|
|
577
|
+
|
|
578
|
+
Compares each module-scope function's body against every other module-scope
|
|
579
|
+
function in the same file, reporting any helper whose body appears verbatim as
|
|
580
|
+
a contiguous statement block inside another function. This is the same-file
|
|
581
|
+
counterpart to ``check_duplicate_function_body_across_files``, which only
|
|
582
|
+
compares whole functions across sibling modules and so misses a helper that
|
|
583
|
+
duplicates a block already inlined in a same-file function.
|
|
584
|
+
|
|
585
|
+
Violations are span-scoped to the lines an edit touched the same way the
|
|
586
|
+
cross-file check scopes its own: a violation blocks when either the helper's
|
|
587
|
+
span or the enclosing function's span intersects the changed lines, so an
|
|
588
|
+
unrelated edit to a file that already carries the duplication does not block,
|
|
589
|
+
while a Write or an edit touching either function still flags.
|
|
590
|
+
|
|
591
|
+
Args:
|
|
592
|
+
content: The full post-edit file content being written.
|
|
593
|
+
file_path: The destination path of the write.
|
|
594
|
+
all_changed_lines: Post-edit line numbers the current edit touched, or
|
|
595
|
+
None to treat the whole file as in scope. When provided, a violation
|
|
596
|
+
blocks only when the helper's span or the enclosing function's span
|
|
597
|
+
intersects the changed lines.
|
|
598
|
+
defer_scope_to_caller: When True, return every violation so the commit/push
|
|
599
|
+
gate's ``split_violations_by_scope`` can scope by added line.
|
|
600
|
+
|
|
601
|
+
Returns:
|
|
602
|
+
Human-readable violation strings, one per inlined-duplicate helper, scoped
|
|
603
|
+
to the changed lines unless *defer_scope_to_caller* is True or
|
|
604
|
+
*all_changed_lines* is None.
|
|
605
|
+
"""
|
|
606
|
+
if Path(file_path).name == DUNDER_INIT_FILENAME:
|
|
607
|
+
return []
|
|
608
|
+
if is_test_file(file_path):
|
|
609
|
+
return []
|
|
610
|
+
try:
|
|
611
|
+
tree = ast.parse(content)
|
|
612
|
+
except SyntaxError:
|
|
613
|
+
return []
|
|
614
|
+
all_top_level_functions = [
|
|
615
|
+
each_node
|
|
616
|
+
for each_node in tree.body
|
|
617
|
+
if isinstance(each_node, ast.FunctionDef | ast.AsyncFunctionDef)
|
|
618
|
+
]
|
|
619
|
+
all_violations_in_walk_order: list[tuple[frozenset[int], str]] = []
|
|
620
|
+
for each_helper in all_top_level_functions:
|
|
621
|
+
helper_window_dumps = _helper_match_window_dumps(each_helper)
|
|
622
|
+
if helper_window_dumps is None:
|
|
623
|
+
continue
|
|
624
|
+
for each_enclosing in all_top_level_functions:
|
|
625
|
+
if each_enclosing is each_helper:
|
|
626
|
+
continue
|
|
627
|
+
if not _function_inlines_window(
|
|
628
|
+
each_helper, each_enclosing, helper_window_dumps
|
|
629
|
+
):
|
|
630
|
+
continue
|
|
631
|
+
helper_span = _function_definition_span(each_helper)
|
|
632
|
+
enclosing_span = _function_definition_span(each_enclosing)
|
|
633
|
+
in_scope_lines = frozenset(helper_span) | frozenset(enclosing_span)
|
|
634
|
+
span_suffix = SAME_FILE_INLINE_DUPLICATE_SPAN_SUFFIX_TEMPLATE.format(
|
|
635
|
+
helper_start=helper_span.start,
|
|
636
|
+
helper_length=len(helper_span),
|
|
637
|
+
enclosing_start=enclosing_span.start,
|
|
638
|
+
enclosing_length=len(enclosing_span),
|
|
639
|
+
)
|
|
640
|
+
message = (
|
|
641
|
+
f"Function {each_helper.name!r} duplicates an inline block in "
|
|
642
|
+
f"{each_enclosing.name!r} — {SAME_FILE_INLINE_DUPLICATE_GUIDANCE} "
|
|
643
|
+
f"{span_suffix}"
|
|
644
|
+
)
|
|
645
|
+
all_violations_in_walk_order.append((in_scope_lines, message))
|
|
646
|
+
break
|
|
647
|
+
if len(all_violations_in_walk_order) >= MAX_DUPLICATE_BODY_ISSUES:
|
|
648
|
+
break
|
|
649
|
+
return _scope_violations_to_changed_lines(
|
|
650
|
+
all_violations_in_walk_order,
|
|
651
|
+
all_changed_lines,
|
|
652
|
+
defer_scope_to_caller,
|
|
653
|
+
)
|
|
654
|
+
|
|
655
|
+
|
|
304
656
|
def _skill_scripts_root(file_path: str) -> Path | None:
|
|
305
657
|
"""Return the ``skills/<name>/scripts`` root the written file sits under.
|
|
306
658
|
|
|
@@ -81,13 +81,17 @@ from code_rules_docstrings import ( # noqa: E402
|
|
|
81
81
|
from code_rules_duplicate_body import ( # noqa: E402
|
|
82
82
|
advise_cross_skill_duplicate_helper,
|
|
83
83
|
check_duplicate_function_body_across_files,
|
|
84
|
+
check_same_file_inline_duplicate_body,
|
|
84
85
|
)
|
|
85
86
|
from code_rules_imports_logging import ( # noqa: E402
|
|
86
87
|
advise_file_line_count,
|
|
87
88
|
check_e2e_test_naming,
|
|
89
|
+
check_import_block_sorted,
|
|
88
90
|
check_imports_at_top,
|
|
91
|
+
check_js_resume_task_enumeration_coverage,
|
|
89
92
|
check_library_print,
|
|
90
93
|
check_logging_fstrings,
|
|
94
|
+
check_logging_printf_tokens,
|
|
91
95
|
check_windows_api_none,
|
|
92
96
|
)
|
|
93
97
|
from code_rules_magic_values import ( # noqa: E402
|
|
@@ -225,7 +229,16 @@ def validate_content(
|
|
|
225
229
|
if not is_test_file(file_path):
|
|
226
230
|
all_issues.extend(check_comment_changes(old_content, content, file_path))
|
|
227
231
|
all_issues.extend(check_imports_at_top(content))
|
|
232
|
+
all_issues.extend(
|
|
233
|
+
check_import_block_sorted(
|
|
234
|
+
effective_content,
|
|
235
|
+
file_path,
|
|
236
|
+
all_changed_lines,
|
|
237
|
+
defer_scope_to_caller,
|
|
238
|
+
)
|
|
239
|
+
)
|
|
228
240
|
all_issues.extend(check_logging_fstrings(content))
|
|
241
|
+
all_issues.extend(check_logging_printf_tokens(content, file_path))
|
|
229
242
|
all_issues.extend(check_windows_api_none(content))
|
|
230
243
|
all_issues.extend(check_magic_values(content, file_path))
|
|
231
244
|
all_issues.extend(check_fstring_structural_literals(content, file_path))
|
|
@@ -241,6 +254,14 @@ def validate_content(
|
|
|
241
254
|
sibling_directory,
|
|
242
255
|
)
|
|
243
256
|
)
|
|
257
|
+
all_issues.extend(
|
|
258
|
+
check_same_file_inline_duplicate_body(
|
|
259
|
+
effective_content,
|
|
260
|
+
file_path,
|
|
261
|
+
all_changed_lines,
|
|
262
|
+
defer_scope_to_caller,
|
|
263
|
+
)
|
|
264
|
+
)
|
|
244
265
|
all_issues.extend(check_type_escape_hatches(effective_content, file_path))
|
|
245
266
|
all_issues.extend(check_banned_identifiers(content, file_path))
|
|
246
267
|
all_issues.extend(
|
|
@@ -368,6 +389,9 @@ def validate_content(
|
|
|
368
389
|
if not is_test_file(file_path):
|
|
369
390
|
all_issues.extend(check_comment_changes(old_content, content, file_path))
|
|
370
391
|
all_issues.extend(check_e2e_test_naming(content, file_path))
|
|
392
|
+
all_issues.extend(
|
|
393
|
+
check_js_resume_task_enumeration_coverage(content, file_path)
|
|
394
|
+
)
|
|
371
395
|
|
|
372
396
|
if extension in ALL_CODE_EXTENSIONS:
|
|
373
397
|
advise_file_line_count(content, file_path)
|
|
@@ -472,11 +496,11 @@ def _hook_infrastructure_blocking_issues(
|
|
|
472
496
|
"""Run the checks that still guard a hook Python target.
|
|
473
497
|
|
|
474
498
|
The whole code-rules verdict stays off hook-infrastructure files, so this
|
|
475
|
-
runs the
|
|
476
|
-
check
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
post-edit file.
|
|
499
|
+
runs the checks that must still guard them: the cross-file duplicate-body
|
|
500
|
+
check and the same-file inline-duplicate-body check, each span-scoped to the
|
|
501
|
+
lines an edit touched exactly as ``validate_content`` scopes it for production
|
|
502
|
+
code; and the zero-payload alias check, whose docstring names hook modules as
|
|
503
|
+
its motivating case, run over the whole post-edit file.
|
|
480
504
|
|
|
481
505
|
Args:
|
|
482
506
|
content: The fragment or whole-file body under validation.
|
|
@@ -501,6 +525,13 @@ def _hook_infrastructure_blocking_issues(
|
|
|
501
525
|
file_path,
|
|
502
526
|
all_changed_lines,
|
|
503
527
|
)
|
|
528
|
+
all_issues.extend(
|
|
529
|
+
check_same_file_inline_duplicate_body(
|
|
530
|
+
effective_content,
|
|
531
|
+
file_path,
|
|
532
|
+
all_changed_lines,
|
|
533
|
+
)
|
|
534
|
+
)
|
|
504
535
|
all_issues.extend(check_zero_payload_function_alias(effective_content, file_path))
|
|
505
536
|
return all_issues
|
|
506
537
|
|