claude-dev-env 2.30.1 → 2.31.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (143) hide show
  1. package/.agents/agents/pr-description-writer.md +1 -1
  2. package/AGENTS.md +6 -0
  3. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_enforcer_loading.py +37 -0
  4. package/bin/install.prune.test.mjs +4 -2
  5. package/docs/CODE_RULES.md +12 -0
  6. package/hooks/advisory/migration_safety_advisor.py +25 -1
  7. package/hooks/advisory/refactor_guard.py +85 -18
  8. package/hooks/advisory/test_migration_safety_advisor.py +89 -5
  9. package/hooks/advisory/test_refactor_guard.py +87 -0
  10. package/hooks/advisory/test_refactor_guard_advisory.py +72 -1
  11. package/hooks/blocking/code_rules_constants_config.py +7 -6
  12. package/hooks/blocking/code_rules_duplicate_body.py +272 -100
  13. package/hooks/blocking/code_rules_enforcer.py +89 -45
  14. package/hooks/blocking/code_rules_shared.py +64 -14
  15. package/hooks/blocking/codex_apply_patch.py +136 -21
  16. package/hooks/blocking/config/codex_apply_patch_constants.py +13 -0
  17. package/hooks/blocking/conftest.py +70 -0
  18. package/hooks/blocking/duplicate_rmtree_helper_blocker.py +26 -16
  19. package/hooks/blocking/hook_prose_detector_consistency.py +30 -20
  20. package/hooks/blocking/pii_payload_scan.py +61 -0
  21. package/hooks/blocking/pii_prevention_blocker.py +5 -0
  22. package/hooks/blocking/pr_description_writer_gate.py +256 -0
  23. package/hooks/blocking/pre_tool_use_dispatcher.py +1 -1
  24. package/hooks/blocking/sensitive_file_protector.py +18 -3
  25. package/hooks/blocking/shell_substitution_blocker.py +25 -47
  26. package/hooks/blocking/stale_comment_reference_blocker.py +94 -24
  27. package/hooks/blocking/state_description_blocker.py +39 -26
  28. package/hooks/blocking/subprocess_budget_completeness.py +17 -43
  29. package/hooks/blocking/tdd_enforcer.py +63 -16
  30. package/hooks/blocking/tdd_enforcer_parts/config/tdd_enforcer_constants.py +11 -1
  31. package/hooks/blocking/tdd_enforcer_parts/content_hash_store.py +180 -0
  32. package/hooks/blocking/tdd_enforcer_parts/decisions.py +6 -2
  33. package/hooks/blocking/tdd_enforcer_parts/freshness.py +18 -3
  34. package/hooks/blocking/tdd_enforcer_parts/git_tracking.py +66 -1
  35. package/hooks/blocking/tdd_enforcer_parts/tests/test_content_hash_store.py +143 -0
  36. package/hooks/blocking/tdd_enforcer_parts/tests/test_decisions.py +12 -0
  37. package/hooks/blocking/tdd_enforcer_parts/tests/test_git_tracking.py +63 -0
  38. package/hooks/blocking/test_bash_pre_tool_use_dispatcher.py +1 -0
  39. package/hooks/blocking/test_code_rules_enforcer_cap_meta.py +81 -2
  40. package/hooks/blocking/test_code_rules_enforcer_codex_apply_patch.py +35 -0
  41. package/hooks/blocking/test_code_rules_enforcer_config_path.py +29 -16
  42. package/hooks/blocking/test_code_rules_enforcer_duplicate_body_hook_routing.py +2 -0
  43. package/hooks/blocking/test_code_rules_enforcer_ephemeral.py +12 -1
  44. package/hooks/blocking/test_code_rules_enforcer_ephemeral_repository_root.py +118 -0
  45. package/hooks/blocking/test_code_rules_enforcer_multi_edit.py +118 -0
  46. package/hooks/blocking/test_code_rules_enforcer_narrow_edit.py +2 -0
  47. package/hooks/blocking/test_code_rules_enforcer_precheck_forecast.py +2 -0
  48. package/hooks/blocking/test_code_rules_enforcer_same_file_inline_duplicate.py +244 -0
  49. package/hooks/blocking/test_code_rules_enforcer_split_constants_config.py +29 -28
  50. package/hooks/blocking/test_code_rules_enforcer_split_entry_2.py +24 -0
  51. package/hooks/blocking/test_code_rules_enforcer_stage.py +2 -0
  52. package/hooks/blocking/test_code_rules_enforcer_zero_payload_alias_hook_routing.py +2 -0
  53. package/hooks/blocking/test_codex_apply_patch.py +43 -0
  54. package/hooks/blocking/test_duplicate_rmtree_helper_blocker.py +68 -0
  55. package/hooks/blocking/test_hook_prose_detector_consistency.py +41 -0
  56. package/hooks/blocking/test_pii_payload_scan.py +65 -2
  57. package/hooks/blocking/test_pii_prevention_blocker.py +80 -10
  58. package/hooks/blocking/test_pr_description_writer_gate.py +234 -0
  59. package/hooks/blocking/test_pre_tool_use_dispatcher.py +215 -25
  60. package/hooks/blocking/test_pre_tool_use_dispatcher_native.py +18 -6
  61. package/hooks/blocking/test_sensitive_file_protector.py +42 -2
  62. package/hooks/blocking/test_stale_comment_reference_blocker.py +71 -0
  63. package/hooks/blocking/test_state_description_blocker.py +163 -49
  64. package/hooks/blocking/test_subprocess_budget_completeness.py +67 -0
  65. package/hooks/blocking/test_tdd_enforcer.py +246 -2
  66. package/hooks/blocking/test_windows_rmtree_blocker.py +63 -0
  67. package/hooks/blocking/test_write_existing_file_blocker.py +122 -0
  68. package/hooks/blocking/windows_rmtree_blocker.py +21 -7
  69. package/hooks/blocking/write_existing_file_blocker.py +75 -29
  70. package/hooks/conftest.py +47 -0
  71. package/hooks/hooks.json +7 -12
  72. package/hooks/hooks_constants/bash_pre_tool_use_dispatcher_constants.py +3 -0
  73. package/hooks/hooks_constants/code_rules_enforcer_constants.py +4 -0
  74. package/hooks/hooks_constants/duplicate_rmtree_helper_blocker_constants.py +3 -0
  75. package/hooks/hooks_constants/hook_prose_detector_consistency_constants.py +7 -2
  76. package/hooks/hooks_constants/migration_safety_advisor_constants.py +7 -0
  77. package/hooks/hooks_constants/multi_edit_reconstruction.py +44 -3
  78. package/hooks/hooks_constants/pii_prevention_constants.py +9 -7
  79. package/hooks/hooks_constants/pr_description_writer_gate_constants.py +73 -0
  80. package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +31 -14
  81. package/hooks/hooks_constants/session_edit_stage_gate_constants.py +15 -1
  82. package/hooks/hooks_constants/state_description_blocker_constants.py +1 -0
  83. package/hooks/hooks_constants/subprocess_budget_completeness_content.py +71 -0
  84. package/hooks/hooks_constants/test_bash_pre_tool_use_dispatcher_constants.py +2 -0
  85. package/hooks/hooks_constants/test_code_rules_enforcer_constants.py +2 -0
  86. package/hooks/hooks_constants/test_multi_edit_reconstruction.py +103 -0
  87. package/hooks/hooks_constants/test_pre_tool_use_dispatcher_constants.py +139 -25
  88. package/hooks/hooks_constants/test_subprocess_budget_completeness_content.py +103 -0
  89. package/hooks/hooks_constants/validation_phase_constants.py +2 -0
  90. package/hooks/hooks_constants/workflow_substitution_slot_blocker_constants.py +7 -3
  91. package/hooks/observability/pr_description_writer_spawn_tracker.py +113 -0
  92. package/hooks/observability/session_file_edit_tracker.py +25 -4
  93. package/hooks/observability/test_pr_description_writer_spawn_tracker.py +150 -0
  94. package/hooks/observability/test_session_file_edit_tracker.py +32 -0
  95. package/hooks/validation/hook_format_validator.py +23 -9
  96. package/hooks/validation/test_hook_format_validator.py +58 -0
  97. package/hooks/validators/README.md +1 -1
  98. package/hooks/validators/abbreviation_checks.py +6 -8
  99. package/hooks/validators/code_quality_checks.py +4 -4
  100. package/hooks/validators/config/abbreviation_checks_constants.py +3 -0
  101. package/hooks/validators/config/baseline_identity_constants.py +12 -0
  102. package/hooks/validators/config/directory_exemption_constants.py +44 -1
  103. package/hooks/validators/config/fast_save_validator_constants.py +32 -0
  104. package/hooks/validators/config/hook_timing_harness_constants.py +3 -1
  105. package/hooks/validators/config/test_abbreviation_checks_constants.py +11 -0
  106. package/hooks/validators/config/validator_base_constants.py +9 -0
  107. package/hooks/validators/fast_save_validators.py +275 -0
  108. package/hooks/validators/hook_timing_harness.py +8 -4
  109. package/hooks/validators/pr_reference_checks.py +2 -2
  110. package/hooks/validators/python_antipattern_checks.py +3 -3
  111. package/hooks/validators/python_style_checks.py +37 -62
  112. package/hooks/validators/python_style_helpers.py +16 -2
  113. package/hooks/validators/python_style_import_bootstrap.py +83 -0
  114. package/hooks/validators/react_checks.py +2 -9
  115. package/hooks/validators/run_all_validators.py +201 -155
  116. package/hooks/validators/security_checks.py +3 -3
  117. package/hooks/validators/test_abbreviation_checks.py +20 -0
  118. package/hooks/validators/test_directory_exemption_constants.py +30 -24
  119. package/hooks/validators/test_fast_save_validators.py +172 -0
  120. package/hooks/validators/test_hook_timing_harness.py +10 -1
  121. package/hooks/validators/test_python_style_checks.py +9 -0
  122. package/hooks/validators/test_python_style_checks_import_order.py +81 -0
  123. package/hooks/validators/test_python_style_helpers.py +35 -0
  124. package/hooks/validators/test_python_style_import_bootstrap.py +69 -0
  125. package/hooks/validators/test_run_all_validators.py +46 -0
  126. package/hooks/validators/test_run_all_validators_pretooluse.py +163 -0
  127. package/hooks/validators/test_run_all_validators_repository_root.py +202 -0
  128. package/hooks/validators/test_validator_base.py +51 -2
  129. package/hooks/validators/todo_checks.py +2 -2
  130. package/hooks/validators/type_safety_checks.py +3 -3
  131. package/hooks/validators/useless_test_checks.py +3 -3
  132. package/hooks/validators/validator_base.py +73 -1
  133. package/package.json +1 -1
  134. package/rules/docstring-prose-matches-implementation.md +1 -1
  135. package/rules/git-workflow.md +2 -0
  136. package/rules/testing.md +4 -0
  137. package/scripts/codex_compat_materializer.py +5 -0
  138. package/scripts/dev_env_scripts_constants/grok_worker_constants.py +15 -0
  139. package/scripts/grok_headless_runner.py +15 -12
  140. package/scripts/shared_tree_paths.py +70 -0
  141. package/scripts/test_shared_tree_paths.py +143 -0
  142. package/scripts/tests/test_codex_compat_materializer.py +37 -11
  143. package/hooks/blocking/tests/conftest.py +0 -10
