claude-dev-env 2.31.0 → 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.
- package/docs/CODE_RULES.md +2 -1
- package/hooks/blocking/code_rules_duplicate_body.py +272 -100
- package/hooks/blocking/code_rules_enforcer.py +32 -14
- package/hooks/blocking/code_rules_shared.py +64 -14
- package/hooks/blocking/pr_description_writer_gate.py +18 -25
- package/hooks/blocking/tdd_enforcer.py +8 -4
- package/hooks/blocking/test_code_rules_enforcer_ephemeral.py +12 -1
- package/hooks/blocking/test_code_rules_enforcer_ephemeral_repository_root.py +118 -0
- package/hooks/blocking/test_code_rules_enforcer_same_file_inline_duplicate.py +244 -0
- package/hooks/blocking/test_code_rules_enforcer_split_entry_2.py +22 -0
- package/hooks/blocking/test_pr_description_writer_gate.py +21 -3
- package/hooks/blocking/test_pre_tool_use_dispatcher_native.py +9 -2
- package/hooks/blocking/test_state_description_blocker.py +99 -53
- package/hooks/blocking/test_tdd_enforcer.py +64 -0
- package/hooks/hooks_constants/pr_description_writer_gate_constants.py +34 -0
- package/hooks/hooks_constants/subprocess_budget_completeness_content.py +2 -2
- package/hooks/hooks_constants/test_multi_edit_reconstruction.py +1 -0
- package/hooks/hooks_constants/test_subprocess_budget_completeness_content.py +33 -0
- package/hooks/observability/pr_description_writer_spawn_tracker.py +4 -12
- package/hooks/observability/test_pr_description_writer_spawn_tracker.py +6 -2
- package/hooks/validators/abbreviation_checks.py +3 -3
- package/hooks/validators/code_quality_checks.py +4 -4
- package/hooks/validators/config/baseline_identity_constants.py +12 -0
- package/hooks/validators/config/directory_exemption_constants.py +44 -1
- package/hooks/validators/config/validator_base_constants.py +9 -0
- package/hooks/validators/fast_save_validators.py +23 -80
- package/hooks/validators/pr_reference_checks.py +2 -2
- package/hooks/validators/python_antipattern_checks.py +3 -3
- package/hooks/validators/python_style_checks.py +3 -2
- package/hooks/validators/react_checks.py +2 -9
- package/hooks/validators/run_all_validators.py +190 -145
- package/hooks/validators/security_checks.py +3 -3
- package/hooks/validators/test_directory_exemption_constants.py +30 -24
- package/hooks/validators/test_fast_save_validators.py +16 -16
- package/hooks/validators/test_hook_timing_harness.py +10 -1
- package/hooks/validators/test_run_all_validators_pretooluse.py +161 -0
- package/hooks/validators/test_run_all_validators_repository_root.py +202 -0
- package/hooks/validators/test_validator_base.py +51 -2
- package/hooks/validators/todo_checks.py +2 -2
- package/hooks/validators/type_safety_checks.py +3 -3
- package/hooks/validators/useless_test_checks.py +3 -3
- package/hooks/validators/validator_base.py +73 -1
- package/package.json +1 -1
- package/scripts/dev_env_scripts_constants/grok_worker_constants.py +15 -0
- package/scripts/grok_headless_runner.py +15 -12
- package/scripts/shared_tree_paths.py +70 -0
- package/scripts/test_shared_tree_paths.py +143 -0
|
@@ -68,3 +68,36 @@ def test_resolved_content_returns_empty_for_an_unreadable_edit_target(
|
|
|
68
68
|
resolved_content({"file_path": str(missing_target), "old_string": "a", "new_string": "b"})
|
|
69
69
|
== ""
|
|
70
70
|
)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def test_reconstructed_edit_content_replaces_every_occurrence_when_replace_all_is_set(
|
|
74
|
+
tmp_path: pathlib.Path,
|
|
75
|
+
) -> None:
|
|
76
|
+
"""An Edit carrying replace_all rewrites every occurrence, as the tool does.
|
|
77
|
+
|
|
78
|
+
The MultiEdit branch has honored the flag since it moved onto apply_edits;
|
|
79
|
+
the Edit branch must agree, or one payload reconstructs two ways.
|
|
80
|
+
"""
|
|
81
|
+
target_file = tmp_path / "module.py"
|
|
82
|
+
target_file.write_text("a = 1\na = 1\n", encoding="utf-8")
|
|
83
|
+
reconstructed = reconstructed_edit_content(
|
|
84
|
+
{
|
|
85
|
+
"file_path": str(target_file),
|
|
86
|
+
"old_string": "a = 1",
|
|
87
|
+
"new_string": "a = 2",
|
|
88
|
+
"replace_all": True,
|
|
89
|
+
}
|
|
90
|
+
)
|
|
91
|
+
assert reconstructed == "a = 2\na = 2\n"
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def test_reconstructed_edit_content_replaces_one_occurrence_without_replace_all(
|
|
95
|
+
tmp_path: pathlib.Path,
|
|
96
|
+
) -> None:
|
|
97
|
+
"""Without the flag, only the first occurrence changes."""
|
|
98
|
+
target_file = tmp_path / "module.py"
|
|
99
|
+
target_file.write_text("a = 1\na = 1\n", encoding="utf-8")
|
|
100
|
+
reconstructed = reconstructed_edit_content(
|
|
101
|
+
{"file_path": str(target_file), "old_string": "a = 1", "new_string": "a = 2"}
|
|
102
|
+
)
|
|
103
|
+
assert reconstructed == "a = 2\na = 1\n"
|
|
@@ -23,7 +23,6 @@ touch each returns quietly.
|
|
|
23
23
|
from __future__ import annotations
|
|
24
24
|
|
|
25
25
|
import sys
|
|
26
|
-
import tempfile
|
|
27
26
|
from pathlib import Path
|
|
28
27
|
|
|
29
28
|
_hooks_dir = str(Path(__file__).resolve().parent.parent)
|
|
@@ -33,18 +32,16 @@ if _hooks_dir not in sys.path:
|
|
|
33
32
|
from hooks_constants.pr_description_writer_gate_constants import ( # noqa: E402
|
|
34
33
|
ALL_AGENT_SPAWN_TOOL_NAMES,
|
|
35
34
|
PR_DESCRIPTION_WRITER_SUBAGENT_TYPE,
|
|
36
|
-
SPAWN_MARKER_FILE_PREFIX,
|
|
37
|
-
SPAWN_MARKER_FILE_SUFFIX,
|
|
38
35
|
SUBAGENT_TYPE_FIELD_NAME,
|
|
39
36
|
TOOL_INPUT_FIELD_NAME,
|
|
40
37
|
TOOL_NAME_FIELD_NAME,
|
|
41
38
|
)
|
|
39
|
+
from hooks_constants.pr_description_writer_gate_constants import ( # noqa: E402
|
|
40
|
+
spawn_marker_path as shared_spawn_marker_path,
|
|
41
|
+
)
|
|
42
42
|
from hooks_constants.pre_tool_use_stdin import ( # noqa: E402
|
|
43
43
|
read_hook_input_dictionary_from_stdin,
|
|
44
44
|
)
|
|
45
|
-
from hooks_constants.session_edit_stage_gate_constants import ( # noqa: E402
|
|
46
|
-
SESSION_ID_UNSAFE_CHARACTERS_PATTERN,
|
|
47
|
-
)
|
|
48
45
|
|
|
49
46
|
|
|
50
47
|
def spawn_marker_path(session_id: str, temp_directory: Path | None = None) -> Path | None:
|
|
@@ -70,12 +67,7 @@ def spawn_marker_path(session_id: str, temp_directory: Path | None = None) -> Pa
|
|
|
70
67
|
Absolute path to this session's marker file, or None when the payload
|
|
71
68
|
names no session this hook can key on.
|
|
72
69
|
"""
|
|
73
|
-
|
|
74
|
-
if not sanitized_session_id:
|
|
75
|
-
return None
|
|
76
|
-
file_name = f"{SPAWN_MARKER_FILE_PREFIX}{sanitized_session_id}{SPAWN_MARKER_FILE_SUFFIX}"
|
|
77
|
-
resolved_temp_directory = temp_directory or Path(tempfile.gettempdir())
|
|
78
|
-
return resolved_temp_directory / file_name
|
|
70
|
+
return shared_spawn_marker_path(session_id, temp_directory)
|
|
79
71
|
|
|
80
72
|
|
|
81
73
|
def _record_spawn(session_id: str) -> None:
|
|
@@ -26,6 +26,10 @@ assert _TRACKER_SPEC.loader is not None
|
|
|
26
26
|
tracker_module = importlib.util.module_from_spec(_TRACKER_SPEC)
|
|
27
27
|
_TRACKER_SPEC.loader.exec_module(tracker_module)
|
|
28
28
|
|
|
29
|
+
_GATE_CONSTANTS = importlib.import_module(
|
|
30
|
+
"hooks_constants.pr_description_writer_gate_constants"
|
|
31
|
+
)
|
|
32
|
+
|
|
29
33
|
_SESSION_ID = "session-abc123"
|
|
30
34
|
_OTHER_SUBAGENT_TYPE = "general-purpose"
|
|
31
35
|
|
|
@@ -65,7 +69,7 @@ def _run_tracker(payload: dict[str, object], temp_directory: Path) -> None:
|
|
|
65
69
|
"""
|
|
66
70
|
with mock.patch("sys.stdin", io.StringIO(json.dumps(payload))):
|
|
67
71
|
with mock.patch.object(
|
|
68
|
-
|
|
72
|
+
_GATE_CONSTANTS.tempfile, "gettempdir", return_value=str(temp_directory)
|
|
69
73
|
):
|
|
70
74
|
tracker_module.main()
|
|
71
75
|
|
|
@@ -120,7 +124,7 @@ def should_record_twice_without_error(tmp_path: Path) -> None:
|
|
|
120
124
|
def should_ignore_a_malformed_payload(tmp_path: Path) -> None:
|
|
121
125
|
"""Unparseable stdin records nothing and raises nothing."""
|
|
122
126
|
with mock.patch("sys.stdin", io.StringIO("this is not json")):
|
|
123
|
-
with mock.patch.object(
|
|
127
|
+
with mock.patch.object(_GATE_CONSTANTS.tempfile, "gettempdir", return_value=str(tmp_path)):
|
|
124
128
|
tracker_module.main()
|
|
125
129
|
assert list(tmp_path.iterdir()) == []
|
|
126
130
|
|
|
@@ -14,7 +14,7 @@ from pathlib import Path
|
|
|
14
14
|
from typing import List
|
|
15
15
|
|
|
16
16
|
from .config.abbreviation_checks_constants import ALL_ALLOWED_SINGLE_LETTERS
|
|
17
|
-
from .validator_base import Violation
|
|
17
|
+
from .validator_base import Violation, source_text, syntax_tree
|
|
18
18
|
|
|
19
19
|
|
|
20
20
|
def check_single_letter_variables(tree: ast.AST, filename: str) -> List[Violation]:
|
|
@@ -39,13 +39,13 @@ def validate_file(file_path: Path) -> List[Violation]:
|
|
|
39
39
|
filename = str(file_path)
|
|
40
40
|
|
|
41
41
|
try:
|
|
42
|
-
source = file_path
|
|
42
|
+
source = source_text(file_path)
|
|
43
43
|
except Exception as error:
|
|
44
44
|
violations.append(Violation(filename, 0, f"Error reading file: {error}"))
|
|
45
45
|
return violations
|
|
46
46
|
|
|
47
47
|
try:
|
|
48
|
-
tree =
|
|
48
|
+
tree = syntax_tree(source)
|
|
49
49
|
except SyntaxError as error:
|
|
50
50
|
violations.append(
|
|
51
51
|
Violation(filename, error.lineno or 0, f"Syntax error: {error.msg}")
|
|
@@ -11,7 +11,7 @@ import sys
|
|
|
11
11
|
from pathlib import Path
|
|
12
12
|
from typing import List
|
|
13
13
|
|
|
14
|
-
from .validator_base import Violation
|
|
14
|
+
from .validator_base import Violation, source_text, syntax_tree
|
|
15
15
|
|
|
16
16
|
|
|
17
17
|
MAX_FUNCTION_LINES = 30
|
|
@@ -75,7 +75,7 @@ def check_file_length(file_path: Path) -> List[Violation]:
|
|
|
75
75
|
filename = str(file_path)
|
|
76
76
|
|
|
77
77
|
try:
|
|
78
|
-
lines = file_path
|
|
78
|
+
lines = source_text(file_path).splitlines()
|
|
79
79
|
except Exception as error:
|
|
80
80
|
return [Violation(filename, 0, f"Error reading file: {error}")]
|
|
81
81
|
|
|
@@ -98,8 +98,8 @@ def validate_file(file_path: Path) -> List[Violation]:
|
|
|
98
98
|
violations.extend(check_file_length(file_path))
|
|
99
99
|
|
|
100
100
|
try:
|
|
101
|
-
source = file_path
|
|
102
|
-
tree =
|
|
101
|
+
source = source_text(file_path)
|
|
102
|
+
tree = syntax_tree(source)
|
|
103
103
|
except Exception as error:
|
|
104
104
|
return violations + [Violation(filename, 0, f"Error: {error}")]
|
|
105
105
|
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""Patterns and placeholders the baseline gate masks a violation message with.
|
|
2
|
+
|
|
3
|
+
Config files are exempt from the string-literal magic-value checks, so the
|
|
4
|
+
regex fragments and the placeholder text the scope-agnostic message shape needs
|
|
5
|
+
live here rather than inline in ``run_all_validators.py``.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
QUALIFIED_SCOPE_NAME_SEPARATOR = "."
|
|
9
|
+
WORD_BOUNDARY_PATTERN = r"\b"
|
|
10
|
+
STANDALONE_NUMBER_PATTERN = r"(?<![A-Za-z0-9_])\d+(?![A-Za-z0-9_])"
|
|
11
|
+
SCOPE_NAME_PLACEHOLDER = "<name>"
|
|
12
|
+
SHAPE_NUMBER_PLACEHOLDER = "<number>"
|
|
@@ -7,7 +7,9 @@ siblings). Exact names come from the authoritative path-exemption pattern sets
|
|
|
7
7
|
and CLI markers, unioned with the config-directory allowlist. Substring
|
|
8
8
|
patterns come from the separator-free members of those same pattern sets so a
|
|
9
9
|
directory such as ``pkg/test_helpers/`` stages with the same test exemption as
|
|
10
|
-
the real path.
|
|
10
|
+
the real path. Directories pytest generates for its own scratch tree are
|
|
11
|
+
recognized by name and contribute no signal, whatever ``--basetemp`` they sit
|
|
12
|
+
under.
|
|
11
13
|
"""
|
|
12
14
|
|
|
13
15
|
from __future__ import annotations
|
|
@@ -27,6 +29,15 @@ from ..exempt_paths import (
|
|
|
27
29
|
POSIX_DIRECTORY_SEPARATOR = "/"
|
|
28
30
|
WINDOWS_DIRECTORY_SEPARATOR = "\\"
|
|
29
31
|
|
|
32
|
+
PYTEST_TEMPORARY_ROOT_NAME_PREFIX = "pytest-"
|
|
33
|
+
DECIMAL_DIGIT_CHARACTERS = "0123456789"
|
|
34
|
+
LOWERCASE_ASCII_LETTER_CHARACTERS = "abcdefghijklmnopqrstuvwxyz"
|
|
35
|
+
UNDERSCORE_CHARACTER = "_"
|
|
36
|
+
PYTEST_SANITIZED_TEST_NAME_LENGTH_LIMIT = 30
|
|
37
|
+
ALL_PYTEST_SANITIZED_TEST_NAME_CHARACTERS: frozenset[str] = frozenset(
|
|
38
|
+
LOWERCASE_ASCII_LETTER_CHARACTERS + DECIMAL_DIGIT_CHARACTERS + UNDERSCORE_CHARACTER
|
|
39
|
+
)
|
|
40
|
+
|
|
30
41
|
ALL_SYSTEM_TEMPORARY_ROOT_ENVIRONMENT_VARIABLE_NAMES: frozenset[str] = frozenset(
|
|
31
42
|
{
|
|
32
43
|
"TEMP",
|
|
@@ -68,6 +79,38 @@ def is_filename_like_path_segment(path_segment: str) -> bool:
|
|
|
68
79
|
return "." in path_segment
|
|
69
80
|
|
|
70
81
|
|
|
82
|
+
def is_pytest_scratch_directory_segment(segment_lower: str) -> bool:
|
|
83
|
+
"""Return True when *segment_lower* is a directory pytest itself created.
|
|
84
|
+
|
|
85
|
+
::
|
|
86
|
+
|
|
87
|
+
is_pytest_scratch_directory_segment("test_drops_leading_anchor0") # True
|
|
88
|
+
is_pytest_scratch_directory_segment("pytest-of-root") # True
|
|
89
|
+
is_pytest_scratch_directory_segment("test_helpers") # False
|
|
90
|
+
|
|
91
|
+
``TempPathFactory`` names its roots ``pytest-of-<user>`` and ``pytest-<n>``, and
|
|
92
|
+
each per-test directory as a length-capped test-name stem with a number appended.
|
|
93
|
+
Those generated names carry ``test_`` without naming a real project directory.
|
|
94
|
+
|
|
95
|
+
Args:
|
|
96
|
+
segment_lower: One lowercased directory component of a target path.
|
|
97
|
+
|
|
98
|
+
Returns:
|
|
99
|
+
True when pytest generated the segment rather than the project.
|
|
100
|
+
"""
|
|
101
|
+
if segment_lower.startswith(PYTEST_TEMPORARY_ROOT_NAME_PREFIX):
|
|
102
|
+
return True
|
|
103
|
+
sanitized_test_name = segment_lower.rstrip(DECIMAL_DIGIT_CHARACTERS)
|
|
104
|
+
if not sanitized_test_name or sanitized_test_name == segment_lower:
|
|
105
|
+
return False
|
|
106
|
+
if len(sanitized_test_name) > PYTEST_SANITIZED_TEST_NAME_LENGTH_LIMIT:
|
|
107
|
+
return False
|
|
108
|
+
return all(
|
|
109
|
+
each_character in ALL_PYTEST_SANITIZED_TEST_NAME_CHARACTERS
|
|
110
|
+
for each_character in sanitized_test_name
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
|
|
71
114
|
def all_directory_segments_in_path_pattern(path_pattern: str) -> list[str]:
|
|
72
115
|
"""Return lowercased directory tokens from one path-exemption pattern.
|
|
73
116
|
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"""Constants for the shared source reader and parser in validator_base."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
# One save validates one file, so a handful of entries covers a run and keeps
|
|
6
|
+
# a long-lived CLI pass from holding every file it ever read.
|
|
7
|
+
SOURCE_CACHE_ENTRIES: int = 8
|
|
8
|
+
|
|
9
|
+
SOURCE_ENCODING: str = "utf-8"
|
|
@@ -45,6 +45,7 @@ from .test_safety_checks import (
|
|
|
45
45
|
from .todo_checks import validate_file as validate_todo_file
|
|
46
46
|
from .type_safety_checks import validate_file as validate_type_safety_file
|
|
47
47
|
from .useless_test_checks import validate_file as validate_useless_test_file
|
|
48
|
+
from .validator_base import ValidatorResult
|
|
48
49
|
|
|
49
50
|
FilePredicate = Callable[[Path], bool]
|
|
50
51
|
FileViolationLineCollector = Callable[[Path], "list[str]"]
|
|
@@ -76,21 +77,6 @@ def _violation_lines_from(raw_validate_file: _RawFileValidator) -> FileViolation
|
|
|
76
77
|
return _collect
|
|
77
78
|
|
|
78
79
|
|
|
79
|
-
@dataclass(frozen=True)
|
|
80
|
-
class FastSaveCheckOutcome:
|
|
81
|
-
"""One save-path check's finding.
|
|
82
|
-
|
|
83
|
-
The caller wraps this into its own reporting type. This module never
|
|
84
|
-
imports that type back, so no circular import exists between the two
|
|
85
|
-
modules.
|
|
86
|
-
"""
|
|
87
|
-
|
|
88
|
-
display_name: str
|
|
89
|
-
checks: str
|
|
90
|
-
is_clean: bool
|
|
91
|
-
violation_report: str
|
|
92
|
-
|
|
93
|
-
|
|
94
80
|
def _is_python_file(file_path: Path) -> bool:
|
|
95
81
|
return file_path.suffix == PYTHON_SOURCE_SUFFIX
|
|
96
82
|
|
|
@@ -122,15 +108,10 @@ def _collect_test_safety_violation_lines(file_path: Path) -> "list[str]":
|
|
|
122
108
|
|
|
123
109
|
|
|
124
110
|
def _collect_react_violation_lines(file_path: Path) -> "list[str]":
|
|
125
|
-
"""Adapt react_checks's whole-batch ``check_no_class_components`` to one file.
|
|
126
|
-
|
|
127
|
-
react_checks.Violation carries no ``__str__`` override, so this formats
|
|
128
|
-
each violation the same way react_checks's own ``main`` prints one.
|
|
129
|
-
"""
|
|
130
|
-
all_violations = check_no_class_components([str(file_path)])
|
|
111
|
+
"""Adapt react_checks's whole-batch ``check_no_class_components`` to one file."""
|
|
131
112
|
return [
|
|
132
|
-
|
|
133
|
-
for each_violation in
|
|
113
|
+
str(each_violation)
|
|
114
|
+
for each_violation in check_no_class_components([str(file_path)])
|
|
134
115
|
]
|
|
135
116
|
|
|
136
117
|
|
|
@@ -145,11 +126,12 @@ class _FastSaveValidatorSpecification:
|
|
|
145
126
|
no_matching_files_message: str
|
|
146
127
|
|
|
147
128
|
|
|
148
|
-
def
|
|
149
|
-
"
|
|
129
|
+
def _all_fast_save_validator_specifications() -> (
|
|
130
|
+
"tuple[_FastSaveValidatorSpecification, ...]"
|
|
150
131
|
):
|
|
151
|
-
"""Return the save-path
|
|
152
|
-
return
|
|
132
|
+
"""Return the save-path roster: every non-Mypy, non-Ruff file-scoped check."""
|
|
133
|
+
return (
|
|
134
|
+
# Python naming, style, and constants.
|
|
153
135
|
_FastSaveValidatorSpecification(
|
|
154
136
|
"Python Style",
|
|
155
137
|
"1,2,3,4",
|
|
@@ -171,14 +153,7 @@ def _python_naming_and_syntax_specifications() -> (
|
|
|
171
153
|
_violation_lines_from(validate_magic_literal_file),
|
|
172
154
|
NO_PYTHON_FILES_MESSAGE,
|
|
173
155
|
),
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
def _python_quality_and_tracking_specifications() -> (
|
|
178
|
-
"list[_FastSaveValidatorSpecification]"
|
|
179
|
-
):
|
|
180
|
-
"""Return the save-path checks for Python code quality and TODO tracking."""
|
|
181
|
-
return [
|
|
156
|
+
# Python code quality and TODO tracking.
|
|
182
157
|
_FastSaveValidatorSpecification(
|
|
183
158
|
"TODO Tracking",
|
|
184
159
|
"36",
|
|
@@ -200,14 +175,7 @@ def _python_quality_and_tracking_specifications() -> (
|
|
|
200
175
|
_violation_lines_from(validate_code_quality_file),
|
|
201
176
|
NO_PYTHON_FILES_MESSAGE,
|
|
202
177
|
),
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
def _python_safety_and_typing_specifications() -> (
|
|
207
|
-
"list[_FastSaveValidatorSpecification]"
|
|
208
|
-
):
|
|
209
|
-
"""Return the save-path checks for Python anti-patterns and typing."""
|
|
210
|
-
return [
|
|
178
|
+
# Python anti-patterns and typing.
|
|
211
179
|
_FastSaveValidatorSpecification(
|
|
212
180
|
"Python Anti-patterns",
|
|
213
181
|
"33,34,35",
|
|
@@ -222,12 +190,7 @@ def _python_safety_and_typing_specifications() -> (
|
|
|
222
190
|
_violation_lines_from(validate_type_safety_file),
|
|
223
191
|
NO_PYTHON_FILES_MESSAGE,
|
|
224
192
|
),
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
def _test_scoped_fast_save_specifications() -> "list[_FastSaveValidatorSpecification]":
|
|
229
|
-
"""Return the save-path checks scoped to a ``test_*.py``-shaped file."""
|
|
230
|
-
return [
|
|
193
|
+
# Scoped to a test_*.py-shaped file.
|
|
231
194
|
_FastSaveValidatorSpecification(
|
|
232
195
|
"Test Safety",
|
|
233
196
|
"11,21",
|
|
@@ -242,14 +205,7 @@ def _test_scoped_fast_save_specifications() -> "list[_FastSaveValidatorSpecifica
|
|
|
242
205
|
_violation_lines_from(validate_useless_test_file),
|
|
243
206
|
NO_TEST_FILES_MESSAGE,
|
|
244
207
|
),
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
def _cross_language_fast_save_specifications() -> (
|
|
249
|
-
"list[_FastSaveValidatorSpecification]"
|
|
250
|
-
):
|
|
251
|
-
"""Return the save-path checks that read a non-Python or multi-language file."""
|
|
252
|
-
return [
|
|
208
|
+
# Non-Python or multi-language files.
|
|
253
209
|
_FastSaveValidatorSpecification(
|
|
254
210
|
"React",
|
|
255
211
|
"17",
|
|
@@ -264,39 +220,26 @@ def _cross_language_fast_save_specifications() -> (
|
|
|
264
220
|
_violation_lines_from(validate_pr_reference_file),
|
|
265
221
|
NO_CODE_REFERENCE_FILES_MESSAGE,
|
|
266
222
|
),
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
def _all_fast_save_validator_specifications() -> (
|
|
271
|
-
"list[_FastSaveValidatorSpecification]"
|
|
272
|
-
):
|
|
273
|
-
"""Return the save-path roster: every non-Mypy, non-Ruff file-scoped check."""
|
|
274
|
-
return [
|
|
275
|
-
*_python_naming_and_syntax_specifications(),
|
|
276
|
-
*_python_quality_and_tracking_specifications(),
|
|
277
|
-
*_python_safety_and_typing_specifications(),
|
|
278
|
-
*_test_scoped_fast_save_specifications(),
|
|
279
|
-
*_cross_language_fast_save_specifications(),
|
|
280
|
-
]
|
|
223
|
+
)
|
|
281
224
|
|
|
282
225
|
|
|
283
226
|
def _fast_save_outcome(
|
|
284
227
|
specification: _FastSaveValidatorSpecification,
|
|
285
228
|
is_clean: bool,
|
|
286
229
|
violation_report: str,
|
|
287
|
-
) ->
|
|
288
|
-
"""Build one
|
|
289
|
-
return
|
|
290
|
-
|
|
230
|
+
) -> ValidatorResult:
|
|
231
|
+
"""Build one result from a specification's display name and check numbers."""
|
|
232
|
+
return ValidatorResult(
|
|
233
|
+
name=specification.display_name,
|
|
291
234
|
checks=specification.checks,
|
|
292
|
-
|
|
293
|
-
|
|
235
|
+
passed=is_clean,
|
|
236
|
+
output=violation_report,
|
|
294
237
|
)
|
|
295
238
|
|
|
296
239
|
|
|
297
240
|
def _run_specification(
|
|
298
241
|
specification: _FastSaveValidatorSpecification, files: "list[Path]"
|
|
299
|
-
) ->
|
|
242
|
+
) -> ValidatorResult:
|
|
300
243
|
"""Run one validator's violation collector in-process over its matching files."""
|
|
301
244
|
matching_files = [
|
|
302
245
|
each_file for each_file in files if specification.applies_to_file(each_file)
|
|
@@ -316,14 +259,14 @@ def _run_specification(
|
|
|
316
259
|
return _fast_save_outcome(specification, False, violation_report)
|
|
317
260
|
|
|
318
261
|
|
|
319
|
-
def run_fast_save_validators(files: "list[Path]") -> "list[
|
|
262
|
+
def run_fast_save_validators(files: "list[Path]") -> "list[ValidatorResult]":
|
|
320
263
|
"""Run every save-path validator in-process, skipping Mypy and Ruff.
|
|
321
264
|
|
|
322
265
|
Args:
|
|
323
266
|
files: The files under validation -- one reconstructed file in gate mode.
|
|
324
267
|
|
|
325
268
|
Returns:
|
|
326
|
-
One
|
|
269
|
+
One result per in-process validator, in roster order. The caller adds
|
|
327
270
|
Ruff and never adds Mypy.
|
|
328
271
|
"""
|
|
329
272
|
return [
|
|
@@ -9,7 +9,7 @@ import sys
|
|
|
9
9
|
from pathlib import Path
|
|
10
10
|
from typing import List
|
|
11
11
|
|
|
12
|
-
from .validator_base import Violation
|
|
12
|
+
from .validator_base import Violation, source_text
|
|
13
13
|
|
|
14
14
|
|
|
15
15
|
PR_PATTERN = re.compile(r"#.*\bPR\s*#?\d+", re.IGNORECASE)
|
|
@@ -42,7 +42,7 @@ def check_pr_references(source: str, filename: str) -> List[Violation]:
|
|
|
42
42
|
def validate_file(file_path: Path) -> List[Violation]:
|
|
43
43
|
filename = str(file_path)
|
|
44
44
|
try:
|
|
45
|
-
source = file_path
|
|
45
|
+
source = source_text(file_path)
|
|
46
46
|
except Exception as error:
|
|
47
47
|
return [Violation(filename, 0, f"Error reading file: {error}")]
|
|
48
48
|
|
|
@@ -11,7 +11,7 @@ import sys
|
|
|
11
11
|
from pathlib import Path
|
|
12
12
|
from typing import List
|
|
13
13
|
|
|
14
|
-
from .validator_base import Violation
|
|
14
|
+
from .validator_base import Violation, source_text, syntax_tree
|
|
15
15
|
|
|
16
16
|
_hooks_directory = str(Path(__file__).resolve().parent.parent)
|
|
17
17
|
|
|
@@ -90,8 +90,8 @@ def validate_file(file_path: Path) -> List[Violation]:
|
|
|
90
90
|
filename = str(file_path)
|
|
91
91
|
|
|
92
92
|
try:
|
|
93
|
-
source = file_path
|
|
94
|
-
tree =
|
|
93
|
+
source = source_text(file_path)
|
|
94
|
+
tree = syntax_tree(source)
|
|
95
95
|
except Exception as error:
|
|
96
96
|
return [Violation(filename, 0, f"Error: {error}")]
|
|
97
97
|
|
|
@@ -21,6 +21,7 @@ from hooks_constants.python_style_checks_constants import ( # noqa: E402
|
|
|
21
21
|
EXPECTED_BLANK_LINES_BETWEEN_FUNCTIONS,
|
|
22
22
|
MINIMUM_ARGUMENT_COUNT,
|
|
23
23
|
)
|
|
24
|
+
from validators.validator_base import source_text, syntax_tree # noqa: E402
|
|
24
25
|
from validators.python_style_helpers import ( # noqa: E402
|
|
25
26
|
FunctionNode,
|
|
26
27
|
blank_line_for_source,
|
|
@@ -331,12 +332,12 @@ def validate_file(file_path: Path) -> list[Violation]:
|
|
|
331
332
|
violations: list[Violation] = []
|
|
332
333
|
filename = str(file_path)
|
|
333
334
|
try:
|
|
334
|
-
source = file_path
|
|
335
|
+
source = source_text(file_path)
|
|
335
336
|
except Exception as error:
|
|
336
337
|
violations.append(Violation(filename, 0, f"Error reading file: {error}"))
|
|
337
338
|
return violations
|
|
338
339
|
try:
|
|
339
|
-
tree =
|
|
340
|
+
tree = syntax_tree(source)
|
|
340
341
|
except SyntaxError as error:
|
|
341
342
|
violations.append(
|
|
342
343
|
Violation(filename, error.lineno or 0, f"Syntax error: {error.msg}")
|
|
@@ -7,16 +7,9 @@ Validates React-specific code standards:
|
|
|
7
7
|
|
|
8
8
|
import re
|
|
9
9
|
import sys
|
|
10
|
-
from dataclasses import dataclass
|
|
11
10
|
from pathlib import Path
|
|
12
11
|
|
|
13
|
-
|
|
14
|
-
@dataclass(frozen=True)
|
|
15
|
-
class Violation:
|
|
16
|
-
"""Represents a validation violation."""
|
|
17
|
-
file: str
|
|
18
|
-
line: int
|
|
19
|
-
message: str
|
|
12
|
+
from .validator_base import Violation
|
|
20
13
|
|
|
21
14
|
|
|
22
15
|
CLASS_COMPONENT_PATTERN = re.compile(
|
|
@@ -81,7 +74,7 @@ def main() -> int:
|
|
|
81
74
|
violations = check_no_class_components(file_paths)
|
|
82
75
|
|
|
83
76
|
for violation in violations:
|
|
84
|
-
print(
|
|
77
|
+
print(violation)
|
|
85
78
|
|
|
86
79
|
return 1 if violations else 0
|
|
87
80
|
|