claude-dev-env 1.0.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/LICENSE +21 -0
- package/README.md +219 -0
- package/agents/agent-writer.md +157 -0
- package/agents/clasp-deployment-orchestrator.md +609 -0
- package/agents/clean-coder.md +295 -0
- package/agents/code-quality-agent.md +40 -0
- package/agents/code-standards-agent.md +93 -0
- package/agents/config-centralizer.md +686 -0
- package/agents/config-extraction-agent.md +225 -0
- package/agents/doc-orchestrator.md +47 -0
- package/agents/docs-agent.md +112 -0
- package/agents/docx-agent.md +211 -0
- package/agents/git-commit-crafter.md +100 -0
- package/agents/magic-value-eliminator-agent.md +72 -0
- package/agents/mandatory-agent-workflow-agent.md +88 -0
- package/agents/parallel-workflow-coordinator.md +779 -0
- package/agents/pdf-agent.md +302 -0
- package/agents/plan-executor.md +226 -0
- package/agents/pr-description-writer.md +87 -0
- package/agents/project-context-loader.md +238 -0
- package/agents/project-docs-analyzer.md +54 -0
- package/agents/project-structure-organizer-agent.md +72 -0
- package/agents/readability-review-agent.md +76 -0
- package/agents/refactoring-specialist.md +69 -0
- package/agents/right-sized-engineer.md +129 -0
- package/agents/session-continuity-manager.md +53 -0
- package/agents/skill-to-agent-converter.md +371 -0
- package/agents/skill-writer-agent.md +470 -0
- package/agents/stub-detector-agent.md +140 -0
- package/agents/tdd-test-writer.md +62 -0
- package/agents/test-data-builder.md +68 -0
- package/agents/tooling-builder.md +78 -0
- package/agents/user-docs-writer.md +67 -0
- package/agents/validation-expert.md +71 -0
- package/agents/workflow-visual-documenter.md +82 -0
- package/agents/xlsx-agent.md +169 -0
- package/bin/install.mjs +256 -0
- package/commands/commit.md +28 -0
- package/commands/docupdate.md +322 -0
- package/commands/implement.md +102 -0
- package/commands/initialize.md +91 -0
- package/commands/plan.md +63 -0
- package/commands/pr-comments.md +47 -0
- package/commands/readability-review.md +20 -0
- package/commands/review-plan.md +7 -0
- package/commands/right-size.md +15 -0
- package/commands/stubcheck.md +89 -0
- package/commands/sum.md +30 -0
- package/docs/CODE_RULES.md +186 -0
- package/docs/DJANGO_PATTERNS.md +80 -0
- package/docs/REACT_PATTERNS.md +185 -0
- package/docs/TEST_QUALITY.md +104 -0
- package/hooks/advisory/migration-safety-advisor.py +49 -0
- package/hooks/advisory/refactor-guard.py +205 -0
- package/hooks/blocking/block-main-commit.py +168 -0
- package/hooks/blocking/code-rules-enforcer.py +549 -0
- package/hooks/blocking/destructive-command-blocker.py +107 -0
- package/hooks/blocking/docker-settings-guard.py +44 -0
- package/hooks/blocking/hedging-language-blocker.py +130 -0
- package/hooks/blocking/parallel-task-blocker.py +69 -0
- package/hooks/blocking/pr-description-enforcer.py +87 -0
- package/hooks/blocking/pyautogui-scroll-blocker.py +74 -0
- package/hooks/blocking/sensitive-file-protector.py +70 -0
- package/hooks/blocking/tdd-enforcer.py +62 -0
- package/hooks/blocking/test-preflight-check.py +343 -0
- package/hooks/blocking/write-existing-file-blocker.py +63 -0
- package/hooks/git-hooks/post-commit.py +103 -0
- package/hooks/github-action/test_workflow.py +33 -0
- package/hooks/hooks.json +246 -0
- package/hooks/lifecycle/config-change-guard.py +84 -0
- package/hooks/lifecycle/session-end-cleanup.py +59 -0
- package/hooks/notification/attention-needed-notify.py +63 -0
- package/hooks/notification/claude-notification-handler.py +59 -0
- package/hooks/notification/notification_utils.py +206 -0
- package/hooks/rewrite-plugin-paths.py +116 -0
- package/hooks/session/bulk-edit-reminder.py +30 -0
- package/hooks/session/code-rules-reminder.py +97 -0
- package/hooks/session/compact-context-reinject.py +39 -0
- package/hooks/session/hook-structure-context.py +140 -0
- package/hooks/session/plugin-data-dir-cleanup.py +39 -0
- package/hooks/validation/code-style-validator.py +145 -0
- package/hooks/validation/e2e-test-validator.py +142 -0
- package/hooks/validation/hook-format-validator.py +66 -0
- package/hooks/validation/mypy_validator.py +180 -0
- package/hooks/validators/README.md +125 -0
- package/hooks/validators/VALIDATION_REPORT.md +287 -0
- package/hooks/validators/__init__.py +19 -0
- package/hooks/validators/abbreviation_checks.py +82 -0
- package/hooks/validators/code_quality_checks.py +133 -0
- package/hooks/validators/comment_checks.py +188 -0
- package/hooks/validators/file_structure_checks.py +182 -0
- package/hooks/validators/git_checks.py +107 -0
- package/hooks/validators/health_check.py +214 -0
- package/hooks/validators/magic_value_checks.py +81 -0
- package/hooks/validators/mypy_integration.py +52 -0
- package/hooks/validators/output_formatter.py +266 -0
- package/hooks/validators/pr_reference_checks.py +72 -0
- package/hooks/validators/python_antipattern_checks.py +110 -0
- package/hooks/validators/python_style_checks.py +364 -0
- package/hooks/validators/react_checks.py +90 -0
- package/hooks/validators/ruff_integration.py +80 -0
- package/hooks/validators/run_all_validators.py +772 -0
- package/hooks/validators/security_checks.py +135 -0
- package/hooks/validators/test_abbreviation_checks.py +76 -0
- package/hooks/validators/test_bad.tsx +7 -0
- package/hooks/validators/test_code_quality_checks.py +129 -0
- package/hooks/validators/test_file_structure_checks.py +307 -0
- package/hooks/validators/test_files/01_basic_component.tsx +10 -0
- package/hooks/validators/test_files/02_component_without_react.tsx +10 -0
- package/hooks/validators/test_files/03_pure_component.tsx +10 -0
- package/hooks/validators/test_files/04_pure_component_import.tsx +10 -0
- package/hooks/validators/test_files/05_typescript_generics.tsx +14 -0
- package/hooks/validators/test_files/06_typescript_two_generics.tsx +18 -0
- package/hooks/validators/test_files/07_multiline_declaration.tsx +11 -0
- package/hooks/validators/test_files/08_error_boundary_valid.tsx +14 -0
- package/hooks/validators/test_files/09_error_boundary_with_other_class.tsx +20 -0
- package/hooks/validators/test_files/10_inheritance_chain.tsx +16 -0
- package/hooks/validators/test_files/11_ts_file.ts +10 -0
- package/hooks/validators/test_files/12_non_react_class.tsx +14 -0
- package/hooks/validators/test_files/13_functional_component.tsx +8 -0
- package/hooks/validators/test_files/14_indented_class.tsx +13 -0
- package/hooks/validators/test_files/15_getDerivedStateFromError.tsx +14 -0
- package/hooks/validators/test_files/16_mixed_components.tsx +20 -0
- package/hooks/validators/test_files/EXECUTIVE_SUMMARY.md +175 -0
- package/hooks/validators/test_files/TEST_RESULTS_TABLE.txt +60 -0
- package/hooks/validators/test_files/VALIDATION_REPORT.md +201 -0
- package/hooks/validators/test_files/async_views.py +23 -0
- package/hooks/validators/test_files/async_with_imports.py +14 -0
- package/hooks/validators/test_files/bad_inline_imports.py +37 -0
- package/hooks/validators/test_files/management/commands/cmd_01_no_debug_check.py +10 -0
- package/hooks/validators/test_files/management/commands/cmd_02_proper_debug_check.py +14 -0
- package/hooks/validators/test_files/management/commands/cmd_03_debug_check_with_return.py +14 -0
- package/hooks/validators/test_files/management/commands/cmd_04_imported_DEBUG.py +14 -0
- package/hooks/validators/test_files/management/commands/cmd_05_debug_check_in_helper.py +16 -0
- package/hooks/validators/test_files/management/commands/cmd_06_debug_check_late.py +22 -0
- package/hooks/validators/test_files/management/commands/cmd_07_positive_debug_check.py +15 -0
- package/hooks/validators/test_files/management/commands/cmd_08_debug_with_and.py +14 -0
- package/hooks/validators/test_files/not_management_command.py +10 -0
- package/hooks/validators/test_files/skip_decorators/test_01_simple_skip.py +8 -0
- package/hooks/validators/test_files/skip_decorators/test_02_pytest_skipif.py +8 -0
- package/hooks/validators/test_files/skip_decorators/test_03_unittest_skipIf.py +8 -0
- package/hooks/validators/test_files/skip_decorators/test_04_skip_with_parens.py +8 -0
- package/hooks/validators/test_files/skip_decorators/test_05_xfail.py +7 -0
- package/hooks/validators/test_files/skip_decorators/test_06_custom_skip.py +11 -0
- package/hooks/validators/test_files/skip_decorators/test_07_capital_Skip.py +8 -0
- package/hooks/validators/test_files/skip_decorators/test_08_skipUnless.py +7 -0
- package/hooks/validators/test_files/skip_decorators/test_09_pytest_mark_skip_simple.py +7 -0
- package/hooks/validators/test_files/test_async_functions.py +45 -0
- package/hooks/validators/test_files/test_purecomponent/PureComponentExample.tsx +7 -0
- package/hooks/validators/test_files/test_purecomponent/ReactPureComponentExample.tsx +7 -0
- package/hooks/validators/test_git_checks.py +295 -0
- package/hooks/validators/test_good.tsx +5 -0
- package/hooks/validators/test_health_check.py +57 -0
- package/hooks/validators/test_magic_value_checks.py +63 -0
- package/hooks/validators/test_mypy_integration.py +27 -0
- package/hooks/validators/test_output_formatter.py +150 -0
- package/hooks/validators/test_pr_reference_checks.py +41 -0
- package/hooks/validators/test_python_antipattern_checks.py +113 -0
- package/hooks/validators/test_python_style_checks.py +439 -0
- package/hooks/validators/test_react_checks.py +213 -0
- package/hooks/validators/test_results.txt +25 -0
- package/hooks/validators/test_ruff_integration.py +27 -0
- package/hooks/validators/test_run_all_validators.py +228 -0
- package/hooks/validators/test_run_all_validators_integration.py +48 -0
- package/hooks/validators/test_safety_checks.py +243 -0
- package/hooks/validators/test_security_checks.py +105 -0
- package/hooks/validators/test_test_safety_checks.py +321 -0
- package/hooks/validators/test_todo_checks.py +39 -0
- package/hooks/validators/test_type_safety_checks.py +85 -0
- package/hooks/validators/test_useless_test_checks.py +55 -0
- package/hooks/validators/test_validator_base.py +26 -0
- package/hooks/validators/test_verify_paths.py +34 -0
- package/hooks/validators/todo_checks.py +59 -0
- package/hooks/validators/type_safety_checks.py +101 -0
- package/hooks/validators/useless_test_checks.py +92 -0
- package/hooks/validators/validator_base.py +19 -0
- package/hooks/validators/verify_paths.py +57 -0
- package/hooks/workflow/auto-formatter.py +114 -0
- package/hooks/workflow/investigation-tracker-reset.py +46 -0
- package/package.json +30 -0
- package/rules/agent-spawn-protocol.md +47 -0
- package/rules/cleanup-temp-files.md +27 -0
- package/rules/code-reviews.md +11 -0
- package/rules/code-standards.md +43 -0
- package/rules/conservative-action.md +20 -0
- package/rules/context7.md +12 -0
- package/rules/explore-thoroughly.md +27 -0
- package/rules/git-workflow.md +42 -0
- package/rules/parallel-tools.md +23 -0
- package/rules/research-mode.md +23 -0
- package/rules/right-sized-engineering.md +28 -0
- package/rules/tdd.md +7 -0
- package/rules/testing.md +12 -0
- package/skills/agent-prompt/SKILL.md +102 -0
- package/skills/anthropic-plan/SKILL.md +107 -0
- package/skills/everything-search/SKILL.md +144 -0
- package/skills/ingest/SKILL.md +40 -0
- package/skills/npm-creator/SKILL.md +183 -0
- package/skills/pr-review-responder/EXAMPLES.md +590 -0
- package/skills/pr-review-responder/PRINCIPLES.md +539 -0
- package/skills/pr-review-responder/README.md +209 -0
- package/skills/pr-review-responder/SKILL.md +202 -0
- package/skills/pr-review-responder/TESTING.md +407 -0
- package/skills/pr-review-responder/scripts/respond_to_reviews.py +376 -0
- package/skills/pr-review-responder/update_skill.py +297 -0
- package/skills/prompt-generator/REFERENCE.md +150 -0
- package/skills/prompt-generator/SKILL.md +154 -0
- package/skills/readability-review/SKILL.md +127 -0
- package/skills/recall/SKILL.md +27 -0
- package/skills/remember/SKILL.md +63 -0
- package/skills/rule-audit/SKILL.md +307 -0
- package/skills/rule-creator/SKILL.md +150 -0
- package/skills/skill-writer/REFERENCE.md +246 -0
- package/skills/skill-writer/SKILL.md +270 -0
- package/skills/tdd-team/SKILL.md +128 -0
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Tests for AST-based test safety validators."""
|
|
3
|
+
|
|
4
|
+
import tempfile
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import List
|
|
7
|
+
|
|
8
|
+
import pytest
|
|
9
|
+
|
|
10
|
+
from test_safety_checks import (
|
|
11
|
+
Violation,
|
|
12
|
+
check_no_skip_decorators,
|
|
13
|
+
check_debug_guard_in_dev_scripts,
|
|
14
|
+
main,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class TestNoSkipDecorators:
|
|
19
|
+
"""Test detection of skip decorators in test files."""
|
|
20
|
+
|
|
21
|
+
def test_detects_pytest_skip_decorator(self) -> None:
|
|
22
|
+
code = """
|
|
23
|
+
import pytest
|
|
24
|
+
|
|
25
|
+
@pytest.mark.skip
|
|
26
|
+
def test_something():
|
|
27
|
+
pass
|
|
28
|
+
"""
|
|
29
|
+
violations = check_no_skip_decorators(code, "test_file.py")
|
|
30
|
+
assert len(violations) == 1
|
|
31
|
+
assert violations[0].line == 5
|
|
32
|
+
assert "skip decorator" in violations[0].message.lower()
|
|
33
|
+
|
|
34
|
+
def test_detects_pytest_skip_with_reason(self) -> None:
|
|
35
|
+
code = """
|
|
36
|
+
import pytest
|
|
37
|
+
|
|
38
|
+
@pytest.mark.skip(reason="not ready")
|
|
39
|
+
def test_something():
|
|
40
|
+
pass
|
|
41
|
+
"""
|
|
42
|
+
violations = check_no_skip_decorators(code, "test_file.py")
|
|
43
|
+
assert len(violations) == 1
|
|
44
|
+
assert violations[0].line == 5
|
|
45
|
+
|
|
46
|
+
def test_detects_unittest_skip(self) -> None:
|
|
47
|
+
code = """
|
|
48
|
+
import unittest
|
|
49
|
+
|
|
50
|
+
class TestCase(unittest.TestCase):
|
|
51
|
+
@unittest.skip("reason")
|
|
52
|
+
def test_something(self):
|
|
53
|
+
pass
|
|
54
|
+
"""
|
|
55
|
+
violations = check_no_skip_decorators(code, "test_file.py")
|
|
56
|
+
assert len(violations) == 1
|
|
57
|
+
assert violations[0].line == 6
|
|
58
|
+
|
|
59
|
+
def test_detects_skipif_decorator(self) -> None:
|
|
60
|
+
code = """
|
|
61
|
+
import pytest
|
|
62
|
+
|
|
63
|
+
@pytest.mark.skipif(True, reason="reason")
|
|
64
|
+
def test_something():
|
|
65
|
+
pass
|
|
66
|
+
"""
|
|
67
|
+
violations = check_no_skip_decorators(code, "test_file.py")
|
|
68
|
+
assert len(violations) == 1
|
|
69
|
+
assert violations[0].line == 5
|
|
70
|
+
|
|
71
|
+
def test_detects_skipunless_decorator(self) -> None:
|
|
72
|
+
code = """
|
|
73
|
+
import unittest
|
|
74
|
+
|
|
75
|
+
class TestCase(unittest.TestCase):
|
|
76
|
+
@unittest.skipUnless(False, "reason")
|
|
77
|
+
def test_something(self):
|
|
78
|
+
pass
|
|
79
|
+
"""
|
|
80
|
+
violations = check_no_skip_decorators(code, "test_file.py")
|
|
81
|
+
assert len(violations) == 1
|
|
82
|
+
assert violations[0].line == 6
|
|
83
|
+
|
|
84
|
+
def test_detects_multiple_skip_decorators(self) -> None:
|
|
85
|
+
code = """
|
|
86
|
+
import pytest
|
|
87
|
+
|
|
88
|
+
@pytest.mark.skip
|
|
89
|
+
def test_one():
|
|
90
|
+
pass
|
|
91
|
+
|
|
92
|
+
@pytest.mark.skipif(True, reason="reason")
|
|
93
|
+
def test_two():
|
|
94
|
+
pass
|
|
95
|
+
"""
|
|
96
|
+
violations = check_no_skip_decorators(code, "test_file.py")
|
|
97
|
+
assert len(violations) == 2
|
|
98
|
+
assert violations[0].line == 5
|
|
99
|
+
assert violations[1].line == 9
|
|
100
|
+
|
|
101
|
+
def test_allows_other_decorators(self) -> None:
|
|
102
|
+
code = """
|
|
103
|
+
import pytest
|
|
104
|
+
|
|
105
|
+
@pytest.mark.parametrize("x", [1, 2, 3])
|
|
106
|
+
def test_something(x):
|
|
107
|
+
pass
|
|
108
|
+
|
|
109
|
+
@pytest.fixture
|
|
110
|
+
def my_fixture():
|
|
111
|
+
pass
|
|
112
|
+
"""
|
|
113
|
+
violations = check_no_skip_decorators(code, "test_file.py")
|
|
114
|
+
assert len(violations) == 0
|
|
115
|
+
|
|
116
|
+
def test_allows_tests_without_decorators(self) -> None:
|
|
117
|
+
code = """
|
|
118
|
+
def test_something():
|
|
119
|
+
pass
|
|
120
|
+
"""
|
|
121
|
+
violations = check_no_skip_decorators(code, "test_file.py")
|
|
122
|
+
assert len(violations) == 0
|
|
123
|
+
|
|
124
|
+
def test_detects_case_insensitive_skip_decorator(self) -> None:
|
|
125
|
+
"""Test that decorator names are matched case-insensitively."""
|
|
126
|
+
code = """
|
|
127
|
+
import pytest
|
|
128
|
+
|
|
129
|
+
@pytest.mark.Skip
|
|
130
|
+
def test_something():
|
|
131
|
+
pass
|
|
132
|
+
"""
|
|
133
|
+
violations = check_no_skip_decorators(code, "test_file.py")
|
|
134
|
+
assert len(violations) == 1
|
|
135
|
+
assert violations[0].line == 5
|
|
136
|
+
assert "skip decorator" in violations[0].message.lower()
|
|
137
|
+
|
|
138
|
+
def test_detects_case_insensitive_skipif(self) -> None:
|
|
139
|
+
"""Test that SkipIf is caught."""
|
|
140
|
+
code = """
|
|
141
|
+
import unittest
|
|
142
|
+
|
|
143
|
+
class TestCase(unittest.TestCase):
|
|
144
|
+
@unittest.SkipIf(True, "reason")
|
|
145
|
+
def test_something(self):
|
|
146
|
+
pass
|
|
147
|
+
"""
|
|
148
|
+
violations = check_no_skip_decorators(code, "test_file.py")
|
|
149
|
+
assert len(violations) == 1
|
|
150
|
+
assert violations[0].line == 6
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
class TestDebugGuardInDevScripts:
|
|
154
|
+
"""Test detection of missing DEBUG checks in management commands."""
|
|
155
|
+
|
|
156
|
+
def test_detects_missing_debug_check(self) -> None:
|
|
157
|
+
code = """
|
|
158
|
+
from django.core.management.base import BaseCommand
|
|
159
|
+
|
|
160
|
+
class Command(BaseCommand):
|
|
161
|
+
def handle(self, *args, **options):
|
|
162
|
+
print("Doing dangerous thing")
|
|
163
|
+
"""
|
|
164
|
+
violations = check_debug_guard_in_dev_scripts(
|
|
165
|
+
code, "management/commands/dev_tool.py"
|
|
166
|
+
)
|
|
167
|
+
assert len(violations) == 1
|
|
168
|
+
assert "DEBUG" in violations[0].message
|
|
169
|
+
|
|
170
|
+
def test_allows_debug_check_at_start(self) -> None:
|
|
171
|
+
code = """
|
|
172
|
+
from django.core.management.base import BaseCommand
|
|
173
|
+
from django.conf import settings
|
|
174
|
+
|
|
175
|
+
class Command(BaseCommand):
|
|
176
|
+
def handle(self, *args, **options):
|
|
177
|
+
if not settings.DEBUG:
|
|
178
|
+
raise CommandError("Only for development")
|
|
179
|
+
print("Doing thing")
|
|
180
|
+
"""
|
|
181
|
+
violations = check_debug_guard_in_dev_scripts(
|
|
182
|
+
code, "management/commands/dev_tool.py"
|
|
183
|
+
)
|
|
184
|
+
assert len(violations) == 0
|
|
185
|
+
|
|
186
|
+
def test_allows_debug_check_with_return(self) -> None:
|
|
187
|
+
code = """
|
|
188
|
+
from django.core.management.base import BaseCommand
|
|
189
|
+
from django.conf import settings
|
|
190
|
+
|
|
191
|
+
class Command(BaseCommand):
|
|
192
|
+
def handle(self, *args, **options):
|
|
193
|
+
if not settings.DEBUG:
|
|
194
|
+
return
|
|
195
|
+
print("Doing thing")
|
|
196
|
+
"""
|
|
197
|
+
violations = check_debug_guard_in_dev_scripts(
|
|
198
|
+
code, "management/commands/dev_tool.py"
|
|
199
|
+
)
|
|
200
|
+
assert len(violations) == 0
|
|
201
|
+
|
|
202
|
+
def test_ignores_non_management_commands(self) -> None:
|
|
203
|
+
code = """
|
|
204
|
+
def some_function():
|
|
205
|
+
print("Doing thing")
|
|
206
|
+
"""
|
|
207
|
+
violations = check_debug_guard_in_dev_scripts(code, "utils/helper.py")
|
|
208
|
+
assert len(violations) == 0
|
|
209
|
+
|
|
210
|
+
def test_ignores_files_outside_management_commands(self) -> None:
|
|
211
|
+
code = """
|
|
212
|
+
from django.core.management.base import BaseCommand
|
|
213
|
+
|
|
214
|
+
class Command(BaseCommand):
|
|
215
|
+
def handle(self, *args, **options):
|
|
216
|
+
print("Doing thing")
|
|
217
|
+
"""
|
|
218
|
+
violations = check_debug_guard_in_dev_scripts(code, "some/other/path.py")
|
|
219
|
+
assert len(violations) == 0
|
|
220
|
+
|
|
221
|
+
def test_allows_positive_debug_check_with_else_raise(self) -> None:
|
|
222
|
+
"""Test that 'if settings.DEBUG: ... else: raise' is a valid guard."""
|
|
223
|
+
code = """
|
|
224
|
+
from django.core.management.base import BaseCommand
|
|
225
|
+
from django.conf import settings
|
|
226
|
+
|
|
227
|
+
class Command(BaseCommand):
|
|
228
|
+
def handle(self, *args, **options):
|
|
229
|
+
if settings.DEBUG:
|
|
230
|
+
print("OK")
|
|
231
|
+
else:
|
|
232
|
+
raise CommandError("Development only")
|
|
233
|
+
print("Doing thing")
|
|
234
|
+
"""
|
|
235
|
+
violations = check_debug_guard_in_dev_scripts(
|
|
236
|
+
code, "management/commands/dev_tool.py"
|
|
237
|
+
)
|
|
238
|
+
assert len(violations) == 0
|
|
239
|
+
|
|
240
|
+
def test_allows_positive_debug_check_with_else_return(self) -> None:
|
|
241
|
+
"""Test that 'if settings.DEBUG: ... else: return' is a valid guard."""
|
|
242
|
+
code = """
|
|
243
|
+
from django.core.management.base import BaseCommand
|
|
244
|
+
from django.conf import settings
|
|
245
|
+
|
|
246
|
+
class Command(BaseCommand):
|
|
247
|
+
def handle(self, *args, **options):
|
|
248
|
+
if settings.DEBUG:
|
|
249
|
+
pass
|
|
250
|
+
else:
|
|
251
|
+
return
|
|
252
|
+
print("Doing thing")
|
|
253
|
+
"""
|
|
254
|
+
violations = check_debug_guard_in_dev_scripts(
|
|
255
|
+
code, "management/commands/dev_tool.py"
|
|
256
|
+
)
|
|
257
|
+
assert len(violations) == 0
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
class TestViolation:
|
|
261
|
+
"""Test Violation dataclass."""
|
|
262
|
+
|
|
263
|
+
def test_violation_creation(self) -> None:
|
|
264
|
+
v = Violation("test.py", 42, "Test message")
|
|
265
|
+
assert v.file == "test.py"
|
|
266
|
+
assert v.line == 42
|
|
267
|
+
assert v.message == "Test message"
|
|
268
|
+
|
|
269
|
+
def test_violation_str(self) -> None:
|
|
270
|
+
v = Violation("test.py", 42, "Test message")
|
|
271
|
+
assert str(v) == "test.py:42: Test message"
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
class TestMainFunction:
|
|
275
|
+
"""Test main CLI function."""
|
|
276
|
+
|
|
277
|
+
def test_main_with_no_violations(self, tmp_path: Path) -> None:
|
|
278
|
+
test_file = tmp_path / "test_good.py"
|
|
279
|
+
test_file.write_text("def test_something():\n pass\n")
|
|
280
|
+
|
|
281
|
+
exit_code = main([str(test_file)])
|
|
282
|
+
assert exit_code == 0
|
|
283
|
+
|
|
284
|
+
def test_main_with_violations(self, tmp_path: Path, capsys) -> None:
|
|
285
|
+
test_file = tmp_path / "test_bad.py"
|
|
286
|
+
test_file.write_text("import pytest\n\n@pytest.mark.skip\ndef test_x():\n pass\n")
|
|
287
|
+
|
|
288
|
+
exit_code = main([str(test_file)])
|
|
289
|
+
assert exit_code == 1
|
|
290
|
+
|
|
291
|
+
captured = capsys.readouterr()
|
|
292
|
+
assert ":4:" in captured.out
|
|
293
|
+
assert "skip decorator" in captured.out.lower()
|
|
294
|
+
|
|
295
|
+
def test_main_with_multiple_files(self, tmp_path: Path) -> None:
|
|
296
|
+
file1 = tmp_path / "test_good.py"
|
|
297
|
+
file1.write_text("def test_something():\n pass\n")
|
|
298
|
+
|
|
299
|
+
file2 = tmp_path / "test_bad.py"
|
|
300
|
+
file2.write_text("import pytest\n\n@pytest.mark.skip\ndef test_x():\n pass\n")
|
|
301
|
+
|
|
302
|
+
exit_code = main([str(file1), str(file2)])
|
|
303
|
+
assert exit_code == 1
|
|
304
|
+
|
|
305
|
+
def test_main_with_management_command(self, tmp_path: Path, capsys) -> None:
|
|
306
|
+
mgmt_dir = tmp_path / "management" / "commands"
|
|
307
|
+
mgmt_dir.mkdir(parents=True)
|
|
308
|
+
cmd_file = mgmt_dir / "dev_tool.py"
|
|
309
|
+
cmd_file.write_text("""
|
|
310
|
+
from django.core.management.base import BaseCommand
|
|
311
|
+
|
|
312
|
+
class Command(BaseCommand):
|
|
313
|
+
def handle(self, *args, **options):
|
|
314
|
+
print("Dangerous")
|
|
315
|
+
""")
|
|
316
|
+
|
|
317
|
+
exit_code = main([str(cmd_file)])
|
|
318
|
+
assert exit_code == 1
|
|
319
|
+
|
|
320
|
+
captured = capsys.readouterr()
|
|
321
|
+
assert "DEBUG" in captured.out
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""Tests for TODO/FIXME detection."""
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
from todo_checks import check_untracked_todos
|
|
6
|
+
from validator_base import Violation
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
GOOD_TODO_WITH_ISSUE = '''
|
|
10
|
+
def process():
|
|
11
|
+
pass
|
|
12
|
+
'''
|
|
13
|
+
|
|
14
|
+
BAD_TODO_WITHOUT_ISSUE = '''
|
|
15
|
+
def process():
|
|
16
|
+
pass
|
|
17
|
+
'''
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class TestUntrackedTodos:
|
|
21
|
+
def test_no_todo_passes(self) -> None:
|
|
22
|
+
violations = check_untracked_todos(GOOD_TODO_WITH_ISSUE, "test.py")
|
|
23
|
+
assert violations == []
|
|
24
|
+
|
|
25
|
+
def test_todo_without_issue_fails(self) -> None:
|
|
26
|
+
code = "# TODO: fix this later\ndef foo(): pass"
|
|
27
|
+
violations = check_untracked_todos(code, "test.py")
|
|
28
|
+
assert len(violations) == 1
|
|
29
|
+
assert "TODO" in violations[0].message
|
|
30
|
+
|
|
31
|
+
def test_todo_with_issue_passes(self) -> None:
|
|
32
|
+
code = "# TODO #123: fix this later\ndef foo(): pass"
|
|
33
|
+
violations = check_untracked_todos(code, "test.py")
|
|
34
|
+
assert violations == []
|
|
35
|
+
|
|
36
|
+
def test_fixme_without_issue_fails(self) -> None:
|
|
37
|
+
code = "# FIXME: broken\ndef foo(): pass"
|
|
38
|
+
violations = check_untracked_todos(code, "test.py")
|
|
39
|
+
assert len(violations) == 1
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""Tests for type safety checks."""
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
|
|
7
|
+
from type_safety_checks import (
|
|
8
|
+
check_missing_type_hints,
|
|
9
|
+
check_any_type,
|
|
10
|
+
)
|
|
11
|
+
from validator_base import Violation
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
GOOD_FULLY_TYPED = '''
|
|
15
|
+
def process(items: list[str]) -> int:
|
|
16
|
+
return len(items)
|
|
17
|
+
'''
|
|
18
|
+
|
|
19
|
+
BAD_NO_RETURN_TYPE = '''
|
|
20
|
+
def process(items: list[str]):
|
|
21
|
+
return len(items)
|
|
22
|
+
'''
|
|
23
|
+
|
|
24
|
+
BAD_NO_PARAM_TYPE = '''
|
|
25
|
+
def process(items) -> int:
|
|
26
|
+
return len(items)
|
|
27
|
+
'''
|
|
28
|
+
|
|
29
|
+
GOOD_NO_ANY = '''
|
|
30
|
+
from typing import List
|
|
31
|
+
|
|
32
|
+
def process(items: List[str]) -> int:
|
|
33
|
+
return len(items)
|
|
34
|
+
'''
|
|
35
|
+
|
|
36
|
+
BAD_ANY_TYPE = '''
|
|
37
|
+
from typing import Any
|
|
38
|
+
|
|
39
|
+
def process(items: Any) -> int:
|
|
40
|
+
return len(items)
|
|
41
|
+
'''
|
|
42
|
+
|
|
43
|
+
BAD_ANY_RETURN = '''
|
|
44
|
+
from typing import Any, List
|
|
45
|
+
|
|
46
|
+
def process(items: List[str]) -> Any:
|
|
47
|
+
return items[0]
|
|
48
|
+
'''
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class TestMissingTypeHints:
|
|
52
|
+
def test_fully_typed_passes(self) -> None:
|
|
53
|
+
tree = ast.parse(GOOD_FULLY_TYPED)
|
|
54
|
+
violations = check_missing_type_hints(tree, "test.py")
|
|
55
|
+
assert violations == []
|
|
56
|
+
|
|
57
|
+
def test_missing_return_type_fails(self) -> None:
|
|
58
|
+
tree = ast.parse(BAD_NO_RETURN_TYPE)
|
|
59
|
+
violations = check_missing_type_hints(tree, "test.py")
|
|
60
|
+
assert len(violations) == 1
|
|
61
|
+
assert "return" in violations[0].message.lower()
|
|
62
|
+
|
|
63
|
+
def test_missing_param_type_fails(self) -> None:
|
|
64
|
+
tree = ast.parse(BAD_NO_PARAM_TYPE)
|
|
65
|
+
violations = check_missing_type_hints(tree, "test.py")
|
|
66
|
+
assert len(violations) == 1
|
|
67
|
+
assert "items" in violations[0].message or "parameter" in violations[0].message.lower()
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class TestAnyType:
|
|
71
|
+
def test_no_any_passes(self) -> None:
|
|
72
|
+
tree = ast.parse(GOOD_NO_ANY)
|
|
73
|
+
violations = check_any_type(tree, "test.py")
|
|
74
|
+
assert violations == []
|
|
75
|
+
|
|
76
|
+
def test_any_param_fails(self) -> None:
|
|
77
|
+
tree = ast.parse(BAD_ANY_TYPE)
|
|
78
|
+
violations = check_any_type(tree, "test.py")
|
|
79
|
+
assert len(violations) == 1
|
|
80
|
+
assert "Any" in violations[0].message
|
|
81
|
+
|
|
82
|
+
def test_any_return_fails(self) -> None:
|
|
83
|
+
tree = ast.parse(BAD_ANY_RETURN)
|
|
84
|
+
violations = check_any_type(tree, "test.py")
|
|
85
|
+
assert len(violations) == 1
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""Tests for useless test detection."""
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
|
|
7
|
+
from useless_test_checks import (
|
|
8
|
+
check_useless_tests,
|
|
9
|
+
)
|
|
10
|
+
from validator_base import Violation
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
GOOD_BEHAVIOR_TEST = '''
|
|
14
|
+
def test_calculate_total():
|
|
15
|
+
result = calculate_total([10, 20, 30])
|
|
16
|
+
assert result == 60
|
|
17
|
+
'''
|
|
18
|
+
|
|
19
|
+
BAD_CALLABLE_CHECK = '''
|
|
20
|
+
def test_function_exists():
|
|
21
|
+
assert callable(process_data)
|
|
22
|
+
'''
|
|
23
|
+
|
|
24
|
+
BAD_EXISTENCE_CHECK = '''
|
|
25
|
+
def test_constant_exists():
|
|
26
|
+
assert hasattr(module, "CONSTANT")
|
|
27
|
+
'''
|
|
28
|
+
|
|
29
|
+
BAD_CONSTANT_VALUE_TEST = '''
|
|
30
|
+
def test_constant_value():
|
|
31
|
+
assert CACHE_DIR == "cache"
|
|
32
|
+
'''
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class TestUselessTests:
|
|
36
|
+
def test_behavior_test_passes(self) -> None:
|
|
37
|
+
tree = ast.parse(GOOD_BEHAVIOR_TEST)
|
|
38
|
+
violations = check_useless_tests(tree, "test_example.py")
|
|
39
|
+
assert violations == []
|
|
40
|
+
|
|
41
|
+
def test_callable_check_fails(self) -> None:
|
|
42
|
+
tree = ast.parse(BAD_CALLABLE_CHECK)
|
|
43
|
+
violations = check_useless_tests(tree, "test_example.py")
|
|
44
|
+
assert len(violations) == 1
|
|
45
|
+
assert "callable" in violations[0].message.lower()
|
|
46
|
+
|
|
47
|
+
def test_existence_check_fails(self) -> None:
|
|
48
|
+
tree = ast.parse(BAD_EXISTENCE_CHECK)
|
|
49
|
+
violations = check_useless_tests(tree, "test_example.py")
|
|
50
|
+
assert len(violations) == 1
|
|
51
|
+
|
|
52
|
+
def test_constant_value_test_fails(self) -> None:
|
|
53
|
+
tree = ast.parse(BAD_CONSTANT_VALUE_TEST)
|
|
54
|
+
violations = check_useless_tests(tree, "test_example.py")
|
|
55
|
+
assert len(violations) == 1
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""Tests for shared validator base types."""
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
from validator_base import Violation
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class TestViolation:
|
|
9
|
+
def test_violation_str_format(self) -> None:
|
|
10
|
+
violation = Violation(file="test.py", line=42, message="Test message")
|
|
11
|
+
assert str(violation) == "test.py:42: Test message"
|
|
12
|
+
|
|
13
|
+
def test_violation_is_immutable(self) -> None:
|
|
14
|
+
violation = Violation(file="test.py", line=42, message="Test message")
|
|
15
|
+
with pytest.raises(AttributeError):
|
|
16
|
+
violation.file = "other.py"
|
|
17
|
+
|
|
18
|
+
def test_violation_equality(self) -> None:
|
|
19
|
+
v1 = Violation(file="test.py", line=42, message="Test message")
|
|
20
|
+
v2 = Violation(file="test.py", line=42, message="Test message")
|
|
21
|
+
assert v1 == v2
|
|
22
|
+
|
|
23
|
+
def test_violation_hashable(self) -> None:
|
|
24
|
+
violation = Violation(file="test.py", line=42, message="Test message")
|
|
25
|
+
violation_set = {violation}
|
|
26
|
+
assert violation in violation_set
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""Tests for validator path verification script."""
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def test_extract_validator_paths_finds_validator_references() -> None:
|
|
7
|
+
"""Test that validator references are extracted from markdown content."""
|
|
8
|
+
from verify_paths import extract_validator_paths
|
|
9
|
+
|
|
10
|
+
content = """
|
|
11
|
+
**Validator:** `validators/import_checks.py`
|
|
12
|
+
Some text here.
|
|
13
|
+
**Validator:** `validators/style_checks.py`
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
result = extract_validator_paths(content)
|
|
17
|
+
|
|
18
|
+
assert "import_checks.py" in result
|
|
19
|
+
assert "style_checks.py" in result
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def test_extract_validator_paths_deduplicates() -> None:
|
|
23
|
+
"""Test that duplicate validator references are deduplicated."""
|
|
24
|
+
from verify_paths import extract_validator_paths
|
|
25
|
+
|
|
26
|
+
content = """
|
|
27
|
+
**Validator:** `validators/import_checks.py`
|
|
28
|
+
**Validator:** `validators/import_checks.py`
|
|
29
|
+
**Validator:** `validators/import_checks.py`
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
result = extract_validator_paths(content)
|
|
33
|
+
|
|
34
|
+
assert result.count("import_checks.py") == 1
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""TODO/FIXME tracking validator.
|
|
2
|
+
|
|
3
|
+
Implements check 36: TODO without issue reference.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import re
|
|
7
|
+
import sys
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import List
|
|
10
|
+
|
|
11
|
+
from validator_base import Violation
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
TODO_PATTERN = re.compile(r"#\s*(TODO|FIXME)\b(?!.*#\d+)", re.IGNORECASE)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def check_untracked_todos(source: str, filename: str) -> List[Violation]:
|
|
18
|
+
violations: List[Violation] = []
|
|
19
|
+
lines = source.splitlines()
|
|
20
|
+
|
|
21
|
+
for line_num, line in enumerate(lines, start=1):
|
|
22
|
+
if TODO_PATTERN.search(line):
|
|
23
|
+
violations.append(
|
|
24
|
+
Violation(
|
|
25
|
+
filename,
|
|
26
|
+
line_num,
|
|
27
|
+
"TODO/FIXME without issue reference - add #<issue_number>",
|
|
28
|
+
)
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
return violations
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def validate_file(file_path: Path) -> List[Violation]:
|
|
35
|
+
filename = str(file_path)
|
|
36
|
+
try:
|
|
37
|
+
source = file_path.read_text(encoding="utf-8")
|
|
38
|
+
except Exception as error:
|
|
39
|
+
return [Violation(filename, 0, f"Error reading file: {error}")]
|
|
40
|
+
|
|
41
|
+
return check_untracked_todos(source, filename)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def main() -> int:
|
|
45
|
+
if len(sys.argv) < 2:
|
|
46
|
+
return 1
|
|
47
|
+
|
|
48
|
+
all_violations: List[Violation] = []
|
|
49
|
+
for file_arg in sys.argv[1:]:
|
|
50
|
+
all_violations.extend(validate_file(Path(file_arg)))
|
|
51
|
+
|
|
52
|
+
for violation in all_violations:
|
|
53
|
+
print(violation)
|
|
54
|
+
|
|
55
|
+
return 1 if all_violations else 0
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
if __name__ == "__main__":
|
|
59
|
+
sys.exit(main())
|