@@ -54,6 +54,7 @@ import ast
54
54
  import copy
55
55
  import sys
56
56
  from pathlib import Path
57
+ from typing import NamedTuple
57
58
 
58
59
  _blocking_directory = str(Path(__file__).resolve().parent)
59
60
  _hooks_directory = str(Path(__file__).resolve().parent.parent)
@@ -84,6 +85,21 @@ from hooks_constants.duplicate_function_body_constants import ( # noqa: E402
84
85
  )
85
86
 
86
87
 
88
+ _ModuleScopeFunction = ast.FunctionDef | ast.AsyncFunctionDef
89
+
90
+
91
+ class _FunctionScanProfile(NamedTuple):
92
+ """The three readings of one module-scope function the inline scan compares.
93
+
94
+ Every reading is a property of that function alone, so each is taken once per
95
+ file and read by each pairing the scan weighs.
96
+ """
97
+
98
+ reachable_statement_count: int
99
+ all_sorted_body_dumps: list[str]
100
+ all_block_dumps: list[list[str]]
101
+
102
+
87
103
  def _body_statements_without_docstring(
88
104
  function_node: ast.FunctionDef | ast.AsyncFunctionDef,
89
105
  ) -> list[ast.stmt]:
@@ -384,6 +400,37 @@ def _normalized_statement_dump(statement: ast.stmt) -> str:
384
400
  return ast.dump(canonical_statement, annotate_fields=False)
