claude-dev-env 2.31.0 → 2.32.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/bin/install.test.mjs +11 -3
- package/docs/CODE_RULES.md +2 -1
- package/hooks/blocking/bash_post_call_dispatcher.py +70 -0
- package/hooks/blocking/bash_pre_tool_use_dispatcher.py +8 -21
- package/hooks/blocking/code_rules_duplicate_body.py +280 -102
- package/hooks/blocking/code_rules_enforcer.py +32 -14
- package/hooks/blocking/code_rules_enforcer_test_support.py +39 -0
- package/hooks/blocking/code_rules_shared.py +64 -14
- package/hooks/blocking/pr_description_writer_gate.py +18 -25
- package/hooks/blocking/shell_substitution_blocker.py +175 -8
- package/hooks/blocking/stop_dispatcher.py +6 -9
- package/hooks/blocking/tdd_enforcer.py +11 -5
- package/hooks/blocking/tdd_enforcer_parts/config/tdd_enforcer_constants.py +7 -0
- package/hooks/blocking/tdd_enforcer_parts/content_hash_store.py +141 -23
- package/hooks/blocking/tdd_enforcer_parts/tests/test_content_hash_store.py +114 -1
- package/hooks/blocking/test_bash_post_call_dispatcher.py +112 -0
- 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 +251 -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 +14 -2
- package/hooks/blocking/test_shell_substitution_blocker.py +105 -0
- package/hooks/blocking/test_state_description_blocker.py +84 -53
- package/hooks/blocking/test_tdd_enforcer.py +131 -1
- package/hooks/hooks.json +2 -2
- package/hooks/hooks_constants/bash_post_call_dispatcher_constants.py +22 -0
- package/hooks/hooks_constants/hosted_hook_runner.py +53 -0
- package/hooks/hooks_constants/piped_pytest_blocker_constants.py +9 -2
- package/hooks/hooks_constants/pr_description_writer_gate_constants.py +34 -0
- package/hooks/hooks_constants/shell_substitution_blocker_constants.py +10 -0
- package/hooks/hooks_constants/state_file_lock.py +76 -0
- package/hooks/hooks_constants/subprocess_budget_completeness_content.py +2 -2
- package/hooks/hooks_constants/test_bash_post_call_dispatcher_constants.py +27 -0
- package/hooks/hooks_constants/test_failure_recorder_constants.py +35 -0
- package/hooks/hooks_constants/test_hosted_hook_runner.py +55 -1
- 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/session_file_edit_tracker.py +2 -55
- package/hooks/observability/test_failure_recorder.py +219 -0
- package/hooks/observability/test_pr_description_writer_spawn_tracker.py +6 -2
- package/hooks/observability/test_test_failure_recorder.py +173 -0
- 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 +42 -8
- package/hooks/validators/config/system_temporary_roots_constants.py +15 -0
- package/hooks/validators/config/validator_base_constants.py +9 -0
- package/hooks/validators/fast_save_validators.py +23 -80
- package/hooks/validators/mypy_integration.py +10 -36
- package/hooks/validators/pr_reference_checks.py +2 -2
- package/hooks/validators/project_roots.py +58 -0
- 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 +247 -148
- package/hooks/validators/security_checks.py +3 -3
- package/hooks/validators/system_temporary_roots.py +1 -1
- 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.py +22 -0
- package/hooks/validators/test_run_all_validators_mutation_tool_parity.py +249 -0
- package/hooks/validators/test_run_all_validators_pretooluse.py +161 -0
- package/hooks/validators/test_run_all_validators_reporting.py +66 -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/rules/shell-invocation.md +1 -1
- package/rules/testing.md +4 -0
- package/scripts/dev_env_scripts_constants/grok_worker_constants.py +19 -0
- package/scripts/dev_env_scripts_constants/shared_tree_constants.py +17 -0
- package/scripts/grok_headless_runner.py +15 -12
- package/scripts/shared_tree_paths.py +104 -0
- package/scripts/test_shared_tree_paths.py +173 -0
|
@@ -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 [
|
|
@@ -13,28 +13,26 @@ _validators_directory = str(Path(__file__).resolve().parent)
|
|
|
13
13
|
_hooks_directory = str(Path(__file__).resolve().parent.parent)
|
|
14
14
|
|
|
15
15
|
try:
|
|
16
|
+
from project_roots import enclosing_project_root
|
|
16
17
|
from pyproject_config_discovery import (
|
|
17
|
-
ancestor_directories,
|
|
18
|
+
ancestor_directories, # noqa: F401 re-exported for validation/mypy_validator
|
|
18
19
|
find_pyproject_configuring_tool,
|
|
19
20
|
)
|
|
20
|
-
from system_temporary_roots import enclosing_system_temporary_root
|
|
21
21
|
except ModuleNotFoundError:
|
|
22
22
|
if _validators_directory not in sys.path:
|
|
23
23
|
sys.path.insert(0, _validators_directory)
|
|
24
|
+
from project_roots import enclosing_project_root
|
|
24
25
|
from pyproject_config_discovery import (
|
|
25
|
-
ancestor_directories,
|
|
26
|
+
ancestor_directories, # noqa: F401 re-exported for validation/mypy_validator
|
|
26
27
|
find_pyproject_configuring_tool,
|
|
27
28
|
)
|
|
28
|
-
from system_temporary_roots import enclosing_system_temporary_root
|
|
29
29
|
|
|
30
30
|
try:
|
|
31
31
|
from hooks_constants.mypy_integration_constants import (
|
|
32
32
|
FOLLOW_IMPORTS_FLAG,
|
|
33
33
|
FOLLOW_IMPORTS_SKIP_VALUE,
|
|
34
|
-
GIT_DIRECTORY_NAME,
|
|
35
34
|
MYPY_DETACHED_SUBPROCESS_TIMEOUT_SECONDS,
|
|
36
35
|
MYPY_DETACHED_TIMEOUT_SKIP_MESSAGE,
|
|
37
|
-
PYPROJECT_FILENAME,
|
|
38
36
|
PYTHON_SOURCE_SUFFIX,
|
|
39
37
|
)
|
|
40
38
|
from hooks_constants.pyproject_config_discovery_constants import MYPY_TOOL_TABLE_NAME
|
|
@@ -44,10 +42,8 @@ except ModuleNotFoundError:
|
|
|
44
42
|
from hooks_constants.mypy_integration_constants import (
|
|
45
43
|
FOLLOW_IMPORTS_FLAG,
|
|
46
44
|
FOLLOW_IMPORTS_SKIP_VALUE,
|
|
47
|
-
GIT_DIRECTORY_NAME,
|
|
48
45
|
MYPY_DETACHED_SUBPROCESS_TIMEOUT_SECONDS,
|
|
49
46
|
MYPY_DETACHED_TIMEOUT_SKIP_MESSAGE,
|
|
50
|
-
PYPROJECT_FILENAME,
|
|
51
47
|
PYTHON_SOURCE_SUFFIX,
|
|
52
48
|
)
|
|
53
49
|
from hooks_constants.pyproject_config_discovery_constants import MYPY_TOOL_TABLE_NAME
|
|
@@ -97,41 +93,19 @@ def find_pyproject_with_mypy_config(starting_file: Path) -> Path | None:
|
|
|
97
93
|
|
|
98
94
|
|
|
99
95
|
def find_module_resolution_root(starting_file: Path) -> Path | None:
|
|
100
|
-
"""Return the
|
|
96
|
+
"""Return the project root mypy resolves this file's first-party imports against.
|
|
101
97
|
|
|
102
|
-
|
|
103
|
-
``
|
|
104
|
-
|
|
105
|
-
project and keeps a foreign ``config`` in the caller's directory out of scope.
|
|
106
|
-
|
|
107
|
-
The walk does not climb out of the system temp directory. A PreToolUse
|
|
108
|
-
staging copy lives under ``%TEMP%``, and a ``.git`` in the user home above
|
|
109
|
-
that temp root is not this file's project.
|
|
110
|
-
|
|
111
|
-
::
|
|
112
|
-
|
|
113
|
-
target_repo/.git + target_repo/tools/x.py -> target_repo
|
|
114
|
-
/tmp/detached/x.py (home .git above temp) -> None
|
|
115
|
-
flag: walk past %TEMP% into ~/.git -> mypy cwd=home, hook timeout
|
|
98
|
+
Mypy resolves a first-party import against its working directory, so
|
|
99
|
+
anchoring there binds ``config.*`` to the target file's own project and
|
|
100
|
+
keeps a foreign ``config`` in the caller's directory out of scope.
|
|
116
101
|
|
|
117
102
|
Args:
|
|
118
103
|
starting_file: The file (or directory) the walk begins from.
|
|
119
104
|
|
|
120
105
|
Returns:
|
|
121
|
-
The
|
|
122
|
-
or ``None`` when no such ancestor exists inside the walk limit.
|
|
106
|
+
The project root, or None when the file sits inside none.
|
|
123
107
|
"""
|
|
124
|
-
|
|
125
|
-
pyproject_filename = PYPROJECT_FILENAME
|
|
126
|
-
enclosing_temporary_root = enclosing_system_temporary_root(starting_file)
|
|
127
|
-
for each_candidate_directory in ancestor_directories(starting_file):
|
|
128
|
-
has_git_entry = (each_candidate_directory / git_entry_name).exists()
|
|
129
|
-
has_pyproject = (each_candidate_directory / pyproject_filename).is_file()
|
|
130
|
-
if has_git_entry or has_pyproject:
|
|
131
|
-
return each_candidate_directory
|
|
132
|
-
if each_candidate_directory == enclosing_temporary_root:
|
|
133
|
-
return None
|
|
134
|
-
return None
|
|
108
|
+
return enclosing_project_root(starting_file)
|
|
135
109
|
|
|
136
110
|
|
|
137
111
|
def _first_module_resolution_root(all_py_files: list[str]) -> Path | None:
|
|
@@ -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
|
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""Resolve the project root that contains a path, if any.
|
|
2
|
+
|
|
3
|
+
A project root is the nearest ancestor holding a ``.git`` entry or a
|
|
4
|
+
``pyproject.toml``. Callers need that anchor for unrelated reasons: binding a
|
|
5
|
+
first-party import, deciding which directory segments a project owns. One
|
|
6
|
+
module answers it rather than each re-coding the walk.
|
|
7
|
+
|
|
8
|
+
::
|
|
9
|
+
|
|
10
|
+
target_repo/.git + target_repo/tools/x.py -> target_repo
|
|
11
|
+
/tmp/detached/x.py (home .git above temp) -> None
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import sys
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
_validators_directory = str(Path(__file__).resolve().parent)
|
|
20
|
+
_hooks_directory = str(Path(__file__).resolve().parent.parent)
|
|
21
|
+
for each_directory in (_validators_directory, _hooks_directory):
|
|
22
|
+
if each_directory not in sys.path:
|
|
23
|
+
sys.path.insert(0, each_directory)
|
|
24
|
+
|
|
25
|
+
from pyproject_config_discovery import ancestor_directories # noqa: E402
|
|
26
|
+
from system_temporary_roots import ( # noqa: E402
|
|
27
|
+
enclosing_system_temporary_root,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
from hooks_constants.mypy_integration_constants import ( # noqa: E402
|
|
31
|
+
GIT_DIRECTORY_NAME,
|
|
32
|
+
PYPROJECT_FILENAME,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def enclosing_project_root(starting_file: Path) -> Path | None:
|
|
37
|
+
"""Return the nearest ancestor directory that roots a project, else None.
|
|
38
|
+
|
|
39
|
+
The walk stops at the system temp root. A staging copy lives under a temp
|
|
40
|
+
directory, and a ``.git`` in the user home above that root roots no
|
|
41
|
+
project of the staged file's.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
starting_file: The file (or directory) the walk begins from.
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
The nearest ancestor holding ``.git`` or ``pyproject.toml``, or None
|
|
48
|
+
when the walk reaches the temp root or runs out of ancestors.
|
|
49
|
+
"""
|
|
50
|
+
enclosing_temporary_root = enclosing_system_temporary_root(starting_file)
|
|
51
|
+
for each_candidate_directory in ancestor_directories(starting_file):
|
|
52
|
+
has_git_entry = (each_candidate_directory / GIT_DIRECTORY_NAME).exists()
|
|
53
|
+
has_pyproject = (each_candidate_directory / PYPROJECT_FILENAME).is_file()
|
|
54
|
+
if has_git_entry or has_pyproject:
|
|
55
|
+
return each_candidate_directory
|
|
56
|
+
if each_candidate_directory == enclosing_temporary_root:
|
|
57
|
+
return None
|
|
58
|
+
return None
|
|
@@ -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
|
|