claude-dev-env 1.77.0 → 1.79.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/audit-rubrics/category_rubrics/category-k-codebase-conflicts.md +1 -0
- package/bin/install.mjs +1 -0
- package/bin/install.test.mjs +3 -2
- package/hooks/blocking/CLAUDE.md +5 -2
- package/hooks/blocking/code_rules_dead_module_constant.py +215 -59
- package/hooks/blocking/code_rules_dead_split_branch.py +225 -0
- package/hooks/blocking/code_rules_docstrings.py +951 -6
- package/hooks/blocking/code_rules_enforcer.py +64 -0
- package/hooks/blocking/code_rules_naming_collection.py +76 -1
- package/hooks/blocking/code_rules_paired_test.py +517 -0
- package/hooks/blocking/code_rules_string_magic.py +71 -1
- package/hooks/blocking/code_rules_test_assertions.py +159 -1
- package/hooks/blocking/convergence_gate_blocker.py +24 -15
- package/hooks/blocking/env_var_table_code_drift_blocker.py +475 -0
- package/hooks/blocking/package_inventory_stale_blocker.py +54 -15
- package/hooks/blocking/test_code_rules_enforcer_dead_module_constant.py +89 -2
- package/hooks/blocking/test_code_rules_enforcer_dead_split_branch.py +105 -0
- package/hooks/blocking/test_code_rules_enforcer_docstring_field_runmode_outcome.py +129 -0
- package/hooks/blocking/test_code_rules_enforcer_docstring_length_constant_superlative.py +198 -0
- package/hooks/blocking/test_code_rules_enforcer_docstring_mark_glyph_enumeration.py +262 -0
- package/hooks/blocking/test_code_rules_enforcer_docstring_no_network.py +115 -0
- package/hooks/blocking/test_code_rules_enforcer_docstring_raises_largezipfile.py +226 -0
- package/hooks/blocking/test_code_rules_enforcer_docstring_unreferenced_param.py +160 -0
- package/hooks/blocking/test_code_rules_enforcer_module_docstring_data_schema_scope.py +82 -0
- package/hooks/blocking/test_code_rules_enforcer_paired_test.py +339 -0
- package/hooks/blocking/test_code_rules_enforcer_polarity_name_contradiction.py +76 -0
- package/hooks/blocking/test_code_rules_enforcer_vacuous_cleanup_assertion.py +132 -0
- package/hooks/blocking/test_code_rules_enforcer_whitespace_indentation_magic.py +74 -0
- package/hooks/blocking/test_convergence_gate_blocker.py +71 -0
- package/hooks/blocking/test_env_var_table_code_drift_blocker.py +94 -0
- package/hooks/blocking/test_package_inventory_stale_blocker.py +46 -0
- package/hooks/blocking/test_pre_tool_use_dispatcher.py +7 -7
- package/hooks/hooks_constants/CLAUDE.md +2 -0
- package/hooks/hooks_constants/blocking_check_limits.py +102 -0
- package/hooks/hooks_constants/code_rules_enforcer_constants.py +28 -0
- package/hooks/hooks_constants/env_var_table_code_drift_constants.py +64 -0
- package/hooks/hooks_constants/package_inventory_stale_blocker_constants.py +20 -9
- package/hooks/hooks_constants/paired_test_coverage_constants.py +35 -0
- package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +4 -0
- package/package.json +1 -1
- package/rules/CLAUDE.md +2 -0
- package/rules/docstring-prose-matches-implementation.md +56 -53
- package/rules/env-var-table-code-drift.md +24 -0
- package/rules/file-global-constants.md +2 -2
- package/rules/package-inventory-stale-entry.md +4 -4
- package/rules/paired-test-coverage.md +35 -0
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"""Tests for vacuous cleanup-on-failure assertion detection.
|
|
2
|
+
|
|
3
|
+
A cleanup-on-failure test that asserts no leftover temp file is left behind,
|
|
4
|
+
yet never proves the temp file was created, passes vacuously: the assertion
|
|
5
|
+
holds even when the on-failure cleanup is entirely broken. The gate flags that
|
|
6
|
+
shape so the author arranges a post-creation failure and asserts real removal.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import importlib.util
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from types import ModuleType
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _load_enforcer_module() -> ModuleType:
|
|
17
|
+
module_path = Path(__file__).parent / "code_rules_enforcer.py"
|
|
18
|
+
spec = importlib.util.spec_from_file_location("code_rules_enforcer", module_path)
|
|
19
|
+
assert spec is not None
|
|
20
|
+
assert spec.loader is not None
|
|
21
|
+
module = importlib.util.module_from_spec(spec)
|
|
22
|
+
spec.loader.exec_module(module)
|
|
23
|
+
return module
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
code_rules_enforcer = _load_enforcer_module()
|
|
27
|
+
|
|
28
|
+
TEST_FILE_PATH = "packages/app/tests/test_archive_rewrite.py"
|
|
29
|
+
PRODUCTION_FILE_PATH = "packages/app/services/archive_rewrite.py"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def test_should_flag_glob_emptiness_cleanup_test_without_temp_creation() -> None:
|
|
33
|
+
source = (
|
|
34
|
+
"def test_rewrite_removes_tmp_file_on_failure(tmp_path: Path) -> None:\n"
|
|
35
|
+
" stp_path = build_corrupt_stp(stp_path=tmp_path / 'corrupt.stp')\n"
|
|
36
|
+
" rewrite_properties_xml_atomically(stp_path=stp_path, patched='<x/>')\n"
|
|
37
|
+
" all_tmp_siblings = list(tmp_path.glob('*.tmp'))\n"
|
|
38
|
+
" assert len(all_tmp_siblings) == 0\n"
|
|
39
|
+
)
|
|
40
|
+
issues = code_rules_enforcer.check_vacuous_cleanup_assertion_tests(source, TEST_FILE_PATH)
|
|
41
|
+
assert any("vacuous" in issue.lower() for issue in issues), (
|
|
42
|
+
f"Expected a vacuous-cleanup issue, got: {issues}"
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def test_should_not_flag_when_post_creation_failure_is_arranged() -> None:
|
|
47
|
+
source = (
|
|
48
|
+
"def test_rewrite_removes_tmp_file_on_replace_failure(tmp_path, monkeypatch)"
|
|
49
|
+
" -> None:\n"
|
|
50
|
+
" stp_path = build_valid_stp(stp_path=tmp_path / 'theme.stp')\n"
|
|
51
|
+
" monkeypatch.setattr(os, 'replace', _raise_oserror)\n"
|
|
52
|
+
" rewrite_properties_xml_atomically(stp_path=stp_path, patched='<x/>')\n"
|
|
53
|
+
" all_tmp_siblings = list(tmp_path.glob('*.tmp'))\n"
|
|
54
|
+
" assert len(all_tmp_siblings) == 0\n"
|
|
55
|
+
)
|
|
56
|
+
issues = code_rules_enforcer.check_vacuous_cleanup_assertion_tests(source, TEST_FILE_PATH)
|
|
57
|
+
assert issues == [], f"Expected no issue when failure is arranged, got: {issues}"
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def test_should_not_flag_when_temp_existence_is_proven_first() -> None:
|
|
61
|
+
source = (
|
|
62
|
+
"def test_cleanup_removes_temp_on_failure(tmp_path: Path) -> None:\n"
|
|
63
|
+
" temporary_path = tmp_path / 'theme.stp.tmp'\n"
|
|
64
|
+
" temporary_path.write_bytes(b'partial')\n"
|
|
65
|
+
" assert temporary_path.exists()\n"
|
|
66
|
+
" run_cleanup_after_failure(temporary_path)\n"
|
|
67
|
+
" assert not list(tmp_path.glob('*.tmp'))\n"
|
|
68
|
+
)
|
|
69
|
+
issues = code_rules_enforcer.check_vacuous_cleanup_assertion_tests(source, TEST_FILE_PATH)
|
|
70
|
+
assert issues == [], f"Expected no issue when temp existence proven, got: {issues}"
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def test_should_not_flag_success_named_cleanup_assertion() -> None:
|
|
74
|
+
source = (
|
|
75
|
+
"def test_rewrite_leaves_no_tmp_sibling_on_success(tmp_path: Path) -> None:\n"
|
|
76
|
+
" stp_path = build_valid_stp(stp_path=tmp_path / 'theme.stp')\n"
|
|
77
|
+
" rewrite_properties_xml_atomically(stp_path=stp_path, patched='<x/>')\n"
|
|
78
|
+
" all_tmp_siblings = list(tmp_path.glob('*.tmp'))\n"
|
|
79
|
+
" assert len(all_tmp_siblings) == 0\n"
|
|
80
|
+
)
|
|
81
|
+
issues = code_rules_enforcer.check_vacuous_cleanup_assertion_tests(source, TEST_FILE_PATH)
|
|
82
|
+
assert issues == [], f"Expected no issue for a success-named test, got: {issues}"
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def test_should_not_flag_failure_test_asserting_real_behavior() -> None:
|
|
86
|
+
source = (
|
|
87
|
+
"def test_rewrite_reports_cleanup_failure_on_directory_tmp("
|
|
88
|
+
"tmp_path: Path) -> None:\n"
|
|
89
|
+
" temporary_path = tmp_path / 'theme.stp.tmp'\n"
|
|
90
|
+
" temporary_path.mkdir()\n"
|
|
91
|
+
" write_outcome = rewrite_properties_xml_atomically("
|
|
92
|
+
"stp_path=tmp_path / 't.stp', patched='<x/>')\n"
|
|
93
|
+
" assert write_outcome.temporary_cleanup_error_message is not None\n"
|
|
94
|
+
)
|
|
95
|
+
issues = code_rules_enforcer.check_vacuous_cleanup_assertion_tests(source, TEST_FILE_PATH)
|
|
96
|
+
assert issues == [], f"Expected no issue for a behavior assertion, got: {issues}"
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def test_should_not_flag_mixed_assertion_cleanup_test() -> None:
|
|
100
|
+
source = (
|
|
101
|
+
"def test_rewrite_removes_tmp_and_reports_failure(tmp_path: Path) -> None:\n"
|
|
102
|
+
" stp_path = build_corrupt_stp(stp_path=tmp_path / 'corrupt.stp')\n"
|
|
103
|
+
" write_outcome = rewrite_properties_xml_atomically("
|
|
104
|
+
"stp_path=stp_path, patched='<x/>')\n"
|
|
105
|
+
" assert write_outcome.is_write_successful is False\n"
|
|
106
|
+
" all_tmp_siblings = list(tmp_path.glob('*.tmp'))\n"
|
|
107
|
+
" assert len(all_tmp_siblings) == 0\n"
|
|
108
|
+
)
|
|
109
|
+
issues = code_rules_enforcer.check_vacuous_cleanup_assertion_tests(source, TEST_FILE_PATH)
|
|
110
|
+
assert issues == [], f"Expected no issue for a mixed-assertion test, got: {issues}"
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def test_should_not_flag_in_production_files() -> None:
|
|
114
|
+
source = (
|
|
115
|
+
"def test_rewrite_removes_tmp_file_on_failure(tmp_path: Path) -> None:\n"
|
|
116
|
+
" all_tmp_siblings = list(tmp_path.glob('*.tmp'))\n"
|
|
117
|
+
" assert len(all_tmp_siblings) == 0\n"
|
|
118
|
+
)
|
|
119
|
+
issues = code_rules_enforcer.check_vacuous_cleanup_assertion_tests(source, PRODUCTION_FILE_PATH)
|
|
120
|
+
assert issues == [], f"Expected no issue in a production file, got: {issues}"
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def test_should_include_line_number_in_issue() -> None:
|
|
124
|
+
source = (
|
|
125
|
+
"def test_rewrite_removes_tmp_file_on_failure(tmp_path: Path) -> None:\n"
|
|
126
|
+
" all_tmp_siblings = list(tmp_path.glob('*.tmp'))\n"
|
|
127
|
+
" assert all_tmp_siblings == []\n"
|
|
128
|
+
)
|
|
129
|
+
issues = code_rules_enforcer.check_vacuous_cleanup_assertion_tests(source, TEST_FILE_PATH)
|
|
130
|
+
assert any("Line 1" in issue for issue in issues), (
|
|
131
|
+
f"Expected a line number in the issue, got: {issues}"
|
|
132
|
+
)
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import importlib.util
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
ENFORCER_PATH = Path(__file__).resolve().parent / "code_rules_enforcer.py"
|
|
7
|
+
specification = importlib.util.spec_from_file_location("code_rules_enforcer", ENFORCER_PATH)
|
|
8
|
+
assert specification is not None and specification.loader is not None
|
|
9
|
+
code_rules_enforcer = importlib.util.module_from_spec(specification)
|
|
10
|
+
specification.loader.exec_module(code_rules_enforcer)
|
|
11
|
+
|
|
12
|
+
PRODUCTION_PATH = "C:/project/pkg/menu_info_colors.py"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _check(source: str) -> list[str]:
|
|
16
|
+
return code_rules_enforcer.check_whitespace_indentation_magic(source, PRODUCTION_PATH)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def test_flags_twelve_space_indent_constant() -> None:
|
|
20
|
+
source = 'def fallback_indent() -> str:\n return " "\n'
|
|
21
|
+
issues = _check(source)
|
|
22
|
+
assert len(issues) == 1
|
|
23
|
+
assert "Line 2" in issues[0]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def test_flags_indent_fragment_inside_fstring() -> None:
|
|
27
|
+
source = 'def build(value: str) -> str:\n return value + f"\\n {value}"\n'
|
|
28
|
+
issues = _check(source)
|
|
29
|
+
assert len(issues) == 1
|
|
30
|
+
assert "Line 2" in issues[0]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def test_flags_tab_indent_constant() -> None:
|
|
34
|
+
source = 'def tabbed() -> str:\n return "\\t\\t"\n'
|
|
35
|
+
assert len(_check(source)) == 1
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def test_does_not_flag_single_tab_delimiter() -> None:
|
|
39
|
+
source = 'def split_columns(text: str) -> list[str]:\n return text.split("\\t")\n'
|
|
40
|
+
assert _check(source) == []
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def test_does_not_flag_single_tab_join_delimiter() -> None:
|
|
44
|
+
source = 'def join_rows(rows: list[str]) -> str:\n return "\\t".join(rows)\n'
|
|
45
|
+
assert _check(source) == []
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def test_does_not_flag_single_space() -> None:
|
|
49
|
+
source = 'def join_words(left: str, right: str) -> str:\n return left + " " + right\n'
|
|
50
|
+
assert _check(source) == []
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def test_does_not_flag_newline_only_fragment() -> None:
|
|
54
|
+
source = 'def build(value: str) -> str:\n return f"\\n{value}"\n'
|
|
55
|
+
assert _check(source) == []
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def test_does_not_flag_docstring_with_spaced_words() -> None:
|
|
59
|
+
source = (
|
|
60
|
+
"def documented() -> str:\n"
|
|
61
|
+
' """A docstring with spaced words."""\n'
|
|
62
|
+
' return documented.__doc__ or ""\n'
|
|
63
|
+
)
|
|
64
|
+
assert _check(source) == []
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def test_does_not_flag_in_config_file() -> None:
|
|
68
|
+
source = 'def fallback_indent() -> str:\n return " "\n'
|
|
69
|
+
assert (
|
|
70
|
+
code_rules_enforcer.check_whitespace_indentation_magic(
|
|
71
|
+
source, "C:/project/pkg/config/indents.py"
|
|
72
|
+
)
|
|
73
|
+
== []
|
|
74
|
+
)
|
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
"""Unit tests for convergence-gate-blocker PreToolUse hook."""
|
|
2
2
|
|
|
3
3
|
import importlib.util
|
|
4
|
+
import io
|
|
5
|
+
import json
|
|
4
6
|
import pathlib
|
|
7
|
+
import subprocess
|
|
5
8
|
import sys
|
|
6
9
|
|
|
10
|
+
import pytest
|
|
11
|
+
|
|
7
12
|
_HOOK_DIR = pathlib.Path(__file__).parent
|
|
8
13
|
if str(_HOOK_DIR) not in sys.path:
|
|
9
14
|
sys.path.insert(0, str(_HOOK_DIR))
|
|
@@ -61,3 +66,69 @@ def test_returns_none_when_no_number_and_no_repo() -> None:
|
|
|
61
66
|
|
|
62
67
|
def test_matches_gh_pr_ready_in_compound_command() -> None:
|
|
63
68
|
assert not _GH_PR_READY_PATTERN.search("gh pr ready --undo && gh pr create")
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def test_run_convergence_check_forwards_cwd_to_subprocess(
|
|
72
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
73
|
+
) -> None:
|
|
74
|
+
captured_cwd: list[object] = []
|
|
75
|
+
|
|
76
|
+
def fake_run(*_run_args: object, **run_keywords: object) -> subprocess.CompletedProcess[str]:
|
|
77
|
+
captured_cwd.append(run_keywords.get("cwd"))
|
|
78
|
+
return subprocess.CompletedProcess(args=[], returncode=0, stdout="", stderr="")
|
|
79
|
+
|
|
80
|
+
monkeypatch.setattr(hook_module.subprocess, "run", fake_run)
|
|
81
|
+
hook_module._run_convergence_check(
|
|
82
|
+
"check_convergence.py", "owner", "repo", 783, "C:/worktrees/pr-783"
|
|
83
|
+
)
|
|
84
|
+
assert captured_cwd == ["C:/worktrees/pr-783"]
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def test_run_convergence_check_forwards_none_cwd_as_none(
|
|
88
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
89
|
+
) -> None:
|
|
90
|
+
captured_cwd: list[object] = []
|
|
91
|
+
|
|
92
|
+
def fake_run(*_run_args: object, **run_keywords: object) -> subprocess.CompletedProcess[str]:
|
|
93
|
+
captured_cwd.append(run_keywords.get("cwd"))
|
|
94
|
+
return subprocess.CompletedProcess(args=[], returncode=0, stdout="", stderr="")
|
|
95
|
+
|
|
96
|
+
monkeypatch.setattr(hook_module.subprocess, "run", fake_run)
|
|
97
|
+
hook_module._run_convergence_check("check_convergence.py", "owner", "repo", 783, None)
|
|
98
|
+
assert captured_cwd == [None]
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def test_main_reads_cwd_from_top_level_payload(
|
|
102
|
+
monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path
|
|
103
|
+
) -> None:
|
|
104
|
+
convergence_script = (
|
|
105
|
+
tmp_path / ".claude" / "skills" / "pr-converge" / "scripts" / "check_convergence.py"
|
|
106
|
+
)
|
|
107
|
+
convergence_script.parent.mkdir(parents=True)
|
|
108
|
+
convergence_script.write_text("")
|
|
109
|
+
monkeypatch.setattr(hook_module.Path, "home", classmethod(lambda _cls: tmp_path))
|
|
110
|
+
|
|
111
|
+
worktree_path = str(tmp_path / "worktrees" / "pr-783")
|
|
112
|
+
monkeypatch.setattr(hook_module, "_resolve_owner_repo", lambda _cwd: ("jl-cmd", "repo"))
|
|
113
|
+
|
|
114
|
+
captured_cwd: list[object] = []
|
|
115
|
+
|
|
116
|
+
def fake_check(
|
|
117
|
+
_script: str, _owner: str, _repo: str, _pr_number: int, cwd: str | None
|
|
118
|
+
) -> subprocess.CompletedProcess[str]:
|
|
119
|
+
captured_cwd.append(cwd)
|
|
120
|
+
return subprocess.CompletedProcess(args=[], returncode=0, stdout="", stderr="")
|
|
121
|
+
|
|
122
|
+
monkeypatch.setattr(hook_module, "_run_convergence_check", fake_check)
|
|
123
|
+
|
|
124
|
+
payload = {
|
|
125
|
+
"tool_name": "Bash",
|
|
126
|
+
"cwd": worktree_path,
|
|
127
|
+
"tool_input": {"command": "gh pr ready 783", "cwd": "C:/wrong/session/dir"},
|
|
128
|
+
}
|
|
129
|
+
monkeypatch.setattr(sys, "stdin", io.StringIO(json.dumps(payload)))
|
|
130
|
+
|
|
131
|
+
with pytest.raises(SystemExit):
|
|
132
|
+
hook_module.main()
|
|
133
|
+
|
|
134
|
+
assert captured_cwd == [worktree_path]
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"""Tests for the env-var-table code-drift blocker.
|
|
2
|
+
|
|
3
|
+
Each test builds a real on-disk directory holding a code file and a markdown
|
|
4
|
+
doc, then drives the blocker's detection function against that tree. The drift
|
|
5
|
+
the blocker catches: a markdown env-var summary table row attributes an
|
|
6
|
+
environment variable to a code file whose source never references that variable.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import sys
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
_blocking_dir = str(Path(__file__).resolve().parent)
|
|
15
|
+
if _blocking_dir not in sys.path:
|
|
16
|
+
sys.path.insert(0, _blocking_dir)
|
|
17
|
+
|
|
18
|
+
from env_var_table_code_drift_blocker import ( # noqa: E402
|
|
19
|
+
find_drift_rows,
|
|
20
|
+
is_markdown_file,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _write(file_path: Path, content: str) -> None:
|
|
25
|
+
file_path.parent.mkdir(parents=True, exist_ok=True)
|
|
26
|
+
file_path.write_text(content, encoding="utf-8")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _anchor_repo_root(repo_root: Path) -> None:
|
|
30
|
+
(repo_root / ".git").mkdir(parents=True, exist_ok=True)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def test_is_markdown_file_accepts_md_and_rejects_python() -> None:
|
|
34
|
+
assert is_markdown_file("docs/configuration.md") is True
|
|
35
|
+
assert is_markdown_file("auth/google_auth.py") is False
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def test_flags_variable_absent_from_named_code_file(tmp_path: Path) -> None:
|
|
39
|
+
_anchor_repo_root(tmp_path)
|
|
40
|
+
_write(tmp_path / "auth" / "google_auth.py", "def load():\n return read_bws_secret()\n")
|
|
41
|
+
doc_path = tmp_path / "docs" / "configuration.md"
|
|
42
|
+
content = (
|
|
43
|
+
"## Summary: Environment Variables\n\n"
|
|
44
|
+
"| Variable | Used By | Purpose |\n"
|
|
45
|
+
"|----------|---------|---------|\n"
|
|
46
|
+
"| `GOOGLE_APPLICATION_CREDENTIALS` | `auth/google_auth.py` | Path to JSON |\n"
|
|
47
|
+
)
|
|
48
|
+
drift_rows = find_drift_rows(content, doc_path.parent)
|
|
49
|
+
assert drift_rows == ["GOOGLE_APPLICATION_CREDENTIALS -> auth/google_auth.py"]
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def test_passes_when_variable_present_in_code_file(tmp_path: Path) -> None:
|
|
53
|
+
_anchor_repo_root(tmp_path)
|
|
54
|
+
_write(
|
|
55
|
+
tmp_path / "automation_logging" / "__init__.py",
|
|
56
|
+
'topic = os.environ["NTFY_TOPIC"]\n',
|
|
57
|
+
)
|
|
58
|
+
doc_path = tmp_path / "docs" / "configuration.md"
|
|
59
|
+
content = (
|
|
60
|
+
"| Variable | Used By | Purpose |\n"
|
|
61
|
+
"|----------|---------|---------|\n"
|
|
62
|
+
"| `NTFY_TOPIC` | `automation_logging/__init__.py` | ntfy topic |\n"
|
|
63
|
+
)
|
|
64
|
+
assert find_drift_rows(content, doc_path.parent) == []
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def test_skips_row_whose_code_file_is_absent(tmp_path: Path) -> None:
|
|
68
|
+
_anchor_repo_root(tmp_path)
|
|
69
|
+
doc_path = tmp_path / "docs" / "configuration.md"
|
|
70
|
+
content = (
|
|
71
|
+
"| Variable | Used By | Purpose |\n"
|
|
72
|
+
"|----------|---------|---------|\n"
|
|
73
|
+
"| `MISSING_VAR` | `nowhere/ghost.py` | unresolved |\n"
|
|
74
|
+
)
|
|
75
|
+
assert find_drift_rows(content, doc_path.parent) == []
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def test_ignores_rows_inside_a_fenced_code_block(tmp_path: Path) -> None:
|
|
79
|
+
_anchor_repo_root(tmp_path)
|
|
80
|
+
_write(tmp_path / "auth" / "google_auth.py", "no variable here\n")
|
|
81
|
+
doc_path = tmp_path / "docs" / "configuration.md"
|
|
82
|
+
content = "```\n| `GOOGLE_APPLICATION_CREDENTIALS` | `auth/google_auth.py` | sample |\n```\n"
|
|
83
|
+
assert find_drift_rows(content, doc_path.parent) == []
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def test_ignores_row_whose_second_cell_is_not_a_code_file(tmp_path: Path) -> None:
|
|
87
|
+
_anchor_repo_root(tmp_path)
|
|
88
|
+
doc_path = tmp_path / "docs" / "configuration.md"
|
|
89
|
+
content = (
|
|
90
|
+
"| Variable | Purpose | Default |\n"
|
|
91
|
+
"|----------|---------|---------|\n"
|
|
92
|
+
"| `SOME_FLAG` | `enables the thing` | off |\n"
|
|
93
|
+
)
|
|
94
|
+
assert find_drift_rows(content, doc_path.parent) == []
|
|
@@ -311,6 +311,52 @@ def test_is_inventoried_production_file_accepts_production_file(tmp_path: Path):
|
|
|
311
311
|
assert is_inventoried_production_file(str(production_file_path)) is True
|
|
312
312
|
|
|
313
313
|
|
|
314
|
+
SKILL_MD_SCRIPTS_LAYOUT = (
|
|
315
|
+
"# base-theme-match\n\n"
|
|
316
|
+
"## Layout\n\n"
|
|
317
|
+
"| Path | Role |\n"
|
|
318
|
+
"|---|---|\n"
|
|
319
|
+
"| `scripts/discover_candidates.py` | Discovers candidates. |\n"
|
|
320
|
+
"| `scripts/resolve_stp.py` | Resolves the STP filename. |\n"
|
|
321
|
+
"| `scripts/select_stp.py` | Selects the STP filename. |\n"
|
|
322
|
+
)
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
def _skill_scripts_directory(tmp_path: Path) -> Path:
|
|
326
|
+
"""Return a skill package's scripts/ directory under a SKILL.md Layout table."""
|
|
327
|
+
skill_directory = tmp_path / "base-theme-match"
|
|
328
|
+
skill_directory.mkdir()
|
|
329
|
+
(skill_directory / "SKILL.md").write_text(SKILL_MD_SCRIPTS_LAYOUT, encoding="utf-8")
|
|
330
|
+
scripts_directory = skill_directory / "scripts"
|
|
331
|
+
scripts_directory.mkdir()
|
|
332
|
+
_write_sibling_files(scripts_directory, ["discover_candidates.py", "resolve_stp.py"])
|
|
333
|
+
return scripts_directory
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def test_blocks_new_script_absent_from_parent_skill_layout(tmp_path: Path):
|
|
337
|
+
scripts_directory = _skill_scripts_directory(tmp_path)
|
|
338
|
+
new_file_path = scripts_directory / "stp_selection.py"
|
|
339
|
+
result = _run_hook(
|
|
340
|
+
"Write",
|
|
341
|
+
{"file_path": str(new_file_path), "content": "x = 1\n"},
|
|
342
|
+
)
|
|
343
|
+
assert result.returncode == 0
|
|
344
|
+
payload = json.loads(result.stdout)
|
|
345
|
+
assert payload["hookSpecificOutput"]["permissionDecision"] == "deny"
|
|
346
|
+
assert "stp_selection.py" in payload["hookSpecificOutput"]["permissionDecisionReason"]
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
def test_allows_new_script_named_in_parent_skill_layout(tmp_path: Path):
|
|
350
|
+
scripts_directory = _skill_scripts_directory(tmp_path)
|
|
351
|
+
new_file_path = scripts_directory / "select_stp.py"
|
|
352
|
+
result = _run_hook(
|
|
353
|
+
"Write",
|
|
354
|
+
{"file_path": str(new_file_path), "content": "x = 1\n"},
|
|
355
|
+
)
|
|
356
|
+
assert result.returncode == 0
|
|
357
|
+
assert result.stdout.strip() == ""
|
|
358
|
+
|
|
359
|
+
|
|
314
360
|
def test_find_stale_inventory_returns_survey_for_omission(tmp_path: Path):
|
|
315
361
|
package_directory = _package_directory_with_readme(tmp_path, README_LISTING_TWO_FILES)
|
|
316
362
|
_write_sibling_files(package_directory, ["dialer_compose.py", "compose_dialer_cli.py"])
|
|
@@ -632,24 +632,24 @@ def test_dispatcher_write_applies_both_groups() -> None:
|
|
|
632
632
|
assert "blocking/plain_language_blocker.py" in all_write_script_paths, (
|
|
633
633
|
"plain_language_blocker (Group B) must be in Write applicable set"
|
|
634
634
|
)
|
|
635
|
-
assert len(all_write_entries) ==
|
|
636
|
-
f"Write tool must apply to all
|
|
635
|
+
assert len(all_write_entries) == 19, (
|
|
636
|
+
f"Write tool must apply to all 19 hosted hooks, got {len(all_write_entries)}"
|
|
637
637
|
)
|
|
638
638
|
|
|
639
639
|
|
|
640
640
|
def test_dispatcher_edit_applies_both_groups() -> None:
|
|
641
641
|
"""Edit tool triggers both Group A and Group B hooks through the dispatcher."""
|
|
642
642
|
all_edit_entries = _applicable_entries_for_tool(EDIT_TOOL_NAME)
|
|
643
|
-
assert len(all_edit_entries) ==
|
|
644
|
-
f"Edit tool must apply to all
|
|
643
|
+
assert len(all_edit_entries) == 19, (
|
|
644
|
+
f"Edit tool must apply to all 19 hosted hooks, got {len(all_edit_entries)}"
|
|
645
645
|
)
|
|
646
646
|
|
|
647
647
|
|
|
648
648
|
def test_dispatcher_multi_edit_applies_only_group_b() -> None:
|
|
649
|
-
"""MultiEdit tool triggers only Group B (
|
|
649
|
+
"""MultiEdit tool triggers only Group B (8 hooks), not Group A."""
|
|
650
650
|
all_multi_edit_entries = _applicable_entries_for_tool(MULTI_EDIT_TOOL_NAME)
|
|
651
|
-
assert len(all_multi_edit_entries) ==
|
|
652
|
-
f"MultiEdit tool must apply to exactly
|
|
651
|
+
assert len(all_multi_edit_entries) == 8, (
|
|
652
|
+
f"MultiEdit tool must apply to exactly 8 Group-B hooks, got {len(all_multi_edit_entries)}"
|
|
653
653
|
)
|
|
654
654
|
|
|
655
655
|
|
|
@@ -13,6 +13,7 @@ Shared constant modules imported by hooks throughout the `hooks/` tree. Each fil
|
|
|
13
13
|
| `bot_mention_comment_blocker_constants.py` | Patterns for detecting bot @-mentions in PR comments |
|
|
14
14
|
| `claude_md_orphan_file_blocker_constants.py` | Table patterns, file extensions, scan budget, and block-message text for the CLAUDE.md orphan-file blocker |
|
|
15
15
|
| `code_rules_enforcer_constants.py` | File-extension sets, test-path patterns, advisory line thresholds, boolean-name prefixes |
|
|
16
|
+
| `env_var_table_code_drift_constants.py` | Table patterns, env-var-name and code-file recognizers, scan budget, and block-message text for the env-var-table code-drift blocker |
|
|
16
17
|
| `code_rules_path_utils_constants.py` | Path-matching helpers used by the code-rules check modules |
|
|
17
18
|
| `code_verifier_spawn_preflight_gate_constants.py` | Subagent type, merge-tree command flags, timeouts, and deny-message text for the code-verifier spawn pre-flight gate |
|
|
18
19
|
| `convergence_branch_constants.py` | Branch and worktree naming patterns for the convergence gate |
|
|
@@ -39,6 +40,7 @@ Shared constant modules imported by hooks throughout the `hooks/` tree. Each fil
|
|
|
39
40
|
| `open_questions_in_plans_blocker_constants.py` | Patterns for detecting unresolved open questions in plan documents |
|
|
40
41
|
| `orphan_css_class_constants.py` | Scan radius and selector patterns for the orphan-CSS-class check |
|
|
41
42
|
| `package_inventory_stale_blocker_constants.py` | Inventory document names, production code extensions, backtick token pattern, smallest inventory size, exempt names, scan budget, and block-message text for the package-inventory stale-entry blocker |
|
|
43
|
+
| `paired_test_coverage_constants.py` | Test-directory name, stem-test filename affixes, test-file globs, exempt public-function names, scan budget, coverage threshold, and guidance text for the public-function paired-test coverage check |
|
|
42
44
|
| `path_rewriter_constants.py` | Path rewriting patterns for the Everything-search path rewriter |
|
|
43
45
|
| `plain_language_blocker_constants.py` | The list of heavy words and their everyday replacements |
|
|
44
46
|
| `pr_converge_bugteam_enforcer_constants.py` | State keys and timing config for the bugteam-parallel enforcer |
|
|
@@ -51,18 +51,73 @@ MAX_DOCSTRING_FALLBACK_BRANCH_ISSUES: int = 3
|
|
|
51
51
|
DOCSTRING_FALLBACK_BRANCH_MINIMUM_ROUTE_COUNT: int = 2
|
|
52
52
|
MAX_DOCSTRING_NO_CONSUMER_CLAIM_ISSUES: int = 3
|
|
53
53
|
MAX_DOCSTRING_UNGUARDED_PAYLOAD_CLAIM_ISSUES: int = 3
|
|
54
|
+
MAX_DOCSTRING_NO_NETWORK_CLAIM_ISSUES: int = 3
|
|
54
55
|
MAX_STALE_TEST_NAME_TARGET_ISSUES: int = 3
|
|
55
56
|
STALE_TEST_NAME_MINIMUM_SHARED_TOKEN_COUNT: int = 2
|
|
56
57
|
MAX_MODULE_DOCSTRING_CHECK_ROSTER_ISSUES: int = 5
|
|
57
58
|
MINIMUM_PUBLIC_CHECKS_FOR_MODULE_DOCSTRING_ROSTER: int = 2
|
|
58
59
|
MAX_DOCSTRING_TUPLE_ENUMERATION_ISSUES: int = 5
|
|
59
60
|
MINIMUM_TUPLE_MEMBERS_FOR_DOCSTRING_ENUMERATION: int = 2
|
|
61
|
+
MAX_DOCSTRING_MARK_GLYPH_ENUMERATION_ISSUES: int = 5
|
|
62
|
+
MINIMUM_NAMED_MARKS_FOR_PROSE_ENUMERATION: int = 2
|
|
63
|
+
MAX_COMPANION_MODULE_RESOLUTION_DEPTH: int = 6
|
|
64
|
+
PYTHON_MODULE_FILE_SUFFIX: str = ".py"
|
|
65
|
+
WORD_BOUNDARY_REGEX: str = r"\b"
|
|
66
|
+
ALL_PUNCTUATION_MARK_GLYPH_PROSE_NAMES: dict[str, tuple[str, ...]] = {
|
|
67
|
+
"—": ("em-dash", "em dash"),
|
|
68
|
+
"–": ("en-dash", "en dash"),
|
|
69
|
+
"--": ("double-hyphen", "double hyphen", "spaced double-hyphen"),
|
|
70
|
+
";": ("semicolon",),
|
|
71
|
+
":": ("colon",),
|
|
72
|
+
",": ("comma",),
|
|
73
|
+
"/": ("slash", "forward slash"),
|
|
74
|
+
"|": ("pipe", "vertical bar"),
|
|
75
|
+
"&": ("ampersand",),
|
|
76
|
+
}
|
|
60
77
|
MAX_DOCSTRING_STEP_DISPATCH_ISSUES: int = 5
|
|
61
78
|
MINIMUM_NAMED_LINEAR_STEPS_FOR_DISPATCH_CHECK: int = 2
|
|
62
79
|
MINIMUM_TOKENS_FOR_DISPATCH_CALLEE: int = 2
|
|
63
80
|
MAX_DOCSTRING_UNDEFINED_CONSTANT_ISSUES: int = 3
|
|
64
81
|
MAX_DOCSTRING_RETURNS_PLURAL_CARDINALITY_ISSUES: int = 5
|
|
65
82
|
SINGLE_DICT_KEY_COUNT_FOR_PLURAL_CARDINALITY_DRIFT: int = 1
|
|
83
|
+
MAX_LENGTH_CONSTANT_SUPERLATIVE_ISSUES: int = 3
|
|
84
|
+
LENGTH_GATE_PACKAGE_SCAN_FILE_LIMIT: int = 400
|
|
85
|
+
LENGTH_CONFIG_SUBDIRECTORY_NAME: str = "config"
|
|
86
|
+
ALL_LENGTH_CONSTANT_NAME_SUFFIXES: tuple[str, ...] = ("_LENGTH", "_LEN")
|
|
87
|
+
ALL_LENGTH_SUPERLATIVE_RANGE_PHRASES: tuple[str, ...] = (
|
|
88
|
+
"longest",
|
|
89
|
+
"maximum length",
|
|
90
|
+
"no longer than",
|
|
91
|
+
)
|
|
92
|
+
MAX_DOCSTRING_FIELD_RUNMODE_OUTCOME_ISSUES: int = 5
|
|
93
|
+
ALL_DOCSTRING_RUNMODE_FLAG_FIELD_NAME_TOKENS: tuple[str, ...] = ("dry_run",)
|
|
94
|
+
ALL_DOCSTRING_PER_RECORD_WRITE_OUTCOME_PHRASES: tuple[str, ...] = (
|
|
95
|
+
"was written",
|
|
96
|
+
"is written",
|
|
97
|
+
"were written",
|
|
98
|
+
"nothing was written",
|
|
99
|
+
"no file was written",
|
|
100
|
+
"wrote",
|
|
101
|
+
"written to disk",
|
|
102
|
+
"written to the file",
|
|
103
|
+
)
|
|
104
|
+
ALL_DOCSTRING_RUN_MODE_PHRASES: tuple[str, ...] = (
|
|
105
|
+
"dry run",
|
|
106
|
+
"dry-run",
|
|
107
|
+
"execute run",
|
|
108
|
+
"--execute",
|
|
109
|
+
"run mode",
|
|
110
|
+
"live run",
|
|
111
|
+
"not an execute",
|
|
112
|
+
)
|
|
113
|
+
MAX_DOCSTRING_RAISES_LARGEZIPFILE_ISSUES: int = 5
|
|
114
|
+
DOCSTRING_LARGE_ZIP_FILE_EXCEPTION_NAME: str = "LargeZipFile"
|
|
115
|
+
ZIPFILE_WRITER_CLASS_NAME: str = "ZipFile"
|
|
116
|
+
ZIPFILE_MODE_KEYWORD: str = "mode"
|
|
117
|
+
ZIPFILE_MODE_POSITIONAL_INDEX: int = 1
|
|
118
|
+
ZIPFILE_ALLOW_ZIP64_KEYWORD: str = "allowZip64"
|
|
119
|
+
ZIPFILE_ALLOW_ZIP64_POSITIONAL_INDEX: int = 3
|
|
120
|
+
ALL_ZIPFILE_WRITE_MODE_VALUES: frozenset[str] = frozenset({"w", "a", "x"})
|
|
66
121
|
MAX_DOCSTRING_ARGS_SPAN_SCOPE_ISSUES: int = 3
|
|
67
122
|
ALL_DOCSTRING_SINGLE_LINE_SCOPE_PHRASES: tuple[str, ...] = (
|
|
68
123
|
"anchor line is among the changed lines",
|
|
@@ -163,6 +218,26 @@ ALL_DOCSTRING_GUARDED_FAILURE_CLAIM_PHRASES: tuple[str, ...] = (
|
|
|
163
218
|
"malformed payload yields none",
|
|
164
219
|
)
|
|
165
220
|
|
|
221
|
+
ALL_DOCSTRING_NO_NETWORK_CLAIM_PHRASES: tuple[str, ...] = (
|
|
222
|
+
"without touching the network",
|
|
223
|
+
"without touching the share",
|
|
224
|
+
"without touching the network share",
|
|
225
|
+
"without hitting the network",
|
|
226
|
+
"without hitting the share",
|
|
227
|
+
"without a network call",
|
|
228
|
+
"without any network call",
|
|
229
|
+
"without network access",
|
|
230
|
+
"without a network round-trip",
|
|
231
|
+
"without a network round trip",
|
|
232
|
+
"no network access",
|
|
233
|
+
"no network round-trip",
|
|
234
|
+
"no network round trip",
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
ALL_PATH_METADATA_ACCESS_METHOD_NAMES: frozenset[str] = frozenset(
|
|
238
|
+
{"is_file", "is_dir", "exists", "stat", "lstat"}
|
|
239
|
+
)
|
|
240
|
+
|
|
166
241
|
MAX_DOCSTRING_INLINE_LITERAL_CLAIM_ISSUES: int = 3
|
|
167
242
|
ALL_DOCSTRING_NO_INLINE_LITERAL_CLAIM_PHRASES: tuple[str, ...] = (
|
|
168
243
|
"no literals appear inline",
|
|
@@ -204,3 +279,30 @@ ALL_TEST_INDICATING_ENVIRONMENT_VARIABLE_NAMES: frozenset[str] = frozenset(
|
|
|
204
279
|
"UNIT_TEST",
|
|
205
280
|
}
|
|
206
281
|
)
|
|
282
|
+
|
|
283
|
+
MAX_MODULE_DOCSTRING_DATA_SCHEMA_SCOPE_ISSUES: int = 1
|
|
284
|
+
MODULE_DOCSTRING_DATA_SCHEMA_CONSTANT_SAMPLE_LIMIT: int = 6
|
|
285
|
+
ALL_USER_FACING_TEXT_SCOPE_DOCSTRING_PHRASES: tuple[str, ...] = (
|
|
286
|
+
"user-facing",
|
|
287
|
+
"user facing",
|
|
288
|
+
)
|
|
289
|
+
ALL_DATA_SCHEMA_CONSTANT_NAME_MARKERS: tuple[str, ...] = (
|
|
290
|
+
"_FIELD_",
|
|
291
|
+
"_KEY_",
|
|
292
|
+
"_SCHEMA_",
|
|
293
|
+
"_ENCODING",
|
|
294
|
+
"_FORMAT_STRING",
|
|
295
|
+
)
|
|
296
|
+
ALL_DATA_SCHEMA_DOCSTRING_ACKNOWLEDGEMENT_PHRASES: tuple[str, ...] = (
|
|
297
|
+
"field key",
|
|
298
|
+
"schema key",
|
|
299
|
+
"schema",
|
|
300
|
+
"metadata",
|
|
301
|
+
"encoding",
|
|
302
|
+
"format string",
|
|
303
|
+
"runtime config",
|
|
304
|
+
"runtime-config",
|
|
305
|
+
"data schema",
|
|
306
|
+
"data-schema",
|
|
307
|
+
"config constant",
|
|
308
|
+
)
|
|
@@ -39,6 +39,20 @@ DENY_REASON_ISSUE_PREVIEW_COUNT = 10
|
|
|
39
39
|
ALL_BOOLEAN_NAME_PREFIXES: tuple[str, ...] = ("is_", "has_", "should_", "can_", "was_", "did_")
|
|
40
40
|
UPPER_SNAKE_CONSTANT_PATTERN = re.compile(r"^[A-Z][A-Z0-9_]*$")
|
|
41
41
|
|
|
42
|
+
ALL_POLARITY_ANTONYM_TOKEN_PAIRS: tuple[tuple[str, str], ...] = (
|
|
43
|
+
("allowed", "forbidden"),
|
|
44
|
+
("allowed", "denied"),
|
|
45
|
+
("allowed", "blocked"),
|
|
46
|
+
("permitted", "forbidden"),
|
|
47
|
+
("permitted", "denied"),
|
|
48
|
+
("included", "excluded"),
|
|
49
|
+
("valid", "invalid"),
|
|
50
|
+
("enabled", "disabled"),
|
|
51
|
+
("visible", "hidden"),
|
|
52
|
+
("present", "missing"),
|
|
53
|
+
)
|
|
54
|
+
POLARITY_TOKEN_BOUNDARY_PATTERN: str = r"(?:^|_)%s(?:_|$)"
|
|
55
|
+
|
|
42
56
|
ALL_MUST_CHECK_RETURN_FUNCTION_NAMES: frozenset[str] = frozenset({"find_and_click", "write_outcome"})
|
|
43
57
|
|
|
44
58
|
DOCSTRING_ARG_ENTRY_PATTERN: re.Pattern[str] = re.compile(r"^([A-Za-z_][A-Za-z0-9_]*)\s*[:(]")
|
|
@@ -137,6 +151,20 @@ MINIMUM_FSTRING_LITERAL_LENGTH = 2
|
|
|
137
151
|
MAX_FSTRING_STRUCTURAL_LITERAL_ISSUES = 100
|
|
138
152
|
ALL_ALLOWED_MAGIC_NUMBER_LITERALS: frozenset[str] = frozenset({"0", "1", "-1", "0.0", "1.0"})
|
|
139
153
|
ALL_NON_MAGIC_FSTRING_STRIPPED_VALUES: frozenset[str] = frozenset({"", "True", "False"})
|
|
154
|
+
INDENTATION_MAGIC_MINIMUM_SPACE_RUN = 4
|
|
155
|
+
INDENTATION_MAGIC_MINIMUM_TAB_RUN = 2
|
|
156
|
+
MAX_WHITESPACE_INDENTATION_MAGIC_ISSUES = 100
|
|
157
|
+
WHITESPACE_INDENTATION_MAGIC_MESSAGE_SUFFIX: str = (
|
|
158
|
+
"whitespace indentation literal in a function body - extract to a named "
|
|
159
|
+
"indent constant in config/"
|
|
160
|
+
)
|
|
161
|
+
ALL_ALWAYS_NONEMPTY_SPLIT_METHOD_NAMES: frozenset[str] = frozenset({"split", "rsplit"})
|
|
162
|
+
MAX_DEAD_SPLIT_BRANCH_ISSUES = 100
|
|
163
|
+
DEAD_SPLIT_BRANCH_MESSAGE_SUFFIX: str = (
|
|
164
|
+
"is bound from a str.split() call with a separator, which never returns an "
|
|
165
|
+
"empty list, so this truthiness test's falsy branch is unreachable dead "
|
|
166
|
+
"code - remove the dead branch"
|
|
167
|
+
)
|
|
140
168
|
DUPLICATED_FORMAT_MINIMUM_REPETITION_COUNT = 3
|
|
141
169
|
DUPLICATED_FORMAT_MINIMUM_LITERAL_CHARACTER_COUNT = 5
|
|
142
170
|
FILE_GLOBAL_UPPER_SNAKE_PATTERN = re.compile(r"^_?[A-Z][A-Z0-9_]*$")
|