385
401
 
386
402
 
403
+ def _memoized_statement_dump(
404
+ statement: ast.stmt,
405
+ dump_by_statement: dict[ast.stmt, str],
406
+ ) -> str:
407
+ """Return the normalized dump of one statement, computing it at most once.
408
+
409
+ The inline scan reads the same statement's dump many times over: once for its
410
+ enclosing function's block window, once for that function's twin-test
411
+ multiset, and again for every helper the function is weighed against. Dumping
412
+ deep-copies the statement subtree before canonicalizing string constants, so
413
+ this memo serves the repeat reads.
414
+
415
+ An ``ast`` node defines no ``__eq__``, so the key is the node's identity and
416
+ the dict holds a strong reference to it: a key cannot be freed and a later
417
+ node cannot reuse its slot. The caller owns one memo per scanned file and
418
+ drops it when the scan returns, so no entry outlives its parse tree.
419
+
420
+ Args:
421
+ statement: The statement node to fingerprint.
422
+ dump_by_statement: The file's statement-to-dump memo, mutated in place.
423
+
424
+ Returns:
425
+ The value ``_normalized_statement_dump`` returns for this statement.
426
+ """
427
+ memoized_dump = dump_by_statement.get(statement)
428
+ if memoized_dump is None:
429
+ memoized_dump = _normalized_statement_dump(statement)
430
+ dump_by_statement[statement] = memoized_dump
431
+ return memoized_dump
432
+
433
+
387
434
  def _statement_blocks_in_function(
388
435
  function_node: ast.FunctionDef | ast.AsyncFunctionDef,
389
436
  ) -> list[list[ast.stmt]]:
