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,364 @@
|
|
|
1
|
+
"""Python style checks using AST-based validation.
|
|
2
|
+
|
|
3
|
+
Implements four style checks:
|
|
4
|
+
1. Imports at top of file
|
|
5
|
+
2. No empty lines after decorators
|
|
6
|
+
3. Single empty line between functions
|
|
7
|
+
4. View functions end with _view suffix
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import ast
|
|
11
|
+
import sys
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import List
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
# Constants
|
|
18
|
+
VIEW_SUFFIX = "_view"
|
|
19
|
+
REQUEST_PARAM = "request"
|
|
20
|
+
VIEWS_FILENAME = "views.py"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass
|
|
24
|
+
class Violation:
|
|
25
|
+
"""Represents a style violation."""
|
|
26
|
+
|
|
27
|
+
file: str
|
|
28
|
+
line: int
|
|
29
|
+
message: str
|
|
30
|
+
|
|
31
|
+
def __str__(self) -> str:
|
|
32
|
+
"""Format as file:line: message."""
|
|
33
|
+
return f"{self.file}:{self.line}: {self.message}"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def check_imports_at_top(tree: ast.AST, filename: str) -> List[Violation]:
|
|
37
|
+
"""Check that all imports are at the top of the file.
|
|
38
|
+
|
|
39
|
+
Catches two violations:
|
|
40
|
+
1. Module-level imports after non-import statements
|
|
41
|
+
2. Imports inside functions/classes (inline imports)
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
tree: AST tree to check
|
|
45
|
+
filename: Name of file being checked
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
List of violations found
|
|
49
|
+
"""
|
|
50
|
+
violations: List[Violation] = []
|
|
51
|
+
|
|
52
|
+
# Check 1: Module-level imports must be at top
|
|
53
|
+
if isinstance(tree, ast.Module):
|
|
54
|
+
seen_non_import = False
|
|
55
|
+
for child in tree.body:
|
|
56
|
+
if isinstance(child, (ast.Import, ast.ImportFrom)):
|
|
57
|
+
if seen_non_import:
|
|
58
|
+
violations.append(
|
|
59
|
+
Violation(
|
|
60
|
+
filename,
|
|
61
|
+
child.lineno,
|
|
62
|
+
"Import statement must be at top of file",
|
|
63
|
+
)
|
|
64
|
+
)
|
|
65
|
+
elif isinstance(child, ast.Expr) and isinstance(child.value, ast.Constant):
|
|
66
|
+
# Allow docstrings at top
|
|
67
|
+
if isinstance(child.value.value, str):
|
|
68
|
+
continue
|
|
69
|
+
seen_non_import = True
|
|
70
|
+
else:
|
|
71
|
+
seen_non_import = True
|
|
72
|
+
|
|
73
|
+
# Check 2: No imports inside functions or methods
|
|
74
|
+
for node in ast.walk(tree):
|
|
75
|
+
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
76
|
+
for child in ast.walk(node):
|
|
77
|
+
if isinstance(child, (ast.Import, ast.ImportFrom)):
|
|
78
|
+
violations.append(
|
|
79
|
+
Violation(
|
|
80
|
+
filename,
|
|
81
|
+
child.lineno,
|
|
82
|
+
"Import inside function - move to top of file",
|
|
83
|
+
)
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
return violations
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def check_no_empty_line_after_decorators(source: str, filename: str) -> List[Violation]:
|
|
90
|
+
"""Check that decorators have no empty line before function.
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
source: Source code as string
|
|
94
|
+
filename: Name of file being checked
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
List of violations found
|
|
98
|
+
"""
|
|
99
|
+
violations: List[Violation] = []
|
|
100
|
+
lines = source.splitlines()
|
|
101
|
+
|
|
102
|
+
try:
|
|
103
|
+
tree = ast.parse(source)
|
|
104
|
+
except SyntaxError:
|
|
105
|
+
return violations
|
|
106
|
+
|
|
107
|
+
for node in ast.walk(tree):
|
|
108
|
+
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)) and node.decorator_list:
|
|
109
|
+
# Get last decorator line
|
|
110
|
+
last_decorator_line = max(d.lineno for d in node.decorator_list)
|
|
111
|
+
function_line = node.lineno
|
|
112
|
+
|
|
113
|
+
# Check if there's an empty line between decorator and function
|
|
114
|
+
if function_line - last_decorator_line > 1:
|
|
115
|
+
violations.append(
|
|
116
|
+
Violation(
|
|
117
|
+
filename,
|
|
118
|
+
last_decorator_line,
|
|
119
|
+
"No empty line allowed between decorator and function",
|
|
120
|
+
)
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
return violations
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def check_single_empty_line_between_functions(
|
|
127
|
+
source: str, filename: str
|
|
128
|
+
) -> List[Violation]:
|
|
129
|
+
"""Check that functions have exactly one empty line between them.
|
|
130
|
+
|
|
131
|
+
Args:
|
|
132
|
+
source: Source code as string
|
|
133
|
+
filename: Name of file being checked
|
|
134
|
+
|
|
135
|
+
Returns:
|
|
136
|
+
List of violations found
|
|
137
|
+
"""
|
|
138
|
+
violations: List[Violation] = []
|
|
139
|
+
lines = source.splitlines()
|
|
140
|
+
|
|
141
|
+
try:
|
|
142
|
+
tree = ast.parse(source)
|
|
143
|
+
except SyntaxError:
|
|
144
|
+
return violations
|
|
145
|
+
|
|
146
|
+
# Get all top-level function definitions
|
|
147
|
+
functions = [
|
|
148
|
+
node for node in ast.walk(tree) if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef))
|
|
149
|
+
]
|
|
150
|
+
|
|
151
|
+
# Filter to only top-level functions (not nested)
|
|
152
|
+
if isinstance(tree, ast.Module):
|
|
153
|
+
top_level_functions = [
|
|
154
|
+
node for node in tree.body if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef))
|
|
155
|
+
]
|
|
156
|
+
else:
|
|
157
|
+
top_level_functions = []
|
|
158
|
+
|
|
159
|
+
# Sort by line number
|
|
160
|
+
top_level_functions.sort(key=lambda f: f.lineno)
|
|
161
|
+
|
|
162
|
+
# Check spacing between consecutive functions
|
|
163
|
+
for i in range(len(top_level_functions) - 1):
|
|
164
|
+
current_func = top_level_functions[i]
|
|
165
|
+
next_func = top_level_functions[i + 1]
|
|
166
|
+
|
|
167
|
+
# Find last line of current function
|
|
168
|
+
current_end = current_func.end_lineno
|
|
169
|
+
next_start = next_func.lineno
|
|
170
|
+
|
|
171
|
+
if current_end is not None:
|
|
172
|
+
# Calculate empty lines between functions
|
|
173
|
+
empty_lines = next_start - current_end - 1
|
|
174
|
+
|
|
175
|
+
if empty_lines != 1:
|
|
176
|
+
violations.append(
|
|
177
|
+
Violation(
|
|
178
|
+
filename,
|
|
179
|
+
current_end,
|
|
180
|
+
f"Expected 1 empty line between functions, found {empty_lines}",
|
|
181
|
+
)
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
return violations
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def check_view_function_naming(tree: ast.AST, filename: str) -> List[Violation]:
|
|
188
|
+
"""Check that view functions end with _view suffix.
|
|
189
|
+
|
|
190
|
+
Only applies to functions in views.py that have 'request' as first parameter.
|
|
191
|
+
|
|
192
|
+
Args:
|
|
193
|
+
tree: AST tree to check
|
|
194
|
+
filename: Name of file being checked
|
|
195
|
+
|
|
196
|
+
Returns:
|
|
197
|
+
List of violations found
|
|
198
|
+
"""
|
|
199
|
+
violations: List[Violation] = []
|
|
200
|
+
|
|
201
|
+
# Only check files named views.py
|
|
202
|
+
if not filename.endswith(VIEWS_FILENAME):
|
|
203
|
+
return violations
|
|
204
|
+
|
|
205
|
+
for node in ast.walk(tree):
|
|
206
|
+
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
207
|
+
# Check if first parameter is 'request'
|
|
208
|
+
if node.args.args and node.args.args[0].arg == REQUEST_PARAM:
|
|
209
|
+
# Check if function name ends with _view
|
|
210
|
+
if not node.name.endswith(VIEW_SUFFIX):
|
|
211
|
+
violations.append(
|
|
212
|
+
Violation(
|
|
213
|
+
filename,
|
|
214
|
+
node.lineno,
|
|
215
|
+
f"View function '{node.name}' must end with '{VIEW_SUFFIX}'",
|
|
216
|
+
)
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
return violations
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def fix_empty_lines_after_decorators(source: str) -> str:
|
|
223
|
+
"""Remove empty lines between decorators and function definitions.
|
|
224
|
+
|
|
225
|
+
Args:
|
|
226
|
+
source: Source code as string
|
|
227
|
+
|
|
228
|
+
Returns:
|
|
229
|
+
Fixed source code
|
|
230
|
+
"""
|
|
231
|
+
lines = source.splitlines(keepends=True)
|
|
232
|
+
result_lines: List[str] = []
|
|
233
|
+
skip_next_blank = False
|
|
234
|
+
|
|
235
|
+
for line in lines:
|
|
236
|
+
stripped = line.strip()
|
|
237
|
+
|
|
238
|
+
if stripped.startswith("@"):
|
|
239
|
+
skip_next_blank = True
|
|
240
|
+
result_lines.append(line)
|
|
241
|
+
elif skip_next_blank and stripped == "":
|
|
242
|
+
continue
|
|
243
|
+
else:
|
|
244
|
+
skip_next_blank = False
|
|
245
|
+
result_lines.append(line)
|
|
246
|
+
|
|
247
|
+
return "".join(result_lines)
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def fix_multiple_blank_lines(source: str) -> str:
|
|
251
|
+
"""Collapse multiple blank lines between functions to single blank line.
|
|
252
|
+
|
|
253
|
+
Args:
|
|
254
|
+
source: Source code as string
|
|
255
|
+
|
|
256
|
+
Returns:
|
|
257
|
+
Fixed source code
|
|
258
|
+
"""
|
|
259
|
+
lines = source.splitlines(keepends=True)
|
|
260
|
+
result_lines: List[str] = []
|
|
261
|
+
blank_count = 0
|
|
262
|
+
|
|
263
|
+
for line in lines:
|
|
264
|
+
if line.strip() == "":
|
|
265
|
+
blank_count += 1
|
|
266
|
+
if blank_count <= 1:
|
|
267
|
+
result_lines.append(line)
|
|
268
|
+
else:
|
|
269
|
+
blank_count = 0
|
|
270
|
+
result_lines.append(line)
|
|
271
|
+
|
|
272
|
+
return "".join(result_lines)
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def fix_file(file_path: Path) -> bool:
|
|
276
|
+
"""Apply all safe fixes to a file.
|
|
277
|
+
|
|
278
|
+
Args:
|
|
279
|
+
file_path: Path to file to fix
|
|
280
|
+
|
|
281
|
+
Returns:
|
|
282
|
+
True if any fixes were applied, False otherwise
|
|
283
|
+
"""
|
|
284
|
+
try:
|
|
285
|
+
original = file_path.read_text(encoding="utf-8")
|
|
286
|
+
except Exception:
|
|
287
|
+
return False
|
|
288
|
+
|
|
289
|
+
fixed = original
|
|
290
|
+
fixed = fix_empty_lines_after_decorators(fixed)
|
|
291
|
+
fixed = fix_multiple_blank_lines(fixed)
|
|
292
|
+
|
|
293
|
+
if fixed != original:
|
|
294
|
+
file_path.write_text(fixed, encoding="utf-8")
|
|
295
|
+
return True
|
|
296
|
+
|
|
297
|
+
return False
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def validate_file(file_path: Path) -> List[Violation]:
|
|
301
|
+
"""Validate a Python file with all style checks.
|
|
302
|
+
|
|
303
|
+
Args:
|
|
304
|
+
file_path: Path to Python file to validate
|
|
305
|
+
|
|
306
|
+
Returns:
|
|
307
|
+
List of all violations found
|
|
308
|
+
"""
|
|
309
|
+
violations: List[Violation] = []
|
|
310
|
+
filename = str(file_path)
|
|
311
|
+
|
|
312
|
+
try:
|
|
313
|
+
source = file_path.read_text(encoding="utf-8")
|
|
314
|
+
except Exception as e:
|
|
315
|
+
violations.append(Violation(filename, 0, f"Error reading file: {e}"))
|
|
316
|
+
return violations
|
|
317
|
+
|
|
318
|
+
try:
|
|
319
|
+
tree = ast.parse(source)
|
|
320
|
+
except SyntaxError as e:
|
|
321
|
+
violations.append(
|
|
322
|
+
Violation(filename, e.lineno or 0, f"Syntax error: {e.msg}")
|
|
323
|
+
)
|
|
324
|
+
return violations
|
|
325
|
+
|
|
326
|
+
# Run all checks
|
|
327
|
+
violations.extend(check_imports_at_top(tree, filename))
|
|
328
|
+
violations.extend(check_no_empty_line_after_decorators(source, filename))
|
|
329
|
+
violations.extend(check_single_empty_line_between_functions(source, filename))
|
|
330
|
+
violations.extend(check_view_function_naming(tree, filename))
|
|
331
|
+
|
|
332
|
+
return violations
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def main() -> int:
|
|
336
|
+
"""Main entry point for command-line usage.
|
|
337
|
+
|
|
338
|
+
Returns:
|
|
339
|
+
Exit code: 0 if all files pass, 1 if violations found
|
|
340
|
+
"""
|
|
341
|
+
if len(sys.argv) < 2:
|
|
342
|
+
print("Usage: python_style_checks.py <file1.py> [file2.py ...]", file=sys.stderr)
|
|
343
|
+
return 1
|
|
344
|
+
|
|
345
|
+
all_violations: List[Violation] = []
|
|
346
|
+
|
|
347
|
+
for file_arg in sys.argv[1:]:
|
|
348
|
+
file_path = Path(file_arg)
|
|
349
|
+
if not file_path.exists():
|
|
350
|
+
print(f"Error: File not found: {file_path}", file=sys.stderr)
|
|
351
|
+
return 1
|
|
352
|
+
|
|
353
|
+
violations = validate_file(file_path)
|
|
354
|
+
all_violations.extend(violations)
|
|
355
|
+
|
|
356
|
+
# Print all violations
|
|
357
|
+
for violation in all_violations:
|
|
358
|
+
print(violation)
|
|
359
|
+
|
|
360
|
+
return 1 if all_violations else 0
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
if __name__ == "__main__":
|
|
364
|
+
sys.exit(main())
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"""React code quality validators.
|
|
2
|
+
|
|
3
|
+
Validates React-specific code standards:
|
|
4
|
+
- No class components (use functional components with hooks)
|
|
5
|
+
- Exception: Error boundaries (until React adds hook-based error boundaries)
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import re
|
|
9
|
+
import sys
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass(frozen=True)
|
|
15
|
+
class Violation:
|
|
16
|
+
"""Represents a validation violation."""
|
|
17
|
+
file: str
|
|
18
|
+
line: int
|
|
19
|
+
message: str
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
CLASS_COMPONENT_PATTERN = re.compile(
|
|
23
|
+
r'^\s*class\s+\w+\s+extends\s+(Component|React\.Component|PureComponent|React\.PureComponent)\b',
|
|
24
|
+
re.MULTILINE
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
CLASS_KEYWORD_PATTERN = re.compile(r'\bclass\b')
|
|
28
|
+
|
|
29
|
+
ERROR_BOUNDARY_PATTERN = re.compile(
|
|
30
|
+
r'\b(componentDidCatch|getDerivedStateFromError)\b'
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def check_no_class_components(file_paths: list[str]) -> list[Violation]:
|
|
35
|
+
"""Check that no class components exist (except error boundaries).
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
file_paths: List of file paths to check
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
List of violations found
|
|
42
|
+
"""
|
|
43
|
+
violations: list[Violation] = []
|
|
44
|
+
|
|
45
|
+
for file_path_str in file_paths:
|
|
46
|
+
file_path = Path(file_path_str)
|
|
47
|
+
|
|
48
|
+
if file_path.suffix not in {'.tsx', '.jsx'}:
|
|
49
|
+
continue
|
|
50
|
+
|
|
51
|
+
content = file_path.read_text(encoding='utf-8')
|
|
52
|
+
|
|
53
|
+
if ERROR_BOUNDARY_PATTERN.search(content):
|
|
54
|
+
continue
|
|
55
|
+
|
|
56
|
+
for match in CLASS_COMPONENT_PATTERN.finditer(content):
|
|
57
|
+
class_match = CLASS_KEYWORD_PATTERN.search(match.group(0))
|
|
58
|
+
if class_match:
|
|
59
|
+
class_position = match.start() + class_match.start()
|
|
60
|
+
line_num = content[:class_position].count('\n') + 1
|
|
61
|
+
violations.append(Violation(
|
|
62
|
+
file=file_path_str,
|
|
63
|
+
line=line_num,
|
|
64
|
+
message="Use functional components with hooks instead of class components"
|
|
65
|
+
))
|
|
66
|
+
|
|
67
|
+
return violations
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def main() -> int:
|
|
71
|
+
"""Main entry point for command-line usage.
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
Exit code: 0 if all checks pass, 1 if violations found
|
|
75
|
+
"""
|
|
76
|
+
if len(sys.argv) < 2:
|
|
77
|
+
print("Usage: python react_checks.py <file1> [file2] ...", file=sys.stderr)
|
|
78
|
+
return 1
|
|
79
|
+
|
|
80
|
+
file_paths = sys.argv[1:]
|
|
81
|
+
violations = check_no_class_components(file_paths)
|
|
82
|
+
|
|
83
|
+
for violation in violations:
|
|
84
|
+
print(f"{violation.file}:{violation.line}: {violation.message}")
|
|
85
|
+
|
|
86
|
+
return 1 if violations else 0
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
if __name__ == '__main__':
|
|
90
|
+
sys.exit(main())
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""Ruff integration for fast Python linting."""
|
|
2
|
+
|
|
3
|
+
import subprocess
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass
|
|
9
|
+
class RuffResult:
|
|
10
|
+
passed: bool
|
|
11
|
+
output: str
|
|
12
|
+
fixed_count: int
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def check_ruff_available() -> bool:
|
|
16
|
+
"""Check if ruff is installed."""
|
|
17
|
+
try:
|
|
18
|
+
result = subprocess.run(
|
|
19
|
+
["ruff", "--version"],
|
|
20
|
+
capture_output=True,
|
|
21
|
+
text=True,
|
|
22
|
+
)
|
|
23
|
+
return result.returncode == 0
|
|
24
|
+
except FileNotFoundError:
|
|
25
|
+
return False
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def run_ruff_check(files: list[Path]) -> RuffResult:
|
|
29
|
+
"""Run ruff check on files."""
|
|
30
|
+
if not files:
|
|
31
|
+
return RuffResult(passed=True, output="No files to check", fixed_count=0)
|
|
32
|
+
|
|
33
|
+
if not check_ruff_available():
|
|
34
|
+
return RuffResult(passed=True, output="Ruff not installed - skipping", fixed_count=0)
|
|
35
|
+
|
|
36
|
+
py_files = [str(f) for f in files if f.suffix == ".py"]
|
|
37
|
+
if not py_files:
|
|
38
|
+
return RuffResult(passed=True, output="No Python files", fixed_count=0)
|
|
39
|
+
|
|
40
|
+
result = subprocess.run(
|
|
41
|
+
["ruff", "check"] + py_files,
|
|
42
|
+
capture_output=True,
|
|
43
|
+
text=True,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
return RuffResult(
|
|
47
|
+
passed=result.returncode == 0,
|
|
48
|
+
output=result.stdout or result.stderr or "No issues found",
|
|
49
|
+
fixed_count=0,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def run_ruff_fix(files: list[Path]) -> RuffResult:
|
|
54
|
+
"""Run ruff with --fix to auto-fix violations."""
|
|
55
|
+
if not check_ruff_available():
|
|
56
|
+
return RuffResult(passed=True, output="Ruff not installed", fixed_count=0)
|
|
57
|
+
|
|
58
|
+
py_files = [str(f) for f in files if f.suffix == ".py"]
|
|
59
|
+
if not py_files:
|
|
60
|
+
return RuffResult(passed=True, output="No Python files", fixed_count=0)
|
|
61
|
+
|
|
62
|
+
result = subprocess.run(
|
|
63
|
+
["ruff", "check", "--fix"] + py_files,
|
|
64
|
+
capture_output=True,
|
|
65
|
+
text=True,
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
fixed_count = 0
|
|
69
|
+
for line in result.stdout.split("\n"):
|
|
70
|
+
if "Fixed" in line:
|
|
71
|
+
try:
|
|
72
|
+
fixed_count = int(line.split()[1])
|
|
73
|
+
except (IndexError, ValueError):
|
|
74
|
+
pass
|
|
75
|
+
|
|
76
|
+
return RuffResult(
|
|
77
|
+
passed=result.returncode == 0,
|
|
78
|
+
output=result.stdout or "No fixes applied",
|
|
79
|
+
fixed_count=fixed_count,
|
|
80
|
+
)
|