claude-dev-env 1.49.1 → 1.50.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.
@@ -0,0 +1,256 @@
1
+ """Tests for check_docstring_args_match_signature — Args:-vs-signature drift.
2
+
3
+ A documented ``Args:`` entry naming a parameter the signature lacks is the
4
+ stale residue of a rename. Only the ``Args:`` section is validated; ``Raises:``
5
+ is left alone because callee-propagated exceptions cause false positives.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import importlib.util
11
+ from pathlib import Path
12
+ from types import ModuleType
13
+
14
+
15
+ def _load_enforcer_module() -> ModuleType:
16
+ module_path = Path(__file__).parent / "code_rules_enforcer.py"
17
+ spec = importlib.util.spec_from_file_location("code_rules_enforcer", module_path)
18
+ assert spec is not None
19
+ assert spec.loader is not None
20
+ module = importlib.util.module_from_spec(spec)
21
+ spec.loader.exec_module(module)
22
+ return module
23
+
24
+
25
+ code_rules_enforcer = _load_enforcer_module()
26
+
27
+
28
+ def check_docstring_args_match_signature(content: str, file_path: str) -> list[str]:
29
+ return code_rules_enforcer.check_docstring_args_match_signature(content, file_path)
30
+
31
+
32
+ def validate_content(content: str, file_path: str, old_content: str) -> list[str]:
33
+ return code_rules_enforcer.validate_content(content, file_path, old_content)
34
+
35
+
36
+ PRODUCTION_FILE_PATH = "/project/src/services.py"
37
+ TEST_FILE_PATH = "/project/src/test_services.py"
38
+ HOOK_INFRASTRUCTURE_PATH = "/home/user/.claude/hooks/blocking/example.py"
39
+
40
+
41
+ def _function_with_stale_arg() -> str:
42
+ return (
43
+ "def fetch_user(account_id: int) -> str:\n"
44
+ ' """Look up a user by id.\n'
45
+ "\n"
46
+ " Args:\n"
47
+ " user_id: The user identifier.\n"
48
+ "\n"
49
+ " Returns:\n"
50
+ " The user name.\n"
51
+ ' """\n'
52
+ " lookup = _registry.get(account_id)\n"
53
+ " if not lookup:\n"
54
+ " return ''\n"
55
+ " return lookup.name\n"
56
+ )
57
+
58
+
59
+ def test_should_flag_documented_arg_not_in_signature() -> None:
60
+ issues = check_docstring_args_match_signature(_function_with_stale_arg(), PRODUCTION_FILE_PATH)
61
+ assert any("user_id" in each for each in issues), (
62
+ f"Expected stale 'user_id' flag, got: {issues!r}"
63
+ )
64
+ assert len(issues) == 1
65
+
66
+
67
+ def test_should_not_flag_when_args_match_signature() -> None:
68
+ source = (
69
+ "def fetch_user(user_id: int) -> str:\n"
70
+ ' """Look up a user by id.\n'
71
+ "\n"
72
+ " Args:\n"
73
+ " user_id: The user identifier.\n"
74
+ "\n"
75
+ " Returns:\n"
76
+ " The user name.\n"
77
+ ' """\n'
78
+ " lookup = _registry.get(user_id)\n"
79
+ " if not lookup:\n"
80
+ " return ''\n"
81
+ " return lookup.name\n"
82
+ )
83
+ issues = check_docstring_args_match_signature(source, PRODUCTION_FILE_PATH)
84
+ assert issues == [], f"Matching Args must not be flagged, got: {issues!r}"
85
+
86
+
87
+ def test_should_handle_parenthesized_type_in_arg_entry() -> None:
88
+ source = (
89
+ "def fetch_user(account_id: int) -> str:\n"
90
+ ' """Look up a user by id.\n'
91
+ "\n"
92
+ " Args:\n"
93
+ " user_id (int): The user identifier.\n"
94
+ "\n"
95
+ " Returns:\n"
96
+ " The user name.\n"
97
+ ' """\n'
98
+ " lookup = _registry.get(account_id)\n"
99
+ " if not lookup:\n"
100
+ " return ''\n"
101
+ " return lookup.name\n"
102
+ )
103
+ issues = check_docstring_args_match_signature(source, PRODUCTION_FILE_PATH)
104
+ assert any("user_id" in each for each in issues), (
105
+ f"Parenthesized-type entry must still be parsed, got: {issues!r}"
106
+ )
107
+
108
+
109
+ def test_should_count_vararg_and_kwarg_as_real_parameters() -> None:
110
+ source = (
111
+ "def collect(first: int, *extra_values, **options) -> int:\n"
112
+ ' """Collect values.\n'
113
+ "\n"
114
+ " Args:\n"
115
+ " first: The leading value.\n"
116
+ " extra_values: Additional positional values.\n"
117
+ " options: Keyword overrides.\n"
118
+ ' """\n'
119
+ " total = first\n"
120
+ " for each_value in extra_values:\n"
121
+ " total += each_value\n"
122
+ " return total\n"
123
+ )
124
+ issues = check_docstring_args_match_signature(source, PRODUCTION_FILE_PATH)
125
+ assert issues == [], f"vararg/kwarg must count as parameters, got: {issues!r}"
126
+
127
+
128
+ def test_should_not_flag_self_documented_when_method_uses_self() -> None:
129
+ source = (
130
+ "class Service:\n"
131
+ " def fetch(self, user_id: int) -> int:\n"
132
+ ' """Fetch a record.\n'
133
+ "\n"
134
+ " Args:\n"
135
+ " user_id: The user identifier.\n"
136
+ ' """\n'
137
+ " record = self._registry.get(user_id)\n"
138
+ " if record is None:\n"
139
+ " return 0\n"
140
+ " return record\n"
141
+ )
142
+ issues = check_docstring_args_match_signature(source, PRODUCTION_FILE_PATH)
143
+ assert issues == [], f"self-using method must not be flagged, got: {issues!r}"
144
+
145
+
146
+ def test_should_stop_parsing_args_at_next_section() -> None:
147
+ source = (
148
+ "def fetch_user(user_id: int) -> str:\n"
149
+ ' """Look up a user by id.\n'
150
+ "\n"
151
+ " Args:\n"
152
+ " user_id: The user identifier.\n"
153
+ "\n"
154
+ " Raises:\n"
155
+ " LookupError: When missing.\n"
156
+ ' """\n'
157
+ " lookup = _registry.get(user_id)\n"
158
+ " if not lookup:\n"
159
+ " raise LookupError('missing')\n"
160
+ " return lookup.name\n"
161
+ )
162
+ issues = check_docstring_args_match_signature(source, PRODUCTION_FILE_PATH)
163
+ assert issues == [], f"Raises entries past Args must be ignored, got: {issues!r}"
164
+
165
+
166
+ def test_should_skip_private_function() -> None:
167
+ source = (
168
+ "def _fetch(account_id: int) -> int:\n"
169
+ ' """Fetch internally.\n'
170
+ "\n"
171
+ " Args:\n"
172
+ " user_id: stale name.\n"
173
+ ' """\n'
174
+ " value = _registry.get(account_id)\n"
175
+ " return value\n"
176
+ )
177
+ issues = check_docstring_args_match_signature(source, PRODUCTION_FILE_PATH)
178
+ assert issues == [], f"Private functions exempt, got: {issues!r}"
179
+
180
+
181
+ def test_should_skip_short_function() -> None:
182
+ source = (
183
+ "def fetch(account_id: int) -> int:\n"
184
+ ' """Args:\n'
185
+ " user_id: stale.\n"
186
+ ' """\n'
187
+ " return account_id\n"
188
+ )
189
+ issues = check_docstring_args_match_signature(source, PRODUCTION_FILE_PATH)
190
+ assert issues == [], f"Trivial-body functions exempt, got: {issues!r}"
191
+
192
+
193
+ def test_should_skip_test_file() -> None:
194
+ issues = check_docstring_args_match_signature(_function_with_stale_arg(), TEST_FILE_PATH)
195
+ assert issues == [], f"Test files exempt, got: {issues!r}"
196
+
197
+
198
+ def test_should_skip_hook_infrastructure() -> None:
199
+ issues = check_docstring_args_match_signature(
200
+ _function_with_stale_arg(), HOOK_INFRASTRUCTURE_PATH
201
+ )
202
+ assert issues == [], f"Hook infrastructure exempt, got: {issues!r}"
203
+
204
+
205
+ def test_should_handle_syntax_error_gracefully() -> None:
206
+ issues = check_docstring_args_match_signature("def fetch(\n", PRODUCTION_FILE_PATH)
207
+ assert issues == [], f"Syntax error must yield no issues, got: {issues!r}"
208
+
209
+
210
+ def test_validate_content_surfaces_args_drift() -> None:
211
+ issues = validate_content(_function_with_stale_arg(), PRODUCTION_FILE_PATH, old_content="")
212
+ matching_issues = [each for each in issues if "user_id" in each and "Args" in each]
213
+ assert matching_issues, (
214
+ f"Expected validate_content to surface the Args drift issue, got: {issues!r}"
215
+ )
216
+
217
+
218
+ def test_should_not_flag_deeper_indented_continuation_line() -> None:
219
+ source = (
220
+ "def fetch_user(user_id: int) -> str:\n"
221
+ ' """Look up a user by id.\n'
222
+ "\n"
223
+ " Args:\n"
224
+ " user_id: The user identifier. Example mapping:\n"
225
+ " shadow_key: not a parameter.\n"
226
+ "\n"
227
+ " Returns:\n"
228
+ " The user name.\n"
229
+ ' """\n'
230
+ " lookup = _registry.get(user_id)\n"
231
+ " if not lookup:\n"
232
+ " return ''\n"
233
+ " return lookup.name\n"
234
+ )
235
+ issues = check_docstring_args_match_signature(source, PRODUCTION_FILE_PATH)
236
+ assert issues == [], f"Continuation lines must not be parsed as args, got: {issues!r}"
237
+
238
+
239
+ def test_should_not_flag_documented_kwargs_keys() -> None:
240
+ source = (
241
+ "def configure(timeout: int, **overrides) -> None:\n"
242
+ ' """Configure the client.\n'
243
+ "\n"
244
+ " Args:\n"
245
+ " timeout: Seconds to wait.\n"
246
+ " max_retries: A documented keyword override.\n"
247
+ "\n"
248
+ " Returns:\n"
249
+ " None.\n"
250
+ ' """\n'
251
+ " settings = dict(overrides)\n"
252
+ " settings['timeout'] = timeout\n"
253
+ " return None\n"
254
+ )
255
+ issues = check_docstring_args_match_signature(source, PRODUCTION_FILE_PATH)
256
+ assert issues == [], f"**kwargs-key docs must not be flagged, got: {issues!r}"
@@ -0,0 +1,256 @@
1
+ """Tests for check_ignored_must_check_return — discarded must-check outcomes.
2
+
3
+ A bare-statement call to a function in ALL_MUST_CHECK_RETURN_FUNCTION_NAMES
4
+ discards the only failure signal it produces. An assigned or branched-on
5
+ call is exempt; only bare ``ast.Expr`` calls are flagged.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import importlib.util
11
+ from pathlib import Path
12
+ from types import ModuleType
13
+
14
+
15
+ def _load_enforcer_module() -> ModuleType:
16
+ module_path = Path(__file__).parent / "code_rules_enforcer.py"
17
+ spec = importlib.util.spec_from_file_location("code_rules_enforcer", module_path)
18
+ assert spec is not None
19
+ assert spec.loader is not None
20
+ module = importlib.util.module_from_spec(spec)
21
+ spec.loader.exec_module(module)
22
+ return module
23
+
24
+
25
+ code_rules_enforcer = _load_enforcer_module()
26
+
27
+
28
+ def check_ignored_must_check_return(content: str, file_path: str) -> list[str]:
29
+ return code_rules_enforcer.check_ignored_must_check_return(content, file_path)
30
+
31
+
32
+ def validate_content(content: str, file_path: str, old_content: str) -> list[str]:
33
+ return code_rules_enforcer.validate_content(content, file_path, old_content)
34
+
35
+
36
+ PRODUCTION_FILE_PATH = "/project/src/clicker.py"
37
+ TEST_FILE_PATH = "/project/src/test_clicker.py"
38
+
39
+
40
+ def test_should_flag_bare_find_and_click_call() -> None:
41
+ source = "def step() -> None:\n find_and_click('#submit')\n"
42
+ issues = check_ignored_must_check_return(source, PRODUCTION_FILE_PATH)
43
+ assert any("find_and_click" in each for each in issues), (
44
+ f"Expected discarded-return flag for find_and_click, got: {issues!r}"
45
+ )
46
+ assert len(issues) == 1
47
+
48
+
49
+ def test_should_flag_bare_write_outcome_call() -> None:
50
+ source = "def step() -> None:\n write_outcome('done')\n"
51
+ issues = check_ignored_must_check_return(source, PRODUCTION_FILE_PATH)
52
+ assert any("write_outcome" in each for each in issues), (
53
+ f"Expected discarded-return flag for write_outcome, got: {issues!r}"
54
+ )
55
+ assert len(issues) == 1
56
+
57
+
58
+ def test_should_flag_attribute_call_with_must_check_name() -> None:
59
+ source = "def step() -> None:\n self.find_and_click('#submit')\n"
60
+ issues = check_ignored_must_check_return(source, PRODUCTION_FILE_PATH)
61
+ assert len(issues) == 1, f"Attribute call terminal name must be resolved, got: {issues!r}"
62
+
63
+
64
+ def test_should_not_flag_assigned_find_and_click() -> None:
65
+ source = "def step() -> None:\n clicked = find_and_click('#submit')\n print(clicked)\n"
66
+ issues = check_ignored_must_check_return(source, PRODUCTION_FILE_PATH)
67
+ assert issues == [], f"Assigned call must not be flagged, got: {issues!r}"
68
+
69
+
70
+ def test_should_not_flag_branched_find_and_click() -> None:
71
+ source = "def step() -> None:\n if find_and_click('#submit'):\n pass\n"
72
+ issues = check_ignored_must_check_return(source, PRODUCTION_FILE_PATH)
73
+ assert issues == [], f"Branched-on call must not be flagged, got: {issues!r}"
74
+
75
+
76
+ def test_should_flag_bare_awaited_find_and_click_call() -> None:
77
+ source = "async def step() -> None:\n await find_and_click('#x')\n"
78
+ issues = check_ignored_must_check_return(source, PRODUCTION_FILE_PATH)
79
+ assert any("find_and_click" in each for each in issues), (
80
+ f"Expected discarded-return flag for awaited find_and_click, got: {issues!r}"
81
+ )
82
+ assert len(issues) == 1
83
+
84
+
85
+ def test_should_not_flag_assigned_awaited_find_and_click() -> None:
86
+ source = (
87
+ "async def step() -> None:\n"
88
+ " clicked = await find_and_click('#x')\n"
89
+ " print(clicked)\n"
90
+ )
91
+ issues = check_ignored_must_check_return(source, PRODUCTION_FILE_PATH)
92
+ assert issues == [], f"Assigned awaited call must not be flagged, got: {issues!r}"
93
+
94
+
95
+ def test_should_not_flag_branched_awaited_find_and_click() -> None:
96
+ source = "async def step() -> None:\n if await find_and_click('#x'):\n pass\n"
97
+ issues = check_ignored_must_check_return(source, PRODUCTION_FILE_PATH)
98
+ assert issues == [], f"Branched-on awaited call must not be flagged, got: {issues!r}"
99
+
100
+
101
+ def test_should_not_flag_unrelated_bare_call() -> None:
102
+ source = "def step() -> None:\n print('hello')\n"
103
+ issues = check_ignored_must_check_return(source, PRODUCTION_FILE_PATH)
104
+ assert issues == [], f"Unrelated call must not be flagged, got: {issues!r}"
105
+
106
+
107
+ def test_should_skip_test_file() -> None:
108
+ source = "def step() -> None:\n find_and_click('#submit')\n"
109
+ issues = check_ignored_must_check_return(source, TEST_FILE_PATH)
110
+ assert issues == [], f"Test files exempt, got: {issues!r}"
111
+
112
+
113
+ def test_should_handle_syntax_error_gracefully() -> None:
114
+ issues = check_ignored_must_check_return("def step(\n", PRODUCTION_FILE_PATH)
115
+ assert issues == [], f"Syntax error must yield no issues, got: {issues!r}"
116
+
117
+
118
+ def test_validate_content_surfaces_discarded_return() -> None:
119
+ source = "def step() -> None:\n find_and_click('#submit')\n"
120
+ issues = validate_content(source, PRODUCTION_FILE_PATH, old_content="")
121
+ matching_issues = [each for each in issues if "find_and_click" in each]
122
+ assert matching_issues, (
123
+ f"Expected validate_content to surface the discarded-return issue, got: {issues!r}"
124
+ )
125
+
126
+
127
+ EDIT_FULL_MODULE_SOURCE = (
128
+ "async def step() -> None:\n"
129
+ " await find_and_click('#x')\n"
130
+ )
131
+ AWAITED_CALL_LINE_NUMBER = 2
132
+ UNCHANGED_LINE_NUMBER = 1
133
+
134
+
135
+ def test_should_flag_when_changed_line_covers_the_bare_await() -> None:
136
+ all_changed_lines = {AWAITED_CALL_LINE_NUMBER}
137
+ issues = code_rules_enforcer.check_ignored_must_check_return(
138
+ EDIT_FULL_MODULE_SOURCE,
139
+ PRODUCTION_FILE_PATH,
140
+ all_changed_lines,
141
+ False,
142
+ )
143
+ assert len(issues) == 1, (
144
+ f"An Edit touching the bare await line must surface exactly one issue, got: {issues!r}"
145
+ )
146
+ assert "find_and_click" in issues[0]
147
+
148
+
149
+ def test_should_not_flag_when_changed_line_excludes_the_bare_await() -> None:
150
+ all_changed_lines = {UNCHANGED_LINE_NUMBER}
151
+ issues = code_rules_enforcer.check_ignored_must_check_return(
152
+ EDIT_FULL_MODULE_SOURCE,
153
+ PRODUCTION_FILE_PATH,
154
+ all_changed_lines,
155
+ False,
156
+ )
157
+ assert issues == [], (
158
+ f"A pre-existing violation on an unedited line must not block the edit, got: {issues!r}"
159
+ )
160
+
161
+
162
+ PRE_EXISTING_BARE_CALL_COUNT = 5
163
+ EDITED_BARE_CALL_LINE_NUMBER = PRE_EXISTING_BARE_CALL_COUNT + 2
164
+
165
+
166
+ def _build_module_with_pre_existing_violations_before_the_edit() -> str:
167
+ all_signature_lines = ["async def step() -> None:"]
168
+ all_pre_existing_call_lines = [
169
+ f" await find_and_click('#x{each_index}')"
170
+ for each_index in range(PRE_EXISTING_BARE_CALL_COUNT)
171
+ ]
172
+ edited_call_line = " await find_and_click('#edited')"
173
+ all_lines = all_signature_lines + all_pre_existing_call_lines + [edited_call_line]
174
+ return "\n".join(all_lines) + "\n"
175
+
176
+
177
+ def test_should_flag_edited_line_even_when_cap_worth_of_violations_precede_it() -> None:
178
+ source = _build_module_with_pre_existing_violations_before_the_edit()
179
+ all_changed_lines = {EDITED_BARE_CALL_LINE_NUMBER}
180
+ issues = code_rules_enforcer.check_ignored_must_check_return(
181
+ source,
182
+ PRODUCTION_FILE_PATH,
183
+ all_changed_lines,
184
+ False,
185
+ )
186
+ assert len(issues) == 1, (
187
+ "Collecting every violation before scoping must surface the edited-line "
188
+ f"violation even with a cap's worth of earlier out-of-scope calls, got: {issues!r}"
189
+ )
190
+ assert f"Line {EDITED_BARE_CALL_LINE_NUMBER}:" in issues[0], (
191
+ f"The single issue must name the edited line {EDITED_BARE_CALL_LINE_NUMBER}, got: {issues!r}"
192
+ )
193
+
194
+
195
+ def _build_module_with_more_than_cap_bare_calls() -> tuple[str, int]:
196
+ bare_call_count = code_rules_enforcer.MAX_IGNORED_MUST_CHECK_RETURN_ISSUES + 3
197
+ all_signature_lines = ["async def step() -> None:"]
198
+ all_call_lines = [
199
+ f" await find_and_click('#x{each_index}')"
200
+ for each_index in range(bare_call_count)
201
+ ]
202
+ source = "\n".join(all_signature_lines + all_call_lines) + "\n"
203
+ return source, bare_call_count
204
+
205
+
206
+ def test_deferred_scope_returns_every_violation_uncapped() -> None:
207
+ source, bare_call_count = _build_module_with_more_than_cap_bare_calls()
208
+ issues = code_rules_enforcer.check_ignored_must_check_return(
209
+ source,
210
+ PRODUCTION_FILE_PATH,
211
+ None,
212
+ True,
213
+ )
214
+ assert len(issues) == bare_call_count, (
215
+ "With defer_scope_to_caller=True the gate must see every violation uncapped "
216
+ f"so it can scope by added line, got: {issues!r}"
217
+ )
218
+
219
+
220
+ def test_terminal_scope_caps_violations_at_the_module_limit() -> None:
221
+ source, _ = _build_module_with_more_than_cap_bare_calls()
222
+ issues = code_rules_enforcer.check_ignored_must_check_return(
223
+ source,
224
+ PRODUCTION_FILE_PATH,
225
+ None,
226
+ False,
227
+ )
228
+ assert len(issues) == code_rules_enforcer.MAX_IGNORED_MUST_CHECK_RETURN_ISSUES, (
229
+ "The terminal hook path with all_changed_lines=None must cap at the module "
230
+ f"limit, got: {issues!r}"
231
+ )
232
+
233
+
234
+ WRAPPED_CALL_OPEN_PAREN_LINE_NUMBER = 2
235
+ WRAPPED_CALL_ARGUMENT_LINE_NUMBER = 3
236
+
237
+
238
+ def test_should_flag_when_changed_line_covers_a_later_line_of_a_wrapped_call() -> None:
239
+ source = (
240
+ "def step() -> None:\n"
241
+ " find_and_click(\n"
242
+ " '#submit',\n"
243
+ " )\n"
244
+ )
245
+ all_changed_lines = {WRAPPED_CALL_ARGUMENT_LINE_NUMBER}
246
+ issues = code_rules_enforcer.check_ignored_must_check_return(
247
+ source,
248
+ PRODUCTION_FILE_PATH,
249
+ all_changed_lines,
250
+ False,
251
+ )
252
+ assert len(issues) == 1, (
253
+ "Editing a later line of a multi-line bare must-check call must still flag it "
254
+ f"because the violation span covers the whole call, got: {issues!r}"
255
+ )
256
+ assert "find_and_click" in issues[0]
@@ -26,7 +26,7 @@ TEST_FILE_PATH = "src/app/test_feature.py"
26
26
  CONFIG_FILE_PATH = "src/config/settings.py"
27
27
  WORKFLOW_FILE_PATH = "src/workflow/orders_tab.py"
28
28
  HOOK_FILE_PATH = "/home/user/.claude/hooks/blocking/my_hook.py"
29
- EXPECTED_PREFIX_GUIDANCE = "prefix with is_/has_/should_/can_"
29
+ EXPECTED_PREFIX_GUIDANCE = "prefix with is_/has_/should_/can_/was_/did_"
30
30
 
31
31
 
32
32
  def _assert_flags_name(issues: list[str], name: str, line_number: int) -> None:
@@ -210,3 +210,139 @@ def test_should_allow_is_prefix_at_start_when_compound_word_follows() -> None:
210
210
  assert issues == [], (
211
211
  f"is_left_upper_snake has prefix at position 0, must pass, got: {issues}"
212
212
  )
213
+
214
+
215
+ PARAMETER_PREFIX_GUIDANCE = "prefix with is_/has_/should_/can_/was_/did_"
216
+
217
+
218
+ def _assert_flags_parameter(issues: list[str], name: str, line_number: int) -> None:
219
+ expected = f"Line {line_number}: Boolean parameter {name} - {PARAMETER_PREFIX_GUIDANCE}"
220
+ assert expected in issues, f"expected {expected!r} in {issues!r}"
221
+
222
+
223
+ def test_should_flag_bool_annotated_parameter_without_prefix() -> None:
224
+ source = "def run(dry_run: bool) -> None:\n print(dry_run)\n"
225
+ issues = check_boolean_naming(source, PRODUCTION_FILE_PATH)
226
+ _assert_flags_parameter(issues, "dry_run", 1)
227
+ assert len(issues) == 1
228
+
229
+
230
+ def test_should_flag_bool_default_parameter_without_annotation() -> None:
231
+ source = "def run(apply_historical_weight=False) -> None:\n print(apply_historical_weight)\n"
232
+ issues = check_boolean_naming(source, PRODUCTION_FILE_PATH)
233
+ _assert_flags_parameter(issues, "apply_historical_weight", 1)
234
+ assert len(issues) == 1
235
+
236
+
237
+ def test_should_flag_keyword_only_bool_parameter_without_prefix() -> None:
238
+ source = "def run(*, click_succeeded: bool = True) -> None:\n print(click_succeeded)\n"
239
+ issues = check_boolean_naming(source, PRODUCTION_FILE_PATH)
240
+ _assert_flags_parameter(issues, "click_succeeded", 1)
241
+ assert len(issues) == 1
242
+
243
+
244
+ def test_should_allow_is_prefixed_bool_parameter() -> None:
245
+ source = "def run(is_dry_run: bool) -> None:\n print(is_dry_run)\n"
246
+ issues = check_boolean_naming(source, PRODUCTION_FILE_PATH)
247
+ assert issues == []
248
+
249
+
250
+ def test_should_allow_was_prefixed_bool_parameter() -> None:
251
+ source = "def run(was_clicked: bool = False) -> None:\n print(was_clicked)\n"
252
+ issues = check_boolean_naming(source, PRODUCTION_FILE_PATH)
253
+ assert issues == []
254
+
255
+
256
+ def test_should_allow_did_prefixed_bool_parameter() -> None:
257
+ source = "def run(did_succeed: bool) -> None:\n print(did_succeed)\n"
258
+ issues = check_boolean_naming(source, PRODUCTION_FILE_PATH)
259
+ assert issues == []
260
+
261
+
262
+ def test_should_allow_was_prefixed_bool_assignment() -> None:
263
+ source = "def f() -> None:\n was_clicked = True\n"
264
+ issues = check_boolean_naming(source, PRODUCTION_FILE_PATH)
265
+ assert issues == []
266
+
267
+
268
+ def test_should_allow_did_prefixed_bool_assignment() -> None:
269
+ source = "def f() -> None:\n did_run = False\n"
270
+ issues = check_boolean_naming(source, PRODUCTION_FILE_PATH)
271
+ assert issues == []
272
+
273
+
274
+ def test_should_skip_single_letter_bool_parameter() -> None:
275
+ source = "def run(x: bool) -> None:\n print(x)\n"
276
+ issues = check_boolean_naming(source, PRODUCTION_FILE_PATH)
277
+ assert issues == []
278
+
279
+
280
+ def test_should_skip_self_parameter_in_method() -> None:
281
+ source = (
282
+ "class Runner:\n"
283
+ " def run(self, enabled: bool) -> None:\n"
284
+ " print(self, enabled)\n"
285
+ )
286
+ issues = check_boolean_naming(source, PRODUCTION_FILE_PATH)
287
+ _assert_flags_parameter(issues, "enabled", 2)
288
+ assert len(issues) == 1
289
+
290
+
291
+ def test_should_not_flag_non_bool_parameter() -> None:
292
+ source = "def run(retries: int) -> None:\n print(retries)\n"
293
+ issues = check_boolean_naming(source, PRODUCTION_FILE_PATH)
294
+ assert issues == []
295
+
296
+
297
+ def test_should_skip_bool_parameter_in_test_file() -> None:
298
+ source = "def run(dry_run: bool) -> None:\n print(dry_run)\n"
299
+ issues = check_boolean_naming(source, TEST_FILE_PATH)
300
+ assert issues == []
301
+
302
+
303
+ def test_should_pair_positional_defaults_right_aligned() -> None:
304
+ source = (
305
+ "def run(name: str, verbose: bool = False) -> None:\n"
306
+ " print(name, verbose)\n"
307
+ )
308
+ issues = check_boolean_naming(source, PRODUCTION_FILE_PATH)
309
+ _assert_flags_parameter(issues, "verbose", 1)
310
+ assert len(issues) == 1
311
+
312
+
313
+ FULL_MODULE_WITH_TWO_UNPREFIXED_BOOL_PARAMETERS = (
314
+ "def pre_existing(verbose: bool) -> None:\n"
315
+ " print(verbose)\n"
316
+ "\n\n"
317
+ "def edited(detailed: bool) -> None:\n"
318
+ " print(detailed)\n"
319
+ )
320
+ PRE_EXISTING_BOOL_PARAMETER_LINE_NUMBER = 1
321
+ EDITED_BOOL_PARAMETER_LINE_NUMBER = 5
322
+
323
+
324
+ def test_should_flag_bool_parameter_on_changed_line() -> None:
325
+ issues = check_boolean_naming(
326
+ FULL_MODULE_WITH_TWO_UNPREFIXED_BOOL_PARAMETERS,
327
+ PRODUCTION_FILE_PATH,
328
+ {EDITED_BOOL_PARAMETER_LINE_NUMBER},
329
+ False,
330
+ )
331
+ _assert_flags_parameter(issues, "detailed", EDITED_BOOL_PARAMETER_LINE_NUMBER)
332
+ assert len(issues) == 1, (
333
+ "Only the bool parameter on the changed line must be flagged, got: "
334
+ f"{issues!r}"
335
+ )
336
+
337
+
338
+ def test_should_not_flag_pre_existing_bool_parameter_on_unchanged_line() -> None:
339
+ issues = check_boolean_naming(
340
+ FULL_MODULE_WITH_TWO_UNPREFIXED_BOOL_PARAMETERS,
341
+ PRODUCTION_FILE_PATH,
342
+ {EDITED_BOOL_PARAMETER_LINE_NUMBER},
343
+ False,
344
+ )
345
+ assert not any("verbose" in each_issue for each_issue in issues), (
346
+ "A pre-existing unprefixed bool parameter on an unedited line must not block "
347
+ f"the edit, got: {issues!r}"
348
+ )