@@ -443,76 +490,6 @@ def _total_reachable_statement_count(
443
490
  return total_statement_count
444
491
 
445
492
 
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
493
  def _helper_match_window_dumps(
517
494
  function_node: ast.FunctionDef | ast.AsyncFunctionDef,
518
495
  ) -> list[str] | None:
@@ -616,40 +593,235 @@ def check_same_file_inline_duplicate_body(
616
593
  for each_node in tree.body
617
594
  if isinstance(each_node, ast.FunctionDef | ast.AsyncFunctionDef)
618
595
  ]
596
+ return _scope_violations_to_changed_lines(
597
+ _inline_duplicate_violations(all_top_level_functions, MAX_DUPLICATE_BODY_ISSUES),
598
+ all_changed_lines,
599
+ defer_scope_to_caller,
600
+ )
601
+
602
+
603
+ def _block_dumps_in_function(
604
+ function_node: _ModuleScopeFunction,
605
+ dump_by_statement: dict[ast.stmt, str],
606
+ ) -> list[list[str]]:
607
+ """Return the per-statement dumps of each reachable block in a function.
608
+
609
+ Args:
610
+ function_node: The module-scope function to read.
611
+ dump_by_statement: The file's statement-to-dump memo, mutated in place.
612
+
613
+ Returns:
614
+ One dump list per reachable block, in block-collection order.
615
+ """
616
+ return [
617
+ [
618
+ _memoized_statement_dump(each_statement, dump_by_statement)
619
+ for each_statement in each_block
620
+ ]
621
+ for each_block in _statement_blocks_in_function(function_node)
622
+ ]
623
+
624
+
625
+ def _function_scan_profile(
626
+ function_node: _ModuleScopeFunction,
627
+ dump_by_statement: dict[ast.stmt, str],
628
+ ) -> _FunctionScanProfile:
629
+ """Take the three readings the inline scan needs from one function.
630
+
631
+ The sorted body dumps carry the docstring-stripped statement multiset that
632
+ tests whether two functions are structural twins. Sorting makes that
633
+ comparison order-independent, so two peer helpers that read different inputs
634
+ but share the same statement shapes compare equal and are left to the
635
+ cross-file check.
636
+
637
+ Args:
638
+ function_node: The module-scope function to read.
639
+ dump_by_statement: The file's statement-to-dump memo, mutated in place.
640
+
641
+ Returns:
642
+ The function's reachable statement count, sorted body dumps, and
643
+ per-block statement dumps.
644
+ """
645
+ return _FunctionScanProfile(
646
+ reachable_statement_count=_total_reachable_statement_count(function_node),
647
+ all_sorted_body_dumps=sorted(
648
+ _memoized_statement_dump(each_statement, dump_by_statement)
649
+ for each_statement in _body_statements_without_docstring(function_node)
650
+ ),
651
+ all_block_dumps=_block_dumps_in_function(function_node, dump_by_statement),
652
+ )
653
+
654
+
655
+ def _block_dumps_contain_window(
656
+ all_block_dumps: list[str],
657
+ all_window_dumps: list[str],
658
+ ) -> bool:
659
+ """Return whether one block carries the window as a contiguous run.
660
+
661
+ A block shorter than the window holds no run and answers False.
662
+
663
+ Args:
664
+ all_block_dumps: One reachable block's per-statement dumps.
665
+ all_window_dumps: The helper's window dumps to seek.
666
+
667
+ Returns:
668
+ True when the window appears in the block as a contiguous run.
669
+ """
670
+ window_length = len(all_window_dumps)
671
+ last_start_index = len(all_block_dumps) - window_length
672
+ for each_start_index in range(last_start_index + 1):
673
+ window_end = each_start_index + window_length
674
+ if all_block_dumps[each_start_index:window_end] == all_window_dumps:
675
+ return True
676
+ return False
677
+
678
+
679
+ def _profile_inlines_window(
680
+ helper_profile: _FunctionScanProfile,
681
+ enclosing_profile: _FunctionScanProfile,
682
+ all_helper_window_dumps: list[str],
683
+ ) -> bool:
684
+ """Return whether a function inlines a helper window inside a larger body.
685
+
686
+ A run inside one of the enclosing function's reachable blocks whose dumps
687
+ match the helper's, in order, is the inlined copy. Two guards hold the report
688
+ to a genuine inlining: the enclosing function carries more reachable
689
+ statements than the window, and the two functions are not structural twins,
690
+ so peer helpers sharing a statement shape are left to the cross-file check.
691
+
692
+ Args:
693
+ helper_profile: The candidate helper's readings, for the twin guard.
694
+ enclosing_profile: The candidate enclosing function's readings.
695
+ all_helper_window_dumps: The helper's substantive-block statement dumps.
696
+
697
+ Returns:
698
+ True when a block carries the window inside a larger, non-twin body.
699
+ """
700
+ if enclosing_profile.reachable_statement_count <= len(all_helper_window_dumps):
701
+ return False
702
+ if helper_profile.all_sorted_body_dumps == enclosing_profile.all_sorted_body_dumps:
703
+ return False
704
+ return any(
705
+ _block_dumps_contain_window(each_block_dumps, all_helper_window_dumps)
706
+ for each_block_dumps in enclosing_profile.all_block_dumps
707
+ )
708
+
709
+
710
+ def _first_enclosing_inliner(
711
+ helper_node: _ModuleScopeFunction,
712
+ all_helper_window_dumps: list[str],
713
+ profile_by_function: dict[_ModuleScopeFunction, _FunctionScanProfile],
714
+ ) -> _ModuleScopeFunction | None:
715
+ """Return the first other module-scope function that inlines this window.
716
+
717
+ Args:
718
+ helper_node: The candidate helper whose window is sought.
719
+ all_helper_window_dumps: The helper's substantive-block statement dumps.
720
+ profile_by_function: Each function's readings, keyed in source order.
721
+
722
+ Returns:
723
+ The first function in source order that carries the window, or None.
724
+ """
725
+ helper_profile = profile_by_function[helper_node]
726
+ for each_enclosing, each_profile in profile_by_function.items():
727
+ if each_enclosing is helper_node:
728
+ continue
729
+ if _profile_inlines_window(
730
+ helper_profile, each_profile, all_helper_window_dumps
731
+ ):
732
+ return each_enclosing
733
+ return None
734
+
735
+
736
+ def _inline_duplicate_violation(
737
+ helper_node: _ModuleScopeFunction,
738
+ enclosing_node: _ModuleScopeFunction,
739
+ ) -> tuple[frozenset[int], str]:
740
+ """Return the scoped lines and report text for one inlined-helper finding.
741
+
742
+ Args:
743
+ helper_node: The helper whose body appears inline elsewhere.
744
+ enclosing_node: The function carrying the inlined copy.
745
+
746
+ Returns:
747
+ The union of both function spans, and the message naming both.
748
+ """
749
+ helper_span = _function_definition_span(helper_node)
750
+ enclosing_span = _function_definition_span(enclosing_node)
751
+ span_suffix = SAME_FILE_INLINE_DUPLICATE_SPAN_SUFFIX_TEMPLATE.format(
752
+ helper_start=helper_span.start,
753
+ helper_length=len(helper_span),
754
+ enclosing_start=enclosing_span.start,
755
+ enclosing_length=len(enclosing_span),
756
+ )
757
+ message = (
758
+ f"Function {helper_node.name!r} duplicates an inline block in "
759
+ f"{enclosing_node.name!r} — {SAME_FILE_INLINE_DUPLICATE_GUIDANCE} "
760
+ f"{span_suffix}"
761
+ )
762
+ return (frozenset(helper_span) | frozenset(enclosing_span), message)
763
+
764
+
765
+ def _violations_for_qualifying_helpers(
766
+ window_by_function: dict[_ModuleScopeFunction, list[str] | None],
767
+ profile_by_function: dict[_ModuleScopeFunction, _FunctionScanProfile],
768
+ maximum_issue_count: int,
769
+ ) -> list[tuple[frozenset[int], str]]:
770
+ """Walk the helpers in source order and report the first inliner of each.
771
+
772
+ Args:
773
+ window_by_function: Each function's helper match window, or None.
774
+ profile_by_function: Each function's readings, keyed in source order.
775
+ maximum_issue_count: The report ceiling the walk stops at.
776
+
777
+ Returns:
778
+ The in-scope lines and message of each report, in walk order.
779
+ """
619
780
  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:
781
+ for each_helper, each_window_dumps in window_by_function.items():
782
+ if each_window_dumps is None:
623
783
  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}"
