claude-dev-env 1.94.0 → 2.0.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/advisor/CLAUDE.md +2 -2
- package/_shared/advisor/advisor-protocol.md +41 -33
- package/_shared/advisor/scripts/config/advisor_scripts_constants/model_tier_run_validator_constants.py +17 -13
- package/_shared/advisor/scripts/model_tier_run_validator.py +27 -18
- package/_shared/advisor/scripts/tests/test_model_tier_run_validator.py +95 -25
- package/_shared/advisor/scripts/tests/test_tier_model_ids.py +17 -17
- package/_shared/advisor/scripts/tier_model_ids.py +18 -18
- package/_shared/pr-loop/CLAUDE.md +1 -0
- package/_shared/pr-loop/scripts/CLAUDE.md +2 -1
- package/_shared/pr-loop/scripts/README.md +1 -0
- package/_shared/pr-loop/scripts/code_rules_gate.py +253 -1980
- package/_shared/pr-loop/scripts/code_rules_gate_parts/CLAUDE.md +32 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/__init__.py +7 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/added_line_maps.py +268 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/enforcer_loading.py +172 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/gate_arguments.py +70 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/gate_running.py +326 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/git_blob_readers.py +85 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/git_file_sets.py +331 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/staged_test_running.py +369 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/conftest.py +14 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_added_line_maps.py +118 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_enforcer_loading.py +17 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_gate_arguments.py +29 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_gate_running.py +99 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_git_blob_readers.py +69 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_git_file_sets.py +137 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_staged_test_running.py +116 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_violation_scoping.py +75 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_wrapper_plumb_check.py +49 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/violation_scoping.py +328 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/wrapper_plumb_check.py +206 -0
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/code_rules_gate_constants.py +24 -17
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/reviews_disabled_constants.py +1 -0
- package/_shared/pr-loop/scripts/reviews_disabled.py +19 -2
- package/_shared/pr-loop/scripts/test_code_rules_gate.py +278 -0
- package/_shared/pr-loop/scripts/tests/test_code_rules_gate_constants.py +6 -39
- package/_shared/pr-loop/scripts/tests/test_reviews_disabled.py +43 -0
- package/_shared/pr-loop/worker-spawn.md +186 -0
- package/agents/code-verifier.md +1 -1
- package/bin/CLAUDE.md +10 -1
- package/bin/ever-shipped-skills.mjs +73 -0
- package/bin/expand_home_directory_tokens.mjs +1 -1
- package/bin/install.mjs +140 -7
- package/bin/install.prune.test.mjs +457 -0
- package/docs/CODE_RULES.md +1 -1
- package/hooks/advisory/refactor_guard.py +3 -4
- package/hooks/blocking/CLAUDE.md +7 -1
- package/hooks/blocking/block_main_commit.py +2 -2
- package/hooks/blocking/claude_md_orphan_file_blocker.py +75 -699
- package/hooks/blocking/claude_md_orphan_file_blocker_parts/CLAUDE.md +28 -0
- package/hooks/blocking/claude_md_orphan_file_blocker_parts/__init__.py +1 -0
- package/hooks/blocking/claude_md_orphan_file_blocker_parts/config/__init__.py +1 -0
- package/hooks/blocking/claude_md_orphan_file_blocker_parts/config/orphan_blocker_constants.py +18 -0
- package/hooks/blocking/claude_md_orphan_file_blocker_parts/decision.py +81 -0
- package/hooks/blocking/claude_md_orphan_file_blocker_parts/references.py +307 -0
- package/hooks/blocking/claude_md_orphan_file_blocker_parts/scan_plan.py +124 -0
- package/hooks/blocking/claude_md_orphan_file_blocker_parts/subtree_scan.py +179 -0
- package/hooks/blocking/claude_md_orphan_file_blocker_parts/tests/conftest.py +10 -0
- package/hooks/blocking/claude_md_orphan_file_blocker_parts/tests/test_decision.py +34 -0
- package/hooks/blocking/claude_md_orphan_file_blocker_parts/tests/test_references.py +42 -0
- package/hooks/blocking/claude_md_orphan_file_blocker_parts/tests/test_scan_plan.py +27 -0
- package/hooks/blocking/claude_md_orphan_file_blocker_parts/tests/test_subtree_scan.py +30 -0
- package/hooks/blocking/code_rules_boolean_mustcheck.py +1 -1
- package/hooks/blocking/code_rules_enforcer.py +4 -0
- package/hooks/blocking/code_rules_mock_completeness.py +1 -1
- package/hooks/blocking/code_rules_optional_params.py +2 -2
- package/hooks/blocking/code_rules_shared.py +83 -1
- package/hooks/blocking/code_rules_test_assertions.py +1 -1
- package/hooks/blocking/code_rules_test_layout.py +9 -3
- package/hooks/blocking/code_rules_typeddict_stub.py +1 -1
- package/hooks/blocking/gh_pr_author_enforcer.py +1 -1
- package/hooks/blocking/inventory_intent_records/CLAUDE.md +26 -0
- package/hooks/blocking/inventory_intent_records/__init__.py +1 -0
- package/hooks/blocking/inventory_intent_records/config/__init__.py +1 -0
- package/hooks/blocking/inventory_intent_records/config/intent_records_constants.py +20 -0
- package/hooks/blocking/inventory_intent_records/records.py +271 -0
- package/hooks/blocking/inventory_intent_records/tests/conftest.py +10 -0
- package/hooks/blocking/inventory_intent_records/tests/test_records.py +80 -0
- package/hooks/blocking/package_inventory_stale_blocker.py +54 -384
- package/hooks/blocking/package_inventory_stale_blocker_parts/CLAUDE.md +26 -0
- package/hooks/blocking/package_inventory_stale_blocker_parts/__init__.py +1 -0
- package/hooks/blocking/package_inventory_stale_blocker_parts/config/__init__.py +1 -0
- package/hooks/blocking/package_inventory_stale_blocker_parts/config/inventory_blocker_constants.py +16 -0
- package/hooks/blocking/package_inventory_stale_blocker_parts/decision.py +84 -0
- package/hooks/blocking/package_inventory_stale_blocker_parts/inventory_detection.py +307 -0
- package/hooks/blocking/package_inventory_stale_blocker_parts/tests/conftest.py +10 -0
- package/hooks/blocking/package_inventory_stale_blocker_parts/tests/test_decision.py +38 -0
- package/hooks/blocking/package_inventory_stale_blocker_parts/tests/test_inventory_detection.py +61 -0
- package/hooks/blocking/pii_payload_scan.py +138 -42
- package/hooks/blocking/pii_prevention_blocker.py +185 -291
- package/hooks/blocking/pii_prevention_blocker_parts/CLAUDE.md +24 -0
- package/hooks/blocking/pii_prevention_blocker_parts/__init__.py +1 -0
- package/hooks/blocking/pii_prevention_blocker_parts/config/__init__.py +1 -0
- package/hooks/blocking/pii_prevention_blocker_parts/config/repository_resolution_constants.py +28 -0
- package/hooks/blocking/pii_prevention_blocker_parts/repository_exemption.py +214 -0
- package/hooks/blocking/pii_prevention_blocker_parts/repository_resolution.py +208 -0
- package/hooks/blocking/plain_language_blocker.py +138 -4
- package/hooks/blocking/pr_description_command_parser.py +8 -4
- package/hooks/blocking/precommit_code_rules_gate.py +3 -3
- package/hooks/blocking/sensitive_file_protector.py +114 -48
- package/hooks/blocking/tdd_enforcer.py +97 -601
- package/hooks/blocking/tdd_enforcer_parts/CLAUDE.md +30 -0
- package/hooks/blocking/tdd_enforcer_parts/__init__.py +1 -0
- package/hooks/blocking/tdd_enforcer_parts/candidate_paths.py +142 -0
- package/hooks/blocking/tdd_enforcer_parts/config/__init__.py +1 -0
- package/hooks/blocking/tdd_enforcer_parts/config/tdd_enforcer_constants.py +32 -0
- package/hooks/blocking/tdd_enforcer_parts/content_analysis.py +268 -0
- package/hooks/blocking/tdd_enforcer_parts/decisions.py +92 -0
- package/hooks/blocking/tdd_enforcer_parts/freshness.py +80 -0
- package/hooks/blocking/tdd_enforcer_parts/git_tracking.py +63 -0
- package/hooks/blocking/tdd_enforcer_parts/path_classification.py +119 -0
- package/hooks/blocking/tdd_enforcer_parts/tests/conftest.py +10 -0
- package/hooks/blocking/tdd_enforcer_parts/tests/test_candidate_paths.py +31 -0
- package/hooks/blocking/tdd_enforcer_parts/tests/test_content_analysis.py +30 -0
- package/hooks/blocking/tdd_enforcer_parts/tests/test_decisions.py +34 -0
- package/hooks/blocking/tdd_enforcer_parts/tests/test_freshness.py +28 -0
- package/hooks/blocking/tdd_enforcer_parts/tests/test_git_tracking.py +48 -0
- package/hooks/blocking/tdd_enforcer_parts/tests/test_path_classification.py +36 -0
- package/hooks/blocking/test_code_rules_enforcer_scratchpad.py +105 -0
- package/hooks/blocking/test_code_rules_shared.py +181 -0
- package/hooks/blocking/test_inventory_deadlock_resolution.py +154 -0
- package/hooks/blocking/test_pii_payload_scan.py +168 -0
- package/hooks/blocking/test_plain_language_blocker_allowlist.py +184 -0
- package/hooks/blocking/test_sensitive_file_protector.py +185 -0
- package/hooks/blocking/test_tdd_enforcer_restore.py +108 -0
- package/hooks/blocking/test_tdd_enforcer_scratchpad.py +105 -0
- package/hooks/blocking/tests/conftest.py +10 -0
- package/hooks/blocking/tests/test_pii_prevention_blocker.py +260 -0
- package/hooks/blocking/tests/test_repository_exemption.py +105 -0
- package/hooks/blocking/tests/test_repository_resolution.py +108 -0
- package/hooks/diagnostic/hook_log_extractor.py +12 -10
- package/hooks/git-hooks/post_commit.py +3 -4
- package/hooks/hooks_constants/CLAUDE.md +4 -2
- package/hooks/hooks_constants/banned_identifiers_constants.py +0 -1
- package/hooks/hooks_constants/code_rules_path_utils_constants.py +1 -1
- package/hooks/hooks_constants/harness_scratchpad_constants.py +17 -0
- package/hooks/hooks_constants/local_identity.py +59 -8
- package/hooks/hooks_constants/pii_prevention_constants.py +0 -6
- package/hooks/hooks_constants/plain_language_blocker_constants.py +5 -0
- package/hooks/hooks_constants/sensitive_file_protector_constants.py +42 -0
- package/hooks/hooks_constants/test_local_identity.py +105 -3
- package/hooks/pyproject.toml +52 -4
- package/hooks/session/plugin_data_dir_cleanup.py +0 -1
- package/hooks/validation/mypy_validator.py +2 -2
- package/hooks/validators/CLAUDE.md +1 -1
- package/hooks/validators/README.md +2 -0
- package/hooks/validators/health_check.py +1 -0
- package/hooks/validators/mypy_integration.py +2 -0
- package/hooks/validators/python_style_checks.py +114 -136
- package/hooks/validators/python_style_helpers.py +95 -0
- package/hooks/validators/ruff_integration.py +3 -0
- package/hooks/validators/test_python_style_checks.py +0 -164
- package/hooks/validators/test_python_style_checks_decorator_gap.py +119 -0
- package/hooks/validators/test_python_style_fixes.py +251 -0
- package/hooks/validators/test_python_style_helpers.py +125 -0
- package/hooks/workflow/auto_formatter.py +5 -4
- package/package.json +1 -1
- package/rules/CLAUDE.md +1 -0
- package/rules/anti-corollary-tests.md +69 -0
- package/rules/bdd.md +1 -3
- package/rules/code-reviews.md +1 -1
- package/rules/gh-paginate.md +1 -1
- package/rules/plain-language.md +2 -0
- package/scripts/CLAUDE.md +4 -0
- package/scripts/dev_env_scripts_constants/CLAUDE.md +6 -4
- package/scripts/dev_env_scripts_constants/code_review_constants.py +71 -0
- package/scripts/dev_env_scripts_constants/grok_worker_constants.py +435 -0
- package/scripts/dev_env_scripts_constants/timing.py +7 -1
- package/scripts/grok_headless_runner.py +294 -0
- package/scripts/grok_worker_preflight.py +410 -0
- package/scripts/invoke_code_review.py +463 -0
- package/scripts/resolve_worker_spawn.py +619 -0
- package/scripts/spawn_grok_batch.py +672 -0
- package/scripts/test_grok_headless_runner.py +626 -0
- package/scripts/test_grok_worker_preflight.py +1054 -0
- package/scripts/test_invoke_code_review.py +672 -0
- package/scripts/test_resolve_worker_spawn.py +1014 -0
- package/scripts/test_spawn_grok_batch.py +1017 -0
- package/skills/CLAUDE.md +6 -3
- package/skills/_shared/pr-loop/scripts/build_audit_prompt.py +72 -13
- package/skills/_shared/pr-loop/scripts/build_fix_prompt.py +121 -14
- package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/path_resolver_constants.py +78 -0
- package/skills/_shared/pr-loop/scripts/test_build_audit_prompt.py +121 -0
- package/skills/_shared/pr-loop/scripts/test_build_fix_prompt.py +196 -6
- package/skills/autoconverge/CLAUDE.md +3 -3
- package/skills/autoconverge/SKILL.md +9 -3
- package/skills/autoconverge/reference/CLAUDE.md +2 -2
- package/skills/autoconverge/reference/convergence.md +33 -11
- package/skills/autoconverge/reference/stop-conditions.md +16 -5
- package/skills/autoconverge/workflow/CLAUDE.md +2 -1
- package/skills/autoconverge/workflow/converge.clean-audit.test.mjs +7 -2
- package/skills/autoconverge/workflow/converge.codex-gate.test.mjs +300 -0
- package/skills/autoconverge/workflow/converge.contract.test.mjs +5 -5
- package/skills/autoconverge/workflow/converge.copilot-gate.test.mjs +29 -29
- package/skills/autoconverge/workflow/converge.fix-progress.test.mjs +1 -1
- package/skills/autoconverge/workflow/converge.mjs +202 -18
- package/skills/bugteam/CLAUDE.md +2 -2
- package/skills/bugteam/CONSTRAINTS.md +3 -2
- package/skills/bugteam/PROMPTS.md +7 -6
- package/skills/bugteam/SKILL.md +18 -13
- package/skills/bugteam/reference/README.md +2 -3
- package/skills/bugteam/reference/audit-and-teammates.md +215 -35
- package/skills/bugteam/reference/design-rationale.md +1 -1
- package/skills/bugteam/reference/obstacles/CLAUDE.md +1 -1
- package/skills/bugteam/reference/team-setup.md +8 -2
- package/skills/closeout/SKILL.md +153 -0
- package/skills/closeout/reference/handoff-prompt-template.md +72 -0
- package/skills/closeout/reference/issue-body-templates.md +108 -0
- package/skills/closeout/reference/pii-redaction-checklist.md +36 -0
- package/skills/codex-review/CLAUDE.md +46 -0
- package/skills/codex-review/SKILL.md +181 -0
- package/skills/codex-review/reference/CLAUDE.md +15 -0
- package/skills/codex-review/reference/cli-contract.md +253 -0
- package/skills/codex-review/reference/loop-integration.md +118 -0
- package/skills/codex-review/scripts/codex_down_classifier.py +98 -0
- package/skills/codex-review/scripts/codex_review_scripts_constants/CLAUDE.md +18 -0
- package/skills/codex-review/scripts/codex_review_scripts_constants/__init__.py +1 -0
- package/skills/codex-review/scripts/codex_review_scripts_constants/classifier_constants.py +35 -0
- package/skills/codex-review/scripts/codex_review_scripts_constants/codex_usage_probe_constants.py +86 -0
- package/skills/codex-review/scripts/codex_review_scripts_constants/findings_constants.py +18 -0
- package/skills/codex-review/scripts/codex_review_scripts_constants/run_constants.py +45 -0
- package/skills/codex-review/scripts/codex_usage_probe.py +573 -0
- package/skills/codex-review/scripts/fixtures/auth_failure_synthetic.txt +1 -0
- package/skills/codex-review/scripts/fixtures/config_load_failure_v0.125.0.txt +1 -0
- package/skills/codex-review/scripts/fixtures/freeform_findings_v0.144.3.txt +6 -0
- package/skills/codex-review/scripts/fixtures/model_rejection_v0.125.0.jsonl +5 -0
- package/skills/codex-review/scripts/fixtures/structured_findings.txt +13 -0
- package/skills/codex-review/scripts/fixtures/success_stream_v0.144.3.jsonl +6 -0
- package/skills/codex-review/scripts/fixtures/unknown_failure_synthetic.txt +1 -0
- package/skills/codex-review/scripts/fixtures/usage_limit_synthetic.txt +1 -0
- package/skills/codex-review/scripts/parse_codex_findings.py +207 -0
- package/skills/codex-review/scripts/run_codex_review.py +415 -0
- package/skills/codex-review/scripts/test_codex_down_classifier.py +143 -0
- package/skills/codex-review/scripts/test_codex_usage_probe.py +678 -0
- package/skills/codex-review/scripts/test_parse_codex_findings.py +130 -0
- package/skills/codex-review/scripts/test_run_codex_review.py +812 -0
- package/skills/codex-review/test_skill_scaffold.py +192 -0
- package/skills/grok-spawn/CLAUDE.md +28 -0
- package/skills/grok-spawn/SKILL.md +226 -0
- package/skills/grok-spawn/reference/flag-profiles.md +132 -0
- package/skills/grok-spawn/reference/worker-briefs.md +152 -0
- package/skills/grokify/SKILL.md +9 -1
- package/skills/grokify/capability-claims.test.mjs +28 -0
- package/skills/grokify/evals/README.md +72 -0
- package/skills/grokify/evals/parse-payload.test.mjs +171 -0
- package/skills/grokify/evals/run-capability-evals.mjs +545 -0
- package/skills/orchestrator/SKILL.md +32 -22
- package/skills/orchestrator-refresh/SKILL.md +12 -8
- package/skills/pr-converge/CLAUDE.md +1 -1
- package/skills/pr-converge/SKILL.md +34 -13
- package/skills/pr-converge/reference/convergence-gates.md +42 -15
- package/skills/pr-converge/reference/fix-protocol.md +1 -1
- package/skills/pr-converge/reference/ground-rules.md +1 -1
- package/skills/pr-converge/reference/per-tick.md +130 -42
- package/skills/pr-converge/reference/state-schema.md +10 -0
- package/skills/pr-converge/scripts/CLAUDE.md +2 -0
- package/skills/pr-converge/scripts/_pr_converge_path_setup.py +5 -1
- package/skills/pr-converge/scripts/check_convergence.py +605 -29
- package/skills/pr-converge/scripts/check_convergence_availability.py +232 -0
- package/skills/pr-converge/scripts/check_convergence_gates.py +279 -235
- package/skills/pr-converge/scripts/check_convergence_thread_gates.py +1 -1
- package/skills/pr-converge/scripts/pr_converge_scripts_constants/convergence_gate_constants.py +36 -2
- package/skills/pr-converge/scripts/test__pr_converge_path_setup.py +4 -0
- package/skills/pr-converge/scripts/test_check_convergence.py +71 -3
- package/skills/pr-converge/scripts/test_check_convergence_availability.py +326 -0
- package/skills/pr-converge/scripts/test_check_convergence_codex.py +507 -0
- package/skills/pr-converge/scripts/test_check_convergence_contract.py +89 -17
- package/skills/pr-converge/scripts/test_check_convergence_fixture.py +179 -0
- package/skills/pr-converge/scripts/test_check_convergence_gates.py +84 -68
- package/skills/pr-converge/scripts/test_check_convergence_thread_gates.py +24 -0
- package/skills/pr-converge/test_step5_host_branch.py +106 -0
- package/skills/pr-fix-protocol/SKILL.md +65 -0
- package/skills/pr-loop-cloud-transport/SKILL.md +2 -0
- package/skills/reviewer-gates/SKILL.md +7 -5
- package/skills/skill-builder/references/skill-modularity.md +1 -1
- package/skills/team-advisor/SKILL.md +16 -12
- package/system-prompts/software-engineer.xml +7 -6
- package/hooks/validators/test_verify_paths.py +0 -32
- package/hooks/validators/verify_paths.py +0 -57
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""Behavioral tests for the wrapper_plumb_check parts module."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from code_rules_gate_parts import wrapper_plumb_check
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def test_is_code_path_recognizes_python() -> None:
|
|
9
|
+
assert wrapper_plumb_check.is_code_path(Path("module.py"))
|
|
10
|
+
assert not wrapper_plumb_check.is_code_path(Path("notes.txt"))
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def test_is_test_path_matches_the_documented_patterns() -> None:
|
|
14
|
+
assert wrapper_plumb_check.is_test_path("pkg/test_bar.py")
|
|
15
|
+
assert wrapper_plumb_check.is_test_path("pkg/bar_test.py")
|
|
16
|
+
assert wrapper_plumb_check.is_test_path("pkg/conftest.py")
|
|
17
|
+
assert wrapper_plumb_check.is_test_path("pkg/tests/thing.py")
|
|
18
|
+
assert not wrapper_plumb_check.is_test_path("pkg/regular.py")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def test_check_wrapper_plumb_through_flags_dropped_kwarg() -> None:
|
|
22
|
+
content = (
|
|
23
|
+
"def build(name, verbose=False):\n"
|
|
24
|
+
" return name\n\n\n"
|
|
25
|
+
"def wrap(name):\n"
|
|
26
|
+
" return build(name)\n"
|
|
27
|
+
)
|
|
28
|
+
findings = wrapper_plumb_check.check_wrapper_plumb_through(content, "module.py")
|
|
29
|
+
assert any("verbose" in each_finding for each_finding in findings)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def test_check_wrapper_plumb_through_is_quiet_for_test_files() -> None:
|
|
33
|
+
content = (
|
|
34
|
+
"def build(name, verbose=False):\n"
|
|
35
|
+
" return name\n\n\n"
|
|
36
|
+
"def wrap(name):\n"
|
|
37
|
+
" return build(name)\n"
|
|
38
|
+
)
|
|
39
|
+
assert wrapper_plumb_check.check_wrapper_plumb_through(content, "test_module.py") == []
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def test_check_wrapper_plumb_through_is_quiet_when_kwarg_is_plumbed() -> None:
|
|
43
|
+
content = (
|
|
44
|
+
"def build(name, verbose=False):\n"
|
|
45
|
+
" return name\n\n\n"
|
|
46
|
+
"def wrap(name, verbose=False):\n"
|
|
47
|
+
" return build(name, verbose=verbose)\n"
|
|
48
|
+
)
|
|
49
|
+
assert wrapper_plumb_check.check_wrapper_plumb_through(content, "module.py") == []
|
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
"""Partition enforcer violations into blocking versus advisory by touched line.
|
|
2
|
+
|
|
3
|
+
Every diff-scoped enforcer message carries the line span of the unit it flags.
|
|
4
|
+
These extractors recover that span from the message text, and
|
|
5
|
+
``split_violations_by_scope`` marks a violation blocking when its span meets a
|
|
6
|
+
line the current diff added, advisory otherwise.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import re
|
|
10
|
+
from collections.abc import Callable
|
|
11
|
+
|
|
12
|
+
from pr_loop_shared_constants.code_rules_gate_constants import (
|
|
13
|
+
BANNED_NOUN_DEFINITION_LINE_GROUP_INDEX,
|
|
14
|
+
BANNED_NOUN_SPAN_GROUP_INDEX,
|
|
15
|
+
BANNED_NOUN_VIOLATION_PATTERN,
|
|
16
|
+
DUPLICATE_BODY_DEFINITION_LINE_GROUP_INDEX,
|
|
17
|
+
DUPLICATE_BODY_SPAN_GROUP_INDEX,
|
|
18
|
+
DUPLICATE_BODY_VIOLATION_PATTERN,
|
|
19
|
+
FUNCTION_LENGTH_DEFINITION_LINE_GROUP_INDEX,
|
|
20
|
+
FUNCTION_LENGTH_SPAN_GROUP_INDEX,
|
|
21
|
+
FUNCTION_LENGTH_VIOLATION_PATTERN,
|
|
22
|
+
ISOLATION_DEFINITION_LINE_GROUP_INDEX,
|
|
23
|
+
ISOLATION_SPAN_GROUP_INDEX,
|
|
24
|
+
ISOLATION_VIOLATION_PATTERN,
|
|
25
|
+
)
|
|
26
|
+
from pr_loop_shared_constants.inline_duplicate_body_span_constants import (
|
|
27
|
+
INLINE_DUPLICATE_BODY_ENCLOSING_LINE_GROUP_INDEX,
|
|
28
|
+
INLINE_DUPLICATE_BODY_ENCLOSING_SPAN_GROUP_INDEX,
|
|
29
|
+
INLINE_DUPLICATE_BODY_HELPER_LINE_GROUP_INDEX,
|
|
30
|
+
INLINE_DUPLICATE_BODY_HELPER_SPAN_GROUP_INDEX,
|
|
31
|
+
INLINE_DUPLICATE_BODY_VIOLATION_PATTERN,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _span_lines(span_match: re.Match[str], line_group_index: int, span_group_index: int) -> range:
|
|
36
|
+
"""Build the line range from a match's start-line and span capture groups.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
span_match: A regex match carrying a start line and a span length.
|
|
40
|
+
line_group_index: The capture-group index of the start line.
|
|
41
|
+
span_group_index: The capture-group index of the span length.
|
|
42
|
+
|
|
43
|
+
Returns:
|
|
44
|
+
The range covering the declared span.
|
|
45
|
+
"""
|
|
46
|
+
start_line = int(span_match.group(line_group_index))
|
|
47
|
+
line_span = int(span_match.group(span_group_index))
|
|
48
|
+
return range(start_line, start_line + line_span)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def hunk_header_pattern() -> re.Pattern[str]:
|
|
52
|
+
"""Return the regex matching a unified-diff hunk header.
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
A pattern capturing the new-side start line and optional count.
|
|
56
|
+
"""
|
|
57
|
+
return re.compile(r"^@@ -\d+(?:,\d+)? \+(\d+)(?:,(\d+))? @@")
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def violation_line_pattern() -> re.Pattern[str]:
|
|
61
|
+
"""Return the regex matching a violation's ``Line N:`` prefix.
|
|
62
|
+
|
|
63
|
+
Returns:
|
|
64
|
+
A pattern capturing the line number named in the prefix.
|
|
65
|
+
"""
|
|
66
|
+
return re.compile(r"^Line (\d+):")
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def parse_added_line_numbers(unified_diff_text: str) -> set[int]:
|
|
70
|
+
"""Extract added line numbers from unified-diff text.
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
unified_diff_text: Output from ``git diff --unified=0``.
|
|
74
|
+
|
|
75
|
+
Returns:
|
|
76
|
+
The set of newly-added line numbers named by the hunk headers.
|
|
77
|
+
"""
|
|
78
|
+
header_regex = hunk_header_pattern()
|
|
79
|
+
added_line_numbers: set[int] = set()
|
|
80
|
+
for each_line in unified_diff_text.splitlines():
|
|
81
|
+
header_match = header_regex.match(each_line)
|
|
82
|
+
if header_match is None:
|
|
83
|
+
continue
|
|
84
|
+
new_start_text, new_count_text = header_match.groups()
|
|
85
|
+
new_start = int(new_start_text)
|
|
86
|
+
new_count = 1 if new_count_text is None else int(new_count_text)
|
|
87
|
+
if new_count <= 0:
|
|
88
|
+
continue
|
|
89
|
+
added_line_numbers.update(range(new_start, new_start + new_count))
|
|
90
|
+
return added_line_numbers
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def extract_violation_line_number(violation_text: str) -> int | None:
|
|
94
|
+
"""Return the line number captured by the violation-line prefix regex.
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
violation_text: A single violation string of the form ``Line N: ...``.
|
|
98
|
+
|
|
99
|
+
Returns:
|
|
100
|
+
The integer line number in the prefix, or None when it does not match.
|
|
101
|
+
"""
|
|
102
|
+
prefix_match = violation_line_pattern().match(violation_text)
|
|
103
|
+
if prefix_match is None:
|
|
104
|
+
return None
|
|
105
|
+
return int(prefix_match.group(1))
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def function_length_span_range(violation_text: str) -> range | None:
|
|
109
|
+
"""Return the declared line span of a function-length violation, or None.
|
|
110
|
+
|
|
111
|
+
::
|
|
112
|
+
|
|
113
|
+
message: Function 'f' (defined at line X) is Y lines - ...
|
|
114
|
+
ok: a function-length message -> range covering the function
|
|
115
|
+
flag: another message -> None
|
|
116
|
+
|
|
117
|
+
Args:
|
|
118
|
+
violation_text: A single violation string emitted by the enforcer.
|
|
119
|
+
|
|
120
|
+
Returns:
|
|
121
|
+
A range over the function's declared span, or None for another message.
|
|
122
|
+
"""
|
|
123
|
+
span_match = FUNCTION_LENGTH_VIOLATION_PATTERN.search(violation_text)
|
|
124
|
+
if span_match is None:
|
|
125
|
+
return None
|
|
126
|
+
return _span_lines(
|
|
127
|
+
span_match,
|
|
128
|
+
FUNCTION_LENGTH_DEFINITION_LINE_GROUP_INDEX,
|
|
129
|
+
FUNCTION_LENGTH_SPAN_GROUP_INDEX,
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def isolation_span_range(violation_text: str) -> range | None:
|
|
134
|
+
"""Return the enclosing test-function span of an isolation violation, or None.
|
|
135
|
+
|
|
136
|
+
::
|
|
137
|
+
|
|
138
|
+
message: Line N: Test 'f' (defined at line X, spanning Y lines) probes ...
|
|
139
|
+
ok: an isolation message -> range covering the test function
|
|
140
|
+
flag: another message -> None
|
|
141
|
+
|
|
142
|
+
Args:
|
|
143
|
+
violation_text: A single violation string emitted by the enforcer.
|
|
144
|
+
|
|
145
|
+
Returns:
|
|
146
|
+
A range over the enclosing test span, or None for another message.
|
|
147
|
+
"""
|
|
148
|
+
span_match = ISOLATION_VIOLATION_PATTERN.search(violation_text)
|
|
149
|
+
if span_match is None:
|
|
150
|
+
return None
|
|
151
|
+
return _span_lines(
|
|
152
|
+
span_match,
|
|
153
|
+
ISOLATION_DEFINITION_LINE_GROUP_INDEX,
|
|
154
|
+
ISOLATION_SPAN_GROUP_INDEX,
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def banned_noun_span_range(violation_text: str) -> range | None:
|
|
159
|
+
"""Return the one-line binding span of a banned-noun violation, or None.
|
|
160
|
+
|
|
161
|
+
::
|
|
162
|
+
|
|
163
|
+
message: Line N: Identifier 'x' ... (binding span at line X, spanning 1 lines)
|
|
164
|
+
ok: a banned-noun message -> range covering the binding line
|
|
165
|
+
flag: another message -> None
|
|
166
|
+
|
|
167
|
+
The span is the binding line alone, so a pre-existing binding stays out of
|
|
168
|
+
scope when an unrelated line of its enclosing function is edited.
|
|
169
|
+
|
|
170
|
+
Args:
|
|
171
|
+
violation_text: A single violation string emitted by the enforcer.
|
|
172
|
+
|
|
173
|
+
Returns:
|
|
174
|
+
A range over the binding's one-line span, or None for another message.
|
|
175
|
+
"""
|
|
176
|
+
span_match = BANNED_NOUN_VIOLATION_PATTERN.search(violation_text)
|
|
177
|
+
if span_match is None:
|
|
178
|
+
return None
|
|
179
|
+
return _span_lines(
|
|
180
|
+
span_match,
|
|
181
|
+
BANNED_NOUN_DEFINITION_LINE_GROUP_INDEX,
|
|
182
|
+
BANNED_NOUN_SPAN_GROUP_INDEX,
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def duplicate_body_span_range(violation_text: str) -> range | None:
|
|
187
|
+
"""Return the copied function's source line span of a duplicate-body issue.
|
|
188
|
+
|
|
189
|
+
::
|
|
190
|
+
|
|
191
|
+
message: Function 'f' duplicates a.py::f — (duplicate body span at line X ...)
|
|
192
|
+
ok: a duplicate-body message -> range covering the copy
|
|
193
|
+
flag: another message -> None
|
|
194
|
+
|
|
195
|
+
A duplicate blocks only when the diff touches the copied function; an
|
|
196
|
+
unrelated edit that leaves a pre-existing copy alone keeps it advisory.
|
|
197
|
+
|
|
198
|
+
Args:
|
|
199
|
+
violation_text: A single violation string emitted by the enforcer.
|
|
200
|
+
|
|
201
|
+
Returns:
|
|
202
|
+
A range over the copied function's span, or None for another message.
|
|
203
|
+
"""
|
|
204
|
+
span_match = DUPLICATE_BODY_VIOLATION_PATTERN.search(violation_text)
|
|
205
|
+
if span_match is None:
|
|
206
|
+
return None
|
|
207
|
+
return _span_lines(
|
|
208
|
+
span_match,
|
|
209
|
+
DUPLICATE_BODY_DEFINITION_LINE_GROUP_INDEX,
|
|
210
|
+
DUPLICATE_BODY_SPAN_GROUP_INDEX,
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def inline_duplicate_body_span_lines(violation_text: str) -> frozenset[int] | None:
|
|
215
|
+
"""Return the union of both spans of a same-file inline-duplicate issue.
|
|
216
|
+
|
|
217
|
+
::
|
|
218
|
+
|
|
219
|
+
message: ... helper at line H spanning P, enclosing at line E spanning Q
|
|
220
|
+
ok: an inline-duplicate message -> frozenset of both spans
|
|
221
|
+
flag: another message -> None
|
|
222
|
+
|
|
223
|
+
Args:
|
|
224
|
+
violation_text: A single violation string emitted by the enforcer.
|
|
225
|
+
|
|
226
|
+
Returns:
|
|
227
|
+
The frozenset of every line in both spans, or None for another message.
|
|
228
|
+
"""
|
|
229
|
+
span_match = INLINE_DUPLICATE_BODY_VIOLATION_PATTERN.search(violation_text)
|
|
230
|
+
if span_match is None:
|
|
231
|
+
return None
|
|
232
|
+
helper_lines = _span_lines(
|
|
233
|
+
span_match,
|
|
234
|
+
INLINE_DUPLICATE_BODY_HELPER_LINE_GROUP_INDEX,
|
|
235
|
+
INLINE_DUPLICATE_BODY_HELPER_SPAN_GROUP_INDEX,
|
|
236
|
+
)
|
|
237
|
+
enclosing_lines = _span_lines(
|
|
238
|
+
span_match,
|
|
239
|
+
INLINE_DUPLICATE_BODY_ENCLOSING_LINE_GROUP_INDEX,
|
|
240
|
+
INLINE_DUPLICATE_BODY_ENCLOSING_SPAN_GROUP_INDEX,
|
|
241
|
+
)
|
|
242
|
+
return frozenset(helper_lines) | frozenset(enclosing_lines)
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def _all_span_range_extractors() -> tuple[Callable[[str], range | None], ...]:
|
|
246
|
+
"""Return every enclosing-unit span extractor, tried in order."""
|
|
247
|
+
return (
|
|
248
|
+
function_length_span_range,
|
|
249
|
+
isolation_span_range,
|
|
250
|
+
banned_noun_span_range,
|
|
251
|
+
duplicate_body_span_range,
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def enclosing_span_range(violation_text: str) -> range | None:
|
|
256
|
+
"""Return the enclosing-unit span of a span-tagged violation, or None.
|
|
257
|
+
|
|
258
|
+
::
|
|
259
|
+
|
|
260
|
+
each extractor in the registry is tried in turn
|
|
261
|
+
ok: a span-tagged message -> the first range an extractor recovers
|
|
262
|
+
flag: an untagged message -> None
|
|
263
|
+
|
|
264
|
+
Args:
|
|
265
|
+
violation_text: A single violation string emitted by the enforcer.
|
|
266
|
+
|
|
267
|
+
Returns:
|
|
268
|
+
The first non-None span any extractor recovers, or None.
|
|
269
|
+
"""
|
|
270
|
+
for each_extractor in _all_span_range_extractors():
|
|
271
|
+
span_range = each_extractor(violation_text)
|
|
272
|
+
if span_range is not None:
|
|
273
|
+
return span_range
|
|
274
|
+
return None
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def _issue_is_blocking(each_issue: str, all_added_line_numbers: set[int]) -> bool:
|
|
278
|
+
"""Return True when *each_issue* is blocking for the given added lines.
|
|
279
|
+
|
|
280
|
+
Args:
|
|
281
|
+
each_issue: A single violation string emitted by the enforcer.
|
|
282
|
+
all_added_line_numbers: Lines the current diff added.
|
|
283
|
+
|
|
284
|
+
Returns:
|
|
285
|
+
True when the issue's span or line meets an added line.
|
|
286
|
+
"""
|
|
287
|
+
inline_duplicate_lines = inline_duplicate_body_span_lines(each_issue)
|
|
288
|
+
if inline_duplicate_lines is not None:
|
|
289
|
+
return bool(inline_duplicate_lines & all_added_line_numbers)
|
|
290
|
+
span_range = enclosing_span_range(each_issue)
|
|
291
|
+
if span_range is not None:
|
|
292
|
+
return any(each_line in all_added_line_numbers for each_line in span_range)
|
|
293
|
+
violation_line = extract_violation_line_number(each_issue)
|
|
294
|
+
if violation_line is None:
|
|
295
|
+
return True
|
|
296
|
+
return violation_line in all_added_line_numbers
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def split_violations_by_scope(
|
|
300
|
+
all_issues: list[str],
|
|
301
|
+
all_added_line_numbers: set[int] | None,
|
|
302
|
+
) -> tuple[list[str], list[str]]:
|
|
303
|
+
"""Partition issues into blocking versus advisory based on touched lines.
|
|
304
|
+
|
|
305
|
+
::
|
|
306
|
+
|
|
307
|
+
all_added_line_numbers is None -> every issue is blocking
|
|
308
|
+
an issue whose span meets an added line -> blocking
|
|
309
|
+
an issue on a pre-existing untouched unit -> advisory
|
|
310
|
+
|
|
311
|
+
Args:
|
|
312
|
+
all_issues: Violation strings emitted by the enforcer.
|
|
313
|
+
all_added_line_numbers: Lines added in the current diff, or None to
|
|
314
|
+
treat every violation as blocking.
|
|
315
|
+
|
|
316
|
+
Returns:
|
|
317
|
+
The tuple ``(blocking, advisory)``.
|
|
318
|
+
"""
|
|
319
|
+
if all_added_line_numbers is None:
|
|
320
|
+
return list(all_issues), []
|
|
321
|
+
blocking: list[str] = []
|
|
322
|
+
advisory: list[str] = []
|
|
323
|
+
for each_issue in all_issues:
|
|
324
|
+
if _issue_is_blocking(each_issue, all_added_line_numbers):
|
|
325
|
+
blocking.append(each_issue)
|
|
326
|
+
else:
|
|
327
|
+
advisory.append(each_issue)
|
|
328
|
+
return blocking, advisory
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
"""Flag public calls that drop a same-file delegate's optional keyword arguments.
|
|
2
|
+
|
|
3
|
+
A thin public wrapper that forwards to a module-level helper should keep the
|
|
4
|
+
helper's optional keyword surface. When the wrapper narrows it, a caller varying
|
|
5
|
+
that keyword hits the wrapper's default instead. This check walks the AST and
|
|
6
|
+
reports each such dropped keyword.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import ast
|
|
10
|
+
from collections.abc import Iterator
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
from pr_loop_shared_constants.code_rules_gate_constants import (
|
|
14
|
+
ALL_CODE_FILE_EXTENSIONS,
|
|
15
|
+
ALL_TEST_FILENAME_GLOB_SUFFIXES,
|
|
16
|
+
ALL_TEST_FILENAME_SUFFIXES,
|
|
17
|
+
MAX_VIOLATIONS_PER_CHECK,
|
|
18
|
+
PYTHON_FILE_EXTENSION,
|
|
19
|
+
TEST_CONFTEST_FILENAME,
|
|
20
|
+
TEST_FILENAME_PREFIX,
|
|
21
|
+
TESTS_PATH_SEGMENT,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def is_code_path(file_path: Path) -> bool:
|
|
26
|
+
"""Return True when *file_path* carries a recognized code extension."""
|
|
27
|
+
return file_path.suffix.lower() in ALL_CODE_FILE_EXTENSIONS
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def is_test_path(file_path: str) -> bool:
|
|
31
|
+
"""Return True when *file_path* matches a test-file detection pattern.
|
|
32
|
+
|
|
33
|
+
::
|
|
34
|
+
|
|
35
|
+
ok(test): test_x.py, x_test.py, x.spec.ts, conftest.py, a/tests/x.py
|
|
36
|
+
ok(not): regular_module.py
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
file_path: Path string to classify; backslashes are normalized first.
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
True when the path matches any test-file pattern; False otherwise.
|
|
43
|
+
"""
|
|
44
|
+
normalized_posix = file_path.replace("\\", "/")
|
|
45
|
+
filename_only = normalized_posix.rsplit("/", maxsplit=1)[-1]
|
|
46
|
+
if TESTS_PATH_SEGMENT in normalized_posix:
|
|
47
|
+
return True
|
|
48
|
+
if filename_only == TEST_CONFTEST_FILENAME:
|
|
49
|
+
return True
|
|
50
|
+
if filename_only.startswith(TEST_FILENAME_PREFIX) and filename_only.endswith(
|
|
51
|
+
PYTHON_FILE_EXTENSION
|
|
52
|
+
):
|
|
53
|
+
return True
|
|
54
|
+
if any(filename_only.endswith(each) for each in ALL_TEST_FILENAME_SUFFIXES):
|
|
55
|
+
return True
|
|
56
|
+
return any(each in filename_only for each in ALL_TEST_FILENAME_GLOB_SUFFIXES)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _iter_calls_excluding_nested_functions(node: ast.AST) -> Iterator[ast.Call]:
|
|
60
|
+
"""Yield calls under *node*, skipping bodies of nested function definitions."""
|
|
61
|
+
for each_child in ast.iter_child_nodes(node):
|
|
62
|
+
if isinstance(each_child, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
63
|
+
continue
|
|
64
|
+
if isinstance(each_child, ast.Call):
|
|
65
|
+
yield each_child
|
|
66
|
+
continue
|
|
67
|
+
yield from _iter_calls_excluding_nested_functions(each_child)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _optional_kwargs_of(
|
|
71
|
+
function_node: ast.FunctionDef | ast.AsyncFunctionDef,
|
|
72
|
+
) -> set[str]:
|
|
73
|
+
"""Return the names of every optional keyword the function accepts."""
|
|
74
|
+
optional_kwargs: set[str] = set()
|
|
75
|
+
for each_kwonly, each_default in zip(
|
|
76
|
+
function_node.args.kwonlyargs, function_node.args.kw_defaults, strict=False
|
|
77
|
+
):
|
|
78
|
+
if each_default is not None:
|
|
79
|
+
optional_kwargs.add(each_kwonly.arg)
|
|
80
|
+
positional_defaults = function_node.args.defaults
|
|
81
|
+
if positional_defaults:
|
|
82
|
+
for each_arg in function_node.args.args[-len(positional_defaults) :]:
|
|
83
|
+
optional_kwargs.add(each_arg.arg)
|
|
84
|
+
return optional_kwargs
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _module_level_optional_kwargs_by_name(tree: ast.Module) -> dict[str, set[str]]:
|
|
88
|
+
"""Return a map from module-level function name to its optional kwargs."""
|
|
89
|
+
all_function_signatures: dict[str, set[str]] = {}
|
|
90
|
+
for each_node in ast.iter_child_nodes(tree):
|
|
91
|
+
if isinstance(each_node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
92
|
+
all_function_signatures[each_node.name] = _optional_kwargs_of(each_node)
|
|
93
|
+
return all_function_signatures
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _method_ids_in_class(class_def: ast.ClassDef) -> set[int]:
|
|
97
|
+
"""Return the object ids of every method defined directly in *class_def*."""
|
|
98
|
+
return {
|
|
99
|
+
id(each_node)
|
|
100
|
+
for each_node in class_def.body
|
|
101
|
+
if isinstance(each_node, (ast.FunctionDef, ast.AsyncFunctionDef))
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _class_method_node_ids(tree: ast.Module) -> set[int]:
|
|
106
|
+
"""Return the object ids of every method defined in any class in the tree."""
|
|
107
|
+
class_method_node_ids: set[int] = set()
|
|
108
|
+
for each_node in ast.walk(tree):
|
|
109
|
+
if isinstance(each_node, ast.ClassDef):
|
|
110
|
+
class_method_node_ids.update(_method_ids_in_class(each_node))
|
|
111
|
+
return class_method_node_ids
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _delegate_name_of(call_node: ast.Call) -> str | None:
|
|
115
|
+
"""Return the callee name of *call_node*, or None for a non-name callee."""
|
|
116
|
+
if isinstance(call_node.func, ast.Name):
|
|
117
|
+
return call_node.func.id
|
|
118
|
+
if isinstance(call_node.func, ast.Attribute):
|
|
119
|
+
return call_node.func.attr
|
|
120
|
+
return None
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _wrapper_dropped_kwarg_findings(
|
|
124
|
+
wrapper_node: ast.FunctionDef | ast.AsyncFunctionDef,
|
|
125
|
+
all_kwargs_by_function_name: dict[str, set[str]],
|
|
126
|
+
) -> Iterator[str]:
|
|
127
|
+
"""Yield one finding per optional kwarg *wrapper_node* drops from a delegate."""
|
|
128
|
+
wrapper_kwargs = all_kwargs_by_function_name.get(wrapper_node.name, set())
|
|
129
|
+
for each_call in _iter_calls_excluding_nested_functions(wrapper_node):
|
|
130
|
+
delegate_name = _delegate_name_of(each_call)
|
|
131
|
+
if delegate_name is None:
|
|
132
|
+
continue
|
|
133
|
+
delegate_kwargs = all_kwargs_by_function_name.get(delegate_name)
|
|
134
|
+
if delegate_kwargs is None:
|
|
135
|
+
continue
|
|
136
|
+
missing = delegate_kwargs - wrapper_kwargs
|
|
137
|
+
if missing:
|
|
138
|
+
yield (
|
|
139
|
+
f"Line {wrapper_node.lineno}: Wrapper {wrapper_node.name!r} drops "
|
|
140
|
+
f"optional kwargs {sorted(missing)!r} of delegate {delegate_name!r}"
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _wrapper_candidate_nodes(
|
|
145
|
+
tree: ast.Module, all_class_method_node_ids: set[int]
|
|
146
|
+
) -> Iterator[ast.FunctionDef | ast.AsyncFunctionDef]:
|
|
147
|
+
"""Yield each public module function that is not a class method."""
|
|
148
|
+
for each_node in ast.walk(tree):
|
|
149
|
+
if not isinstance(each_node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
150
|
+
continue
|
|
151
|
+
if id(each_node) in all_class_method_node_ids:
|
|
152
|
+
continue
|
|
153
|
+
if not each_node.name.startswith("_"):
|
|
154
|
+
yield each_node
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _wrapper_findings_from_tree(tree: ast.Module) -> list[str]:
|
|
158
|
+
"""Collect capped wrapper-plumb-through findings from a parsed module."""
|
|
159
|
+
all_signatures = _module_level_optional_kwargs_by_name(tree)
|
|
160
|
+
class_method_node_ids = _class_method_node_ids(tree)
|
|
161
|
+
issues: list[str] = []
|
|
162
|
+
for each_node in _wrapper_candidate_nodes(tree, class_method_node_ids):
|
|
163
|
+
issues.extend(_wrapper_dropped_kwarg_findings(each_node, all_signatures))
|
|
164
|
+
if len(issues) >= MAX_VIOLATIONS_PER_CHECK:
|
|
165
|
+
return issues[:MAX_VIOLATIONS_PER_CHECK]
|
|
166
|
+
return issues
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def _is_non_python_or_test(file_path: str) -> bool:
|
|
170
|
+
"""Return True for a non-Python code file or a test file."""
|
|
171
|
+
non_python_code_extensions = ALL_CODE_FILE_EXTENSIONS - {PYTHON_FILE_EXTENSION}
|
|
172
|
+
lowercase_file_path = file_path.lower()
|
|
173
|
+
if any(lowercase_file_path.endswith(each) for each in non_python_code_extensions):
|
|
174
|
+
return True
|
|
175
|
+
return is_test_path(file_path)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def check_wrapper_plumb_through(content: str, file_path: str) -> list[str]:
|
|
179
|
+
"""Flag public calls that drop a same-file delegate's optional kwargs.
|
|
180
|
+
|
|
181
|
+
::
|
|
182
|
+
|
|
183
|
+
def build(name, verbose=False): ... def wrap(name): return build(name)
|
|
184
|
+
flag: wrap drops optional kwarg 'verbose' of delegate 'build'
|
|
185
|
+
ok: def wrap(name, verbose=False): return build(name, verbose=verbose)
|
|
186
|
+
|
|
187
|
+
Only module-level functions contribute signatures; class methods are skipped
|
|
188
|
+
as signature sources and as wrapper candidates. Emission caps at
|
|
189
|
+
``MAX_VIOLATIONS_PER_CHECK`` findings.
|
|
190
|
+
|
|
191
|
+
Args:
|
|
192
|
+
content: File content as a single string for AST parsing.
|
|
193
|
+
file_path: Repository-relative POSIX path, for skipping non-Python and
|
|
194
|
+
test files early.
|
|
195
|
+
|
|
196
|
+
Returns:
|
|
197
|
+
Violation strings, one per dropped optional kwarg; empty for a
|
|
198
|
+
non-Python file, a test file, or a syntax error.
|
|
199
|
+
"""
|
|
200
|
+
if _is_non_python_or_test(file_path):
|
|
201
|
+
return []
|
|
202
|
+
try:
|
|
203
|
+
tree = ast.parse(content)
|
|
204
|
+
except SyntaxError:
|
|
205
|
+
return []
|
|
206
|
+
return _wrapper_findings_from_tree(tree)
|
|
@@ -3,7 +3,16 @@
|
|
|
3
3
|
import re
|
|
4
4
|
|
|
5
5
|
MAX_VIOLATIONS_PER_CHECK: int = 3
|
|
6
|
-
|
|
6
|
+
|
|
7
|
+
GATE_ERROR_EXIT_CODE: int = 2
|
|
8
|
+
|
|
9
|
+
EMPTY_FILE_SET_EXIT_CODE: int = 3
|
|
10
|
+
|
|
11
|
+
EMPTY_FILE_SET_MESSAGE: str = (
|
|
12
|
+
"code_rules_gate: the resolved file set is empty; nothing was inspected."
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
INSPECTED_COUNT_MESSAGE: str = "code_rules_gate: inspected {inspected_count} file(s)."
|
|
7
16
|
|
|
8
17
|
FUNCTION_LENGTH_VIOLATION_PATTERN: re.Pattern[str] = re.compile(
|
|
9
18
|
r"\(defined at line (\d+)\) is (\d+) lines"
|
|
@@ -29,15 +38,7 @@ DUPLICATE_BODY_VIOLATION_PATTERN: re.Pattern[str] = re.compile(
|
|
|
29
38
|
DUPLICATE_BODY_DEFINITION_LINE_GROUP_INDEX: int = 1
|
|
30
39
|
DUPLICATE_BODY_SPAN_GROUP_INDEX: int = 2
|
|
31
40
|
|
|
32
|
-
ALL_CODE_FILE_EXTENSIONS: frozenset[str] = frozenset(
|
|
33
|
-
{".py", ".js", ".ts", ".tsx", ".jsx"}
|
|
34
|
-
)
|
|
35
|
-
|
|
36
|
-
ALL_LITERAL_KEYWORD_EXEMPTIONS: frozenset[str] = frozenset(
|
|
37
|
-
{"true", "false", "none", "null"}
|
|
38
|
-
)
|
|
39
|
-
|
|
40
|
-
CONFIG_PATH_SEGMENT: str = "/config/"
|
|
41
|
+
ALL_CODE_FILE_EXTENSIONS: frozenset[str] = frozenset({".py", ".js", ".ts", ".tsx", ".jsx"})
|
|
41
42
|
|
|
42
43
|
TESTS_PATH_SEGMENT: str = "/tests/"
|
|
43
44
|
|
|
@@ -52,10 +53,6 @@ TEST_CONFTEST_FILENAME: str = "conftest.py"
|
|
|
52
53
|
|
|
53
54
|
TEST_FILENAME_PREFIX: str = "test_"
|
|
54
55
|
|
|
55
|
-
MINIMUM_COLUMN_NAME_LENGTH_AFTER_FIRST_CHAR: int = 2
|
|
56
|
-
|
|
57
|
-
COLUMN_KEY_PATTERN_TEMPLATE: str = r"^[a-z][a-z0-9_]{{{minimum_length},}}$"
|
|
58
|
-
|
|
59
56
|
GIT_NAME_STATUS_ADDED_PREFIX: str = "A"
|
|
60
57
|
|
|
61
58
|
GIT_NAME_STATUS_RENAMED_PREFIX: str = "R"
|
|
@@ -81,6 +78,14 @@ ALL_GIT_DIFF_NAME_ONLY_NULL_TERMINATED_COMMAND_PREFIX: tuple[str, ...] = (
|
|
|
81
78
|
"-z",
|
|
82
79
|
)
|
|
83
80
|
|
|
81
|
+
ALL_GIT_LS_FILES_UNTRACKED_NULL_TERMINATED_COMMAND: tuple[str, ...] = (
|
|
82
|
+
"git",
|
|
83
|
+
"ls-files",
|
|
84
|
+
"--others",
|
|
85
|
+
"--exclude-standard",
|
|
86
|
+
"-z",
|
|
87
|
+
)
|
|
88
|
+
|
|
84
89
|
|
|
85
90
|
ALL_PYTEST_MODULE_INVOCATION: tuple[str, ...] = (
|
|
86
91
|
"-m",
|
|
@@ -96,7 +101,10 @@ PYTHONPATH_ENV_VAR: str = "PYTHONPATH"
|
|
|
96
101
|
|
|
97
102
|
ALL_VENV_DIRECTORY_NAMES: tuple[str, ...] = (".venv", "venv")
|
|
98
103
|
|
|
99
|
-
ALL_WINDOWS_VENV_PYTHON_RELATIVE_PATH_SEGMENTS: tuple[str, ...] = (
|
|
104
|
+
ALL_WINDOWS_VENV_PYTHON_RELATIVE_PATH_SEGMENTS: tuple[str, ...] = (
|
|
105
|
+
"Scripts",
|
|
106
|
+
"python.exe",
|
|
107
|
+
)
|
|
100
108
|
|
|
101
109
|
ALL_POSIX_VENV_PYTHON_RELATIVE_PATH_SEGMENTS: tuple[str, ...] = ("bin", "python")
|
|
102
110
|
|
|
@@ -132,8 +140,7 @@ ALL_PYTEST_CONFIG_FILE_SECTIONS: tuple[tuple[str, str | None], ...] = (
|
|
|
132
140
|
)
|
|
133
141
|
|
|
134
142
|
STAGED_TEST_GROUP_FAILURE_MESSAGE: str = (
|
|
135
|
-
"code_rules_gate: staged test group rooted at {group_root} "
|
|
136
|
-
"failed under pytest; commit blocked."
|
|
143
|
+
"code_rules_gate: staged test group rooted at {group_root} failed under pytest; commit blocked."
|
|
137
144
|
)
|
|
138
145
|
|
|
139
146
|
MINIMUM_STAGED_PYTEST_PYTHON_MAJOR: int = 3
|
|
@@ -8,4 +8,5 @@ CLAUDE_REVIEWS_DISABLED_TOKEN_SEPARATOR: str = ","
|
|
|
8
8
|
CLAUDE_REVIEWS_DISABLED_BUGTEAM_TOKEN: str = "bugteam"
|
|
9
9
|
CLAUDE_REVIEWS_DISABLED_BUGBOT_TOKEN: str = "bugbot"
|
|
10
10
|
CLAUDE_REVIEWS_DISABLED_COPILOT_TOKEN: str = "copilot"
|
|
11
|
+
CLAUDE_REVIEWS_DISABLED_CODEX_TOKEN: str = "codex"
|
|
11
12
|
EXIT_CODE_BUGTEAM_DISABLED_VIA_ENV: int = 7
|