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.
Files changed (47) hide show
  1. package/docs/CODE_RULES.md +2 -1
  2. package/hooks/blocking/code_rules_duplicate_body.py +272 -100
  3. package/hooks/blocking/code_rules_enforcer.py +32 -14
  4. package/hooks/blocking/code_rules_shared.py +64 -14
  5. package/hooks/blocking/pr_description_writer_gate.py +18 -25
  6. package/hooks/blocking/tdd_enforcer.py +8 -4
  7. package/hooks/blocking/test_code_rules_enforcer_ephemeral.py +12 -1
  8. package/hooks/blocking/test_code_rules_enforcer_ephemeral_repository_root.py +118 -0
  9. package/hooks/blocking/test_code_rules_enforcer_same_file_inline_duplicate.py +244 -0
  10. package/hooks/blocking/test_code_rules_enforcer_split_entry_2.py +22 -0
  11. package/hooks/blocking/test_pr_description_writer_gate.py +21 -3
  12. package/hooks/blocking/test_pre_tool_use_dispatcher_native.py +9 -2
  13. package/hooks/blocking/test_state_description_blocker.py +99 -53
  14. package/hooks/blocking/test_tdd_enforcer.py +64 -0
  15. package/hooks/hooks_constants/pr_description_writer_gate_constants.py +34 -0
  16. package/hooks/hooks_constants/subprocess_budget_completeness_content.py +2 -2
  17. package/hooks/hooks_constants/test_multi_edit_reconstruction.py +1 -0
  18. package/hooks/hooks_constants/test_subprocess_budget_completeness_content.py +33 -0
  19. package/hooks/observability/pr_description_writer_spawn_tracker.py +4 -12
  20. package/hooks/observability/test_pr_description_writer_spawn_tracker.py +6 -2
  21. package/hooks/validators/abbreviation_checks.py +3 -3
  22. package/hooks/validators/code_quality_checks.py +4 -4
  23. package/hooks/validators/config/baseline_identity_constants.py +12 -0
  24. package/hooks/validators/config/directory_exemption_constants.py +44 -1
  25. package/hooks/validators/config/validator_base_constants.py +9 -0
  26. package/hooks/validators/fast_save_validators.py +23 -80
  27. package/hooks/validators/pr_reference_checks.py +2 -2
  28. package/hooks/validators/python_antipattern_checks.py +3 -3
  29. package/hooks/validators/python_style_checks.py +3 -2
  30. package/hooks/validators/react_checks.py +2 -9
  31. package/hooks/validators/run_all_validators.py +190 -145
  32. package/hooks/validators/security_checks.py +3 -3
  33. package/hooks/validators/test_directory_exemption_constants.py +30 -24
  34. package/hooks/validators/test_fast_save_validators.py +16 -16
  35. package/hooks/validators/test_hook_timing_harness.py +10 -1
  36. package/hooks/validators/test_run_all_validators_pretooluse.py +161 -0
  37. package/hooks/validators/test_run_all_validators_repository_root.py +202 -0
  38. package/hooks/validators/test_validator_base.py +51 -2
  39. package/hooks/validators/todo_checks.py +2 -2
  40. package/hooks/validators/type_safety_checks.py +3 -3
  41. package/hooks/validators/useless_test_checks.py +3 -3
  42. package/hooks/validators/validator_base.py +73 -1
  43. package/package.json +1 -1
  44. package/scripts/dev_env_scripts_constants/grok_worker_constants.py +15 -0
  45. package/scripts/grok_headless_runner.py +15 -12
  46. package/scripts/shared_tree_paths.py +70 -0
  47. 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
- sanitized_session_id = SESSION_ID_UNSAFE_CHARACTERS_PATTERN.sub("", session_id)
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
- tracker_module.tempfile, "gettempdir", return_value=str(temp_directory)
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(tracker_module.tempfile, "gettempdir", return_value=str(tmp_path)):
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.read_text(encoding="utf-8")
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 = ast.parse(source)
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.read_text(encoding="utf-8").splitlines()
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.read_text(encoding="utf-8")
102
- tree = ast.parse(source)
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
- f"{each_violation.file}:{each_violation.line}: {each_violation.message}"
133
- for each_violation in all_violations
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 _python_naming_and_syntax_specifications() -> (
149
- "list[_FastSaveValidatorSpecification]"
129
+ def _all_fast_save_validator_specifications() -> (
130
+ "tuple[_FastSaveValidatorSpecification, ...]"
150
131
  ):
151
- """Return the save-path checks for Python naming, style, and constants."""
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
- ) -> FastSaveCheckOutcome:
288
- """Build one outcome from a specification's display name and check numbers."""
289
- return FastSaveCheckOutcome(
290
- display_name=specification.display_name,
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
- is_clean=is_clean,
293
- violation_report=violation_report,
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
- ) -> FastSaveCheckOutcome:
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[FastSaveCheckOutcome]":
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 outcome per in-process validator, in roster order. The caller adds
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.read_text(encoding="utf-8")
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.read_text(encoding="utf-8")
94
- tree = ast.parse(source)
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.read_text(encoding="utf-8")
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 = ast.parse(source)
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(f"{violation.file}:{violation.line}: {violation.message}")
77
+ print(violation)
85
78
 
86
79
  return 1 if violations else 0
87
80