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,135 @@
|
|
|
1
|
+
"""Security vulnerability detection validator.
|
|
2
|
+
|
|
3
|
+
Implements:
|
|
4
|
+
- Check 27: Hardcoded secrets (API keys, passwords, tokens)
|
|
5
|
+
- Check 28: SQL injection risk (f-strings/format in SQL)
|
|
6
|
+
- Check 29: XSS risk (mark_safe without sanitization)
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import ast
|
|
10
|
+
import re
|
|
11
|
+
import sys
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import List
|
|
14
|
+
|
|
15
|
+
from validator_base import Violation
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
SECRET_PATTERNS: frozenset[str] = frozenset({
|
|
19
|
+
"api_key", "apikey", "api-key",
|
|
20
|
+
"password", "passwd", "pwd",
|
|
21
|
+
"secret", "token", "auth",
|
|
22
|
+
"private_key", "privatekey",
|
|
23
|
+
"credential", "credentials",
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
SQL_EXECUTE_PATTERN = re.compile(r"\.execute\s*\(\s*f['\"]|\.execute\s*\([^)]*\.format\(", re.IGNORECASE)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def check_hardcoded_secrets(tree: ast.AST, filename: str) -> List[Violation]:
|
|
30
|
+
violations: List[Violation] = []
|
|
31
|
+
|
|
32
|
+
for node in ast.walk(tree):
|
|
33
|
+
if isinstance(node, ast.Assign):
|
|
34
|
+
for target in node.targets:
|
|
35
|
+
if isinstance(target, ast.Name):
|
|
36
|
+
var_name = target.id.lower()
|
|
37
|
+
if any(pattern in var_name for pattern in SECRET_PATTERNS):
|
|
38
|
+
if isinstance(node.value, ast.Constant) and isinstance(node.value.value, str):
|
|
39
|
+
if len(node.value.value) > 3:
|
|
40
|
+
violations.append(
|
|
41
|
+
Violation(
|
|
42
|
+
filename,
|
|
43
|
+
node.lineno,
|
|
44
|
+
f"Hardcoded secret in '{target.id}' - use environment variable",
|
|
45
|
+
)
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
return violations
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def check_sql_injection(source: str, filename: str) -> List[Violation]:
|
|
52
|
+
violations: List[Violation] = []
|
|
53
|
+
lines = source.splitlines()
|
|
54
|
+
|
|
55
|
+
for line_num, line in enumerate(lines, start=1):
|
|
56
|
+
if SQL_EXECUTE_PATTERN.search(line):
|
|
57
|
+
violations.append(
|
|
58
|
+
Violation(
|
|
59
|
+
filename,
|
|
60
|
+
line_num,
|
|
61
|
+
"SQL injection risk - use parameterized queries instead of f-string/format",
|
|
62
|
+
)
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
return violations
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def check_xss_risk(tree: ast.AST, filename: str) -> List[Violation]:
|
|
69
|
+
violations: List[Violation] = []
|
|
70
|
+
|
|
71
|
+
for node in ast.walk(tree):
|
|
72
|
+
if isinstance(node, ast.Call):
|
|
73
|
+
if isinstance(node.func, ast.Name) and node.func.id == "mark_safe":
|
|
74
|
+
violations.append(
|
|
75
|
+
Violation(
|
|
76
|
+
filename,
|
|
77
|
+
node.lineno,
|
|
78
|
+
"XSS risk - mark_safe() on user input is dangerous",
|
|
79
|
+
)
|
|
80
|
+
)
|
|
81
|
+
elif isinstance(node.func, ast.Attribute) and node.func.attr == "mark_safe":
|
|
82
|
+
violations.append(
|
|
83
|
+
Violation(
|
|
84
|
+
filename,
|
|
85
|
+
node.lineno,
|
|
86
|
+
"XSS risk - mark_safe() on user input is dangerous",
|
|
87
|
+
)
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
return violations
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def validate_file(file_path: Path) -> List[Violation]:
|
|
94
|
+
violations: List[Violation] = []
|
|
95
|
+
filename = str(file_path)
|
|
96
|
+
|
|
97
|
+
try:
|
|
98
|
+
source = file_path.read_text(encoding="utf-8")
|
|
99
|
+
except Exception as error:
|
|
100
|
+
return [Violation(filename, 0, f"Error reading file: {error}")]
|
|
101
|
+
|
|
102
|
+
try:
|
|
103
|
+
tree = ast.parse(source)
|
|
104
|
+
except SyntaxError as error:
|
|
105
|
+
return [Violation(filename, error.lineno or 0, f"Syntax error: {error.msg}")]
|
|
106
|
+
|
|
107
|
+
violations.extend(check_hardcoded_secrets(tree, filename))
|
|
108
|
+
violations.extend(check_sql_injection(source, filename))
|
|
109
|
+
violations.extend(check_xss_risk(tree, filename))
|
|
110
|
+
|
|
111
|
+
return violations
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def main() -> int:
|
|
115
|
+
if len(sys.argv) < 2:
|
|
116
|
+
print("Usage: security_checks.py <file1.py> [file2.py ...]", file=sys.stderr)
|
|
117
|
+
return 1
|
|
118
|
+
|
|
119
|
+
all_violations: List[Violation] = []
|
|
120
|
+
|
|
121
|
+
for file_arg in sys.argv[1:]:
|
|
122
|
+
file_path = Path(file_arg)
|
|
123
|
+
if not file_path.exists():
|
|
124
|
+
print(f"Error: File not found: {file_path}", file=sys.stderr)
|
|
125
|
+
return 1
|
|
126
|
+
all_violations.extend(validate_file(file_path))
|
|
127
|
+
|
|
128
|
+
for violation in all_violations:
|
|
129
|
+
print(violation)
|
|
130
|
+
|
|
131
|
+
return 1 if all_violations else 0
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
if __name__ == "__main__":
|
|
135
|
+
sys.exit(main())
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""Tests for abbreviation detection."""
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
import pytest
|
|
7
|
+
|
|
8
|
+
from abbreviation_checks import (
|
|
9
|
+
check_single_letter_variables,
|
|
10
|
+
validate_file,
|
|
11
|
+
)
|
|
12
|
+
from validator_base import Violation
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
GOOD_DESCRIPTIVE_NAMES = '''
|
|
16
|
+
def process_data(items):
|
|
17
|
+
item_data = get_item()
|
|
18
|
+
for file_path in files:
|
|
19
|
+
result = process(file_path)
|
|
20
|
+
return [item for item in items if item.active]
|
|
21
|
+
'''
|
|
22
|
+
|
|
23
|
+
BAD_SINGLE_LETTER_ASSIGNMENT = '''
|
|
24
|
+
def process():
|
|
25
|
+
t = get_item()
|
|
26
|
+
return t
|
|
27
|
+
'''
|
|
28
|
+
|
|
29
|
+
BAD_SINGLE_LETTER_LOOP = '''
|
|
30
|
+
def process(files):
|
|
31
|
+
for f in files:
|
|
32
|
+
print(f)
|
|
33
|
+
'''
|
|
34
|
+
|
|
35
|
+
BAD_SINGLE_LETTER_COMPREHENSION = '''
|
|
36
|
+
def process(items):
|
|
37
|
+
return [x for x in items if x.active]
|
|
38
|
+
'''
|
|
39
|
+
|
|
40
|
+
ALLOWED_LOOP_COUNTERS = '''
|
|
41
|
+
def process(matrix):
|
|
42
|
+
for i in range(10):
|
|
43
|
+
for j in range(10):
|
|
44
|
+
for k in range(10):
|
|
45
|
+
matrix[i][j][k] = 0
|
|
46
|
+
'''
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class TestSingleLetterVariables:
|
|
50
|
+
def test_descriptive_names_pass(self) -> None:
|
|
51
|
+
tree = ast.parse(GOOD_DESCRIPTIVE_NAMES)
|
|
52
|
+
violations = check_single_letter_variables(tree, "test.py")
|
|
53
|
+
assert violations == []
|
|
54
|
+
|
|
55
|
+
def test_single_letter_assignment_fails(self) -> None:
|
|
56
|
+
tree = ast.parse(BAD_SINGLE_LETTER_ASSIGNMENT)
|
|
57
|
+
violations = check_single_letter_variables(tree, "test.py")
|
|
58
|
+
assert len(violations) == 1
|
|
59
|
+
assert "t" in violations[0].message
|
|
60
|
+
|
|
61
|
+
def test_single_letter_loop_variable_fails(self) -> None:
|
|
62
|
+
tree = ast.parse(BAD_SINGLE_LETTER_LOOP)
|
|
63
|
+
violations = check_single_letter_variables(tree, "test.py")
|
|
64
|
+
assert len(violations) == 1
|
|
65
|
+
assert "f" in violations[0].message
|
|
66
|
+
|
|
67
|
+
def test_single_letter_comprehension_fails(self) -> None:
|
|
68
|
+
tree = ast.parse(BAD_SINGLE_LETTER_COMPREHENSION)
|
|
69
|
+
violations = check_single_letter_variables(tree, "test.py")
|
|
70
|
+
assert len(violations) == 1
|
|
71
|
+
assert "x" in violations[0].message
|
|
72
|
+
|
|
73
|
+
def test_loop_counters_ijk_allowed(self) -> None:
|
|
74
|
+
tree = ast.parse(ALLOWED_LOOP_COUNTERS)
|
|
75
|
+
violations = check_single_letter_variables(tree, "test.py")
|
|
76
|
+
assert violations == []
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"""Tests for code quality checks."""
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
import tempfile
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
|
|
9
|
+
from code_quality_checks import (
|
|
10
|
+
check_function_length,
|
|
11
|
+
check_nesting_depth,
|
|
12
|
+
check_file_length,
|
|
13
|
+
)
|
|
14
|
+
from validator_base import Violation
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
GOOD_SHORT_FUNCTION = '''
|
|
18
|
+
def process(items):
|
|
19
|
+
result = []
|
|
20
|
+
for item in items:
|
|
21
|
+
if item.active:
|
|
22
|
+
result.append(item)
|
|
23
|
+
return result
|
|
24
|
+
'''
|
|
25
|
+
|
|
26
|
+
BAD_LONG_FUNCTION = '''
|
|
27
|
+
def process(items):
|
|
28
|
+
line1 = 1
|
|
29
|
+
line2 = 2
|
|
30
|
+
line3 = 3
|
|
31
|
+
line4 = 4
|
|
32
|
+
line5 = 5
|
|
33
|
+
line6 = 6
|
|
34
|
+
line7 = 7
|
|
35
|
+
line8 = 8
|
|
36
|
+
line9 = 9
|
|
37
|
+
line10 = 10
|
|
38
|
+
line11 = 11
|
|
39
|
+
line12 = 12
|
|
40
|
+
line13 = 13
|
|
41
|
+
line14 = 14
|
|
42
|
+
line15 = 15
|
|
43
|
+
line16 = 16
|
|
44
|
+
line17 = 17
|
|
45
|
+
line18 = 18
|
|
46
|
+
line19 = 19
|
|
47
|
+
line20 = 20
|
|
48
|
+
line21 = 21
|
|
49
|
+
line22 = 22
|
|
50
|
+
line23 = 23
|
|
51
|
+
line24 = 24
|
|
52
|
+
line25 = 25
|
|
53
|
+
line26 = 26
|
|
54
|
+
line27 = 27
|
|
55
|
+
line28 = 28
|
|
56
|
+
line29 = 29
|
|
57
|
+
line30 = 30
|
|
58
|
+
line31 = 31
|
|
59
|
+
return line31
|
|
60
|
+
'''
|
|
61
|
+
|
|
62
|
+
GOOD_SHALLOW_NESTING = '''
|
|
63
|
+
def process(items):
|
|
64
|
+
for item in items:
|
|
65
|
+
if item.active:
|
|
66
|
+
result.append(item)
|
|
67
|
+
return result
|
|
68
|
+
'''
|
|
69
|
+
|
|
70
|
+
BAD_DEEP_NESTING = '''
|
|
71
|
+
def process(items):
|
|
72
|
+
for item in items:
|
|
73
|
+
if item.active:
|
|
74
|
+
if item.valid:
|
|
75
|
+
if item.enabled:
|
|
76
|
+
result.append(item)
|
|
77
|
+
return result
|
|
78
|
+
'''
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class TestFunctionLength:
|
|
82
|
+
def test_short_function_passes(self) -> None:
|
|
83
|
+
tree = ast.parse(GOOD_SHORT_FUNCTION)
|
|
84
|
+
violations = check_function_length(tree, "test.py")
|
|
85
|
+
assert violations == []
|
|
86
|
+
|
|
87
|
+
def test_long_function_fails(self) -> None:
|
|
88
|
+
tree = ast.parse(BAD_LONG_FUNCTION)
|
|
89
|
+
violations = check_function_length(tree, "test.py")
|
|
90
|
+
assert len(violations) == 1
|
|
91
|
+
assert "30" in violations[0].message or "lines" in violations[0].message.lower()
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class TestNestingDepth:
|
|
95
|
+
def test_shallow_nesting_passes(self) -> None:
|
|
96
|
+
tree = ast.parse(GOOD_SHALLOW_NESTING)
|
|
97
|
+
violations = check_nesting_depth(tree, "test.py")
|
|
98
|
+
assert violations == []
|
|
99
|
+
|
|
100
|
+
def test_deep_nesting_fails(self) -> None:
|
|
101
|
+
tree = ast.parse(BAD_DEEP_NESTING)
|
|
102
|
+
violations = check_nesting_depth(tree, "test.py")
|
|
103
|
+
assert len(violations) == 1
|
|
104
|
+
assert "nesting" in violations[0].message.lower()
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class TestFileLength:
|
|
108
|
+
def test_short_file_passes(self) -> None:
|
|
109
|
+
with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as temp_file:
|
|
110
|
+
temp_file.write("x = 1\n" * 100)
|
|
111
|
+
temp_path = Path(temp_file.name)
|
|
112
|
+
|
|
113
|
+
try:
|
|
114
|
+
violations = check_file_length(temp_path)
|
|
115
|
+
assert violations == []
|
|
116
|
+
finally:
|
|
117
|
+
temp_path.unlink()
|
|
118
|
+
|
|
119
|
+
def test_long_file_fails(self) -> None:
|
|
120
|
+
with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as temp_file:
|
|
121
|
+
temp_file.write("x = 1\n" * 450)
|
|
122
|
+
temp_path = Path(temp_file.name)
|
|
123
|
+
|
|
124
|
+
try:
|
|
125
|
+
violations = check_file_length(temp_path)
|
|
126
|
+
assert len(violations) == 1
|
|
127
|
+
assert "400" in violations[0].message or "lines" in violations[0].message.lower()
|
|
128
|
+
finally:
|
|
129
|
+
temp_path.unlink()
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
"""Tests for file structure validation checks."""
|
|
2
|
+
|
|
3
|
+
import tempfile
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import List
|
|
6
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
|
|
9
|
+
from file_structure_checks import (
|
|
10
|
+
Violation,
|
|
11
|
+
check_multiple_requirements_txt,
|
|
12
|
+
check_empty_init_files,
|
|
13
|
+
main,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class TestMultipleRequirementsTxt:
|
|
18
|
+
"""Test detection of multiple requirements.txt files."""
|
|
19
|
+
|
|
20
|
+
def test_single_requirements_txt_passes(self, tmp_path: Path) -> None:
|
|
21
|
+
"""Single requirements.txt at root should pass."""
|
|
22
|
+
(tmp_path / "requirements.txt").write_text("pytest==7.4.0\n")
|
|
23
|
+
|
|
24
|
+
violations = check_multiple_requirements_txt(tmp_path)
|
|
25
|
+
|
|
26
|
+
assert violations == []
|
|
27
|
+
|
|
28
|
+
def test_multiple_requirements_txt_fails(self, tmp_path: Path) -> None:
|
|
29
|
+
"""Multiple requirements.txt files should fail."""
|
|
30
|
+
(tmp_path / "requirements.txt").write_text("pytest==7.4.0\n")
|
|
31
|
+
tools_dir = tmp_path / "tools"
|
|
32
|
+
tools_dir.mkdir()
|
|
33
|
+
(tools_dir / "requirements.txt").write_text("requests==2.31.0\n")
|
|
34
|
+
|
|
35
|
+
violations = check_multiple_requirements_txt(tmp_path)
|
|
36
|
+
|
|
37
|
+
assert len(violations) == 1
|
|
38
|
+
assert "multiple requirements.txt" in violations[0].message.lower()
|
|
39
|
+
assert "tools/requirements.txt" in violations[0].message
|
|
40
|
+
|
|
41
|
+
def test_no_requirements_txt_passes(self, tmp_path: Path) -> None:
|
|
42
|
+
"""No requirements.txt files should pass (not our concern)."""
|
|
43
|
+
violations = check_multiple_requirements_txt(tmp_path)
|
|
44
|
+
|
|
45
|
+
assert violations == []
|
|
46
|
+
|
|
47
|
+
def test_excludes_venv_directories(self, tmp_path: Path) -> None:
|
|
48
|
+
"""Should ignore requirements.txt in venv/node_modules."""
|
|
49
|
+
(tmp_path / "requirements.txt").write_text("pytest==7.4.0\n")
|
|
50
|
+
|
|
51
|
+
venv_dir = tmp_path / ".venv" / "lib"
|
|
52
|
+
venv_dir.mkdir(parents=True)
|
|
53
|
+
(venv_dir / "requirements.txt").write_text("ignored")
|
|
54
|
+
|
|
55
|
+
node_dir = tmp_path / "node_modules" / "package"
|
|
56
|
+
node_dir.mkdir(parents=True)
|
|
57
|
+
(node_dir / "requirements.txt").write_text("ignored")
|
|
58
|
+
|
|
59
|
+
violations = check_multiple_requirements_txt(tmp_path)
|
|
60
|
+
|
|
61
|
+
assert violations == []
|
|
62
|
+
|
|
63
|
+
def test_three_requirements_files_reports_all(self, tmp_path: Path) -> None:
|
|
64
|
+
"""Should report all extra requirements.txt files."""
|
|
65
|
+
(tmp_path / "requirements.txt").write_text("pytest==7.4.0\n")
|
|
66
|
+
(tmp_path / "tools").mkdir()
|
|
67
|
+
(tmp_path / "tools" / "requirements.txt").write_text("requests==2.31.0\n")
|
|
68
|
+
(tmp_path / "scripts").mkdir()
|
|
69
|
+
(tmp_path / "scripts" / "requirements.txt").write_text("pandas==2.0.0\n")
|
|
70
|
+
|
|
71
|
+
violations = check_multiple_requirements_txt(tmp_path)
|
|
72
|
+
|
|
73
|
+
assert len(violations) == 1
|
|
74
|
+
assert "tools/requirements.txt" in violations[0].message
|
|
75
|
+
assert "scripts/requirements.txt" in violations[0].message
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class TestEmptyInitFiles:
|
|
79
|
+
"""Test detection of empty __init__.py files."""
|
|
80
|
+
|
|
81
|
+
def test_no_init_files_passes(self, tmp_path: Path) -> None:
|
|
82
|
+
"""No __init__.py files should pass."""
|
|
83
|
+
violations = check_empty_init_files(tmp_path)
|
|
84
|
+
|
|
85
|
+
assert violations == []
|
|
86
|
+
|
|
87
|
+
def test_non_empty_init_passes(self, tmp_path: Path) -> None:
|
|
88
|
+
"""__init__.py with content should pass."""
|
|
89
|
+
package_dir = tmp_path / "mypackage"
|
|
90
|
+
package_dir.mkdir()
|
|
91
|
+
(package_dir / "__init__.py").write_text("from .module import func\n")
|
|
92
|
+
|
|
93
|
+
violations = check_empty_init_files(tmp_path)
|
|
94
|
+
|
|
95
|
+
assert violations == []
|
|
96
|
+
|
|
97
|
+
def test_empty_init_fails(self, tmp_path: Path) -> None:
|
|
98
|
+
"""Empty __init__.py should fail."""
|
|
99
|
+
package_dir = tmp_path / "mypackage"
|
|
100
|
+
package_dir.mkdir()
|
|
101
|
+
(package_dir / "__init__.py").write_text("")
|
|
102
|
+
|
|
103
|
+
violations = check_empty_init_files(tmp_path)
|
|
104
|
+
|
|
105
|
+
assert len(violations) == 1
|
|
106
|
+
assert violations[0].file == "mypackage/__init__.py"
|
|
107
|
+
assert violations[0].line == 1
|
|
108
|
+
assert "empty" in violations[0].message.lower()
|
|
109
|
+
|
|
110
|
+
def test_whitespace_only_init_fails(self, tmp_path: Path) -> None:
|
|
111
|
+
"""__init__.py with only whitespace should fail."""
|
|
112
|
+
package_dir = tmp_path / "mypackage"
|
|
113
|
+
package_dir.mkdir()
|
|
114
|
+
(package_dir / "__init__.py").write_text(" \n\n \t\n")
|
|
115
|
+
|
|
116
|
+
violations = check_empty_init_files(tmp_path)
|
|
117
|
+
|
|
118
|
+
assert len(violations) == 1
|
|
119
|
+
assert "empty" in violations[0].message.lower()
|
|
120
|
+
|
|
121
|
+
def test_multiple_empty_init_files(self, tmp_path: Path) -> None:
|
|
122
|
+
"""Should detect all empty __init__.py files."""
|
|
123
|
+
(tmp_path / "pkg1").mkdir()
|
|
124
|
+
(tmp_path / "pkg1" / "__init__.py").write_text("")
|
|
125
|
+
|
|
126
|
+
(tmp_path / "pkg2").mkdir()
|
|
127
|
+
(tmp_path / "pkg2" / "__init__.py").write_text(" ")
|
|
128
|
+
|
|
129
|
+
(tmp_path / "pkg3").mkdir()
|
|
130
|
+
(tmp_path / "pkg3" / "__init__.py").write_text("# comment\n")
|
|
131
|
+
|
|
132
|
+
violations = check_empty_init_files(tmp_path)
|
|
133
|
+
|
|
134
|
+
assert len(violations) == 2
|
|
135
|
+
files = [v.file for v in violations]
|
|
136
|
+
assert "pkg1/__init__.py" in files
|
|
137
|
+
assert "pkg2/__init__.py" in files
|
|
138
|
+
|
|
139
|
+
def test_excludes_venv_directories(self, tmp_path: Path) -> None:
|
|
140
|
+
"""Should ignore __init__.py in excluded directories."""
|
|
141
|
+
venv_dir = tmp_path / ".venv" / "lib" / "package"
|
|
142
|
+
venv_dir.mkdir(parents=True)
|
|
143
|
+
(venv_dir / "__init__.py").write_text("")
|
|
144
|
+
|
|
145
|
+
node_dir = tmp_path / "node_modules" / "package"
|
|
146
|
+
node_dir.mkdir(parents=True)
|
|
147
|
+
(node_dir / "__init__.py").write_text("")
|
|
148
|
+
|
|
149
|
+
violations = check_empty_init_files(tmp_path)
|
|
150
|
+
|
|
151
|
+
assert violations == []
|
|
152
|
+
|
|
153
|
+
def test_excludes_django_migrations_init(self, tmp_path: Path) -> None:
|
|
154
|
+
"""Should ignore empty __init__.py in Django migrations directory."""
|
|
155
|
+
migrations_dir = tmp_path / "myapp" / "migrations"
|
|
156
|
+
migrations_dir.mkdir(parents=True)
|
|
157
|
+
(migrations_dir / "__init__.py").write_text("")
|
|
158
|
+
|
|
159
|
+
violations = check_empty_init_files(tmp_path)
|
|
160
|
+
|
|
161
|
+
assert violations == []
|
|
162
|
+
|
|
163
|
+
def test_excludes_django_management_init(self, tmp_path: Path) -> None:
|
|
164
|
+
"""Should ignore empty __init__.py in Django management directory."""
|
|
165
|
+
mgmt_dir = tmp_path / "myapp" / "management"
|
|
166
|
+
mgmt_dir.mkdir(parents=True)
|
|
167
|
+
(mgmt_dir / "__init__.py").write_text("")
|
|
168
|
+
|
|
169
|
+
violations = check_empty_init_files(tmp_path)
|
|
170
|
+
|
|
171
|
+
assert violations == []
|
|
172
|
+
|
|
173
|
+
def test_excludes_django_commands_init(self, tmp_path: Path) -> None:
|
|
174
|
+
"""Should ignore empty __init__.py in Django commands directory."""
|
|
175
|
+
cmd_dir = tmp_path / "myapp" / "management" / "commands"
|
|
176
|
+
cmd_dir.mkdir(parents=True)
|
|
177
|
+
(cmd_dir / "__init__.py").write_text("")
|
|
178
|
+
|
|
179
|
+
violations = check_empty_init_files(tmp_path)
|
|
180
|
+
|
|
181
|
+
assert violations == []
|
|
182
|
+
|
|
183
|
+
def test_excludes_django_templatetags_init(self, tmp_path: Path) -> None:
|
|
184
|
+
"""Should ignore empty __init__.py in Django templatetags directory."""
|
|
185
|
+
tags_dir = tmp_path / "myapp" / "templatetags"
|
|
186
|
+
tags_dir.mkdir(parents=True)
|
|
187
|
+
(tags_dir / "__init__.py").write_text("")
|
|
188
|
+
|
|
189
|
+
violations = check_empty_init_files(tmp_path)
|
|
190
|
+
|
|
191
|
+
assert violations == []
|
|
192
|
+
|
|
193
|
+
def test_still_catches_non_django_empty_init(self, tmp_path: Path) -> None:
|
|
194
|
+
"""Should still catch empty __init__.py in non-Django directories."""
|
|
195
|
+
# Django directory - should be skipped
|
|
196
|
+
migrations_dir = tmp_path / "myapp" / "migrations"
|
|
197
|
+
migrations_dir.mkdir(parents=True)
|
|
198
|
+
(migrations_dir / "__init__.py").write_text("")
|
|
199
|
+
|
|
200
|
+
# Non-Django directory - should be caught
|
|
201
|
+
utils_dir = tmp_path / "myapp" / "utils"
|
|
202
|
+
utils_dir.mkdir(parents=True)
|
|
203
|
+
(utils_dir / "__init__.py").write_text("")
|
|
204
|
+
|
|
205
|
+
violations = check_empty_init_files(tmp_path)
|
|
206
|
+
|
|
207
|
+
assert len(violations) == 1
|
|
208
|
+
assert "utils/__init__.py" in violations[0].file
|
|
209
|
+
|
|
210
|
+
def test_excludes_django_app_with_models(self, tmp_path: Path) -> None:
|
|
211
|
+
"""Should ignore empty __init__.py in Django app directory with models.py."""
|
|
212
|
+
app_dir = tmp_path / "myapp"
|
|
213
|
+
app_dir.mkdir()
|
|
214
|
+
(app_dir / "__init__.py").write_text("")
|
|
215
|
+
(app_dir / "models.py").write_text("from django.db import models\n")
|
|
216
|
+
|
|
217
|
+
violations = check_empty_init_files(tmp_path)
|
|
218
|
+
|
|
219
|
+
assert violations == []
|
|
220
|
+
|
|
221
|
+
def test_excludes_django_app_with_views(self, tmp_path: Path) -> None:
|
|
222
|
+
"""Should ignore empty __init__.py in Django app directory with views.py."""
|
|
223
|
+
app_dir = tmp_path / "myapp"
|
|
224
|
+
app_dir.mkdir()
|
|
225
|
+
(app_dir / "__init__.py").write_text("")
|
|
226
|
+
(app_dir / "views.py").write_text("from django.http import HttpResponse\n")
|
|
227
|
+
|
|
228
|
+
violations = check_empty_init_files(tmp_path)
|
|
229
|
+
|
|
230
|
+
assert violations == []
|
|
231
|
+
|
|
232
|
+
def test_excludes_django_app_with_apps(self, tmp_path: Path) -> None:
|
|
233
|
+
"""Should ignore empty __init__.py in Django app directory with apps.py."""
|
|
234
|
+
app_dir = tmp_path / "myapp"
|
|
235
|
+
app_dir.mkdir()
|
|
236
|
+
(app_dir / "__init__.py").write_text("")
|
|
237
|
+
(app_dir / "apps.py").write_text("from django.apps import AppConfig\n")
|
|
238
|
+
|
|
239
|
+
violations = check_empty_init_files(tmp_path)
|
|
240
|
+
|
|
241
|
+
assert violations == []
|
|
242
|
+
|
|
243
|
+
def test_excludes_django_settings_module(self, tmp_path: Path) -> None:
|
|
244
|
+
"""Should ignore empty __init__.py in Django settings module."""
|
|
245
|
+
settings_dir = tmp_path / "myproject"
|
|
246
|
+
settings_dir.mkdir()
|
|
247
|
+
(settings_dir / "__init__.py").write_text("")
|
|
248
|
+
(settings_dir / "settings.py").write_text("DEBUG = True\n")
|
|
249
|
+
|
|
250
|
+
violations = check_empty_init_files(tmp_path)
|
|
251
|
+
|
|
252
|
+
assert violations == []
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
class TestMain:
|
|
256
|
+
"""Test main function integration."""
|
|
257
|
+
|
|
258
|
+
def test_main_no_violations_exits_zero(self, tmp_path: Path, capsys) -> None:
|
|
259
|
+
"""main() should exit 0 when no violations found."""
|
|
260
|
+
(tmp_path / "requirements.txt").write_text("pytest==7.4.0\n")
|
|
261
|
+
|
|
262
|
+
with pytest.raises(SystemExit) as exc_info:
|
|
263
|
+
main([str(tmp_path)])
|
|
264
|
+
|
|
265
|
+
assert exc_info.value.code == 0
|
|
266
|
+
captured = capsys.readouterr()
|
|
267
|
+
assert captured.out == ""
|
|
268
|
+
|
|
269
|
+
def test_main_with_violations_exits_one(self, tmp_path: Path, capsys) -> None:
|
|
270
|
+
"""main() should exit 1 and print violations when found."""
|
|
271
|
+
package_dir = tmp_path / "mypackage"
|
|
272
|
+
package_dir.mkdir()
|
|
273
|
+
(package_dir / "__init__.py").write_text("")
|
|
274
|
+
|
|
275
|
+
with pytest.raises(SystemExit) as exc_info:
|
|
276
|
+
main([str(tmp_path)])
|
|
277
|
+
|
|
278
|
+
assert exc_info.value.code == 1
|
|
279
|
+
captured = capsys.readouterr()
|
|
280
|
+
assert "mypackage/__init__.py:1:" in captured.out
|
|
281
|
+
assert "empty" in captured.out.lower()
|
|
282
|
+
|
|
283
|
+
def test_main_prints_all_violations(self, tmp_path: Path, capsys) -> None:
|
|
284
|
+
"""main() should print all violations in file:line: format."""
|
|
285
|
+
(tmp_path / "requirements.txt").write_text("pytest==7.4.0\n")
|
|
286
|
+
(tmp_path / "tools").mkdir()
|
|
287
|
+
(tmp_path / "tools" / "requirements.txt").write_text("requests==2.31.0\n")
|
|
288
|
+
|
|
289
|
+
(tmp_path / "pkg").mkdir()
|
|
290
|
+
(tmp_path / "pkg" / "__init__.py").write_text("")
|
|
291
|
+
|
|
292
|
+
with pytest.raises(SystemExit) as exc_info:
|
|
293
|
+
main([str(tmp_path)])
|
|
294
|
+
|
|
295
|
+
assert exc_info.value.code == 1
|
|
296
|
+
captured = capsys.readouterr()
|
|
297
|
+
assert "requirements.txt:1:" in captured.out
|
|
298
|
+
assert "pkg/__init__.py:1:" in captured.out
|
|
299
|
+
|
|
300
|
+
def test_main_requires_project_root_arg(self, capsys) -> None:
|
|
301
|
+
"""main() should exit 1 if no project root provided."""
|
|
302
|
+
with pytest.raises(SystemExit) as exc_info:
|
|
303
|
+
main([])
|
|
304
|
+
|
|
305
|
+
assert exc_info.value.code == 1
|
|
306
|
+
captured = capsys.readouterr()
|
|
307
|
+
assert "usage" in captured.out.lower()
|