claude-dev-env 1.34.0 → 1.35.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/agents/docs-agent.md +1 -1
- package/agents/project-docs-analyzer.md +0 -1
- package/agents/skill-to-agent-converter.md +0 -1
- package/commands/initialize.md +0 -1
- package/commands/readability-review.md +4 -4
- package/commands/review-plan.md +2 -4
- package/commands/stubcheck.md +1 -2
- package/hooks/blocking/code_rules_enforcer.py +250 -36
- package/hooks/blocking/test_code_rules_enforcer.py +91 -39
- package/hooks/blocking/test_code_rules_enforcer_annotations.py +97 -0
- package/hooks/blocking/test_code_rules_enforcer_collection_prefix.py +137 -0
- package/hooks/blocking/test_code_rules_enforcer_config_path.py +0 -20
- package/hooks/blocking/test_code_rules_enforcer_constant_equality.py +0 -18
- package/hooks/blocking/test_code_rules_enforcer_existence_checks.py +0 -18
- package/hooks/blocking/test_code_rules_enforcer_inline_literal_collections.py +155 -0
- package/hooks/blocking/test_code_rules_enforcer_loop_variable_naming.py +110 -0
- package/hooks/blocking/test_code_rules_enforcer_naming_pattern.py +0 -13
- package/hooks/blocking/test_code_rules_enforcer_skip_decorators.py +0 -26
- package/hooks/blocking/test_code_rules_enforcer_string_magic.py +234 -0
- package/package.json +1 -1
- package/skills/bugteam/PROMPTS.md +0 -39
- package/skills/bugteam/SKILL.md +17 -35
- package/skills/bugteam/reference/copilot-gap-analysis.md +12 -0
- package/skills/pr-converge/SKILL.md +185 -167
- package/agents/agent-writer.md +0 -157
- package/agents/config-centralizer.md +0 -686
- package/agents/config-extraction-agent.md +0 -225
- package/agents/doc-orchestrator.md +0 -47
- package/agents/docx-agent.md +0 -211
- package/agents/magic-value-eliminator-agent.md +0 -72
- package/agents/mandatory-agent-workflow-agent.md +0 -88
- package/agents/parallel-workflow-coordinator.md +0 -779
- package/agents/pdf-agent.md +0 -302
- package/agents/project-context-loader.md +0 -238
- package/agents/readability-review-agent.md +0 -76
- package/agents/refactoring-specialist.md +0 -69
- package/agents/right-sized-engineer.md +0 -129
- package/agents/session-continuity-manager.md +0 -53
- package/agents/stub-detector-agent.md +0 -140
- package/agents/tdd-test-writer.md +0 -62
- package/agents/test-data-builder.md +0 -68
- package/agents/tooling-builder.md +0 -78
- package/agents/validation-expert.md +0 -71
- package/agents/xlsx-agent.md +0 -169
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
import importlib.util
|
|
5
|
+
|
|
6
|
+
ENFORCER_PATH = Path(__file__).resolve().parent / "code_rules_enforcer.py"
|
|
7
|
+
specification = importlib.util.spec_from_file_location(
|
|
8
|
+
"code_rules_enforcer", ENFORCER_PATH
|
|
9
|
+
)
|
|
10
|
+
code_rules_enforcer = importlib.util.module_from_spec(specification)
|
|
11
|
+
specification.loader.exec_module(code_rules_enforcer)
|
|
12
|
+
|
|
13
|
+
PRODUCTION_FILE_PATH = "packages/app/services/foo.py"
|
|
14
|
+
TEST_FILE_PATH = "packages/app/tests/test_foo.py"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def test_should_flag_parameter_without_annotation() -> None:
|
|
18
|
+
source = "def consume(value) -> None:\n return None\n"
|
|
19
|
+
issues = code_rules_enforcer.check_parameter_annotations(
|
|
20
|
+
source, PRODUCTION_FILE_PATH
|
|
21
|
+
)
|
|
22
|
+
assert any("value" in each_issue for each_issue in issues), (
|
|
23
|
+
f"Expected unannotated parameter flagged, got: {issues}"
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def test_should_not_flag_annotated_parameters() -> None:
|
|
28
|
+
source = (
|
|
29
|
+
"def consume(value: int, label: str = 'default') -> None:\n return None\n"
|
|
30
|
+
)
|
|
31
|
+
issues = code_rules_enforcer.check_parameter_annotations(
|
|
32
|
+
source, PRODUCTION_FILE_PATH
|
|
33
|
+
)
|
|
34
|
+
assert issues == [], f"Expected no issues for annotated params, got: {issues}"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def test_should_exempt_self_and_cls_parameters() -> None:
|
|
38
|
+
source = (
|
|
39
|
+
"class Foo:\n"
|
|
40
|
+
" def method(self, value: int) -> None:\n"
|
|
41
|
+
" return None\n"
|
|
42
|
+
" @classmethod\n"
|
|
43
|
+
" def factory(cls, value: int) -> 'Foo':\n"
|
|
44
|
+
" return cls()\n"
|
|
45
|
+
)
|
|
46
|
+
issues = code_rules_enforcer.check_parameter_annotations(
|
|
47
|
+
source, PRODUCTION_FILE_PATH
|
|
48
|
+
)
|
|
49
|
+
assert issues == [], (
|
|
50
|
+
f"self/cls must be exempt from annotation requirement, got: {issues}"
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def test_should_flag_class_method_parameter_without_annotation() -> None:
|
|
55
|
+
source = "class Foo:\n def method(self, value) -> None:\n return None\n"
|
|
56
|
+
issues = code_rules_enforcer.check_parameter_annotations(
|
|
57
|
+
source, PRODUCTION_FILE_PATH
|
|
58
|
+
)
|
|
59
|
+
assert any("value" in each_issue for each_issue in issues), (
|
|
60
|
+
f"Expected method param flagged, got: {issues}"
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def test_should_skip_parameter_check_in_test_files() -> None:
|
|
65
|
+
source = "def consume(value) -> None:\n return None\n"
|
|
66
|
+
issues = code_rules_enforcer.check_parameter_annotations(source, TEST_FILE_PATH)
|
|
67
|
+
assert issues == [], f"Test files must be exempt, got: {issues}"
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def test_should_flag_function_without_return_annotation() -> None:
|
|
71
|
+
source = "def fetch(url: str):\n return url\n"
|
|
72
|
+
issues = code_rules_enforcer.check_return_annotations(source, PRODUCTION_FILE_PATH)
|
|
73
|
+
assert any("fetch" in each_issue for each_issue in issues), (
|
|
74
|
+
f"Expected function without return type flagged, got: {issues}"
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def test_should_not_flag_function_with_return_annotation() -> None:
|
|
79
|
+
source = "def fetch(url: str) -> str:\n return url\n"
|
|
80
|
+
issues = code_rules_enforcer.check_return_annotations(source, PRODUCTION_FILE_PATH)
|
|
81
|
+
assert issues == [], f"Function with return type must not be flagged, got: {issues}"
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def test_should_flag_async_function_without_return_annotation() -> None:
|
|
85
|
+
source = "async def fetch(url: str):\n return url\n"
|
|
86
|
+
issues = code_rules_enforcer.check_return_annotations(source, PRODUCTION_FILE_PATH)
|
|
87
|
+
assert any("fetch" in each_issue for each_issue in issues), (
|
|
88
|
+
f"Expected async function without return type flagged, got: {issues}"
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def test_should_skip_return_check_in_test_files() -> None:
|
|
93
|
+
source = "def fetch(url: str):\n return url\n"
|
|
94
|
+
issues = code_rules_enforcer.check_return_annotations(source, TEST_FILE_PATH)
|
|
95
|
+
assert issues == [], f"Test files must be exempt, got: {issues}"
|
|
96
|
+
|
|
97
|
+
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
import importlib.util
|
|
5
|
+
|
|
6
|
+
ENFORCER_PATH = Path(__file__).resolve().parent / "code_rules_enforcer.py"
|
|
7
|
+
specification = importlib.util.spec_from_file_location(
|
|
8
|
+
"code_rules_enforcer", ENFORCER_PATH
|
|
9
|
+
)
|
|
10
|
+
code_rules_enforcer = importlib.util.module_from_spec(specification)
|
|
11
|
+
specification.loader.exec_module(code_rules_enforcer)
|
|
12
|
+
|
|
13
|
+
PRODUCTION_FILE_PATH = "packages/app/services/foo.py"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def test_should_flag_bare_set_parameter_without_all_prefix() -> None:
|
|
17
|
+
source = "def consume(numbers: set[int]) -> None:\n return None\n"
|
|
18
|
+
issues = code_rules_enforcer.check_collection_prefix(source, PRODUCTION_FILE_PATH)
|
|
19
|
+
assert any("numbers" in each_issue for each_issue in issues), (
|
|
20
|
+
f"Expected bare set[int] parameter flagged, got: {issues}"
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def test_should_flag_pep604_union_set_with_none_right() -> None:
|
|
25
|
+
source = "def consume(numbers: set[int] | None = None) -> None:\n return None\n"
|
|
26
|
+
issues = code_rules_enforcer.check_collection_prefix(source, PRODUCTION_FILE_PATH)
|
|
27
|
+
assert any("numbers" in each_issue for each_issue in issues), (
|
|
28
|
+
f"Expected set[int] | None parameter flagged, got: {issues}"
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def test_should_flag_pep604_union_set_with_none_left() -> None:
|
|
33
|
+
source = "def consume(numbers: None | set[int] = None) -> None:\n return None\n"
|
|
34
|
+
issues = code_rules_enforcer.check_collection_prefix(source, PRODUCTION_FILE_PATH)
|
|
35
|
+
assert any("numbers" in each_issue for each_issue in issues), (
|
|
36
|
+
f"Expected None | set[int] parameter flagged, got: {issues}"
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def test_should_flag_optional_set_parameter() -> None:
|
|
41
|
+
source = (
|
|
42
|
+
"from typing import Optional\n"
|
|
43
|
+
"\n"
|
|
44
|
+
"def consume(numbers: Optional[set[int]] = None) -> None:\n"
|
|
45
|
+
" return None\n"
|
|
46
|
+
)
|
|
47
|
+
issues = code_rules_enforcer.check_collection_prefix(source, PRODUCTION_FILE_PATH)
|
|
48
|
+
assert any("numbers" in each_issue for each_issue in issues), (
|
|
49
|
+
f"Expected Optional[set[int]] parameter flagged, got: {issues}"
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def test_should_flag_union_set_with_none_parameter() -> None:
|
|
54
|
+
source = (
|
|
55
|
+
"from typing import Union\n"
|
|
56
|
+
"\n"
|
|
57
|
+
"def consume(numbers: Union[set[int], None] = None) -> None:\n"
|
|
58
|
+
" return None\n"
|
|
59
|
+
)
|
|
60
|
+
issues = code_rules_enforcer.check_collection_prefix(source, PRODUCTION_FILE_PATH)
|
|
61
|
+
assert any("numbers" in each_issue for each_issue in issues), (
|
|
62
|
+
f"Expected Union[set[int], None] parameter flagged, got: {issues}"
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def test_should_flag_pep604_union_dict_parameter() -> None:
|
|
67
|
+
source = (
|
|
68
|
+
"from pathlib import Path\n"
|
|
69
|
+
"\n"
|
|
70
|
+
"def consume(per_path: dict[Path, set[int]] | None = None) -> None:\n"
|
|
71
|
+
" return None\n"
|
|
72
|
+
)
|
|
73
|
+
issues = code_rules_enforcer.check_collection_prefix(source, PRODUCTION_FILE_PATH)
|
|
74
|
+
assert any("per_path" in each_issue for each_issue in issues), (
|
|
75
|
+
f"Expected dict[..] | None parameter flagged, got: {issues}"
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def test_should_not_flag_pep604_union_when_param_has_all_prefix() -> None:
|
|
80
|
+
source = (
|
|
81
|
+
"def consume(all_numbers: set[int] | None = None) -> None:\n return None\n"
|
|
82
|
+
)
|
|
83
|
+
issues = code_rules_enforcer.check_collection_prefix(source, PRODUCTION_FILE_PATH)
|
|
84
|
+
assert not any("all_numbers" in each_issue for each_issue in issues), (
|
|
85
|
+
f"all_-prefixed parameter must not be flagged, got: {issues}"
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def test_should_not_flag_optional_when_param_uses_x_by_y_pattern() -> None:
|
|
90
|
+
source = (
|
|
91
|
+
"from typing import Optional\n"
|
|
92
|
+
"\n"
|
|
93
|
+
"def consume(price_by_product: Optional[dict[str, int]] = None) -> None:\n"
|
|
94
|
+
" return None\n"
|
|
95
|
+
)
|
|
96
|
+
issues = code_rules_enforcer.check_collection_prefix(source, PRODUCTION_FILE_PATH)
|
|
97
|
+
assert not any("price_by_product" in each_issue for each_issue in issues), (
|
|
98
|
+
f"X_by_Y-pattern parameter must not be flagged, got: {issues}"
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def test_should_flag_qualified_typing_optional_set_parameter() -> None:
|
|
103
|
+
source = (
|
|
104
|
+
"import typing\n"
|
|
105
|
+
"\n"
|
|
106
|
+
"def consume(numbers: typing.Optional[set[int]] = None) -> None:\n"
|
|
107
|
+
" return None\n"
|
|
108
|
+
)
|
|
109
|
+
issues = code_rules_enforcer.check_collection_prefix(source, PRODUCTION_FILE_PATH)
|
|
110
|
+
assert any("numbers" in each_issue for each_issue in issues), (
|
|
111
|
+
f"Expected typing.Optional[set[int]] (ast.Attribute outer subscript) flagged, got: {issues}"
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def test_should_flag_qualified_typing_union_with_none_parameter() -> None:
|
|
116
|
+
source = (
|
|
117
|
+
"import typing\n"
|
|
118
|
+
"\n"
|
|
119
|
+
"def consume(numbers: typing.Union[set[int], None] = None) -> None:\n"
|
|
120
|
+
" return None\n"
|
|
121
|
+
)
|
|
122
|
+
issues = code_rules_enforcer.check_collection_prefix(source, PRODUCTION_FILE_PATH)
|
|
123
|
+
assert any("numbers" in each_issue for each_issue in issues), (
|
|
124
|
+
f"Expected typing.Union[set[int], None] (ast.Attribute outer subscript) flagged, got: {issues}"
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def test_should_flag_module_level_qualified_typing_optional_constant() -> None:
|
|
129
|
+
source = (
|
|
130
|
+
"import typing\n"
|
|
131
|
+
"\n"
|
|
132
|
+
"RAW_NUMBERS: typing.Optional[set[int]] = None\n"
|
|
133
|
+
)
|
|
134
|
+
issues = code_rules_enforcer.check_collection_prefix(source, PRODUCTION_FILE_PATH)
|
|
135
|
+
assert any("RAW_NUMBERS" in each_issue for each_issue in issues), (
|
|
136
|
+
f"Expected typing.Optional[set[int]] module-level constant flagged, got: {issues}"
|
|
137
|
+
)
|
|
@@ -83,26 +83,6 @@ def test_should_produce_blocking_for_module_level_upper_snake_outside_config() -
|
|
|
83
83
|
assert any("MAX_RETRIES" in issue for issue in blocking_issues)
|
|
84
84
|
|
|
85
85
|
|
|
86
|
-
def test_should_include_file_path_in_advisory_stderr_output() -> None:
|
|
87
|
-
source = (
|
|
88
|
-
"def fetch_data():\n"
|
|
89
|
-
" MAX_RETRIES = 3\n"
|
|
90
|
-
" for attempt in range(MAX_RETRIES):\n"
|
|
91
|
-
" pass\n"
|
|
92
|
-
)
|
|
93
|
-
captured_stderr = io.StringIO()
|
|
94
|
-
old_stderr = sys.stderr
|
|
95
|
-
sys.stderr = captured_stderr
|
|
96
|
-
try:
|
|
97
|
-
code_rules_enforcer.validate_content(source, PRODUCTION_FILE_PATH, "")
|
|
98
|
-
finally:
|
|
99
|
-
sys.stderr = old_stderr
|
|
100
|
-
advisory_output = captured_stderr.getvalue()
|
|
101
|
-
assert PRODUCTION_FILE_PATH in advisory_output, (
|
|
102
|
-
f"Advisory stderr must include the file path; got: {advisory_output!r}"
|
|
103
|
-
)
|
|
104
|
-
|
|
105
|
-
|
|
106
86
|
def test_should_produce_stable_ordering_sorted_by_line_number() -> None:
|
|
107
87
|
source = (
|
|
108
88
|
"ALPHA_CONSTANT = 1\n"
|
|
@@ -110,21 +110,3 @@ def test_should_not_flag_non_test_function() -> None:
|
|
|
110
110
|
assert issues == [], f"Expected no issues for non-test function, got: {issues}"
|
|
111
111
|
|
|
112
112
|
|
|
113
|
-
def test_stops_at_max_issues_per_check() -> None:
|
|
114
|
-
source = (
|
|
115
|
-
"ALPHA = 1\nBETA = 2\nGAMMA = 3\nDELTA = 4\nEPSILON = 5\n"
|
|
116
|
-
"\n"
|
|
117
|
-
"def test_alpha() -> None:\n assert ALPHA == 1\n"
|
|
118
|
-
"\n"
|
|
119
|
-
"def test_beta() -> None:\n assert BETA == 2\n"
|
|
120
|
-
"\n"
|
|
121
|
-
"def test_gamma() -> None:\n assert GAMMA == 3\n"
|
|
122
|
-
"\n"
|
|
123
|
-
"def test_delta() -> None:\n assert DELTA == 4\n"
|
|
124
|
-
"\n"
|
|
125
|
-
"def test_epsilon() -> None:\n assert EPSILON == 5\n"
|
|
126
|
-
)
|
|
127
|
-
issues = code_rules_enforcer.check_constant_equality_tests(source, TEST_FILE_PATH)
|
|
128
|
-
assert len(issues) == code_rules_enforcer.MAX_ISSUES_PER_CHECK, (
|
|
129
|
-
f"Expected exactly MAX_ISSUES_PER_CHECK issues, got {len(issues)}: {issues}"
|
|
130
|
-
)
|
|
@@ -101,24 +101,6 @@ def test_should_not_flag_non_test_function_with_existence_check() -> None:
|
|
|
101
101
|
assert issues == [], f"Expected no issues for non-test function, got: {issues}"
|
|
102
102
|
|
|
103
103
|
|
|
104
|
-
def test_stops_at_max_issues_per_check() -> None:
|
|
105
|
-
source = (
|
|
106
|
-
"def test_one() -> None:\n assert callable(func_one)\n"
|
|
107
|
-
"\n"
|
|
108
|
-
"def test_two() -> None:\n assert callable(func_two)\n"
|
|
109
|
-
"\n"
|
|
110
|
-
"def test_three() -> None:\n assert callable(func_three)\n"
|
|
111
|
-
"\n"
|
|
112
|
-
"def test_four() -> None:\n assert callable(func_four)\n"
|
|
113
|
-
"\n"
|
|
114
|
-
"def test_five() -> None:\n assert callable(func_five)\n"
|
|
115
|
-
)
|
|
116
|
-
issues = code_rules_enforcer.check_existence_check_tests(source, TEST_FILE_PATH)
|
|
117
|
-
assert len(issues) == code_rules_enforcer.MAX_ISSUES_PER_CHECK, (
|
|
118
|
-
f"Expected exactly MAX_ISSUES_PER_CHECK issues, got {len(issues)}: {issues}"
|
|
119
|
-
)
|
|
120
|
-
|
|
121
|
-
|
|
122
104
|
def test_should_not_flag_outer_test_when_nested_helper_contains_existence_check() -> None:
|
|
123
105
|
source = (
|
|
124
106
|
"def test_outer_behavior() -> None:\n"
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
import importlib.util
|
|
5
|
+
|
|
6
|
+
ENFORCER_PATH = Path(__file__).resolve().parent / "code_rules_enforcer.py"
|
|
7
|
+
specification = importlib.util.spec_from_file_location(
|
|
8
|
+
"code_rules_enforcer", ENFORCER_PATH
|
|
9
|
+
)
|
|
10
|
+
code_rules_enforcer = importlib.util.module_from_spec(specification)
|
|
11
|
+
specification.loader.exec_module(code_rules_enforcer)
|
|
12
|
+
|
|
13
|
+
PRODUCTION_FILE_PATH = "packages/app/services/foo.py"
|
|
14
|
+
TEST_FILE_PATH = "packages/app/tests/test_foo.py"
|
|
15
|
+
CONFIG_FILE_PATH = "packages/app/config/constants.py"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def test_should_flag_set_literal_with_three_string_constants_in_function_body() -> None:
|
|
19
|
+
source = (
|
|
20
|
+
"def is_known(value: str) -> bool:\n"
|
|
21
|
+
" return value in {'true', 'false', 'none'}\n"
|
|
22
|
+
)
|
|
23
|
+
issues = code_rules_enforcer.check_inline_literal_collections(
|
|
24
|
+
source, PRODUCTION_FILE_PATH
|
|
25
|
+
)
|
|
26
|
+
assert len(issues) == 1, f"Expected 3-element set flagged, got: {issues}"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def test_should_flag_list_literal_with_five_string_constants_in_function_body() -> None:
|
|
30
|
+
source = (
|
|
31
|
+
"def is_code_path(suffix: str) -> bool:\n"
|
|
32
|
+
" return suffix in ['.py', '.js', '.ts', '.tsx', '.jsx']\n"
|
|
33
|
+
)
|
|
34
|
+
issues = code_rules_enforcer.check_inline_literal_collections(
|
|
35
|
+
source, PRODUCTION_FILE_PATH
|
|
36
|
+
)
|
|
37
|
+
assert len(issues) == 1, f"Expected 5-element list flagged, got: {issues}"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def test_should_not_flag_two_element_set_literal() -> None:
|
|
41
|
+
source = "def is_binary(value: str) -> bool:\n return value in {'on', 'off'}\n"
|
|
42
|
+
issues = code_rules_enforcer.check_inline_literal_collections(
|
|
43
|
+
source, PRODUCTION_FILE_PATH
|
|
44
|
+
)
|
|
45
|
+
assert issues == [], f"2-element literal must not be flagged, got: {issues}"
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def test_should_not_flag_literal_with_variable_references() -> None:
|
|
49
|
+
source = (
|
|
50
|
+
"def consume(a: int, b: int, c: int, d: int) -> list[int]:\n"
|
|
51
|
+
" return [a, b, c, d]\n"
|
|
52
|
+
)
|
|
53
|
+
issues = code_rules_enforcer.check_inline_literal_collections(
|
|
54
|
+
source, PRODUCTION_FILE_PATH
|
|
55
|
+
)
|
|
56
|
+
assert issues == [], (
|
|
57
|
+
f"Variable-reference literal must not be flagged, got: {issues}"
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def test_should_not_flag_module_level_literal() -> None:
|
|
62
|
+
source = "ALL_VALID_KEYS = {'true', 'false', 'none'}\n"
|
|
63
|
+
issues = code_rules_enforcer.check_inline_literal_collections(
|
|
64
|
+
source, PRODUCTION_FILE_PATH
|
|
65
|
+
)
|
|
66
|
+
assert issues == [], (
|
|
67
|
+
f"Module-level literal must not be flagged by inline-collection check, got: {issues}"
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def test_should_skip_in_test_files() -> None:
|
|
72
|
+
source = (
|
|
73
|
+
"def test_something() -> None:\n"
|
|
74
|
+
" keys = {'true', 'false', 'none'}\n"
|
|
75
|
+
" assert 'true' in keys\n"
|
|
76
|
+
)
|
|
77
|
+
issues = code_rules_enforcer.check_inline_literal_collections(
|
|
78
|
+
source, TEST_FILE_PATH
|
|
79
|
+
)
|
|
80
|
+
assert issues == [], f"Test files exempt, got: {issues}"
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def test_should_skip_in_config_files() -> None:
|
|
84
|
+
source = "def known_keys() -> set[str]:\n return {'true', 'false', 'none'}\n"
|
|
85
|
+
issues = code_rules_enforcer.check_inline_literal_collections(
|
|
86
|
+
source, CONFIG_FILE_PATH
|
|
87
|
+
)
|
|
88
|
+
assert issues == [], f"Config files exempt (they own such tables), got: {issues}"
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def test_should_flag_multiple_inline_collections_in_same_function() -> None:
|
|
92
|
+
source = (
|
|
93
|
+
"def consume(value: str) -> bool:\n"
|
|
94
|
+
" return value in {'a', 'b', 'c'} or value in {'x', 'y', 'z'}\n"
|
|
95
|
+
)
|
|
96
|
+
issues = code_rules_enforcer.check_inline_literal_collections(
|
|
97
|
+
source, PRODUCTION_FILE_PATH
|
|
98
|
+
)
|
|
99
|
+
assert len(issues) == 2, f"Expected 2 flagged literals, got: {issues}"
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def test_should_not_flag_default_argument_set_literal() -> None:
|
|
103
|
+
source = (
|
|
104
|
+
"def consume(keys: set[str] = {'a', 'b', 'c'}) -> set[str]:\n"
|
|
105
|
+
" return keys\n"
|
|
106
|
+
)
|
|
107
|
+
issues = code_rules_enforcer.check_inline_literal_collections(
|
|
108
|
+
source, PRODUCTION_FILE_PATH
|
|
109
|
+
)
|
|
110
|
+
assert issues == [], (
|
|
111
|
+
f"Default argument value (signature, not body) must not be flagged, got: {issues}"
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def test_should_not_flag_default_argument_list_literal() -> None:
|
|
116
|
+
source = (
|
|
117
|
+
"def consume(suffixes: list[str] = ['.py', '.js', '.ts']) -> list[str]:\n"
|
|
118
|
+
" return suffixes\n"
|
|
119
|
+
)
|
|
120
|
+
issues = code_rules_enforcer.check_inline_literal_collections(
|
|
121
|
+
source, PRODUCTION_FILE_PATH
|
|
122
|
+
)
|
|
123
|
+
assert issues == [], (
|
|
124
|
+
f"Default argument list (signature, not body) must not be flagged, got: {issues}"
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def test_should_not_flag_default_arg_set_of_nested_function_from_outer_scan() -> None:
|
|
129
|
+
source = (
|
|
130
|
+
"def outer() -> None:\n"
|
|
131
|
+
" def inner(keys: set[str] = {'a', 'b', 'c'}) -> set[str]:\n"
|
|
132
|
+
" return keys\n"
|
|
133
|
+
" return None\n"
|
|
134
|
+
)
|
|
135
|
+
issues = code_rules_enforcer.check_inline_literal_collections(
|
|
136
|
+
source, PRODUCTION_FILE_PATH
|
|
137
|
+
)
|
|
138
|
+
assert issues == [], (
|
|
139
|
+
f"Nested function's default-arg set (signature) must not be flagged from outer scan, got: {issues}"
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def test_should_still_flag_set_literal_in_nested_function_body() -> None:
|
|
144
|
+
source = (
|
|
145
|
+
"def outer() -> bool:\n"
|
|
146
|
+
" def inner(value: str) -> bool:\n"
|
|
147
|
+
" return value in {'a', 'b', 'c'}\n"
|
|
148
|
+
" return inner('a')\n"
|
|
149
|
+
)
|
|
150
|
+
issues = code_rules_enforcer.check_inline_literal_collections(
|
|
151
|
+
source, PRODUCTION_FILE_PATH
|
|
152
|
+
)
|
|
153
|
+
assert len(issues) == 1, (
|
|
154
|
+
f"Inner function's body set literal must be flagged exactly once (no duplicate from outer walk), got: {issues}"
|
|
155
|
+
)
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
import importlib.util
|
|
5
|
+
|
|
6
|
+
ENFORCER_PATH = Path(__file__).resolve().parent / "code_rules_enforcer.py"
|
|
7
|
+
specification = importlib.util.spec_from_file_location(
|
|
8
|
+
"code_rules_enforcer", ENFORCER_PATH
|
|
9
|
+
)
|
|
10
|
+
code_rules_enforcer = importlib.util.module_from_spec(specification)
|
|
11
|
+
specification.loader.exec_module(code_rules_enforcer)
|
|
12
|
+
|
|
13
|
+
PRODUCTION_FILE_PATH = "packages/app/services/foo.py"
|
|
14
|
+
TEST_FILE_PATH = "packages/app/tests/test_foo.py"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def test_should_flag_loop_variable_without_each_prefix() -> None:
|
|
18
|
+
source = "def consume() -> None:\n for marker in []:\n return None\n"
|
|
19
|
+
issues = code_rules_enforcer.check_loop_variable_naming(
|
|
20
|
+
source, PRODUCTION_FILE_PATH
|
|
21
|
+
)
|
|
22
|
+
assert any("marker" in each_issue for each_issue in issues), (
|
|
23
|
+
f"Expected 'marker' loop variable flagged, got: {issues}"
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def test_should_not_flag_loop_variable_with_each_prefix() -> None:
|
|
28
|
+
source = "def consume() -> None:\n for each_marker in []:\n return None\n"
|
|
29
|
+
issues = code_rules_enforcer.check_loop_variable_naming(
|
|
30
|
+
source, PRODUCTION_FILE_PATH
|
|
31
|
+
)
|
|
32
|
+
assert issues == [], f"each_marker must not be flagged, got: {issues}"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def test_should_exempt_index_letters_i_j_k() -> None:
|
|
36
|
+
source = (
|
|
37
|
+
"def consume() -> None:\n"
|
|
38
|
+
" for i in range(3):\n"
|
|
39
|
+
" for j in range(3):\n"
|
|
40
|
+
" for k in range(3):\n"
|
|
41
|
+
" return None\n"
|
|
42
|
+
)
|
|
43
|
+
issues = code_rules_enforcer.check_loop_variable_naming(
|
|
44
|
+
source, PRODUCTION_FILE_PATH
|
|
45
|
+
)
|
|
46
|
+
assert issues == [], f"i/j/k must be exempt, got: {issues}"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def test_should_flag_bare_each_without_subject() -> None:
|
|
50
|
+
source = "def consume() -> None:\n for each in []:\n return None\n"
|
|
51
|
+
issues = code_rules_enforcer.check_loop_variable_naming(
|
|
52
|
+
source, PRODUCTION_FILE_PATH
|
|
53
|
+
)
|
|
54
|
+
assert any("each" in each_issue for each_issue in issues), (
|
|
55
|
+
f"Expected bare 'each' flagged, got: {issues}"
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def test_should_not_flag_tuple_unpacking_targets() -> None:
|
|
60
|
+
source = (
|
|
61
|
+
"def consume() -> None:\n"
|
|
62
|
+
" for key, value in {}.items():\n"
|
|
63
|
+
" return None\n"
|
|
64
|
+
)
|
|
65
|
+
issues = code_rules_enforcer.check_loop_variable_naming(
|
|
66
|
+
source, PRODUCTION_FILE_PATH
|
|
67
|
+
)
|
|
68
|
+
assert issues == [], f"Tuple-unpack targets exempt, got: {issues}"
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def test_should_not_flag_list_comprehension_target() -> None:
|
|
72
|
+
source = "def consume() -> None:\n return [x for x in []]\n"
|
|
73
|
+
issues = code_rules_enforcer.check_loop_variable_naming(
|
|
74
|
+
source, PRODUCTION_FILE_PATH
|
|
75
|
+
)
|
|
76
|
+
assert issues == [], f"Comprehension target exempt, got: {issues}"
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def test_should_skip_in_test_files() -> None:
|
|
80
|
+
source = "def test_consume() -> None:\n for marker in []:\n return None\n"
|
|
81
|
+
issues = code_rules_enforcer.check_loop_variable_naming(source, TEST_FILE_PATH)
|
|
82
|
+
assert issues == [], f"Test files exempt, got: {issues}"
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def test_should_flag_async_for_loop_variable() -> None:
|
|
86
|
+
source = (
|
|
87
|
+
"async def consume() -> None:\n"
|
|
88
|
+
" async for marker in stream():\n"
|
|
89
|
+
" return None\n"
|
|
90
|
+
)
|
|
91
|
+
issues = code_rules_enforcer.check_loop_variable_naming(
|
|
92
|
+
source, PRODUCTION_FILE_PATH
|
|
93
|
+
)
|
|
94
|
+
assert any("marker" in each_issue for each_issue in issues), (
|
|
95
|
+
f"Expected async-for variable flagged, got: {issues}"
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def test_should_exempt_underscore_throwaway_loop_variable() -> None:
|
|
100
|
+
source = (
|
|
101
|
+
"def consume(count: int) -> None:\n"
|
|
102
|
+
" for _ in range(count):\n"
|
|
103
|
+
" return None\n"
|
|
104
|
+
)
|
|
105
|
+
issues = code_rules_enforcer.check_loop_variable_naming(
|
|
106
|
+
source, PRODUCTION_FILE_PATH
|
|
107
|
+
)
|
|
108
|
+
assert issues == [], (
|
|
109
|
+
f"Throwaway '_' loop variable must be exempt (Python idiom for 'value intentionally unused'), got: {issues}"
|
|
110
|
+
)
|
|
@@ -148,19 +148,6 @@ def test_should_skip_workflow_registry_files() -> None:
|
|
|
148
148
|
assert issues == []
|
|
149
149
|
|
|
150
150
|
|
|
151
|
-
def test_should_cap_issues_at_three() -> None:
|
|
152
|
-
source = (
|
|
153
|
-
"def f() -> None:\n"
|
|
154
|
-
" one = True\n"
|
|
155
|
-
" two = False\n"
|
|
156
|
-
" three = True\n"
|
|
157
|
-
" four = False\n"
|
|
158
|
-
" five = True\n"
|
|
159
|
-
)
|
|
160
|
-
issues = check_boolean_naming(source, PRODUCTION_FILE_PATH)
|
|
161
|
-
assert len(issues) == 3
|
|
162
|
-
|
|
163
|
-
|
|
164
151
|
def test_should_not_flag_syntax_error_as_issue() -> None:
|
|
165
152
|
source = "def f(:\n valid = True\n"
|
|
166
153
|
issues = check_boolean_naming(source, PRODUCTION_FILE_PATH)
|
|
@@ -122,29 +122,3 @@ def test_should_flag_decorator_with_skip_in_name() -> None:
|
|
|
122
122
|
)
|
|
123
123
|
|
|
124
124
|
|
|
125
|
-
def test_stops_at_max_issues_per_check() -> None:
|
|
126
|
-
source = (
|
|
127
|
-
"import pytest\n"
|
|
128
|
-
"\n"
|
|
129
|
-
"@pytest.mark.skip(reason='a')\n"
|
|
130
|
-
"def test_one() -> None:\n"
|
|
131
|
-
" pass\n"
|
|
132
|
-
"\n"
|
|
133
|
-
"@pytest.mark.skip(reason='b')\n"
|
|
134
|
-
"def test_two() -> None:\n"
|
|
135
|
-
" pass\n"
|
|
136
|
-
"\n"
|
|
137
|
-
"@pytest.mark.skip(reason='c')\n"
|
|
138
|
-
"def test_three() -> None:\n"
|
|
139
|
-
" pass\n"
|
|
140
|
-
"\n"
|
|
141
|
-
"@pytest.mark.skip(reason='d')\n"
|
|
142
|
-
"def test_four() -> None:\n"
|
|
143
|
-
" pass\n"
|
|
144
|
-
"\n"
|
|
145
|
-
"@pytest.mark.skip(reason='e')\n"
|
|
146
|
-
"def test_five() -> None:\n"
|
|
147
|
-
" pass\n"
|
|
148
|
-
)
|
|
149
|
-
issues = code_rules_enforcer.check_skip_decorators_in_tests(source, TEST_FILE_PATH)
|
|
150
|
-
assert len(issues) == code_rules_enforcer.MAX_ISSUES_PER_CHECK
|