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
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
"""Behavior tests for the code_rules_optional_params code-rules check module."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import ast
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from types import SimpleNamespace
|
|
9
|
+
|
|
10
|
+
_BLOCKING_DIRECTORY = str(Path(__file__).resolve().parent)
|
|
11
|
+
_HOOKS_DIRECTORY = str(Path(__file__).resolve().parent.parent)
|
|
12
|
+
if _BLOCKING_DIRECTORY not in sys.path:
|
|
13
|
+
sys.path.insert(0, _BLOCKING_DIRECTORY)
|
|
14
|
+
if _HOOKS_DIRECTORY not in sys.path:
|
|
15
|
+
sys.path.insert(0, _HOOKS_DIRECTORY)
|
|
16
|
+
|
|
17
|
+
from code_rules_optional_params import ( # noqa: E402
|
|
18
|
+
_build_fstring_skeleton,
|
|
19
|
+
check_duplicated_format_patterns,
|
|
20
|
+
check_unused_optional_parameters,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
code_rules_enforcer = SimpleNamespace(
|
|
24
|
+
_build_fstring_skeleton=_build_fstring_skeleton,
|
|
25
|
+
check_duplicated_format_patterns=check_duplicated_format_patterns,
|
|
26
|
+
check_unused_optional_parameters=check_unused_optional_parameters,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
DUPLICATED_FORMAT_PRODUCTION_FILE_PATH = "packages/app/services/api_client.py"
|
|
31
|
+
|
|
32
|
+
DUPLICATED_FORMAT_TEST_FILE_PATH = "packages/app/tests/test_api_client.py"
|
|
33
|
+
|
|
34
|
+
KWARGS_EXPANSION_PRODUCTION_FILE_PATH = "packages/app/services/fetcher.py"
|
|
35
|
+
|
|
36
|
+
NESTED_FUNCTION_PRODUCTION_FILE_PATH = "packages/app/services/nested.py"
|
|
37
|
+
|
|
38
|
+
UNUSED_OPTIONAL_CONFIG_FILE_PATH = "packages/app/config/constants.py"
|
|
39
|
+
|
|
40
|
+
UNUSED_OPTIONAL_PRODUCTION_FILE_PATH = "packages/app/services/feature.py"
|
|
41
|
+
|
|
42
|
+
UNUSED_OPTIONAL_TEST_FILE_PATH = "packages/app/tests/test_feature.py"
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def test_should_flag_optional_param_never_varied_in_file() -> None:
|
|
46
|
+
source = (
|
|
47
|
+
"def build_url(path: str, prefix: str = '/api') -> str:\n"
|
|
48
|
+
" return f'{prefix}{path}'\n"
|
|
49
|
+
"\n"
|
|
50
|
+
"def call_first() -> str:\n"
|
|
51
|
+
" return build_url('/users')\n"
|
|
52
|
+
"\n"
|
|
53
|
+
"def call_second() -> str:\n"
|
|
54
|
+
" return build_url('/items')\n"
|
|
55
|
+
)
|
|
56
|
+
issues = code_rules_enforcer.check_unused_optional_parameters(
|
|
57
|
+
source, UNUSED_OPTIONAL_PRODUCTION_FILE_PATH
|
|
58
|
+
)
|
|
59
|
+
assert any("prefix" in issue for issue in issues), (
|
|
60
|
+
f"Expected 'prefix' flagged as never-varied, got: {issues}"
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def test_should_not_flag_when_param_is_varied_at_call_site() -> None:
|
|
65
|
+
source = (
|
|
66
|
+
"def build_url(path: str, prefix: str = '/api') -> str:\n"
|
|
67
|
+
" return f'{prefix}{path}'\n"
|
|
68
|
+
"\n"
|
|
69
|
+
"def call_with_default() -> str:\n"
|
|
70
|
+
" return build_url('/users')\n"
|
|
71
|
+
"\n"
|
|
72
|
+
"def call_with_override() -> str:\n"
|
|
73
|
+
" return build_url('/items', prefix='/v2')\n"
|
|
74
|
+
)
|
|
75
|
+
issues = code_rules_enforcer.check_unused_optional_parameters(
|
|
76
|
+
source, UNUSED_OPTIONAL_PRODUCTION_FILE_PATH
|
|
77
|
+
)
|
|
78
|
+
assert not any("prefix" in issue for issue in issues), (
|
|
79
|
+
f"Expected 'prefix' not flagged when varied, got: {issues}"
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def test_should_not_flag_unused_optional_in_test_files() -> None:
|
|
84
|
+
source = (
|
|
85
|
+
"def build_url(path: str, prefix: str = '/api') -> str:\n"
|
|
86
|
+
" return f'{prefix}{path}'\n"
|
|
87
|
+
"\n"
|
|
88
|
+
"def call_first() -> str:\n"
|
|
89
|
+
" return build_url('/users')\n"
|
|
90
|
+
)
|
|
91
|
+
issues = code_rules_enforcer.check_unused_optional_parameters(
|
|
92
|
+
source, UNUSED_OPTIONAL_TEST_FILE_PATH
|
|
93
|
+
)
|
|
94
|
+
assert issues == [], f"Expected no issues in test file, got: {issues}"
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def test_should_not_flag_unused_optional_in_config_files() -> None:
|
|
98
|
+
source = (
|
|
99
|
+
"def build_url(path: str, prefix: str = '/api') -> str:\n"
|
|
100
|
+
" return f'{prefix}{path}'\n"
|
|
101
|
+
"\n"
|
|
102
|
+
"def call_first() -> str:\n"
|
|
103
|
+
" return build_url('/users')\n"
|
|
104
|
+
)
|
|
105
|
+
issues = code_rules_enforcer.check_unused_optional_parameters(
|
|
106
|
+
source, UNUSED_OPTIONAL_CONFIG_FILE_PATH
|
|
107
|
+
)
|
|
108
|
+
assert issues == [], f"Expected no issues in config file, got: {issues}"
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def test_should_not_flag_when_no_same_file_call_sites_exist() -> None:
|
|
112
|
+
source = (
|
|
113
|
+
"def build_url(path: str, prefix: str = '/api') -> str:\n"
|
|
114
|
+
" return f'{prefix}{path}'\n"
|
|
115
|
+
)
|
|
116
|
+
issues = code_rules_enforcer.check_unused_optional_parameters(
|
|
117
|
+
source, UNUSED_OPTIONAL_PRODUCTION_FILE_PATH
|
|
118
|
+
)
|
|
119
|
+
assert issues == [], (
|
|
120
|
+
f"Expected no issues when no same-file call sites, got: {issues}"
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def test_should_include_line_number_and_param_name_in_issue() -> None:
|
|
125
|
+
source = (
|
|
126
|
+
"def fetch(url: str, timeout: int = 30) -> str:\n"
|
|
127
|
+
" return get(url, timeout=timeout)\n"
|
|
128
|
+
"\n"
|
|
129
|
+
"def run_fetch() -> str:\n"
|
|
130
|
+
" return fetch('http://example.com')\n"
|
|
131
|
+
)
|
|
132
|
+
issues = code_rules_enforcer.check_unused_optional_parameters(
|
|
133
|
+
source, UNUSED_OPTIONAL_PRODUCTION_FILE_PATH
|
|
134
|
+
)
|
|
135
|
+
assert any("Line 1" in issue and "timeout" in issue for issue in issues), (
|
|
136
|
+
f"Expected issue with line number and param name, got: {issues}"
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def test_should_flag_when_every_call_passes_the_exact_default() -> None:
|
|
141
|
+
source = (
|
|
142
|
+
"def fetch(url: str, timeout: int = 30) -> str:\n"
|
|
143
|
+
" return get(url, timeout=timeout)\n"
|
|
144
|
+
"\n"
|
|
145
|
+
"def run_fetch() -> str:\n"
|
|
146
|
+
" return fetch('http://example.com', timeout=30)\n"
|
|
147
|
+
)
|
|
148
|
+
issues = code_rules_enforcer.check_unused_optional_parameters(
|
|
149
|
+
source, UNUSED_OPTIONAL_PRODUCTION_FILE_PATH
|
|
150
|
+
)
|
|
151
|
+
assert any("timeout" in issue for issue in issues), (
|
|
152
|
+
f"Expected 'timeout' flagged when every call passes the exact default, got: {issues}"
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def test_should_advise_when_fstring_skeleton_appears_three_or_more_times(capsys: object) -> None:
|
|
157
|
+
source = (
|
|
158
|
+
"def get_user(user_id: str) -> str:\n"
|
|
159
|
+
" return f'/api/{user_id}'\n"
|
|
160
|
+
"\n"
|
|
161
|
+
"def get_order(order_id: str) -> str:\n"
|
|
162
|
+
" return f'/api/{order_id}'\n"
|
|
163
|
+
"\n"
|
|
164
|
+
"def get_product(product_id: str) -> str:\n"
|
|
165
|
+
" return f'/api/{product_id}'\n"
|
|
166
|
+
)
|
|
167
|
+
code_rules_enforcer.check_duplicated_format_patterns(
|
|
168
|
+
source, DUPLICATED_FORMAT_PRODUCTION_FILE_PATH
|
|
169
|
+
)
|
|
170
|
+
captured = getattr(capsys, "readouterr")()
|
|
171
|
+
assert "/api/" in captured.err and "3" in captured.err, (
|
|
172
|
+
f"Expected advisory for repeated /api/<x> pattern, got: {captured.err!r}"
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def test_should_not_advise_when_fstring_skeleton_appears_fewer_than_three_times(capsys: object) -> None:
|
|
177
|
+
source = (
|
|
178
|
+
"def get_user(user_id: str) -> str:\n"
|
|
179
|
+
" return f'/api/{user_id}'\n"
|
|
180
|
+
"\n"
|
|
181
|
+
"def get_order(order_id: str) -> str:\n"
|
|
182
|
+
" return f'/api/{order_id}'\n"
|
|
183
|
+
)
|
|
184
|
+
code_rules_enforcer.check_duplicated_format_patterns(
|
|
185
|
+
source, DUPLICATED_FORMAT_PRODUCTION_FILE_PATH
|
|
186
|
+
)
|
|
187
|
+
captured = getattr(capsys, "readouterr")()
|
|
188
|
+
assert "/api/" not in captured.err, (
|
|
189
|
+
f"Expected no advisory for pattern appearing only twice, got: {captured.err!r}"
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def test_should_not_advise_for_duplicated_format_patterns_in_test_files(capsys: object) -> None:
|
|
194
|
+
source = (
|
|
195
|
+
"def test_user() -> None:\n"
|
|
196
|
+
" url_a = f'/api/{1}'\n"
|
|
197
|
+
" url_b = f'/api/{2}'\n"
|
|
198
|
+
" url_c = f'/api/{3}'\n"
|
|
199
|
+
)
|
|
200
|
+
code_rules_enforcer.check_duplicated_format_patterns(
|
|
201
|
+
source, DUPLICATED_FORMAT_TEST_FILE_PATH
|
|
202
|
+
)
|
|
203
|
+
captured = getattr(capsys, "readouterr")()
|
|
204
|
+
assert "/api/" not in captured.err, (
|
|
205
|
+
f"Expected no advisory in test file, got: {captured.err!r}"
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def test_should_advise_with_distinct_skeletons(capsys: object) -> None:
|
|
210
|
+
source = (
|
|
211
|
+
"def first(team: str, user: str) -> str:\n"
|
|
212
|
+
" return f'/teams/{team}/users/{user}'\n"
|
|
213
|
+
"\n"
|
|
214
|
+
"def second(team: str, role: str) -> str:\n"
|
|
215
|
+
" return f'/teams/{team}/users/{role}'\n"
|
|
216
|
+
"\n"
|
|
217
|
+
"def third(team: str, admin: str) -> str:\n"
|
|
218
|
+
" return f'/teams/{team}/users/{admin}'\n"
|
|
219
|
+
)
|
|
220
|
+
code_rules_enforcer.check_duplicated_format_patterns(
|
|
221
|
+
source, DUPLICATED_FORMAT_PRODUCTION_FILE_PATH
|
|
222
|
+
)
|
|
223
|
+
captured = getattr(capsys, "readouterr")()
|
|
224
|
+
assert "/teams/" in captured.err, (
|
|
225
|
+
f"Expected advisory for repeated /teams/<x>/users/<x> pattern, got: {captured.err!r}"
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def test_build_fstring_skeleton_preserves_literal_interp_substring() -> None:
|
|
230
|
+
joined_str_expression = ast.parse("f'PREFIX INTERP {value} SUFFIX'", mode="eval").body
|
|
231
|
+
assert isinstance(joined_str_expression, ast.JoinedStr)
|
|
232
|
+
skeleton = code_rules_enforcer._build_fstring_skeleton(joined_str_expression)
|
|
233
|
+
assert skeleton == "PREFIX INTERP <x> SUFFIX", (
|
|
234
|
+
"Literal 'INTERP' text inside an f-string must survive skeleton building — "
|
|
235
|
+
f"only interpolation slots should become '<x>'. Got: {skeleton!r}"
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def test_should_not_flag_nested_function_optional_param() -> None:
|
|
240
|
+
source = (
|
|
241
|
+
"def outer() -> None:\n"
|
|
242
|
+
" def inner(timeout: int = 30) -> None:\n"
|
|
243
|
+
" pass\n"
|
|
244
|
+
" inner()\n"
|
|
245
|
+
" inner()\n"
|
|
246
|
+
)
|
|
247
|
+
issues = code_rules_enforcer.check_unused_optional_parameters(
|
|
248
|
+
source, NESTED_FUNCTION_PRODUCTION_FILE_PATH
|
|
249
|
+
)
|
|
250
|
+
assert not any("timeout" in issue for issue in issues), (
|
|
251
|
+
f"Expected nested function 'timeout' not flagged, got: {issues}"
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def test_should_not_flag_optional_param_when_only_call_site_uses_kwargs_expansion() -> None:
|
|
256
|
+
"""A call using **defaults passes unknown values — the param must NOT be flagged."""
|
|
257
|
+
source = (
|
|
258
|
+
"def fetch(url: str, timeout: int = 30) -> str:\n"
|
|
259
|
+
" return url\n"
|
|
260
|
+
"\n"
|
|
261
|
+
"def run() -> str:\n"
|
|
262
|
+
" defaults = {'timeout': 30}\n"
|
|
263
|
+
" return fetch('http://example.com', **defaults)\n"
|
|
264
|
+
)
|
|
265
|
+
issues = code_rules_enforcer.check_unused_optional_parameters(
|
|
266
|
+
source, KWARGS_EXPANSION_PRODUCTION_FILE_PATH
|
|
267
|
+
)
|
|
268
|
+
assert not any("timeout" in issue for issue in issues), (
|
|
269
|
+
f"Expected 'timeout' NOT flagged when call uses **kwargs expansion, got: {issues}"
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def test_should_not_advise_when_duplicated_fstring_literal_is_short(capsys: object) -> None:
|
|
274
|
+
"""Short logger-prefix style f-strings must not emit a duplication advisory.
|
|
275
|
+
|
|
276
|
+
A three-times-repeated ``f"Got {x}"`` has only four characters of literal
|
|
277
|
+
text (``"Got "``). Flagging such short fragments creates noise for common
|
|
278
|
+
logging prefixes. The heuristic requires a minimum amount of structural
|
|
279
|
+
literal text before an advisory fires.
|
|
280
|
+
"""
|
|
281
|
+
source = (
|
|
282
|
+
"def first(value: str) -> str:\n"
|
|
283
|
+
" return f'Got {value}'\n"
|
|
284
|
+
"\n"
|
|
285
|
+
"def second(value: str) -> str:\n"
|
|
286
|
+
" return f'Got {value}'\n"
|
|
287
|
+
"\n"
|
|
288
|
+
"def third(value: str) -> str:\n"
|
|
289
|
+
" return f'Got {value}'\n"
|
|
290
|
+
)
|
|
291
|
+
code_rules_enforcer.check_duplicated_format_patterns(
|
|
292
|
+
source, DUPLICATED_FORMAT_PRODUCTION_FILE_PATH
|
|
293
|
+
)
|
|
294
|
+
captured = getattr(capsys, "readouterr")()
|
|
295
|
+
assert "Got" not in captured.err, (
|
|
296
|
+
"Expected no advisory for a short repeated f-string literal fragment, "
|
|
297
|
+
f"got: {captured.err!r}"
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def test_should_still_advise_when_duplicated_fstring_literal_is_long(capsys: object) -> None:
|
|
302
|
+
"""Longer duplicated f-string skeletons must continue to fire.
|
|
303
|
+
|
|
304
|
+
The short-literal heuristic must not regress the existing
|
|
305
|
+
``/api/<x>`` and ``/teams/<x>/users/<x>`` advisories — those path
|
|
306
|
+
skeletons carry enough structural literal text to warrant a helper.
|
|
307
|
+
"""
|
|
308
|
+
source = (
|
|
309
|
+
"def get_user(user_id: str) -> str:\n"
|
|
310
|
+
" return f'/api/{user_id}'\n"
|
|
311
|
+
"\n"
|
|
312
|
+
"def get_order(order_id: str) -> str:\n"
|
|
313
|
+
" return f'/api/{order_id}'\n"
|
|
314
|
+
"\n"
|
|
315
|
+
"def get_product(product_id: str) -> str:\n"
|
|
316
|
+
" return f'/api/{product_id}'\n"
|
|
317
|
+
)
|
|
318
|
+
code_rules_enforcer.check_duplicated_format_patterns(
|
|
319
|
+
source, DUPLICATED_FORMAT_PRODUCTION_FILE_PATH
|
|
320
|
+
)
|
|
321
|
+
captured = getattr(capsys, "readouterr")()
|
|
322
|
+
assert "/api/" in captured.err, (
|
|
323
|
+
"Expected the existing /api/<x> path-shape advisory to still fire, "
|
|
324
|
+
f"got: {captured.err!r}"
|
|
325
|
+
)
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"""Behavior tests for the code_rules_paths_syspath 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_paths_syspath import ( # noqa: E402
|
|
17
|
+
HARDCODED_USER_PATH_GUIDANCE,
|
|
18
|
+
HARDCODED_USER_PATH_PATTERN,
|
|
19
|
+
MAX_HARDCODED_USER_PATH_ISSUES,
|
|
20
|
+
check_sys_path_insert_deduplication_guard,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
from hooks_constants.hardcoded_user_path_constants import ( # noqa: E402
|
|
24
|
+
HARDCODED_USER_PATH_GUIDANCE as config_hardcoded_user_path_guidance,
|
|
25
|
+
)
|
|
26
|
+
from hooks_constants.hardcoded_user_path_constants import ( # noqa: E402
|
|
27
|
+
HARDCODED_USER_PATH_PATTERN as config_hardcoded_user_path_pattern,
|
|
28
|
+
)
|
|
29
|
+
from hooks_constants.hardcoded_user_path_constants import ( # noqa: E402
|
|
30
|
+
MAX_HARDCODED_USER_PATH_ISSUES as config_max_hardcoded_user_path_issues,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
code_rules_enforcer = SimpleNamespace(
|
|
34
|
+
HARDCODED_USER_PATH_GUIDANCE=HARDCODED_USER_PATH_GUIDANCE,
|
|
35
|
+
HARDCODED_USER_PATH_PATTERN=HARDCODED_USER_PATH_PATTERN,
|
|
36
|
+
MAX_HARDCODED_USER_PATH_ISSUES=MAX_HARDCODED_USER_PATH_ISSUES,
|
|
37
|
+
check_sys_path_insert_deduplication_guard=check_sys_path_insert_deduplication_guard,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
SYS_PATH_INSERT_HOOK_INFRASTRUCTURE_FILE_PATH = "/repo/.claude/hooks/blocking/some_hook.py"
|
|
42
|
+
|
|
43
|
+
SYS_PATH_INSERT_PRODUCTION_FILE_PATH = "packages/app/services/loader.py"
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def test_should_reexport_hardcoded_user_path_pattern_from_config() -> None:
|
|
47
|
+
assert code_rules_enforcer.HARDCODED_USER_PATH_PATTERN is config_hardcoded_user_path_pattern
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def test_should_reexport_max_hardcoded_user_path_issues_from_config() -> None:
|
|
51
|
+
assert code_rules_enforcer.MAX_HARDCODED_USER_PATH_ISSUES == config_max_hardcoded_user_path_issues
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def test_should_reexport_hardcoded_user_path_guidance_from_config() -> None:
|
|
55
|
+
assert code_rules_enforcer.HARDCODED_USER_PATH_GUIDANCE == config_hardcoded_user_path_guidance
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def test_sys_path_insert_should_flag_mismatched_guard_path() -> None:
|
|
59
|
+
source = (
|
|
60
|
+
"import sys\n"
|
|
61
|
+
'if "wrong_path" not in sys.path:\n'
|
|
62
|
+
' sys.path.insert(0, "actual_path")\n'
|
|
63
|
+
)
|
|
64
|
+
issues = code_rules_enforcer.check_sys_path_insert_deduplication_guard(
|
|
65
|
+
source, SYS_PATH_INSERT_PRODUCTION_FILE_PATH
|
|
66
|
+
)
|
|
67
|
+
assert any("sys.path.insert" in each_issue for each_issue in issues), (
|
|
68
|
+
"Guard testing a different value than what is inserted must be flagged, "
|
|
69
|
+
f"got: {issues}"
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def test_sys_path_insert_should_not_flag_matching_guard_path() -> None:
|
|
74
|
+
source = (
|
|
75
|
+
"import sys\n"
|
|
76
|
+
'if "correct_path" not in sys.path:\n'
|
|
77
|
+
' sys.path.insert(0, "correct_path")\n'
|
|
78
|
+
)
|
|
79
|
+
issues = code_rules_enforcer.check_sys_path_insert_deduplication_guard(
|
|
80
|
+
source, SYS_PATH_INSERT_PRODUCTION_FILE_PATH
|
|
81
|
+
)
|
|
82
|
+
assert issues == [], (
|
|
83
|
+
f"Guard testing the same value that is inserted must not be flagged, got: {issues}"
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def test_sys_path_insert_should_not_flag_guarded_insert_in_class_body() -> None:
|
|
88
|
+
source = (
|
|
89
|
+
"import sys\n"
|
|
90
|
+
"class Configurator:\n"
|
|
91
|
+
" target = '/some/path'\n"
|
|
92
|
+
" if target not in sys.path:\n"
|
|
93
|
+
" sys.path.insert(0, target)\n"
|
|
94
|
+
)
|
|
95
|
+
issues = code_rules_enforcer.check_sys_path_insert_deduplication_guard(
|
|
96
|
+
source, SYS_PATH_INSERT_PRODUCTION_FILE_PATH
|
|
97
|
+
)
|
|
98
|
+
assert issues == [], (
|
|
99
|
+
f"Guarded sys.path.insert directly in a class body must not be flagged, got: {issues}"
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def test_sys_path_insert_should_skip_hook_infrastructure_files() -> None:
|
|
104
|
+
source = "import sys\nsys.path.insert(0, '/some/path')\n"
|
|
105
|
+
issues = code_rules_enforcer.check_sys_path_insert_deduplication_guard(
|
|
106
|
+
source, SYS_PATH_INSERT_HOOK_INFRASTRUCTURE_FILE_PATH
|
|
107
|
+
)
|
|
108
|
+
assert issues == [], (
|
|
109
|
+
f"Hook infrastructure files are exempt from this rule, got: {issues}"
|
|
110
|
+
)
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""Behavior tests for the code_rules_shared 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_enforcer import ( # noqa: E402
|
|
17
|
+
prior_and_post_edit_content,
|
|
18
|
+
)
|
|
19
|
+
from code_rules_shared import ( # noqa: E402
|
|
20
|
+
changed_line_numbers,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
code_rules_enforcer = SimpleNamespace(
|
|
24
|
+
changed_line_numbers=changed_line_numbers,
|
|
25
|
+
prior_and_post_edit_content=prior_and_post_edit_content,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def test_readable_prior_yields_consistent_prior_and_reconstruction(tmp_path) -> None:
|
|
30
|
+
"""When the prior reads cleanly, the helper returns the same prior content it
|
|
31
|
+
reconstructed the post-edit view from, so the two never diverge across two
|
|
32
|
+
independent reads."""
|
|
33
|
+
source_file = tmp_path / "module.py"
|
|
34
|
+
original = "alpha = 1\nbeta = 2\n"
|
|
35
|
+
source_file.write_text(original, encoding="utf-8")
|
|
36
|
+
prior_content, post_edit_content = code_rules_enforcer.prior_and_post_edit_content(
|
|
37
|
+
str(source_file),
|
|
38
|
+
old_string="beta = 2\n",
|
|
39
|
+
new_string="beta = 3\n",
|
|
40
|
+
)
|
|
41
|
+
assert prior_content == original
|
|
42
|
+
assert post_edit_content == "alpha = 1\nbeta = 3\n"
|
|
43
|
+
changed = code_rules_enforcer.changed_line_numbers(prior_content, post_edit_content)
|
|
44
|
+
assert changed == {2}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""Behavior tests for the code_rules_string_magic 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_string_magic import ( # noqa: E402
|
|
17
|
+
check_inline_literal_collections,
|
|
18
|
+
check_string_literal_magic,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
code_rules_enforcer = SimpleNamespace(
|
|
22
|
+
check_inline_literal_collections=check_inline_literal_collections,
|
|
23
|
+
check_string_literal_magic=check_string_literal_magic,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
INLINE_LITERAL_PRODUCTION_FILE_PATH = "packages/app/services/inline_literal.py"
|
|
28
|
+
|
|
29
|
+
STRING_MAGIC_PRODUCTION_FILE_PATH = "packages/app/services/string_magic.py"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def test_check_inline_literal_collections_flags_three_string_set_in_function() -> None:
|
|
33
|
+
source = (
|
|
34
|
+
"def is_known(value: str) -> bool:\n"
|
|
35
|
+
" return value in {'true', 'false', 'none'}\n"
|
|
36
|
+
)
|
|
37
|
+
issues = code_rules_enforcer.check_inline_literal_collections(
|
|
38
|
+
source, INLINE_LITERAL_PRODUCTION_FILE_PATH
|
|
39
|
+
)
|
|
40
|
+
assert len(issues) == 1, f"Expected 3-element string set flagged, got: {issues}"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def test_check_string_literal_magic_flags_env_var_name() -> None:
|
|
44
|
+
source = (
|
|
45
|
+
"import os\n"
|
|
46
|
+
"\n"
|
|
47
|
+
"def fetch_secret() -> str:\n"
|
|
48
|
+
" return os.environ['STRIPE_SECRET']\n"
|
|
49
|
+
)
|
|
50
|
+
issues = code_rules_enforcer.check_string_literal_magic(
|
|
51
|
+
source, STRING_MAGIC_PRODUCTION_FILE_PATH
|
|
52
|
+
)
|
|
53
|
+
assert any("STRIPE_SECRET" in each_issue for each_issue in issues), (
|
|
54
|
+
f"Expected env-var name flagged, got: {issues}"
|
|
55
|
+
)
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""Behavior tests for the code_rules_test_assertions 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_test_assertions import ( # noqa: E402
|
|
17
|
+
check_constant_equality_tests,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
code_rules_enforcer = SimpleNamespace(
|
|
21
|
+
check_constant_equality_tests=check_constant_equality_tests,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
CONSTANT_EQUALITY_TEST_FILE_PATH = "packages/app/tests/test_constants.py"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def test_should_not_flag_two_named_constants_compared_to_each_other() -> None:
|
|
29
|
+
source = (
|
|
30
|
+
"FOO = 'a'\n"
|
|
31
|
+
"BAR = 'b'\n"
|
|
32
|
+
"\n"
|
|
33
|
+
"def test_constants_differ() -> None:\n"
|
|
34
|
+
" assert FOO == BAR\n"
|
|
35
|
+
)
|
|
36
|
+
issues = code_rules_enforcer.check_constant_equality_tests(
|
|
37
|
+
source, CONSTANT_EQUALITY_TEST_FILE_PATH
|
|
38
|
+
)
|
|
39
|
+
assert issues == [], (
|
|
40
|
+
f"Expected no flag when both sides are named constants, got: {issues}"
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def test_should_flag_named_constant_compared_to_literal() -> None:
|
|
45
|
+
source = (
|
|
46
|
+
"FOO = 'a'\n"
|
|
47
|
+
"\n"
|
|
48
|
+
"def test_foo_value() -> None:\n"
|
|
49
|
+
" assert FOO == 'literal'\n"
|
|
50
|
+
)
|
|
51
|
+
issues = code_rules_enforcer.check_constant_equality_tests(
|
|
52
|
+
source, CONSTANT_EQUALITY_TEST_FILE_PATH
|
|
53
|
+
)
|
|
54
|
+
assert any("constant-value test" in issue for issue in issues), (
|
|
55
|
+
f"Expected flag when UPPER_SNAKE compared to literal, got: {issues}"
|
|
56
|
+
)
|
|
@@ -7,22 +7,28 @@ rule blocks every authored TODO. These tests pin the exemption.
|
|
|
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
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
12
|
+
from types import SimpleNamespace
|
|
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)
|
|
20
|
+
|
|
21
|
+
from code_rules_comments import ( # noqa: E402
|
|
22
|
+
check_comments_javascript,
|
|
23
|
+
check_comments_python,
|
|
24
|
+
extract_comment_texts,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
code_rules_enforcer = SimpleNamespace(
|
|
28
|
+
check_comments_javascript=check_comments_javascript,
|
|
29
|
+
check_comments_python=check_comments_python,
|
|
30
|
+
extract_comment_texts=extract_comment_texts,
|
|
31
|
+
)
|
|
26
32
|
|
|
27
33
|
|
|
28
34
|
def test_python_check_should_exempt_standalone_todo_comment() -> None:
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""Regression guard pinning docstring prose in code_rules_paths_syspath."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
_BLOCKING_DIRECTORY = str(Path(__file__).resolve().parent)
|
|
9
|
+
_HOOKS_DIRECTORY = str(Path(__file__).resolve().parent.parent)
|
|
10
|
+
if _BLOCKING_DIRECTORY not in sys.path:
|
|
11
|
+
sys.path.insert(0, _BLOCKING_DIRECTORY)
|
|
12
|
+
if _HOOKS_DIRECTORY not in sys.path:
|
|
13
|
+
sys.path.insert(0, _HOOKS_DIRECTORY)
|
|
14
|
+
|
|
15
|
+
from code_rules_magic_values import check_magic_values # noqa: E402
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def test_module_source_carries_no_docstring_magic_value() -> None:
|
|
19
|
+
module_path = Path(__file__).resolve().parent / "code_rules_paths_syspath.py"
|
|
20
|
+
module_source = module_path.read_text(encoding="utf-8")
|
|
21
|
+
magic_value_issues = check_magic_values(module_source, str(module_path))
|
|
22
|
+
assert magic_value_issues == [], (
|
|
23
|
+
"Docstring prose in code_rules_paths_syspath.py must not carry a "
|
|
24
|
+
"bare-number token that the magic-value check flags as a literal, "
|
|
25
|
+
f"got: {magic_value_issues}"
|
|
26
|
+
)
|