claude-dev-env 1.50.0 → 1.50.2
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/hooks/blocking/_gh_body_arg_utils.py +67 -11
- package/hooks/blocking/_md_to_html_blocker_test_support.py +65 -0
- package/hooks/blocking/code_rules_annotations_length.py +167 -0
- package/hooks/blocking/code_rules_banned_identifiers.py +385 -0
- package/hooks/blocking/code_rules_boolean_mustcheck.py +350 -0
- package/hooks/blocking/code_rules_comments.py +337 -0
- package/hooks/blocking/code_rules_constants_config.py +252 -0
- package/hooks/blocking/code_rules_docstrings.py +308 -0
- package/hooks/blocking/code_rules_enforcer.py +98 -5765
- package/hooks/blocking/code_rules_imports_logging.py +276 -0
- package/hooks/blocking/code_rules_magic_values.py +180 -0
- package/hooks/blocking/code_rules_mock_completeness.py +295 -0
- package/hooks/blocking/code_rules_naming_collection.py +264 -0
- package/hooks/blocking/code_rules_optional_params.py +288 -0
- package/hooks/blocking/code_rules_paths_syspath.py +186 -0
- package/hooks/blocking/code_rules_probe_chains.py +305 -0
- package/hooks/blocking/code_rules_probe_detection.py +257 -0
- package/hooks/blocking/code_rules_probe_recording.py +225 -0
- package/hooks/blocking/code_rules_scope_binding.py +151 -0
- package/hooks/blocking/code_rules_shared.py +301 -0
- package/hooks/blocking/code_rules_string_magic.py +207 -0
- package/hooks/blocking/code_rules_test_assertions.py +226 -0
- package/hooks/blocking/code_rules_test_branching_except.py +181 -0
- package/hooks/blocking/code_rules_test_isolation.py +341 -0
- package/hooks/blocking/code_rules_type_escape.py +341 -0
- package/hooks/blocking/code_rules_typeddict_stub.py +305 -0
- package/hooks/blocking/code_rules_unused_imports.py +256 -0
- package/hooks/blocking/conftest.py +30 -0
- package/hooks/blocking/pr_description_body_audit.py +148 -0
- package/hooks/blocking/pr_description_command_parser.py +233 -0
- package/hooks/blocking/pr_description_enforcer.py +36 -825
- package/hooks/blocking/pr_description_pr_number.py +153 -0
- package/hooks/blocking/pr_description_readability.py +366 -0
- package/hooks/blocking/tdd_enforcer.py +31 -0
- package/hooks/blocking/test_code_rules_constants_config.py +26 -0
- package/hooks/blocking/test_code_rules_enforcer_banned_noun_word.py +5 -2
- package/hooks/blocking/test_code_rules_enforcer_cap_meta.py +0 -5
- package/hooks/blocking/test_code_rules_enforcer_comment_string_awareness.py +21 -15
- package/hooks/blocking/test_code_rules_enforcer_config_path.py +20 -16
- package/hooks/blocking/test_code_rules_enforcer_exempt_marker_chained.py +4 -2
- package/hooks/blocking/test_code_rules_enforcer_function_length.py +154 -18
- package/hooks/blocking/test_code_rules_enforcer_hardcoded_user_path.py +1 -2
- package/hooks/blocking/test_code_rules_enforcer_ignored_must_check_return.py +22 -12
- package/hooks/blocking/test_code_rules_enforcer_split_annotations_length.py +55 -0
- package/hooks/blocking/test_code_rules_enforcer_split_banned.py +170 -0
- package/hooks/blocking/test_code_rules_enforcer_split_comments.py +60 -0
- package/hooks/blocking/test_code_rules_enforcer_split_config_path.py +52 -0
- package/hooks/blocking/test_code_rules_enforcer_split_constants_config.py +236 -0
- package/hooks/blocking/test_code_rules_enforcer_split_entry_1.py +296 -0
- package/hooks/blocking/test_code_rules_enforcer_split_entry_2.py +238 -0
- package/hooks/blocking/test_code_rules_enforcer_split_isolation_1.py +271 -0
- package/hooks/blocking/test_code_rules_enforcer_split_isolation_2.py +283 -0
- package/hooks/blocking/test_code_rules_enforcer_split_isolation_3.py +268 -0
- package/hooks/blocking/test_code_rules_enforcer_split_isolation_4.py +85 -0
- package/hooks/blocking/test_code_rules_enforcer_split_mocks_1.py +303 -0
- package/hooks/blocking/test_code_rules_enforcer_split_mocks_2.py +111 -0
- package/hooks/blocking/test_code_rules_enforcer_split_mustcheck.py +87 -0
- package/hooks/blocking/test_code_rules_enforcer_split_naming.py +107 -0
- package/hooks/blocking/test_code_rules_enforcer_split_optional_params.py +325 -0
- package/hooks/blocking/test_code_rules_enforcer_split_paths_syspath.py +110 -0
- package/hooks/blocking/test_code_rules_enforcer_split_shared.py +44 -0
- package/hooks/blocking/test_code_rules_enforcer_split_string_magic.py +55 -0
- package/hooks/blocking/test_code_rules_enforcer_split_test_assertions.py +56 -0
- package/hooks/blocking/test_code_rules_enforcer_todo_markers.py +21 -15
- package/hooks/blocking/test_code_rules_paths_syspath.py +26 -0
- package/hooks/blocking/test_md_to_html_blocker_exemptions.py +368 -0
- package/hooks/blocking/test_md_to_html_blocker_extensions.py +157 -0
- package/hooks/blocking/test_md_to_html_blocker_path_resolution.py +336 -0
- package/hooks/blocking/test_pr_description_enforcer.py +13 -1499
- package/hooks/blocking/test_pr_description_enforcer_body_audit.py +247 -0
- package/hooks/blocking/test_pr_description_enforcer_body_rules.py +493 -0
- package/hooks/blocking/test_pr_description_enforcer_command_parser.py +366 -0
- package/hooks/blocking/test_pr_description_enforcer_pr_number.py +159 -0
- package/hooks/blocking/test_pr_description_enforcer_readability.py +443 -0
- package/hooks/blocking/test_tdd_enforcer.py +116 -0
- package/hooks/hooks_constants/blocking_check_limits.py +3 -0
- package/hooks/hooks_constants/code_rules_enforcer_constants.py +8 -0
- package/hooks/hooks_constants/pr_description_enforcer_constants.py +7 -0
- package/hooks/hooks_constants/sys_path_insert_constants.py +1 -0
- package/package.json +1 -1
- package/hooks/blocking/test_code_rules_enforcer.py +0 -2669
- package/hooks/blocking/test_md_to_html_blocker.py +0 -810
|
@@ -8,24 +8,28 @@ Covers:
|
|
|
8
8
|
|
|
9
9
|
from __future__ import annotations
|
|
10
10
|
|
|
11
|
-
import importlib.util
|
|
12
|
-
import io
|
|
13
11
|
import sys
|
|
14
12
|
from pathlib import Path
|
|
15
|
-
from types import
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
13
|
+
from types import SimpleNamespace
|
|
14
|
+
|
|
15
|
+
_BLOCKING_DIRECTORY = str(Path(__file__).resolve().parent)
|
|
16
|
+
_HOOKS_DIRECTORY = str(Path(__file__).resolve().parent.parent)
|
|
17
|
+
if _BLOCKING_DIRECTORY not in sys.path:
|
|
18
|
+
sys.path.insert(0, _BLOCKING_DIRECTORY)
|
|
19
|
+
if _HOOKS_DIRECTORY not in sys.path:
|
|
20
|
+
sys.path.insert(0, _HOOKS_DIRECTORY)
|
|
21
|
+
|
|
22
|
+
from code_rules_constants_config import ( # noqa: E402
|
|
23
|
+
check_constants_outside_config,
|
|
24
|
+
check_constants_outside_config_advisory,
|
|
25
|
+
)
|
|
26
|
+
from code_rules_path_utils import is_config_file # noqa: E402
|
|
27
|
+
|
|
28
|
+
code_rules_enforcer = SimpleNamespace(
|
|
29
|
+
check_constants_outside_config=check_constants_outside_config,
|
|
30
|
+
check_constants_outside_config_advisory=check_constants_outside_config_advisory,
|
|
31
|
+
is_config_file=is_config_file,
|
|
32
|
+
)
|
|
29
33
|
|
|
30
34
|
PRODUCTION_FILE_PATH = "packages/claude-dev-env/src/example.py"
|
|
31
35
|
|
|
@@ -31,9 +31,11 @@ assert hook_spec is not None
|
|
|
31
31
|
assert hook_spec.loader is not None
|
|
32
32
|
hook_module = importlib.util.module_from_spec(hook_spec)
|
|
33
33
|
hook_spec.loader.exec_module(hook_module)
|
|
34
|
-
_is_exempt_python_comment = hook_module._is_exempt_python_comment
|
|
35
|
-
check_comments_python = hook_module.check_comments_python
|
|
36
34
|
|
|
35
|
+
from code_rules_comments import ( # noqa: E402
|
|
36
|
+
_is_exempt_python_comment,
|
|
37
|
+
check_comments_python,
|
|
38
|
+
)
|
|
37
39
|
|
|
38
40
|
FIXTURE_INLINE_COMMENT_LINE = 5
|
|
39
41
|
FIXTURE_INLINE_COMMENT_COLUMN = 4
|
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
"""Tests for ``check_function_length``.
|
|
2
2
|
|
|
3
|
-
Functions whose
|
|
4
|
-
inclusive
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
3
|
+
Functions whose executable span (signature line through last body statement,
|
|
4
|
+
inclusive, minus the leading docstring lines of the function and of every
|
|
5
|
+
function or class nested within it) is at or above
|
|
6
|
+
``FUNCTION_LENGTH_BLOCKING_THRESHOLD`` (60) block the write (small-function
|
|
7
|
+
basis: Robert C. Martin, Clean Code Ch. 3 "Functions"; Google Python Style
|
|
8
|
+
Guide ~40-line function review hint — a measure of executable complexity,
|
|
9
|
+
paired with the Guide's complete-docstring mandate for public APIs). Executable
|
|
10
|
+
spans below the threshold pass silently, whatever the docstring adds to the
|
|
11
|
+
full declared span; the issue message keeps reporting the full declared span so
|
|
12
|
+
the commit gate's span recovery holds.
|
|
8
13
|
|
|
9
14
|
Cited SYNTHESIS evidence: pa#143 F4, F9, F14 (three recurrences in one PR);
|
|
10
15
|
pa#136 F20.
|
|
@@ -12,23 +17,28 @@ pa#136 F20.
|
|
|
12
17
|
|
|
13
18
|
from __future__ import annotations
|
|
14
19
|
|
|
15
|
-
import importlib.util
|
|
16
20
|
import pathlib
|
|
17
21
|
import sys
|
|
22
|
+
from types import SimpleNamespace
|
|
23
|
+
|
|
24
|
+
_BLOCKING_DIRECTORY = str(pathlib.Path(__file__).resolve().parent)
|
|
25
|
+
_HOOKS_DIRECTORY = str(pathlib.Path(__file__).resolve().parent.parent)
|
|
26
|
+
if _BLOCKING_DIRECTORY not in sys.path:
|
|
27
|
+
sys.path.insert(0, _BLOCKING_DIRECTORY)
|
|
28
|
+
if _HOOKS_DIRECTORY not in sys.path:
|
|
29
|
+
sys.path.insert(0, _HOOKS_DIRECTORY)
|
|
30
|
+
|
|
31
|
+
from code_rules_annotations_length import ( # noqa: E402
|
|
32
|
+
FUNCTION_LENGTH_BLOCKING_MESSAGE_SUFFIX,
|
|
33
|
+
FUNCTION_LENGTH_BLOCKING_THRESHOLD,
|
|
34
|
+
check_function_length,
|
|
35
|
+
)
|
|
18
36
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
hook_spec = importlib.util.spec_from_file_location(
|
|
24
|
-
"code_rules_enforcer",
|
|
25
|
-
_HOOK_DIR / "code_rules_enforcer.py",
|
|
37
|
+
hook_module = SimpleNamespace(
|
|
38
|
+
FUNCTION_LENGTH_BLOCKING_MESSAGE_SUFFIX=FUNCTION_LENGTH_BLOCKING_MESSAGE_SUFFIX,
|
|
39
|
+
FUNCTION_LENGTH_BLOCKING_THRESHOLD=FUNCTION_LENGTH_BLOCKING_THRESHOLD,
|
|
40
|
+
check_function_length=check_function_length,
|
|
26
41
|
)
|
|
27
|
-
assert hook_spec is not None
|
|
28
|
-
assert hook_spec.loader is not None
|
|
29
|
-
hook_module = importlib.util.module_from_spec(hook_spec)
|
|
30
|
-
hook_spec.loader.exec_module(hook_module)
|
|
31
|
-
check_function_length = hook_module.check_function_length
|
|
32
42
|
|
|
33
43
|
PRODUCTION_FILE_PATH = "/project/src/long_module.py"
|
|
34
44
|
TEST_FILE_PATH = "/project/src/test_long_module.py"
|
|
@@ -208,3 +218,129 @@ def test_reports_only_in_scope_violation_among_untouched_ones() -> None:
|
|
|
208
218
|
)
|
|
209
219
|
assert any("target_function" in each_issue for each_issue in issues)
|
|
210
220
|
assert not any("leading_" in each_issue for each_issue in issues)
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def _build_docstring_function_source(
|
|
224
|
+
name: str, docstring_line_count: int, body_line_count: int
|
|
225
|
+
) -> str:
|
|
226
|
+
"""Build a function whose leading docstring spans ``docstring_line_count + 2``
|
|
227
|
+
source lines (opening summary line, the counted filler lines, closing quotes)
|
|
228
|
+
followed by ``body_line_count`` executable statements."""
|
|
229
|
+
docstring_lines = [
|
|
230
|
+
' """Documented helper.',
|
|
231
|
+
*(
|
|
232
|
+
f" documentation line {each_index}."
|
|
233
|
+
for each_index in range(docstring_line_count)
|
|
234
|
+
),
|
|
235
|
+
' """',
|
|
236
|
+
]
|
|
237
|
+
all_source_lines = [
|
|
238
|
+
f"def {name}() -> None:",
|
|
239
|
+
*docstring_lines,
|
|
240
|
+
*(
|
|
241
|
+
f" statement_{each_index} = {each_index}"
|
|
242
|
+
for each_index in range(body_line_count)
|
|
243
|
+
),
|
|
244
|
+
]
|
|
245
|
+
return "\n".join(all_source_lines) + "\n"
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def test_docstring_heavy_function_with_small_body_passes() -> None:
|
|
249
|
+
"""A complete Google-style docstring must not push a small-bodied function
|
|
250
|
+
over the gate: the threshold measures executable lines only."""
|
|
251
|
+
source = _build_docstring_function_source(
|
|
252
|
+
"documented_compact_helper",
|
|
253
|
+
docstring_line_count=hook_module.FUNCTION_LENGTH_BLOCKING_THRESHOLD,
|
|
254
|
+
body_line_count=5,
|
|
255
|
+
)
|
|
256
|
+
issues = check_function_length(source, PRODUCTION_FILE_PATH)
|
|
257
|
+
assert issues == [], f"docstring lines must not count toward the gate, got: {issues!r}"
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def test_oversized_executable_body_blocks_despite_docstring() -> None:
|
|
261
|
+
"""A docstring does not acquit a genuinely large executable body, and the
|
|
262
|
+
issue message reports the full declared span so the commit gate's
|
|
263
|
+
``function_length_span_range`` recovery keeps covering the whole function."""
|
|
264
|
+
docstring_line_count = 10
|
|
265
|
+
body_line_count = hook_module.FUNCTION_LENGTH_BLOCKING_THRESHOLD - 1
|
|
266
|
+
source = _build_docstring_function_source(
|
|
267
|
+
"documented_oversized_helper",
|
|
268
|
+
docstring_line_count=docstring_line_count,
|
|
269
|
+
body_line_count=body_line_count,
|
|
270
|
+
)
|
|
271
|
+
full_declared_span = 1 + (docstring_line_count + 2) + body_line_count
|
|
272
|
+
issues = check_function_length(source, PRODUCTION_FILE_PATH)
|
|
273
|
+
assert any("documented_oversized_helper" in each_issue for each_issue in issues)
|
|
274
|
+
assert any(f"is {full_declared_span} lines" in each_issue for each_issue in issues)
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def test_executable_span_boundary_sits_one_below_threshold() -> None:
|
|
278
|
+
"""With the docstring excluded, an executable span of THRESHOLD - 1 passes
|
|
279
|
+
even though the full declared span sits far above the threshold."""
|
|
280
|
+
source = _build_docstring_function_source(
|
|
281
|
+
"documented_boundary_helper",
|
|
282
|
+
docstring_line_count=20,
|
|
283
|
+
body_line_count=hook_module.FUNCTION_LENGTH_BLOCKING_THRESHOLD - 2,
|
|
284
|
+
)
|
|
285
|
+
issues = check_function_length(source, PRODUCTION_FILE_PATH)
|
|
286
|
+
assert issues == []
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def test_builder_zero_docstring_line_count_keeps_span_contract() -> None:
|
|
290
|
+
"""The builder's docstring-span contract (``docstring_line_count + 2``) holds
|
|
291
|
+
at the zero boundary, so hand-computed span oracles in tests cannot drift."""
|
|
292
|
+
source = _build_docstring_function_source(
|
|
293
|
+
"documented_minimal_helper", docstring_line_count=0, body_line_count=3
|
|
294
|
+
)
|
|
295
|
+
expected_total_lines = 1 + (0 + 2) + 3
|
|
296
|
+
assert len(source.splitlines()) == expected_total_lines
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def test_builder_zero_body_line_count_keeps_span_contract() -> None:
|
|
300
|
+
"""The builder's span contract holds at the zero-body boundary, so a
|
|
301
|
+
docstring-only function's hand-computed span oracle cannot drift."""
|
|
302
|
+
source = _build_docstring_function_source(
|
|
303
|
+
"documented_bodyless_helper", docstring_line_count=5, body_line_count=0
|
|
304
|
+
)
|
|
305
|
+
expected_total_lines = 1 + (5 + 2) + 0
|
|
306
|
+
assert len(source.splitlines()) == expected_total_lines
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def test_nested_function_docstring_does_not_count_toward_outer() -> None:
|
|
310
|
+
"""A nested helper's docstring is documentation too: the outer function's
|
|
311
|
+
executable span excludes every leading docstring within its declared span."""
|
|
312
|
+
nested_docstring_filler = "\n".join(
|
|
313
|
+
f" nested documentation line {each_index}."
|
|
314
|
+
for each_index in range(hook_module.FUNCTION_LENGTH_BLOCKING_THRESHOLD)
|
|
315
|
+
)
|
|
316
|
+
source = (
|
|
317
|
+
"def outer_documented_orchestrator() -> None:\n"
|
|
318
|
+
" def nested_documented_helper() -> None:\n"
|
|
319
|
+
' """Documented nested helper.\n'
|
|
320
|
+
f"{nested_docstring_filler}\n"
|
|
321
|
+
' """\n'
|
|
322
|
+
" nested_statement = 1\n"
|
|
323
|
+
" outer_statement = 2\n"
|
|
324
|
+
)
|
|
325
|
+
issues = check_function_length(source, PRODUCTION_FILE_PATH)
|
|
326
|
+
assert issues == [], f"nested docstring lines must not count toward the gate, got: {issues!r}"
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def test_nested_class_docstring_does_not_count_toward_outer() -> None:
|
|
330
|
+
"""A nested class's docstring is documentation too: the outer function's
|
|
331
|
+
executable span excludes leading docstrings of nested classes as well."""
|
|
332
|
+
nested_class_docstring_filler = "\n".join(
|
|
333
|
+
f" nested class documentation line {each_index}."
|
|
334
|
+
for each_index in range(hook_module.FUNCTION_LENGTH_BLOCKING_THRESHOLD)
|
|
335
|
+
)
|
|
336
|
+
source = (
|
|
337
|
+
"def outer_class_documented_orchestrator() -> None:\n"
|
|
338
|
+
" class NestedDocumentedConfig:\n"
|
|
339
|
+
' """Documented nested class.\n'
|
|
340
|
+
f"{nested_class_docstring_filler}\n"
|
|
341
|
+
' """\n'
|
|
342
|
+
" nested_field = 1\n"
|
|
343
|
+
" outer_statement = 2\n"
|
|
344
|
+
)
|
|
345
|
+
issues = check_function_length(source, PRODUCTION_FILE_PATH)
|
|
346
|
+
assert issues == [], f"nested class docstring lines must not count toward the gate, got: {issues!r}"
|
|
@@ -12,7 +12,6 @@ import importlib.util
|
|
|
12
12
|
import pathlib
|
|
13
13
|
import sys
|
|
14
14
|
|
|
15
|
-
|
|
16
15
|
_HOOK_DIRECTORY = pathlib.Path(__file__).parent
|
|
17
16
|
if str(_HOOK_DIRECTORY) not in sys.path:
|
|
18
17
|
sys.path.insert(0, str(_HOOK_DIRECTORY))
|
|
@@ -26,8 +25,8 @@ assert _hook_spec.loader is not None
|
|
|
26
25
|
_hook_module = importlib.util.module_from_spec(_hook_spec)
|
|
27
26
|
_hook_spec.loader.exec_module(_hook_module)
|
|
28
27
|
check_hardcoded_user_paths = _hook_module.check_hardcoded_user_paths
|
|
29
|
-
HARDCODED_USER_PATH_PATTERN = _hook_module.HARDCODED_USER_PATH_PATTERN
|
|
30
28
|
|
|
29
|
+
from code_rules_paths_syspath import HARDCODED_USER_PATH_PATTERN # noqa: E402
|
|
31
30
|
|
|
32
31
|
PRODUCTION_FILE_PATH = "packages/app/services/loader.py"
|
|
33
32
|
TEST_FILE_PATH = "packages/app/tests/test_loader.py"
|
|
@@ -7,22 +7,32 @@ call is exempt; only bare ``ast.Expr`` calls are flagged.
|
|
|
7
7
|
|
|
8
8
|
from __future__ import annotations
|
|
9
9
|
|
|
10
|
-
import
|
|
10
|
+
import sys
|
|
11
11
|
from pathlib import Path
|
|
12
|
-
from types import
|
|
12
|
+
from types import SimpleNamespace
|
|
13
13
|
|
|
14
|
+
_BLOCKING_DIRECTORY = str(Path(__file__).resolve().parent)
|
|
15
|
+
_HOOKS_DIRECTORY = str(Path(__file__).resolve().parent.parent)
|
|
16
|
+
if _BLOCKING_DIRECTORY not in sys.path:
|
|
17
|
+
sys.path.insert(0, _BLOCKING_DIRECTORY)
|
|
18
|
+
if _HOOKS_DIRECTORY not in sys.path:
|
|
19
|
+
sys.path.insert(0, _HOOKS_DIRECTORY)
|
|
14
20
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
21
|
+
from code_rules_boolean_mustcheck import ( # noqa: E402
|
|
22
|
+
MAX_IGNORED_MUST_CHECK_RETURN_ISSUES,
|
|
23
|
+
)
|
|
24
|
+
from code_rules_boolean_mustcheck import ( # noqa: E402
|
|
25
|
+
check_ignored_must_check_return as _check_ignored_must_check_return,
|
|
26
|
+
)
|
|
27
|
+
from code_rules_enforcer import ( # noqa: E402
|
|
28
|
+
validate_content as _validate_content,
|
|
29
|
+
)
|
|
24
30
|
|
|
25
|
-
code_rules_enforcer =
|
|
31
|
+
code_rules_enforcer = SimpleNamespace(
|
|
32
|
+
MAX_IGNORED_MUST_CHECK_RETURN_ISSUES=MAX_IGNORED_MUST_CHECK_RETURN_ISSUES,
|
|
33
|
+
check_ignored_must_check_return=_check_ignored_must_check_return,
|
|
34
|
+
validate_content=_validate_content,
|
|
35
|
+
)
|
|
26
36
|
|
|
27
37
|
|
|
28
38
|
def check_ignored_must_check_return(content: str, file_path: str) -> list[str]:
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""Behavior tests for the code_rules_annotations_length code-rules check module."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from types import SimpleNamespace
|
|
8
|
+
|
|
9
|
+
_BLOCKING_DIRECTORY = str(Path(__file__).resolve().parent)
|
|
10
|
+
_HOOKS_DIRECTORY = str(Path(__file__).resolve().parent.parent)
|
|
11
|
+
if _BLOCKING_DIRECTORY not in sys.path:
|
|
12
|
+
sys.path.insert(0, _BLOCKING_DIRECTORY)
|
|
13
|
+
if _HOOKS_DIRECTORY not in sys.path:
|
|
14
|
+
sys.path.insert(0, _HOOKS_DIRECTORY)
|
|
15
|
+
|
|
16
|
+
from code_rules_annotations_length import ( # noqa: E402
|
|
17
|
+
FUNCTION_LENGTH_BLOCKING_MESSAGE_SUFFIX,
|
|
18
|
+
check_function_length,
|
|
19
|
+
is_hook_infrastructure,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
code_rules_enforcer = SimpleNamespace(
|
|
23
|
+
FUNCTION_LENGTH_BLOCKING_MESSAGE_SUFFIX=FUNCTION_LENGTH_BLOCKING_MESSAGE_SUFFIX,
|
|
24
|
+
check_function_length=check_function_length,
|
|
25
|
+
is_hook_infrastructure=is_hook_infrastructure,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def test_should_treat_repo_relative_hook_path_as_hook_infrastructure() -> None:
|
|
30
|
+
relative_hook_path = "packages/claude-dev-env/hooks/blocking/code_rules_enforcer.py"
|
|
31
|
+
assert code_rules_enforcer.is_hook_infrastructure(relative_hook_path) is True
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def test_should_treat_backslash_repo_relative_hook_path_as_hook_infrastructure() -> None:
|
|
35
|
+
relative_hook_path = "packages\\claude-dev-env\\hooks\\blocking\\code_rules_enforcer.py"
|
|
36
|
+
assert code_rules_enforcer.is_hook_infrastructure(relative_hook_path) is True
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def test_should_not_treat_unrelated_repo_relative_path_as_hook_infrastructure() -> None:
|
|
40
|
+
relative_source_path = "packages/claude-dev-env/skills/bugteam/scripts/runner.py"
|
|
41
|
+
assert code_rules_enforcer.is_hook_infrastructure(relative_source_path) is False
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def test_should_exempt_repo_relative_hook_file_from_function_length() -> None:
|
|
45
|
+
body_lines = "\n".join(f" bound_{each_index} = {each_index}" for each_index in range(70))
|
|
46
|
+
grown_function_source = "def grown_function() -> None:\n" + body_lines + "\n"
|
|
47
|
+
relative_hook_path = "packages/claude-dev-env/hooks/blocking/code_rules_enforcer.py"
|
|
48
|
+
assert code_rules_enforcer.check_function_length(grown_function_source, relative_hook_path) == []
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def test_function_length_message_does_not_cite_file_length_section() -> None:
|
|
52
|
+
"""The blocking message must cite a function-length basis, not the
|
|
53
|
+
advisory file-length section (CODE_RULES §6.5)."""
|
|
54
|
+
assert "6.5" not in code_rules_enforcer.FUNCTION_LENGTH_BLOCKING_MESSAGE_SUFFIX
|
|
55
|
+
assert "Clean Code" in code_rules_enforcer.FUNCTION_LENGTH_BLOCKING_MESSAGE_SUFFIX
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
"""Behavior tests for the code_rules_banned_identifiers code-rules check module."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from types import SimpleNamespace
|
|
8
|
+
|
|
9
|
+
_BLOCKING_DIRECTORY = str(Path(__file__).resolve().parent)
|
|
10
|
+
_HOOKS_DIRECTORY = str(Path(__file__).resolve().parent.parent)
|
|
11
|
+
if _BLOCKING_DIRECTORY not in sys.path:
|
|
12
|
+
sys.path.insert(0, _BLOCKING_DIRECTORY)
|
|
13
|
+
if _HOOKS_DIRECTORY not in sys.path:
|
|
14
|
+
sys.path.insert(0, _HOOKS_DIRECTORY)
|
|
15
|
+
|
|
16
|
+
from code_rules_banned_identifiers import ( # noqa: E402
|
|
17
|
+
ALL_BANNED_IDENTIFIERS,
|
|
18
|
+
BANNED_IDENTIFIER_MESSAGE_SUFFIX,
|
|
19
|
+
BANNED_IDENTIFIER_SKIP_ADVISORY,
|
|
20
|
+
MAX_BANNED_IDENTIFIER_ISSUES,
|
|
21
|
+
check_banned_noun_word_boundary,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
from hooks_constants.banned_identifiers_constants import ( # noqa: E402
|
|
25
|
+
ALL_BANNED_IDENTIFIERS as config_all_banned_identifiers,
|
|
26
|
+
)
|
|
27
|
+
from hooks_constants.banned_identifiers_constants import ( # noqa: E402
|
|
28
|
+
BANNED_IDENTIFIER_MESSAGE_SUFFIX as config_banned_identifier_message_suffix,
|
|
29
|
+
)
|
|
30
|
+
from hooks_constants.banned_identifiers_constants import ( # noqa: E402
|
|
31
|
+
BANNED_IDENTIFIER_SKIP_ADVISORY as config_banned_identifier_skip_advisory,
|
|
32
|
+
)
|
|
33
|
+
from hooks_constants.banned_identifiers_constants import ( # noqa: E402
|
|
34
|
+
MAX_BANNED_IDENTIFIER_ISSUES as config_max_banned_identifier_issues,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
code_rules_enforcer = SimpleNamespace(
|
|
38
|
+
ALL_BANNED_IDENTIFIERS=ALL_BANNED_IDENTIFIERS,
|
|
39
|
+
BANNED_IDENTIFIER_MESSAGE_SUFFIX=BANNED_IDENTIFIER_MESSAGE_SUFFIX,
|
|
40
|
+
BANNED_IDENTIFIER_SKIP_ADVISORY=BANNED_IDENTIFIER_SKIP_ADVISORY,
|
|
41
|
+
MAX_BANNED_IDENTIFIER_ISSUES=MAX_BANNED_IDENTIFIER_ISSUES,
|
|
42
|
+
check_banned_noun_word_boundary=check_banned_noun_word_boundary,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def test_should_expose_all_banned_identifiers_from_config() -> None:
|
|
47
|
+
expected_banned_identifiers = frozenset({
|
|
48
|
+
"result", "data", "output", "response", "value", "item", "temp",
|
|
49
|
+
"argv", "args", "kwargs", "argc",
|
|
50
|
+
})
|
|
51
|
+
actual_banned_identifiers = getattr(
|
|
52
|
+
code_rules_enforcer, "ALL_BANNED_IDENTIFIERS", None
|
|
53
|
+
)
|
|
54
|
+
assert actual_banned_identifiers is not None, (
|
|
55
|
+
"Renamed constant ALL_BANNED_IDENTIFIERS must be importable from "
|
|
56
|
+
"config/banned_identifiers_constants.py and re-exposed on the "
|
|
57
|
+
f"enforcer module, got: {actual_banned_identifiers!r}"
|
|
58
|
+
)
|
|
59
|
+
assert expected_banned_identifiers <= actual_banned_identifiers, (
|
|
60
|
+
"ALL_BANNED_IDENTIFIERS must contain every expected banned identifier; "
|
|
61
|
+
f"missing: {expected_banned_identifiers - actual_banned_identifiers!r}"
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def test_should_source_banned_identifier_companion_constants_from_config() -> None:
|
|
66
|
+
assert (
|
|
67
|
+
code_rules_enforcer.MAX_BANNED_IDENTIFIER_ISSUES
|
|
68
|
+
is config_max_banned_identifier_issues
|
|
69
|
+
)
|
|
70
|
+
assert (
|
|
71
|
+
code_rules_enforcer.BANNED_IDENTIFIER_MESSAGE_SUFFIX
|
|
72
|
+
is config_banned_identifier_message_suffix
|
|
73
|
+
)
|
|
74
|
+
assert (
|
|
75
|
+
code_rules_enforcer.BANNED_IDENTIFIER_SKIP_ADVISORY
|
|
76
|
+
is config_banned_identifier_skip_advisory
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def test_should_reexport_all_banned_identifiers_from_config() -> None:
|
|
81
|
+
assert code_rules_enforcer.ALL_BANNED_IDENTIFIERS is config_all_banned_identifiers
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def test_banned_noun_word_skips_non_aliased_upstream_import() -> None:
|
|
85
|
+
"""A non-aliased upstream import the author cannot rename
|
|
86
|
+
(`from typing import ItemsView`) must not be flagged, while an
|
|
87
|
+
author-coined alias still is."""
|
|
88
|
+
production_path = "packages/myapp/services/customer_pipeline.py"
|
|
89
|
+
upstream_issues = code_rules_enforcer.check_banned_noun_word_boundary(
|
|
90
|
+
"from typing import ItemsView\n", production_path
|
|
91
|
+
)
|
|
92
|
+
aliased_issues = code_rules_enforcer.check_banned_noun_word_boundary(
|
|
93
|
+
"import legacy_helper as cached_response\n", production_path
|
|
94
|
+
)
|
|
95
|
+
assert upstream_issues == []
|
|
96
|
+
assert any("cached_response" in each_issue for each_issue in aliased_issues)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def test_banned_noun_word_defers_scope_to_caller_when_requested() -> None:
|
|
100
|
+
"""loop7-P1: when the gate sets the deferral flag, the banned-noun check must
|
|
101
|
+
return every violation so ``split_violations_by_scope`` can scope by added
|
|
102
|
+
line before reporting the in-scope set."""
|
|
103
|
+
binding_count = 5
|
|
104
|
+
source = "".join(
|
|
105
|
+
f"BINDING_{each_index}_RESULT_PATH = {each_index}\n"
|
|
106
|
+
for each_index in range(binding_count)
|
|
107
|
+
)
|
|
108
|
+
issues = code_rules_enforcer.check_banned_noun_word_boundary(
|
|
109
|
+
source,
|
|
110
|
+
"/project/src/many_nouns.py",
|
|
111
|
+
defer_scope_to_caller=True,
|
|
112
|
+
)
|
|
113
|
+
assert len(issues) == binding_count, (
|
|
114
|
+
"deferral must return every banned-noun violation, "
|
|
115
|
+
f"got: {issues!r}"
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def test_banned_noun_message_carries_binding_line_span() -> None:
|
|
120
|
+
"""A banned-noun binding carries its own binding line as a one-line span so
|
|
121
|
+
the commit gate reconstructs it through the same shared span mechanism the
|
|
122
|
+
other diff-scoped checks use, while keeping the Line N: prefix intact. The
|
|
123
|
+
binding-line granularity matches the companion exact-match
|
|
124
|
+
check_banned_identifiers and avoids re-flagging a pre-existing binding when
|
|
125
|
+
an unrelated line of its enclosing function is edited."""
|
|
126
|
+
source = (
|
|
127
|
+
"def aggregate() -> list[int]:\n"
|
|
128
|
+
" canned_results = [1, 2, 3]\n"
|
|
129
|
+
" return canned_results\n"
|
|
130
|
+
)
|
|
131
|
+
issues = code_rules_enforcer.check_banned_noun_word_boundary(
|
|
132
|
+
source, "/project/src/has_noun.py"
|
|
133
|
+
)
|
|
134
|
+
binding_line = 2
|
|
135
|
+
expected_fragment = f"(binding span at line {binding_line}, spanning 1 lines)"
|
|
136
|
+
assert any(
|
|
137
|
+
each_issue.startswith(f"Line {binding_line}:") and expected_fragment in each_issue
|
|
138
|
+
for each_issue in issues
|
|
139
|
+
), f"banned-noun message must carry the binding-line span fragment, got: {issues!r}"
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def test_banned_noun_message_module_level_binding_spans_one_line() -> None:
|
|
143
|
+
"""A module-level banned-noun binding spans its own binding line alone
|
|
144
|
+
(span 1)."""
|
|
145
|
+
source = "SAFE_OUTPUT_PATH = '/var/run/x'\n"
|
|
146
|
+
issues = code_rules_enforcer.check_banned_noun_word_boundary(
|
|
147
|
+
source, "/project/src/module_noun.py"
|
|
148
|
+
)
|
|
149
|
+
expected_fragment = "(binding span at line 1, spanning 1 lines)"
|
|
150
|
+
assert any(expected_fragment in each_issue for each_issue in issues), (
|
|
151
|
+
f"module-level banned-noun span must be one line, got: {issues!r}"
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def test_banned_noun_word_boundary_flags_plural_results_identifier() -> None:
|
|
156
|
+
"""A plural banned noun ('results') embedded in an identifier must flag.
|
|
157
|
+
|
|
158
|
+
``ALL_BANNED_NOUN_WORDS`` contains plural forms (results, outputs,
|
|
159
|
+
responses, values, items) in addition to the singular nouns, so an
|
|
160
|
+
identifier such as ``canned_results`` is flagged even though no singular
|
|
161
|
+
exact-match identifier appears.
|
|
162
|
+
"""
|
|
163
|
+
source = "canned_results = []\n"
|
|
164
|
+
issues = code_rules_enforcer.check_banned_noun_word_boundary(
|
|
165
|
+
source, "/project/src/pipeline.py"
|
|
166
|
+
)
|
|
167
|
+
assert any("canned_results" in each_issue for each_issue in issues), (
|
|
168
|
+
"a plural banned-noun identifier must be flagged by the word-boundary "
|
|
169
|
+
f"check; got: {issues!r}"
|
|
170
|
+
)
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""Behavior tests for the code_rules_comments code-rules check module."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from types import SimpleNamespace
|
|
8
|
+
|
|
9
|
+
_BLOCKING_DIRECTORY = str(Path(__file__).resolve().parent)
|
|
10
|
+
_HOOKS_DIRECTORY = str(Path(__file__).resolve().parent.parent)
|
|
11
|
+
if _BLOCKING_DIRECTORY not in sys.path:
|
|
12
|
+
sys.path.insert(0, _BLOCKING_DIRECTORY)
|
|
13
|
+
if _HOOKS_DIRECTORY not in sys.path:
|
|
14
|
+
sys.path.insert(0, _HOOKS_DIRECTORY)
|
|
15
|
+
|
|
16
|
+
from code_rules_comments import ( # noqa: E402
|
|
17
|
+
check_comments_python,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
code_rules_enforcer = SimpleNamespace(
|
|
21
|
+
check_comments_python=check_comments_python,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def test_exempt_comment_rejects_noqa_prefixed_prose_lacking_boundary() -> None:
|
|
26
|
+
"""A comment body that merely starts with `noqa` followed by non-boundary
|
|
27
|
+
characters is not a real noqa directive and must stay subject to the
|
|
28
|
+
no-new-comments rule."""
|
|
29
|
+
source = "x = compute() # noqa-but-not-really: explanation\n"
|
|
30
|
+
issues = code_rules_enforcer.check_comments_python(source)
|
|
31
|
+
assert issues
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def test_exempt_comment_keeps_bare_and_coded_noqa_exempt() -> None:
|
|
35
|
+
"""A bare `# noqa` and a coded `# noqa: E501` remain exempt under the
|
|
36
|
+
tightened boundary rule."""
|
|
37
|
+
bare_source = "x = compute() # noqa\n"
|
|
38
|
+
coded_source = "x = compute() # noqa: E501\n"
|
|
39
|
+
assert code_rules_enforcer.check_comments_python(bare_source) == []
|
|
40
|
+
assert code_rules_enforcer.check_comments_python(coded_source) == []
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def test_exempt_comment_keeps_colon_terminated_markers_without_trailing_space() -> None:
|
|
44
|
+
"""A colon-terminated marker (`pylint:`, `type:`, `pragma:`) is self-bounded
|
|
45
|
+
by its own colon, so the directive stays exempt even when the next character
|
|
46
|
+
follows the colon immediately."""
|
|
47
|
+
pylint_source = "import os # pylint:disable=unused-import\n"
|
|
48
|
+
type_ignore_source = "x = compute() # type:ignore\n"
|
|
49
|
+
pragma_source = "x = compute() # pragma:no-cover\n"
|
|
50
|
+
assert code_rules_enforcer.check_comments_python(pylint_source) == []
|
|
51
|
+
assert code_rules_enforcer.check_comments_python(type_ignore_source) == []
|
|
52
|
+
assert code_rules_enforcer.check_comments_python(pragma_source) == []
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def test_exempt_comment_still_flags_noqa_glued_to_prose_without_boundary() -> None:
|
|
56
|
+
"""The colon-terminated allowance must not loosen the boundary rule for
|
|
57
|
+
markers that do not end in a colon: `# noqaFOO` still lacks a real boundary
|
|
58
|
+
after `noqa` and stays subject to the no-new-comments rule."""
|
|
59
|
+
source = "x = compute() # noqaFOO\n"
|
|
60
|
+
assert code_rules_enforcer.check_comments_python(source)
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""Behavior tests for the code_rules_path_utils code-rules check module."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from types import SimpleNamespace
|
|
8
|
+
|
|
9
|
+
_BLOCKING_DIRECTORY = str(Path(__file__).resolve().parent)
|
|
10
|
+
_HOOKS_DIRECTORY = str(Path(__file__).resolve().parent.parent)
|
|
11
|
+
if _BLOCKING_DIRECTORY not in sys.path:
|
|
12
|
+
sys.path.insert(0, _BLOCKING_DIRECTORY)
|
|
13
|
+
if _HOOKS_DIRECTORY not in sys.path:
|
|
14
|
+
sys.path.insert(0, _HOOKS_DIRECTORY)
|
|
15
|
+
|
|
16
|
+
from code_rules_path_utils import is_config_file # noqa: E402
|
|
17
|
+
from validators.exempt_paths import ( # noqa: E402
|
|
18
|
+
is_config_file as exempt_paths_is_config_file,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
code_rules_enforcer = SimpleNamespace(
|
|
22
|
+
is_config_file=is_config_file,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def test_is_config_file_rejects_filename_only_config_pattern() -> None:
|
|
27
|
+
"""Paths where 'config' appears only in the filename (not as a directory segment) must return False."""
|
|
28
|
+
assert code_rules_enforcer.is_config_file("scripts/db/config.py") is False, (
|
|
29
|
+
"scripts/db/config.py — filename is config.py but parent dir is db, must be False"
|
|
30
|
+
)
|
|
31
|
+
assert code_rules_enforcer.is_config_file("lib/myconfig.py") is False, (
|
|
32
|
+
"lib/myconfig.py — config appears only in the filename stem, must be False"
|
|
33
|
+
)
|
|
34
|
+
assert code_rules_enforcer.is_config_file("src/app_config.py") is False, (
|
|
35
|
+
"src/app_config.py — config appears only in the filename stem, must be False"
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def test_is_config_file_reexported_by_exempt_paths_matches_canonical() -> None:
|
|
40
|
+
"""The exempt_paths re-export of is_config_file must agree with the canonical code_rules_path_utils implementation on all sample paths."""
|
|
41
|
+
all_sample_paths = [
|
|
42
|
+
"scripts/db/config.py",
|
|
43
|
+
"config/timing.py",
|
|
44
|
+
"settings.py",
|
|
45
|
+
]
|
|
46
|
+
for each_path in all_sample_paths:
|
|
47
|
+
canonical_result = code_rules_enforcer.is_config_file(each_path)
|
|
48
|
+
exempt_paths_result = exempt_paths_is_config_file(each_path)
|
|
49
|
+
assert canonical_result == exempt_paths_result, (
|
|
50
|
+
f"is_config_file diverged for {each_path!r}: "
|
|
51
|
+
f"code_rules_path_utils={canonical_result}, exempt_paths={exempt_paths_result}"
|
|
52
|
+
)
|