claude-dev-env 1.50.1 → 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/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 -5807
- 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/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 +18 -13
- 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_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/sys_path_insert_constants.py +1 -0
- package/package.json +1 -1
- package/hooks/blocking/test_code_rules_enforcer.py +0 -2669
|
@@ -13,22 +13,28 @@ a string still flag.
|
|
|
13
13
|
|
|
14
14
|
from __future__ import annotations
|
|
15
15
|
|
|
16
|
-
import
|
|
16
|
+
import sys
|
|
17
17
|
from pathlib import Path
|
|
18
|
-
from types import
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
18
|
+
from types import SimpleNamespace
|
|
19
|
+
|
|
20
|
+
_BLOCKING_DIRECTORY = str(Path(__file__).resolve().parent)
|
|
21
|
+
_HOOKS_DIRECTORY = str(Path(__file__).resolve().parent.parent)
|
|
22
|
+
if _BLOCKING_DIRECTORY not in sys.path:
|
|
23
|
+
sys.path.insert(0, _BLOCKING_DIRECTORY)
|
|
24
|
+
if _HOOKS_DIRECTORY not in sys.path:
|
|
25
|
+
sys.path.insert(0, _HOOKS_DIRECTORY)
|
|
26
|
+
|
|
27
|
+
from code_rules_comments import ( # noqa: E402
|
|
28
|
+
check_comment_changes,
|
|
29
|
+
check_comments_python,
|
|
30
|
+
extract_comment_texts,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
code_rules_enforcer = SimpleNamespace(
|
|
34
|
+
check_comment_changes=check_comment_changes,
|
|
35
|
+
check_comments_python=check_comments_python,
|
|
36
|
+
extract_comment_texts=extract_comment_texts,
|
|
37
|
+
)
|
|
32
38
|
|
|
33
39
|
|
|
34
40
|
def test_python_check_should_not_flag_hex_color_literal() -> None:
|
|
@@ -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
|
|
@@ -17,23 +17,28 @@ pa#136 F20.
|
|
|
17
17
|
|
|
18
18
|
from __future__ import annotations
|
|
19
19
|
|
|
20
|
-
import importlib.util
|
|
21
20
|
import pathlib
|
|
22
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
|
+
)
|
|
23
36
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
hook_spec = importlib.util.spec_from_file_location(
|
|
29
|
-
"code_rules_enforcer",
|
|
30
|
-
_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,
|
|
31
41
|
)
|
|
32
|
-
assert hook_spec is not None
|
|
33
|
-
assert hook_spec.loader is not None
|
|
34
|
-
hook_module = importlib.util.module_from_spec(hook_spec)
|
|
35
|
-
hook_spec.loader.exec_module(hook_module)
|
|
36
|
-
check_function_length = hook_module.check_function_length
|
|
37
42
|
|
|
38
43
|
PRODUCTION_FILE_PATH = "/project/src/long_module.py"
|
|
39
44
|
TEST_FILE_PATH = "/project/src/test_long_module.py"
|
|
@@ -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
|
+
)
|