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,287 @@
|
|
|
1
|
+
# Test Safety Checks Validator - Comprehensive Validation Report
|
|
2
|
+
|
|
3
|
+
## Executive Summary
|
|
4
|
+
|
|
5
|
+
**Critical Gaps Found: 2**
|
|
6
|
+
**Minor Issues: 1**
|
|
7
|
+
**Tests Passed: 14/17**
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## Check 1: No Skip Decorators
|
|
12
|
+
|
|
13
|
+
### Test Results
|
|
14
|
+
|
|
15
|
+
| Test Case | Expected | Result | Status |
|
|
16
|
+
|-----------|----------|--------|--------|
|
|
17
|
+
| `@skip` | CAUGHT | CAUGHT | PASS |
|
|
18
|
+
| `@pytest.mark.skipif(...)` | CAUGHT | CAUGHT | PASS |
|
|
19
|
+
| `@unittest.skipIf(...)` | CAUGHT | CAUGHT | PASS |
|
|
20
|
+
| `@skip()` with parentheses | CAUGHT | CAUGHT | PASS |
|
|
21
|
+
| `@pytest.mark.xfail` | ALLOWED | ALLOWED | PASS |
|
|
22
|
+
| `@skip_on_windows` custom | ALLOWED | ALLOWED | PASS |
|
|
23
|
+
| `@Skip` (capital S) | CAUGHT | **ALLOWED** | **FAIL** |
|
|
24
|
+
| `@unittest.skipUnless(...)` | CAUGHT | CAUGHT | PASS |
|
|
25
|
+
| `@pytest.mark.skip` (no args) | CAUGHT | CAUGHT | PASS |
|
|
26
|
+
|
|
27
|
+
### GAP #1: Case-Sensitive Decorator Matching
|
|
28
|
+
|
|
29
|
+
**Severity: MEDIUM**
|
|
30
|
+
|
|
31
|
+
**Issue:** The validator uses case-sensitive matching, so `@Skip` (capital S) is NOT caught.
|
|
32
|
+
|
|
33
|
+
**Current Code (lines 16-22):**
|
|
34
|
+
```python
|
|
35
|
+
SKIP_DECORATOR_NAMES = frozenset([
|
|
36
|
+
"skip",
|
|
37
|
+
"skipif",
|
|
38
|
+
"skipunless",
|
|
39
|
+
"skipIf",
|
|
40
|
+
"skipUnless",
|
|
41
|
+
])
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
**Problem:** This misses decorators with different capitalization like `@Skip`, `@SkipIf`, `@SKIP`, etc.
|
|
45
|
+
|
|
46
|
+
**Recommended Fix:**
|
|
47
|
+
```python
|
|
48
|
+
def _get_decorator_name(decorator: ast.expr) -> str:
|
|
49
|
+
"""Extract the name from a decorator node (lowercase for comparison)."""
|
|
50
|
+
# ... existing logic ...
|
|
51
|
+
return name.lower() # Convert to lowercase for case-insensitive matching
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Then update the frozenset to all lowercase:
|
|
55
|
+
```python
|
|
56
|
+
SKIP_DECORATOR_NAMES = frozenset([
|
|
57
|
+
"skip",
|
|
58
|
+
"skipif",
|
|
59
|
+
"skipunless",
|
|
60
|
+
])
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
**Impact:** Currently allows `@Skip`, `@SkipIf`, `@SkipUnless` to slip through.
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
## Check 2: DEBUG Guard in Management Commands
|
|
68
|
+
|
|
69
|
+
### Test Results
|
|
70
|
+
|
|
71
|
+
| Test Case | Expected | Result | Status |
|
|
72
|
+
|-----------|----------|--------|--------|
|
|
73
|
+
| No DEBUG check | CAUGHT | CAUGHT | PASS |
|
|
74
|
+
| `if not settings.DEBUG: raise` | ALLOWED | ALLOWED | PASS |
|
|
75
|
+
| `if not settings.DEBUG: return` | ALLOWED | ALLOWED | PASS |
|
|
76
|
+
| Using imported `DEBUG` variable | CAUGHT | CAUGHT | PASS |
|
|
77
|
+
| DEBUG check in helper function | CAUGHT | CAUGHT | PASS |
|
|
78
|
+
| DEBUG check after 5 statements | CAUGHT | CAUGHT | PASS |
|
|
79
|
+
| `if settings.DEBUG: ... else: raise` | ALLOWED | **CAUGHT** | **FAIL** |
|
|
80
|
+
| `if not settings.DEBUG and X:` | DEBATABLE | CAUGHT | WARN |
|
|
81
|
+
| File not in `management/commands/` | IGNORED | IGNORED | PASS |
|
|
82
|
+
|
|
83
|
+
### GAP #2: Positive DEBUG Check Pattern Rejected
|
|
84
|
+
|
|
85
|
+
**Severity: HIGH**
|
|
86
|
+
|
|
87
|
+
**Issue:** The validator incorrectly rejects the pattern `if settings.DEBUG: ... else: raise`.
|
|
88
|
+
|
|
89
|
+
**Test File:** `cmd_07_positive_debug_check.py`
|
|
90
|
+
```python
|
|
91
|
+
def handle(self, *args, **options):
|
|
92
|
+
if settings.DEBUG:
|
|
93
|
+
print("Running in DEBUG mode")
|
|
94
|
+
else:
|
|
95
|
+
raise CommandError("This command can only be run in DEBUG mode")
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
**Expected:** ALLOWED (this is a valid DEBUG guard)
|
|
99
|
+
**Actual:** CAUGHT (flagged as violation)
|
|
100
|
+
|
|
101
|
+
**Root Cause (lines 186-187):**
|
|
102
|
+
```python
|
|
103
|
+
if _is_debug_check(test) and stmt.orelse:
|
|
104
|
+
return False # This returns False when it should return True!
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
**The Logic Flaw:**
|
|
108
|
+
- Line 186: Detects `if settings.DEBUG:` and checks if there's an `else` clause
|
|
109
|
+
- Line 187: Returns `False` (no DEBUG guard found)
|
|
110
|
+
- **BUG:** This should return `True` because the pattern IS a valid guard!
|
|
111
|
+
|
|
112
|
+
**Recommended Fix:**
|
|
113
|
+
```python
|
|
114
|
+
def _has_debug_guard(func: ast.FunctionDef) -> bool:
|
|
115
|
+
"""Check if a function has a settings.DEBUG guard at the start."""
|
|
116
|
+
if not func.body:
|
|
117
|
+
return False
|
|
118
|
+
|
|
119
|
+
for stmt in func.body[:5]:
|
|
120
|
+
if isinstance(stmt, ast.If):
|
|
121
|
+
test = stmt.test
|
|
122
|
+
|
|
123
|
+
# Pattern 1: if not settings.DEBUG: raise/return
|
|
124
|
+
if isinstance(test, ast.UnaryOp) and isinstance(test.op, ast.Not):
|
|
125
|
+
if _is_debug_check(test.operand):
|
|
126
|
+
return True
|
|
127
|
+
|
|
128
|
+
# Pattern 2: if settings.DEBUG: ... else: raise/return
|
|
129
|
+
if _is_debug_check(test):
|
|
130
|
+
if stmt.orelse:
|
|
131
|
+
# Check if else clause has raise/return
|
|
132
|
+
if _has_early_exit(stmt.orelse):
|
|
133
|
+
return True
|
|
134
|
+
# if settings.DEBUG: with no else is also valid
|
|
135
|
+
return True
|
|
136
|
+
|
|
137
|
+
return False
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _has_early_exit(statements: List[ast.stmt]) -> bool:
|
|
141
|
+
"""Check if statements contain raise or return."""
|
|
142
|
+
for stmt in statements:
|
|
143
|
+
if isinstance(stmt, (ast.Raise, ast.Return)):
|
|
144
|
+
return True
|
|
145
|
+
# Check nested if/else
|
|
146
|
+
if isinstance(stmt, ast.If):
|
|
147
|
+
if _has_early_exit(stmt.body) or (stmt.orelse and _has_early_exit(stmt.orelse)):
|
|
148
|
+
return True
|
|
149
|
+
return False
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
**Impact:** Currently REJECTS valid DEBUG guard patterns, forcing developers to use only negative checks.
|
|
153
|
+
|
|
154
|
+
### MINOR ISSUE: Complex Boolean Expressions
|
|
155
|
+
|
|
156
|
+
**Severity: LOW**
|
|
157
|
+
|
|
158
|
+
**Issue:** The validator rejects `if not settings.DEBUG and other_condition:`.
|
|
159
|
+
|
|
160
|
+
**Test File:** `cmd_08_debug_with_and.py`
|
|
161
|
+
```python
|
|
162
|
+
if not settings.DEBUG and True:
|
|
163
|
+
raise CommandError("This command can only be run in DEBUG mode")
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
**Expected:** DEBATABLE (could be allowed if DEBUG is the primary condition)
|
|
167
|
+
**Actual:** CAUGHT (flagged as violation)
|
|
168
|
+
|
|
169
|
+
**Analysis:**
|
|
170
|
+
- Current code only checks for simple `UnaryOp(Not)` with `settings.DEBUG`
|
|
171
|
+
- Doesn't handle `BoolOp` (and/or) expressions
|
|
172
|
+
- This is arguably correct behavior - keeps the check simple and strict
|
|
173
|
+
|
|
174
|
+
**Recommendation:** Document this limitation. Developers should use:
|
|
175
|
+
```python
|
|
176
|
+
if not settings.DEBUG:
|
|
177
|
+
raise CommandError("...")
|
|
178
|
+
# Then check other conditions separately
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
---
|
|
182
|
+
|
|
183
|
+
## Additional Edge Cases Tested
|
|
184
|
+
|
|
185
|
+
### Nested Directory Structure
|
|
186
|
+
- File in `management/commands/subdir/command.py` - NOT tested
|
|
187
|
+
- **Recommendation:** Add test case to verify path matching works with subdirectories
|
|
188
|
+
|
|
189
|
+
### File Encoding Issues
|
|
190
|
+
- Files with syntax errors - Handled gracefully (returns empty violations)
|
|
191
|
+
- Files that don't exist - Handled with error message
|
|
192
|
+
|
|
193
|
+
### Multiple Commands in One File
|
|
194
|
+
- NOT tested
|
|
195
|
+
- **Recommendation:** Add test case with multiple Command classes in one file
|
|
196
|
+
|
|
197
|
+
---
|
|
198
|
+
|
|
199
|
+
## Recommended Priority Fixes
|
|
200
|
+
|
|
201
|
+
1. **HIGH PRIORITY:** Fix GAP #2 (positive DEBUG check pattern)
|
|
202
|
+
- Current behavior rejects valid code
|
|
203
|
+
- Breaking change for developers using this pattern
|
|
204
|
+
|
|
205
|
+
2. **MEDIUM PRIORITY:** Fix GAP #1 (case-sensitive skip decorators)
|
|
206
|
+
- Low probability but easy to fix
|
|
207
|
+
- Prevents trivial evasion
|
|
208
|
+
|
|
209
|
+
3. **LOW PRIORITY:** Add test for nested management commands
|
|
210
|
+
- Verify path matching works with subdirectories
|
|
211
|
+
|
|
212
|
+
---
|
|
213
|
+
|
|
214
|
+
## Test Coverage Summary
|
|
215
|
+
|
|
216
|
+
**Skip Decorators:**
|
|
217
|
+
- Simple decorators: TESTED
|
|
218
|
+
- Attribute decorators: TESTED
|
|
219
|
+
- Called decorators: TESTED
|
|
220
|
+
- Case variations: **GAP FOUND**
|
|
221
|
+
- Custom decorators: TESTED (correctly allowed)
|
|
222
|
+
|
|
223
|
+
**DEBUG Guards:**
|
|
224
|
+
- No check: TESTED
|
|
225
|
+
- Negative check (`if not`): TESTED
|
|
226
|
+
- Positive check (`if ... else`): **GAP FOUND**
|
|
227
|
+
- Imported DEBUG: TESTED (correctly rejected)
|
|
228
|
+
- Helper function: TESTED (correctly rejected)
|
|
229
|
+
- Late check (>5 statements): TESTED (correctly rejected)
|
|
230
|
+
- Path matching: TESTED
|
|
231
|
+
- Complex boolean: TESTED (debatable behavior)
|
|
232
|
+
|
|
233
|
+
---
|
|
234
|
+
|
|
235
|
+
## Files Created for Testing
|
|
236
|
+
|
|
237
|
+
### Skip Decorators (9 files)
|
|
238
|
+
```
|
|
239
|
+
test_files/skip_decorators/
|
|
240
|
+
├── test_01_simple_skip.py
|
|
241
|
+
├── test_02_pytest_skipif.py
|
|
242
|
+
├── test_03_unittest_skipIf.py
|
|
243
|
+
├── test_04_skip_with_parens.py
|
|
244
|
+
├── test_05_xfail.py
|
|
245
|
+
├── test_06_custom_skip.py
|
|
246
|
+
├── test_07_capital_Skip.py (GAP)
|
|
247
|
+
├── test_08_skipUnless.py
|
|
248
|
+
└── test_09_pytest_mark_skip_simple.py
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
### Management Commands (9 files)
|
|
252
|
+
```
|
|
253
|
+
test_files/management/commands/
|
|
254
|
+
├── cmd_01_no_debug_check.py
|
|
255
|
+
├── cmd_02_proper_debug_check.py
|
|
256
|
+
├── cmd_03_debug_check_with_return.py
|
|
257
|
+
├── cmd_04_imported_DEBUG.py
|
|
258
|
+
├── cmd_05_debug_check_in_helper.py
|
|
259
|
+
├── cmd_06_debug_check_late.py
|
|
260
|
+
├── cmd_07_positive_debug_check.py (GAP)
|
|
261
|
+
├── cmd_08_debug_with_and.py
|
|
262
|
+
test_files/
|
|
263
|
+
└── not_management_command.py
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
---
|
|
267
|
+
|
|
268
|
+
## Conclusion
|
|
269
|
+
|
|
270
|
+
The validator is **mostly effective** but has **2 critical gaps**:
|
|
271
|
+
|
|
272
|
+
1. **Case-sensitive decorator matching** - Easy fix, low impact
|
|
273
|
+
2. **Positive DEBUG check rejection** - HIGH PRIORITY, rejects valid code
|
|
274
|
+
|
|
275
|
+
The implementation correctly handles:
|
|
276
|
+
- Most skip decorator patterns
|
|
277
|
+
- Simple DEBUG guard patterns
|
|
278
|
+
- Path filtering for management commands
|
|
279
|
+
- Graceful error handling
|
|
280
|
+
|
|
281
|
+
**Overall Assessment: 82% effective (14/17 test cases passed)**
|
|
282
|
+
|
|
283
|
+
**Action Required:**
|
|
284
|
+
1. Fix GAP #2 immediately (breaking valid code)
|
|
285
|
+
2. Fix GAP #1 for completeness
|
|
286
|
+
3. Add test for nested management command directories
|
|
287
|
+
4. Document the complex boolean limitation
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""Python style validation package."""
|
|
2
|
+
|
|
3
|
+
from .python_style_checks import (
|
|
4
|
+
Violation,
|
|
5
|
+
check_imports_at_top,
|
|
6
|
+
check_no_empty_line_after_decorators,
|
|
7
|
+
check_single_empty_line_between_functions,
|
|
8
|
+
check_view_function_naming,
|
|
9
|
+
validate_file,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"Violation",
|
|
14
|
+
"check_imports_at_top",
|
|
15
|
+
"check_no_empty_line_after_decorators",
|
|
16
|
+
"check_single_empty_line_between_functions",
|
|
17
|
+
"check_view_function_naming",
|
|
18
|
+
"validate_file",
|
|
19
|
+
]
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""Abbreviation detection validator.
|
|
2
|
+
|
|
3
|
+
Implements check 5: No single-letter variable names (except i, j, k for loop counters).
|
|
4
|
+
|
|
5
|
+
Detects:
|
|
6
|
+
- Single-letter assignments: t = value
|
|
7
|
+
- Single-letter loop variables: for f in files
|
|
8
|
+
- Single-letter comprehension variables: [x for x in items]
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import ast
|
|
12
|
+
import sys
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import List, Set
|
|
15
|
+
|
|
16
|
+
from validator_base import Violation
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
ALLOWED_SINGLE_LETTERS: Set[str] = frozenset({"i", "j", "k", "_"})
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def check_single_letter_variables(tree: ast.AST, filename: str) -> List[Violation]:
|
|
23
|
+
violations: List[Violation] = []
|
|
24
|
+
|
|
25
|
+
for node in ast.walk(tree):
|
|
26
|
+
if isinstance(node, ast.Name) and isinstance(node.ctx, ast.Store):
|
|
27
|
+
if len(node.id) == 1 and node.id not in ALLOWED_SINGLE_LETTERS:
|
|
28
|
+
violations.append(
|
|
29
|
+
Violation(
|
|
30
|
+
filename,
|
|
31
|
+
node.lineno,
|
|
32
|
+
f"Single-letter variable '{node.id}' - use descriptive name",
|
|
33
|
+
)
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
return violations
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def validate_file(file_path: Path) -> List[Violation]:
|
|
40
|
+
violations: List[Violation] = []
|
|
41
|
+
filename = str(file_path)
|
|
42
|
+
|
|
43
|
+
try:
|
|
44
|
+
source = file_path.read_text(encoding="utf-8")
|
|
45
|
+
except Exception as error:
|
|
46
|
+
violations.append(Violation(filename, 0, f"Error reading file: {error}"))
|
|
47
|
+
return violations
|
|
48
|
+
|
|
49
|
+
try:
|
|
50
|
+
tree = ast.parse(source)
|
|
51
|
+
except SyntaxError as error:
|
|
52
|
+
violations.append(
|
|
53
|
+
Violation(filename, error.lineno or 0, f"Syntax error: {error.msg}")
|
|
54
|
+
)
|
|
55
|
+
return violations
|
|
56
|
+
|
|
57
|
+
violations.extend(check_single_letter_variables(tree, filename))
|
|
58
|
+
return violations
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def main() -> int:
|
|
62
|
+
if len(sys.argv) < 2:
|
|
63
|
+
print("Usage: abbreviation_checks.py <file1.py> [file2.py ...]", file=sys.stderr)
|
|
64
|
+
return 1
|
|
65
|
+
|
|
66
|
+
all_violations: List[Violation] = []
|
|
67
|
+
|
|
68
|
+
for file_arg in sys.argv[1:]:
|
|
69
|
+
file_path = Path(file_arg)
|
|
70
|
+
if not file_path.exists():
|
|
71
|
+
print(f"Error: File not found: {file_path}", file=sys.stderr)
|
|
72
|
+
return 1
|
|
73
|
+
all_violations.extend(validate_file(file_path))
|
|
74
|
+
|
|
75
|
+
for violation in all_violations:
|
|
76
|
+
print(violation)
|
|
77
|
+
|
|
78
|
+
return 1 if all_violations else 0
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
if __name__ == "__main__":
|
|
82
|
+
sys.exit(main())
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"""Code quality checks validator.
|
|
2
|
+
|
|
3
|
+
Implements:
|
|
4
|
+
- Check 30: Function too long (max 30 lines)
|
|
5
|
+
- Check 31: Nesting too deep (max 2 levels)
|
|
6
|
+
- Check 32: File too long (max 400 lines)
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import ast
|
|
10
|
+
import sys
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import List
|
|
13
|
+
|
|
14
|
+
from validator_base import Violation
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
MAX_FUNCTION_LINES = 30
|
|
18
|
+
MAX_NESTING_DEPTH = 2
|
|
19
|
+
MAX_FILE_LINES = 400
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def check_function_length(tree: ast.AST, filename: str) -> List[Violation]:
|
|
23
|
+
violations: List[Violation] = []
|
|
24
|
+
|
|
25
|
+
for node in ast.walk(tree):
|
|
26
|
+
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
27
|
+
if node.end_lineno and node.lineno:
|
|
28
|
+
length = node.end_lineno - node.lineno + 1
|
|
29
|
+
if length > MAX_FUNCTION_LINES:
|
|
30
|
+
violations.append(
|
|
31
|
+
Violation(
|
|
32
|
+
filename,
|
|
33
|
+
node.lineno,
|
|
34
|
+
f"Function '{node.name}' is {length} lines (max {MAX_FUNCTION_LINES})",
|
|
35
|
+
)
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
return violations
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def check_nesting_depth(tree: ast.AST, filename: str) -> List[Violation]:
|
|
42
|
+
violations: List[Violation] = []
|
|
43
|
+
|
|
44
|
+
for node in ast.walk(tree):
|
|
45
|
+
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
46
|
+
max_depth = _get_max_nesting_depth(node, 0)
|
|
47
|
+
if max_depth > MAX_NESTING_DEPTH:
|
|
48
|
+
violations.append(
|
|
49
|
+
Violation(
|
|
50
|
+
filename,
|
|
51
|
+
node.lineno,
|
|
52
|
+
f"Function '{node.name}' has nesting depth {max_depth} (max {MAX_NESTING_DEPTH})",
|
|
53
|
+
)
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
return violations
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _get_max_nesting_depth(node: ast.AST, current_depth: int) -> int:
|
|
60
|
+
max_depth = current_depth
|
|
61
|
+
|
|
62
|
+
for child in ast.iter_child_nodes(node):
|
|
63
|
+
if isinstance(child, (ast.If, ast.For, ast.While, ast.With, ast.Try)):
|
|
64
|
+
child_depth = _get_max_nesting_depth(child, current_depth + 1)
|
|
65
|
+
max_depth = max(max_depth, child_depth)
|
|
66
|
+
else:
|
|
67
|
+
child_depth = _get_max_nesting_depth(child, current_depth)
|
|
68
|
+
max_depth = max(max_depth, child_depth)
|
|
69
|
+
|
|
70
|
+
return max_depth
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def check_file_length(file_path: Path) -> List[Violation]:
|
|
74
|
+
violations: List[Violation] = []
|
|
75
|
+
filename = str(file_path)
|
|
76
|
+
|
|
77
|
+
try:
|
|
78
|
+
lines = file_path.read_text(encoding="utf-8").splitlines()
|
|
79
|
+
except Exception as error:
|
|
80
|
+
return [Violation(filename, 0, f"Error reading file: {error}")]
|
|
81
|
+
|
|
82
|
+
if len(lines) > MAX_FILE_LINES:
|
|
83
|
+
violations.append(
|
|
84
|
+
Violation(
|
|
85
|
+
filename,
|
|
86
|
+
1,
|
|
87
|
+
f"File is {len(lines)} lines (max {MAX_FILE_LINES}) - consider splitting",
|
|
88
|
+
)
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
return violations
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def validate_file(file_path: Path) -> List[Violation]:
|
|
95
|
+
violations: List[Violation] = []
|
|
96
|
+
filename = str(file_path)
|
|
97
|
+
|
|
98
|
+
violations.extend(check_file_length(file_path))
|
|
99
|
+
|
|
100
|
+
try:
|
|
101
|
+
source = file_path.read_text(encoding="utf-8")
|
|
102
|
+
tree = ast.parse(source)
|
|
103
|
+
except Exception as error:
|
|
104
|
+
return violations + [Violation(filename, 0, f"Error: {error}")]
|
|
105
|
+
|
|
106
|
+
violations.extend(check_function_length(tree, filename))
|
|
107
|
+
violations.extend(check_nesting_depth(tree, filename))
|
|
108
|
+
|
|
109
|
+
return violations
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def main() -> int:
|
|
113
|
+
if len(sys.argv) < 2:
|
|
114
|
+
print("Usage: code_quality_checks.py <file1.py> [file2.py ...]", file=sys.stderr)
|
|
115
|
+
return 1
|
|
116
|
+
|
|
117
|
+
all_violations: List[Violation] = []
|
|
118
|
+
|
|
119
|
+
for file_arg in sys.argv[1:]:
|
|
120
|
+
file_path = Path(file_arg)
|
|
121
|
+
if not file_path.exists():
|
|
122
|
+
print(f"Error: File not found: {file_path}", file=sys.stderr)
|
|
123
|
+
return 1
|
|
124
|
+
all_violations.extend(validate_file(file_path))
|
|
125
|
+
|
|
126
|
+
for violation in all_violations:
|
|
127
|
+
print(violation)
|
|
128
|
+
|
|
129
|
+
return 1 if all_violations else 0
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
if __name__ == "__main__":
|
|
133
|
+
sys.exit(main())
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
"""Comment detection validator.
|
|
2
|
+
|
|
3
|
+
Implements check 26: No comments in code (self-documenting code principle).
|
|
4
|
+
|
|
5
|
+
Detects:
|
|
6
|
+
- Python comments (# ...)
|
|
7
|
+
- TypeScript/JavaScript comments (// ... and /* ... */)
|
|
8
|
+
|
|
9
|
+
Exceptions (NOT flagged):
|
|
10
|
+
- Shebang lines (#!/...)
|
|
11
|
+
- Type annotations (type:, noqa, eslint-disable)
|
|
12
|
+
- Docstrings (triple-quoted strings for function contracts)
|
|
13
|
+
|
|
14
|
+
Note: Docstrings documenting function Args/Returns/Raises are acceptable
|
|
15
|
+
per CODE_RULES.md. This validator only flags # and // style comments.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
import re
|
|
19
|
+
import sys
|
|
20
|
+
from dataclasses import dataclass
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
from typing import List
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
SHEBANG_PATTERN = re.compile(r"^#!")
|
|
26
|
+
PYTHON_COMMENT_PATTERN = re.compile(r"(?<!.)#(?!!).+$|(?<=\s)#.+$", re.MULTILINE)
|
|
27
|
+
JS_SINGLE_COMMENT_PATTERN = re.compile(r"//.*$", re.MULTILINE)
|
|
28
|
+
JS_BLOCK_COMMENT_PATTERN = re.compile(r"/\*[\s\S]*?\*/", re.MULTILINE)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass
|
|
32
|
+
class Violation:
|
|
33
|
+
"""Represents a comment violation."""
|
|
34
|
+
|
|
35
|
+
file: str
|
|
36
|
+
line: int
|
|
37
|
+
message: str
|
|
38
|
+
|
|
39
|
+
def __str__(self) -> str:
|
|
40
|
+
"""Format as file:line: message."""
|
|
41
|
+
return f"{self.file}:{self.line}: {self.message}"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def check_python_comments(source: str, filename: str) -> List[Violation]:
|
|
45
|
+
"""Check for comments in Python files.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
source: Source code as string
|
|
49
|
+
filename: Name of file being checked
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
List of violations found
|
|
53
|
+
"""
|
|
54
|
+
violations: List[Violation] = []
|
|
55
|
+
lines = source.splitlines()
|
|
56
|
+
|
|
57
|
+
for line_num, line in enumerate(lines, start=1):
|
|
58
|
+
stripped = line.lstrip()
|
|
59
|
+
|
|
60
|
+
if not stripped:
|
|
61
|
+
continue
|
|
62
|
+
|
|
63
|
+
if stripped.startswith("#"):
|
|
64
|
+
if SHEBANG_PATTERN.match(stripped):
|
|
65
|
+
continue
|
|
66
|
+
|
|
67
|
+
comment_text = stripped[1:].strip()[:40]
|
|
68
|
+
violations.append(
|
|
69
|
+
Violation(
|
|
70
|
+
filename,
|
|
71
|
+
line_num,
|
|
72
|
+
f"Comment found: '# {comment_text}...' - code should be self-documenting",
|
|
73
|
+
)
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
elif "#" in line:
|
|
77
|
+
code_part, _, comment_part = line.partition("#")
|
|
78
|
+
if code_part.count('"') % 2 == 0 and code_part.count("'") % 2 == 0:
|
|
79
|
+
comment_text = comment_part.strip()[:40]
|
|
80
|
+
violations.append(
|
|
81
|
+
Violation(
|
|
82
|
+
filename,
|
|
83
|
+
line_num,
|
|
84
|
+
f"Inline comment found: '# {comment_text}...' - code should be self-documenting",
|
|
85
|
+
)
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
return violations
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def check_js_comments(source: str, filename: str) -> List[Violation]:
|
|
92
|
+
"""Check for comments in JavaScript/TypeScript files.
|
|
93
|
+
|
|
94
|
+
Args:
|
|
95
|
+
source: Source code as string
|
|
96
|
+
filename: Name of file being checked
|
|
97
|
+
|
|
98
|
+
Returns:
|
|
99
|
+
List of violations found
|
|
100
|
+
"""
|
|
101
|
+
violations: List[Violation] = []
|
|
102
|
+
lines = source.splitlines()
|
|
103
|
+
|
|
104
|
+
for line_num, line in enumerate(lines, start=1):
|
|
105
|
+
stripped = line.lstrip()
|
|
106
|
+
|
|
107
|
+
if stripped.startswith("//"):
|
|
108
|
+
comment_text = stripped[2:].strip()[:40]
|
|
109
|
+
violations.append(
|
|
110
|
+
Violation(
|
|
111
|
+
filename,
|
|
112
|
+
line_num,
|
|
113
|
+
f"Comment found: '// {comment_text}...' - code should be self-documenting",
|
|
114
|
+
)
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
for match in JS_BLOCK_COMMENT_PATTERN.finditer(source):
|
|
118
|
+
start_pos = match.start()
|
|
119
|
+
line_num = source[:start_pos].count("\n") + 1
|
|
120
|
+
comment_preview = match.group()[:40].replace("\n", " ")
|
|
121
|
+
violations.append(
|
|
122
|
+
Violation(
|
|
123
|
+
filename,
|
|
124
|
+
line_num,
|
|
125
|
+
f"Block comment found: '{comment_preview}...' - code should be self-documenting",
|
|
126
|
+
)
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
return violations
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def validate_file(file_path: Path) -> List[Violation]:
|
|
133
|
+
"""Validate a file for comment violations.
|
|
134
|
+
|
|
135
|
+
Args:
|
|
136
|
+
file_path: Path to file to validate
|
|
137
|
+
|
|
138
|
+
Returns:
|
|
139
|
+
List of all violations found
|
|
140
|
+
"""
|
|
141
|
+
violations: List[Violation] = []
|
|
142
|
+
filename = str(file_path)
|
|
143
|
+
|
|
144
|
+
try:
|
|
145
|
+
source = file_path.read_text(encoding="utf-8")
|
|
146
|
+
except Exception as error:
|
|
147
|
+
violations.append(Violation(filename, 0, f"Error reading file: {error}"))
|
|
148
|
+
return violations
|
|
149
|
+
|
|
150
|
+
suffix = file_path.suffix.lower()
|
|
151
|
+
|
|
152
|
+
if suffix == ".py":
|
|
153
|
+
violations.extend(check_python_comments(source, filename))
|
|
154
|
+
elif suffix in (".ts", ".tsx", ".js", ".jsx"):
|
|
155
|
+
violations.extend(check_js_comments(source, filename))
|
|
156
|
+
|
|
157
|
+
return violations
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def main() -> int:
|
|
161
|
+
"""Main entry point for command-line usage.
|
|
162
|
+
|
|
163
|
+
Returns:
|
|
164
|
+
Exit code: 0 if all files pass, 1 if violations found
|
|
165
|
+
"""
|
|
166
|
+
if len(sys.argv) < 2:
|
|
167
|
+
print("Usage: comment_checks.py <file1> [file2 ...]", file=sys.stderr)
|
|
168
|
+
return 1
|
|
169
|
+
|
|
170
|
+
all_violations: List[Violation] = []
|
|
171
|
+
|
|
172
|
+
for file_arg in sys.argv[1:]:
|
|
173
|
+
file_path = Path(file_arg)
|
|
174
|
+
if not file_path.exists():
|
|
175
|
+
print(f"Error: File not found: {file_path}", file=sys.stderr)
|
|
176
|
+
return 1
|
|
177
|
+
|
|
178
|
+
violations = validate_file(file_path)
|
|
179
|
+
all_violations.extend(violations)
|
|
180
|
+
|
|
181
|
+
for violation in all_violations:
|
|
182
|
+
print(violation)
|
|
183
|
+
|
|
184
|
+
return 1 if all_violations else 0
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
if __name__ == "__main__":
|
|
188
|
+
sys.exit(main())
|