784
+ each_enclosing = _first_enclosing_inliner(
785
+ each_helper, each_window_dumps, profile_by_function
786
+ )
787
+ if each_enclosing is not None:
788
+ all_violations_in_walk_order.append(
789
+ _inline_duplicate_violation(each_helper, each_enclosing)
644
790
  )
645
- all_violations_in_walk_order.append((in_scope_lines, message))
791
+ if len(all_violations_in_walk_order) >= maximum_issue_count:
646
792
  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,
793
+ return all_violations_in_walk_order
794
+
795
+
796
+ def _inline_duplicate_violations(
797
+ all_top_level_functions: list[_ModuleScopeFunction],
798
+ maximum_issue_count: int,
799
+ ) -> list[tuple[frozenset[int], str]]:
800
+ """Report every module-scope helper whose body is inlined in another function.
801
+
802
+ The dump memo and the per-function readings live for this one call and are
803
+ dropped when it returns, so no reading outlives the parse tree it describes.
804
+
805
+ Args:
806
+ all_top_level_functions: The module-scope functions in source order.
807
+ maximum_issue_count: The report ceiling the walk stops at.
808
+
809
+ Returns:
810
+ The in-scope lines and message of each report, in walk order.
811
+ """
812
+ dump_by_statement: dict[ast.stmt, str] = {}
813
+ window_by_function = {
814
+ each_function: _helper_match_window_dumps(each_function)
815
+ for each_function in all_top_level_functions
816
+ }
817
+ if all(each_window is None for each_window in window_by_function.values()):
818
+ return []
819
+ profile_by_function = {
820
+ each_function: _function_scan_profile(each_function, dump_by_statement)
821
+ for each_function in all_top_level_functions
822
+ }
823
+ return _violations_for_qualifying_helpers(
824
+ window_by_function, profile_by_function, maximum_issue_count
653
825
  )
654
826
 
655
827