claude-dev-env 1.34.1 → 1.36.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/clean-coder.md +109 -1
- 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/bin/install.mjs +28 -8
- package/bin/install.test.mjs +9 -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/docs/CODE_RULES.md +3 -0
- package/docs/agents-md-alignment-plan.md +123 -0
- package/hooks/blocking/code_rules_enforcer.py +686 -60
- package/hooks/blocking/es_exe_path_rewriter.py +10 -4
- package/hooks/blocking/test_code_rules_enforcer.py +273 -39
- package/hooks/blocking/test_code_rules_enforcer_annotations.py +97 -0
- package/hooks/blocking/test_code_rules_enforcer_banned_identifier.py +106 -0
- package/hooks/blocking/test_code_rules_enforcer_cap_meta.py +173 -0
- package/hooks/blocking/test_code_rules_enforcer_collection_prefix.py +328 -0
- package/hooks/blocking/test_code_rules_enforcer_config_path.py +0 -20
- package/hooks/blocking/test_code_rules_enforcer_constant_equality.py +33 -11
- package/hooks/blocking/test_code_rules_enforcer_existence_checks.py +0 -18
- package/hooks/blocking/test_code_rules_enforcer_hardcoded_user_path.py +291 -0
- package/hooks/blocking/test_code_rules_enforcer_inline_literal_collections.py +155 -0
- package/hooks/blocking/test_code_rules_enforcer_loop_variable_naming.py +194 -0
- package/hooks/blocking/test_code_rules_enforcer_naming_pattern.py +49 -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/hooks/blocking/test_code_rules_enforcer_sys_path_insert.py +157 -0
- package/hooks/blocking/test_code_rules_enforcer_unused_imports.py +244 -0
- package/hooks/blocking/test_es_exe_path_rewriter.py +81 -3
- package/hooks/blocking/test_windows_rmtree_blocker.py +120 -8
- package/hooks/blocking/windows_rmtree_blocker.py +23 -6
- package/hooks/config/banned_identifiers_constants.py +24 -0
- package/hooks/config/hardcoded_user_path_constants.py +12 -0
- package/hooks/config/hook_log_extractor_constants.py +1 -1
- package/hooks/config/pre_tool_use_stdin.py +48 -0
- package/hooks/config/setup_project_paths_constants.py +4 -0
- package/hooks/config/stuttering_check_config.py +14 -0
- package/hooks/config/stuttering_import_binding_constants.py +11 -0
- package/hooks/config/sys_path_insert_constants.py +4 -0
- package/hooks/config/test_banned_identifiers_constants.py +48 -0
- package/hooks/config/test_hardcoded_user_path_constants.py +78 -0
- package/hooks/config/test_hook_log_extractor_constants.py +3 -3
- package/hooks/config/test_pre_tool_use_stdin.py +80 -0
- package/hooks/config/unused_module_import_constants.py +7 -0
- package/hooks/config/windows_rmtree_blocker_constants.py +3 -0
- package/hooks/diagnostic/hook_log_stop_wrapper.py +7 -4
- package/hooks/git-hooks/config.py +3 -3
- package/hooks/git-hooks/test_gate_utils.py +10 -10
- package/hooks/mypy.ini +2 -0
- package/package.json +1 -1
- package/rules/gh-paginate.md +125 -0
- package/skills/bugteam/CONSTRAINTS.md +12 -6
- package/skills/bugteam/PROMPTS.md +0 -39
- package/skills/bugteam/SKILL.md +93 -125
- package/skills/bugteam/SKILL_EVALS.md +25 -23
- package/skills/bugteam/reference/README.md +2 -0
- package/skills/bugteam/reference/audit-and-teammates.md +2 -2
- package/skills/bugteam/reference/copilot-gap-analysis.md +12 -0
- package/skills/bugteam/reference/teardown-publish-permissions.md +1 -1
- package/skills/bugteam/reference/workflow-path-a-orchestrated-teams.md +113 -0
- package/skills/bugteam/reference/workflow-path-b-task-harness.md +48 -0
- package/skills/bugteam/test_skill_additions.py +13 -4
- package/skills/bugteam/test_team_lifecycle.py +94 -0
- package/skills/findbugs/SKILL.md +3 -3
- package/skills/fixbugs/SKILL.md +4 -4
- package/skills/monitor-open-prs/SKILL.md +32 -2
- package/skills/monitor-open-prs/test_team_lifecycle.py +46 -0
- package/skills/pr-converge/SKILL.md +576 -95
- package/skills/pr-converge/scripts/README.md +145 -0
- package/skills/pr-converge/scripts/caller-window-pid.ps1 +86 -0
- package/skills/pr-converge/scripts/check_pr_mergeability.py +79 -0
- package/skills/pr-converge/scripts/config/pr_converge_constants.py +65 -0
- package/skills/pr-converge/scripts/config/test_pr_converge_constants.py +176 -0
- package/skills/pr-converge/scripts/cursor-agents-continue-caller.cmd +9 -0
- package/skills/pr-converge/scripts/cursor-agents-continue-stop-others.ps1 +16 -0
- package/skills/pr-converge/scripts/cursor-agents-continue.ahk +172 -0
- package/skills/pr-converge/scripts/cursor-agents-continue.cmd +2 -0
- package/skills/pr-converge/scripts/evict_cached_config_modules.py +20 -0
- package/skills/pr-converge/scripts/fetch_bugbot_inline_comments.py +110 -0
- package/skills/pr-converge/scripts/fetch_bugbot_reviews.py +103 -0
- package/skills/pr-converge/scripts/fetch_copilot_inline_comments.py +112 -0
- package/skills/pr-converge/scripts/fetch_copilot_reviews.py +121 -0
- package/skills/pr-converge/scripts/mark_pr_ready.py +54 -0
- package/skills/pr-converge/scripts/open_followup_copilot_pr.py +136 -0
- package/skills/pr-converge/scripts/post-bugbot-run.helpers.ps1 +49 -0
- package/skills/pr-converge/scripts/post-bugbot-run.ps1 +33 -0
- package/skills/pr-converge/scripts/reply_to_inline_comment.py +84 -0
- package/skills/pr-converge/scripts/request_copilot_review.py +71 -0
- package/skills/pr-converge/scripts/resolve_pr_head.py +58 -0
- package/skills/pr-converge/scripts/review_field_helpers.py +43 -0
- package/skills/pr-converge/scripts/test_check_pr_mergeability.py +126 -0
- package/skills/pr-converge/scripts/test_evict_cached_config_modules.py +22 -0
- package/skills/pr-converge/scripts/test_fetch_bugbot_inline_comments.py +342 -0
- package/skills/pr-converge/scripts/test_fetch_bugbot_reviews.py +220 -0
- package/skills/pr-converge/scripts/test_fetch_copilot_inline_comments.py +372 -0
- package/skills/pr-converge/scripts/test_fetch_copilot_reviews.py +280 -0
- package/skills/pr-converge/scripts/test_mark_pr_ready.py +69 -0
- package/skills/pr-converge/scripts/test_open_followup_copilot_pr.py +236 -0
- package/skills/pr-converge/scripts/test_post_bugbot_run.py +195 -0
- package/skills/pr-converge/scripts/test_reply_to_inline_comment.py +159 -0
- package/skills/pr-converge/scripts/test_request_copilot_review.py +101 -0
- package/skills/pr-converge/scripts/test_resolve_pr_head.py +79 -0
- package/skills/pr-converge/scripts/test_review_field_helpers.py +80 -0
- package/skills/pr-converge/scripts/test_trigger_bugbot.py +139 -0
- package/skills/pr-converge/scripts/test_view_pr_context.py +111 -0
- package/skills/pr-converge/scripts/trigger_bugbot.py +77 -0
- package/skills/pr-converge/scripts/view_pr_context.py +47 -0
- package/skills/pr-converge/test_team_lifecycle.py +47 -0
- package/skills/pr-converge/workflows/ahk-auto-continue-loop.md +108 -0
- package/skills/pr-converge/workflows/schedule-wakeup-loop.md +37 -0
- package/skills/qbug/SKILL.md +4 -4
- package/skills/qbug/test_qbug_skill_post_fix_audit.py +2 -2
- package/skills/resume-review/SKILL.md +261 -0
- 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
- package/skills/bugteam/scripts/README.md +0 -58
- package/skills/bugteam/scripts/_claude_permissions_common.py +0 -219
- package/skills/bugteam/scripts/bugteam_code_rules_gate.py +0 -633
- package/skills/bugteam/scripts/bugteam_fix_hookspath.py +0 -260
- package/skills/bugteam/scripts/bugteam_preflight.py +0 -201
- package/skills/bugteam/scripts/config/bugteam_fix_hookspath_constants.py +0 -17
- package/skills/bugteam/scripts/grant_project_claude_permissions.py +0 -109
- package/skills/bugteam/scripts/revoke_project_claude_permissions.py +0 -135
- package/skills/bugteam/scripts/test_bugteam_code_rules_gate.py +0 -271
- package/skills/bugteam/scripts/test_bugteam_fix_hookspath.py +0 -267
- package/skills/bugteam/scripts/test_bugteam_preflight.py +0 -189
- package/skills/bugteam/scripts/test_claude_permissions_common.py +0 -44
- /package/skills/{bugteam → pr-converge}/scripts/config/__init__.py +0 -0
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
"""Tests for hardcoded user path detection.
|
|
2
|
+
|
|
3
|
+
Bot reviewers on PR #257 flagged 6+ instances of 'C:/Users/jon/' embedded
|
|
4
|
+
in production source code, which breaks portability across machines.
|
|
5
|
+
The new rule flags any string literal in production code that names a
|
|
6
|
+
specific user's home directory.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import importlib.util
|
|
12
|
+
import pathlib
|
|
13
|
+
import sys
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
_HOOK_DIRECTORY = pathlib.Path(__file__).parent
|
|
17
|
+
if str(_HOOK_DIRECTORY) not in sys.path:
|
|
18
|
+
sys.path.insert(0, str(_HOOK_DIRECTORY))
|
|
19
|
+
|
|
20
|
+
_hook_spec = importlib.util.spec_from_file_location(
|
|
21
|
+
"code_rules_enforcer",
|
|
22
|
+
_HOOK_DIRECTORY / "code_rules_enforcer.py",
|
|
23
|
+
)
|
|
24
|
+
assert _hook_spec is not None
|
|
25
|
+
assert _hook_spec.loader is not None
|
|
26
|
+
_hook_module = importlib.util.module_from_spec(_hook_spec)
|
|
27
|
+
_hook_spec.loader.exec_module(_hook_module)
|
|
28
|
+
check_hardcoded_user_paths = _hook_module.check_hardcoded_user_paths
|
|
29
|
+
HARDCODED_USER_PATH_PATTERN = _hook_module.HARDCODED_USER_PATH_PATTERN
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
PRODUCTION_FILE_PATH = "packages/app/services/loader.py"
|
|
33
|
+
TEST_FILE_PATH = "packages/app/tests/test_loader.py"
|
|
34
|
+
CONFIG_FILE_PATH = "packages/app/config/paths.py"
|
|
35
|
+
HOOK_INFRASTRUCTURE_FILE_PATH = "/repo/packages/claude-dev-env/hooks/blocking/code_rules_enforcer.py"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def test_should_match_user_directory_without_consuming_following_separator() -> None:
|
|
39
|
+
windows = HARDCODED_USER_PATH_PATTERN.search("C:/Users/jon/more")
|
|
40
|
+
macos = HARDCODED_USER_PATH_PATTERN.search("/Users/bob/more")
|
|
41
|
+
linux = HARDCODED_USER_PATH_PATTERN.search("/home/alice/more")
|
|
42
|
+
assert windows is not None and windows.group(0) == "C:/Users/jon"
|
|
43
|
+
assert macos is not None and macos.group(0) == "/Users/bob"
|
|
44
|
+
assert linux is not None and linux.group(0) == "/home/alice"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def test_should_flag_windows_user_path_with_forward_slashes() -> None:
|
|
48
|
+
source = 'def find() -> str:\n return "C:/Users/jon/notes.md"\n'
|
|
49
|
+
issues = check_hardcoded_user_paths(source, PRODUCTION_FILE_PATH)
|
|
50
|
+
assert any("C:/Users/jon" in each_issue for each_issue in issues), (
|
|
51
|
+
f"Expected Windows user path flagged, got: {issues}"
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def test_should_flag_windows_user_path_when_users_segment_is_not_title_case() -> None:
|
|
56
|
+
source = 'def find() -> str:\n return "c:/users/jon/notes.md"\n'
|
|
57
|
+
issues = check_hardcoded_user_paths(source, PRODUCTION_FILE_PATH)
|
|
58
|
+
assert any("users" in each_issue.lower() for each_issue in issues), (
|
|
59
|
+
f"Expected Windows user path flagged (case-insensitive Users segment), got: {issues}"
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def test_should_flag_windows_user_path_with_backslashes() -> None:
|
|
64
|
+
source = 'def find() -> str:\n return "C:\\\\Users\\\\jon\\\\notes.md"\n'
|
|
65
|
+
issues = check_hardcoded_user_paths(source, PRODUCTION_FILE_PATH)
|
|
66
|
+
assert any("Users" in each_issue for each_issue in issues), (
|
|
67
|
+
f"Expected Windows backslash user path flagged, got: {issues}"
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def test_should_flag_unix_home_path() -> None:
|
|
72
|
+
source = 'def find() -> str:\n return "/home/alice/notes.md"\n'
|
|
73
|
+
issues = check_hardcoded_user_paths(source, PRODUCTION_FILE_PATH)
|
|
74
|
+
assert any("/home/alice" in each_issue for each_issue in issues), (
|
|
75
|
+
f"Expected Unix home path flagged, got: {issues}"
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def test_should_flag_macos_user_path() -> None:
|
|
80
|
+
source = 'def find() -> str:\n return "/Users/bob/Documents/data.json"\n'
|
|
81
|
+
issues = check_hardcoded_user_paths(source, PRODUCTION_FILE_PATH)
|
|
82
|
+
assert any("/Users/bob" in each_issue for each_issue in issues), (
|
|
83
|
+
f"Expected macOS user path flagged, got: {issues}"
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def test_should_flag_macos_user_path_when_home_is_entire_path() -> None:
|
|
88
|
+
source = 'def find() -> str:\n return "/Users/bob"\n'
|
|
89
|
+
issues = check_hardcoded_user_paths(source, PRODUCTION_FILE_PATH)
|
|
90
|
+
assert any("/Users/bob" in each_issue for each_issue in issues), (
|
|
91
|
+
f"Expected macOS user home literal flagged without trailing slash, got: {issues}"
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def test_should_not_flag_tilde_home_alias() -> None:
|
|
96
|
+
source = 'def find() -> str:\n return "~/notes.md"\n'
|
|
97
|
+
issues = check_hardcoded_user_paths(source, PRODUCTION_FILE_PATH)
|
|
98
|
+
assert issues == [], f"Tilde alias is portable, must not flag, got: {issues}"
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def test_should_not_flag_users_directory_without_specific_user() -> None:
|
|
102
|
+
source = 'def root_dir() -> str:\n return "C:/Users"\n'
|
|
103
|
+
issues = check_hardcoded_user_paths(source, PRODUCTION_FILE_PATH)
|
|
104
|
+
assert issues == [], (
|
|
105
|
+
f"Generic /Users root with no specific user has no portability cost, got: {issues}"
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def test_should_skip_test_files() -> None:
|
|
110
|
+
source = 'def test_path() -> None:\n fixture = "C:/Users/jon/scratch.txt"\n'
|
|
111
|
+
issues = check_hardcoded_user_paths(source, TEST_FILE_PATH)
|
|
112
|
+
assert issues == [], (
|
|
113
|
+
f"Test files exempt — fixtures often need real paths, got: {issues}"
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def test_should_skip_config_files() -> None:
|
|
118
|
+
source = 'DEFAULT_PATH = "C:/Users/jon/notes.md"\n'
|
|
119
|
+
issues = check_hardcoded_user_paths(source, CONFIG_FILE_PATH)
|
|
120
|
+
assert issues == [], (
|
|
121
|
+
f"Config files exempt — that is the right place for paths, got: {issues}"
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def test_should_include_line_number_in_issue() -> None:
|
|
126
|
+
source = '\n\ndef find() -> str:\n return "C:/Users/jon/notes.md"\n'
|
|
127
|
+
issues = check_hardcoded_user_paths(source, PRODUCTION_FILE_PATH)
|
|
128
|
+
assert any("Line 4" in each_issue for each_issue in issues), (
|
|
129
|
+
f"Expected line 4 reference, got: {issues}"
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def test_should_handle_syntax_error_gracefully() -> None:
|
|
134
|
+
source = "def broken(\n not python\n"
|
|
135
|
+
issues = check_hardcoded_user_paths(source, PRODUCTION_FILE_PATH)
|
|
136
|
+
assert issues == [], f"Parse failure must return empty, got: {issues}"
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def test_should_suggest_path_home_or_expanduser_in_message() -> None:
|
|
140
|
+
source = 'def find() -> str:\n return "/home/alice/x"\n'
|
|
141
|
+
issues = check_hardcoded_user_paths(source, PRODUCTION_FILE_PATH)
|
|
142
|
+
assert any(
|
|
143
|
+
"Path.home" in each_issue or "expanduser" in each_issue for each_issue in issues
|
|
144
|
+
), (
|
|
145
|
+
f"Error message should suggest Path.home() or os.path.expanduser('~'), got: {issues}"
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
def test_should_flag_standalone_home_segment_for_symmetry_with_macos() -> None:
|
|
149
|
+
source = 'def route() -> str:\n return "/home/dashboard"\n'
|
|
150
|
+
issues = check_hardcoded_user_paths(source, PRODUCTION_FILE_PATH)
|
|
151
|
+
assert any("/home/dashboard" in each_issue for each_issue in issues), (
|
|
152
|
+
f"Linux '/home/<segment>' is structurally indistinguishable from a real"
|
|
153
|
+
f" home directory and must flag for symmetry with macOS, got: {issues}"
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def test_should_not_flag_standalone_users_segment_without_trailing_path() -> None:
|
|
158
|
+
source = 'def system_path() -> str:\n return "/Users/Shared"\n'
|
|
159
|
+
issues = check_hardcoded_user_paths(source, PRODUCTION_FILE_PATH)
|
|
160
|
+
assert issues == [], (
|
|
161
|
+
f"'/Users/Shared' without trailing path component is not navigating into a user home, got: {issues}"
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def test_should_skip_hook_infrastructure_files() -> None:
|
|
166
|
+
source = (
|
|
167
|
+
'HARDCODED_USER_PATH_PATTERN = "/Users/[^/]+|/home/[^/]+"\n'
|
|
168
|
+
'def find() -> str:\n'
|
|
169
|
+
' return "C:/Users/jon/notes.md"\n'
|
|
170
|
+
)
|
|
171
|
+
issues = check_hardcoded_user_paths(source, HOOK_INFRASTRUCTURE_FILE_PATH)
|
|
172
|
+
assert issues == [], (
|
|
173
|
+
f"Hook infrastructure files exempt — the enforcer itself encodes user-path"
|
|
174
|
+
f" patterns and would otherwise self-block, got: {issues}"
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def test_should_not_flag_docstring_mentioning_user_path() -> None:
|
|
179
|
+
source = (
|
|
180
|
+
'def load_data() -> None:\n'
|
|
181
|
+
' """Reads from /home/alice/data for testing."""\n'
|
|
182
|
+
' pass\n'
|
|
183
|
+
)
|
|
184
|
+
issues = check_hardcoded_user_paths(source, PRODUCTION_FILE_PATH)
|
|
185
|
+
assert issues == [], (
|
|
186
|
+
f"Docstrings are allowed to mention paths, got: {issues}"
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def test_should_flag_linux_home_path_when_home_is_entire_path() -> None:
|
|
191
|
+
source = 'def find() -> str:\n return "/home/alice"\n'
|
|
192
|
+
issues = check_hardcoded_user_paths(source, PRODUCTION_FILE_PATH)
|
|
193
|
+
assert any("/home/alice" in each_issue for each_issue in issues), (
|
|
194
|
+
f"Expected Linux home literal flagged without trailing slash, got: {issues}"
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def test_should_not_flag_windows_public_shared_folder() -> None:
|
|
199
|
+
source = 'def find() -> str:\n return "C:/Users/Public/Documents"\n'
|
|
200
|
+
issues = check_hardcoded_user_paths(source, PRODUCTION_FILE_PATH)
|
|
201
|
+
assert issues == [], (
|
|
202
|
+
f"Windows 'C:/Users/Public' is a system shared folder, not a user home, got: {issues}"
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def test_should_not_flag_windows_shared_folder() -> None:
|
|
207
|
+
source = 'def find() -> str:\n return "C:/Users/Shared/data"\n'
|
|
208
|
+
issues = check_hardcoded_user_paths(source, PRODUCTION_FILE_PATH)
|
|
209
|
+
assert issues == [], (
|
|
210
|
+
f"Windows 'C:/Users/Shared' is a system shared folder, not a user home, got: {issues}"
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def test_should_not_flag_windows_all_users_folder() -> None:
|
|
215
|
+
source = 'def find() -> str:\n return "C:/Users/All Users/AppData"\n'
|
|
216
|
+
issues = check_hardcoded_user_paths(source, PRODUCTION_FILE_PATH)
|
|
217
|
+
assert issues == [], (
|
|
218
|
+
f"Windows 'C:/Users/All Users' is a legacy shared folder, not a user home, got: {issues}"
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def test_should_not_flag_macos_public_shared_folder() -> None:
|
|
223
|
+
source = 'def find() -> str:\n return "/Users/Public/Documents"\n'
|
|
224
|
+
issues = check_hardcoded_user_paths(source, PRODUCTION_FILE_PATH)
|
|
225
|
+
assert issues == [], (
|
|
226
|
+
f"macOS '/Users/Public' is a default shared folder on every macOS install,"
|
|
227
|
+
f" not a user home — symmetry with the Windows exclusion, got: {issues}"
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def test_should_not_flag_windows_lowercase_public_shared_folder() -> None:
|
|
232
|
+
source = 'def find() -> str:\n return "c:/users/public/Documents"\n'
|
|
233
|
+
issues = check_hardcoded_user_paths(source, PRODUCTION_FILE_PATH)
|
|
234
|
+
assert issues == [], (
|
|
235
|
+
f"Windows 'c:/users/public' is the same shared folder regardless of case,"
|
|
236
|
+
f" the exclusion list must be case-insensitive, got: {issues}"
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def test_should_not_flag_windows_lowercase_shared_folder() -> None:
|
|
241
|
+
source = 'def find() -> str:\n return "c:/users/shared/data"\n'
|
|
242
|
+
issues = check_hardcoded_user_paths(source, PRODUCTION_FILE_PATH)
|
|
243
|
+
assert issues == [], (
|
|
244
|
+
f"Windows 'c:/users/shared' is a system shared folder regardless of case,"
|
|
245
|
+
f" got: {issues}"
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def test_should_not_flag_windows_lowercase_all_users_folder() -> None:
|
|
250
|
+
source = 'def find() -> str:\n return "c:/users/all users/AppData"\n'
|
|
251
|
+
issues = check_hardcoded_user_paths(source, PRODUCTION_FILE_PATH)
|
|
252
|
+
assert issues == [], (
|
|
253
|
+
f"Windows 'c:/users/all users' is a legacy shared folder regardless of case,"
|
|
254
|
+
f" got: {issues}"
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def test_should_not_flag_windows_mixed_case_public_shared_folder() -> None:
|
|
259
|
+
source = 'def find() -> str:\n return "C:/Users/PuBlIc/Documents"\n'
|
|
260
|
+
issues = check_hardcoded_user_paths(source, PRODUCTION_FILE_PATH)
|
|
261
|
+
assert issues == [], (
|
|
262
|
+
f"Windows 'C:/Users/PuBlIc' is the same shared folder in any casing,"
|
|
263
|
+
f" got: {issues}"
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def test_should_not_flag_windows_uppercase_public_shared_folder() -> None:
|
|
268
|
+
source = 'def find() -> str:\n return "C:/Users/PUBLIC/Documents"\n'
|
|
269
|
+
issues = check_hardcoded_user_paths(source, PRODUCTION_FILE_PATH)
|
|
270
|
+
assert issues == [], (
|
|
271
|
+
f"Windows 'C:/Users/PUBLIC' is the same shared folder in any casing,"
|
|
272
|
+
f" got: {issues}"
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def test_should_not_flag_macos_lowercase_shared_folder() -> None:
|
|
277
|
+
source = 'def find() -> str:\n return "/Users/shared/data"\n'
|
|
278
|
+
issues = check_hardcoded_user_paths(source, PRODUCTION_FILE_PATH)
|
|
279
|
+
assert issues == [], (
|
|
280
|
+
f"macOS '/Users/shared' is a system shared folder regardless of case,"
|
|
281
|
+
f" got: {issues}"
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def test_should_not_flag_macos_lowercase_public_shared_folder() -> None:
|
|
286
|
+
source = 'def find() -> str:\n return "/Users/public/Documents"\n'
|
|
287
|
+
issues = check_hardcoded_user_paths(source, PRODUCTION_FILE_PATH)
|
|
288
|
+
assert issues == [], (
|
|
289
|
+
f"macOS '/Users/public' is a default shared folder regardless of case,"
|
|
290
|
+
f" got: {issues}"
|
|
291
|
+
)
|
|
@@ -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,194 @@
|
|
|
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_flag_tuple_unpacking_targets_lacking_each_prefix() -> None:
|
|
60
|
+
source = (
|
|
61
|
+
"def consume() -> None:\n"
|
|
62
|
+
" for accessed_field, access_line in []:\n"
|
|
63
|
+
" return None\n"
|
|
64
|
+
)
|
|
65
|
+
issues = code_rules_enforcer.check_loop_variable_naming(
|
|
66
|
+
source, PRODUCTION_FILE_PATH
|
|
67
|
+
)
|
|
68
|
+
assert any("accessed_field" in each_issue for each_issue in issues), (
|
|
69
|
+
f"Expected 'accessed_field' tuple-unpack target flagged, got: {issues}"
|
|
70
|
+
)
|
|
71
|
+
assert any("access_line" in each_issue for each_issue in issues), (
|
|
72
|
+
f"Expected 'access_line' tuple-unpack target flagged, got: {issues}"
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def test_should_not_flag_tuple_unpacking_when_all_targets_have_each_prefix() -> None:
|
|
77
|
+
source = (
|
|
78
|
+
"def consume() -> None:\n"
|
|
79
|
+
" for each_key, each_value in {}.items():\n"
|
|
80
|
+
" return None\n"
|
|
81
|
+
)
|
|
82
|
+
issues = code_rules_enforcer.check_loop_variable_naming(
|
|
83
|
+
source, PRODUCTION_FILE_PATH
|
|
84
|
+
)
|
|
85
|
+
assert issues == [], (
|
|
86
|
+
f"Tuple-unpack with each_ prefix on all targets must pass, got: {issues}"
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def test_should_exempt_underscore_inside_tuple_unpacking() -> None:
|
|
91
|
+
source = (
|
|
92
|
+
"def consume() -> None:\n"
|
|
93
|
+
" for _, each_position in []:\n"
|
|
94
|
+
" return None\n"
|
|
95
|
+
)
|
|
96
|
+
issues = code_rules_enforcer.check_loop_variable_naming(
|
|
97
|
+
source, PRODUCTION_FILE_PATH
|
|
98
|
+
)
|
|
99
|
+
assert issues == [], (
|
|
100
|
+
f"'_' must remain exempt inside tuple unpacking, got: {issues}"
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def test_should_flag_partially_compliant_tuple_unpacking() -> None:
|
|
105
|
+
source = (
|
|
106
|
+
"def consume() -> None:\n"
|
|
107
|
+
" for each_key, raw_value in {}.items():\n"
|
|
108
|
+
" return None\n"
|
|
109
|
+
)
|
|
110
|
+
issues = code_rules_enforcer.check_loop_variable_naming(
|
|
111
|
+
source, PRODUCTION_FILE_PATH
|
|
112
|
+
)
|
|
113
|
+
assert any("raw_value" in each_issue for each_issue in issues), (
|
|
114
|
+
f"Mixed-compliance tuple unpack must flag the offender, got: {issues}"
|
|
115
|
+
)
|
|
116
|
+
assert not any("each_key" in each_issue for each_issue in issues), (
|
|
117
|
+
f"each_key compliant target must not be flagged, got: {issues}"
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def test_should_flag_nested_tuple_unpacking_targets() -> None:
|
|
122
|
+
source = (
|
|
123
|
+
"def consume() -> None:\n"
|
|
124
|
+
" for outer_label, (inner_first, inner_second) in []:\n"
|
|
125
|
+
" return None\n"
|
|
126
|
+
)
|
|
127
|
+
issues = code_rules_enforcer.check_loop_variable_naming(
|
|
128
|
+
source, PRODUCTION_FILE_PATH
|
|
129
|
+
)
|
|
130
|
+
assert any("inner_first" in each_issue for each_issue in issues), (
|
|
131
|
+
f"Nested tuple-unpack targets must be inspected, got: {issues}"
|
|
132
|
+
)
|
|
133
|
+
assert any("inner_second" in each_issue for each_issue in issues), (
|
|
134
|
+
f"Nested tuple-unpack targets must be inspected, got: {issues}"
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def test_should_flag_starred_tuple_unpacking_target() -> None:
|
|
139
|
+
source = (
|
|
140
|
+
"def consume() -> None:\n"
|
|
141
|
+
" for first, *rest in []:\n"
|
|
142
|
+
" return None\n"
|
|
143
|
+
)
|
|
144
|
+
issues = code_rules_enforcer.check_loop_variable_naming(
|
|
145
|
+
source, PRODUCTION_FILE_PATH
|
|
146
|
+
)
|
|
147
|
+
assert any("first" in each_issue for each_issue in issues), (
|
|
148
|
+
f"First tuple-unpack target must be flagged, got: {issues}"
|
|
149
|
+
)
|
|
150
|
+
assert any("rest" in each_issue for each_issue in issues), (
|
|
151
|
+
f"Starred tuple-unpack target must be flagged, got: {issues}"
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def test_should_not_flag_list_comprehension_target() -> None:
|
|
156
|
+
source = "def consume() -> None:\n return [x for x in []]\n"
|
|
157
|
+
issues = code_rules_enforcer.check_loop_variable_naming(
|
|
158
|
+
source, PRODUCTION_FILE_PATH
|
|
159
|
+
)
|
|
160
|
+
assert issues == [], f"Comprehension target exempt, got: {issues}"
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def test_should_skip_in_test_files() -> None:
|
|
164
|
+
source = "def test_consume() -> None:\n for marker in []:\n return None\n"
|
|
165
|
+
issues = code_rules_enforcer.check_loop_variable_naming(source, TEST_FILE_PATH)
|
|
166
|
+
assert issues == [], f"Test files exempt, got: {issues}"
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def test_should_flag_async_for_loop_variable() -> None:
|
|
170
|
+
source = (
|
|
171
|
+
"async def consume() -> None:\n"
|
|
172
|
+
" async for marker in stream():\n"
|
|
173
|
+
" return None\n"
|
|
174
|
+
)
|
|
175
|
+
issues = code_rules_enforcer.check_loop_variable_naming(
|
|
176
|
+
source, PRODUCTION_FILE_PATH
|
|
177
|
+
)
|
|
178
|
+
assert any("marker" in each_issue for each_issue in issues), (
|
|
179
|
+
f"Expected async-for variable flagged, got: {issues}"
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def test_should_exempt_underscore_throwaway_loop_variable() -> None:
|
|
184
|
+
source = (
|
|
185
|
+
"def consume(count: int) -> None:\n"
|
|
186
|
+
" for _ in range(count):\n"
|
|
187
|
+
" return None\n"
|
|
188
|
+
)
|
|
189
|
+
issues = code_rules_enforcer.check_loop_variable_naming(
|
|
190
|
+
source, PRODUCTION_FILE_PATH
|
|
191
|
+
)
|
|
192
|
+
assert issues == [], (
|
|
193
|
+
f"Throwaway '_' loop variable must be exempt (Python idiom for 'value intentionally unused'), got: {issues}"
|
|
194
|
+
)
|