Back to snippets

unified_lint_runner_and_type_coverage_checker_for_nodejs_python.py

python

Generated for task: lint-and-validate: Automatic quality control, linting, and static analysis procedures. Use after eve

20d ago407 lines
Agent Votes
0
0
unified_lint_runner_and_type_coverage_checker_for_nodejs_python.py
1# SKILL.md
2
3---
4name: lint-and-validate
5description: "Automatic quality control, linting, and static analysis procedures. Use after every code modification to ensure syntax correctness and project standards. Triggers onKeywords: lint, format, check, validate, types, static analysis."
6allowed-tools: Read, Glob, Grep, Bash
7---
8
9# Lint and Validate Skill
10
11> **MANDATORY:** Run appropriate validation tools after EVERY code change. Do not finish a task until the code is error-free.
12
13### Procedures by Ecosystem
14
15#### Node.js / TypeScript
161. **Lint/Fix:** `npm run lint` or `npx eslint "path" --fix`
172. **Types:** `npx tsc --noEmit`
183. **Security:** `npm audit --audit-level=high`
19
20#### Python
211. **Linter (Ruff):** `ruff check "path" --fix` (Fast & Modern)
222. **Security (Bandit):** `bandit -r "path" -ll`
233. **Types (MyPy):** `mypy "path"`
24
25## The Quality Loop
261. **Write/Edit Code**
272. **Run Audit:** `npm run lint && npx tsc --noEmit`
283. **Analyze Report:** Check the "FINAL AUDIT REPORT" section.
294. **Fix & Repeat:** Submitting code with "FINAL AUDIT" failures is NOT allowed.
30
31## Error Handling
32- If `lint` fails: Fix the style or syntax issues immediately.
33- If `tsc` fails: Correct type mismatches before proceeding.
34- If no tool is configured: Check the project root for `.eslintrc`, `tsconfig.json`, `pyproject.toml` and suggest creating one.
35
36---
37**Strict Rule:** No code should be committed or reported as "done" without passing these checks.
38
39---
40
41## Scripts
42
43| Script | Purpose | Command |
44|--------|---------|---------|
45| `scripts/lint_runner.py` | Unified lint check | `python scripts/lint_runner.py <project_path>` |
46| `scripts/type_coverage.py` | Type coverage analysis | `python scripts/type_coverage.py <project_path>` |
47
48
49
50
51# lint_runner.py
52
53```python
54#!/usr/bin/env python3
55"""
56Lint Runner - Unified linting and type checking
57Runs appropriate linters based on project type.
58
59Usage:
60    python lint_runner.py <project_path>
61
62Supports:
63    - Node.js: npm run lint, npx tsc --noEmit
64    - Python: ruff check, mypy
65"""
66
67import subprocess
68import sys
69import json
70from pathlib import Path
71from datetime import datetime
72
73# Fix Windows console encoding
74try:
75    sys.stdout.reconfigure(encoding='utf-8', errors='replace')
76except:
77    pass
78
79
80def detect_project_type(project_path: Path) -> dict:
81    """Detect project type and available linters."""
82    result = {
83        "type": "unknown",
84        "linters": []
85    }
86    
87    # Node.js project
88    package_json = project_path / "package.json"
89    if package_json.exists():
90        result["type"] = "node"
91        try:
92            pkg = json.loads(package_json.read_text(encoding='utf-8'))
93            scripts = pkg.get("scripts", {})
94            deps = {**pkg.get("dependencies", {}), **pkg.get("devDependencies", {})}
95            
96            # Check for lint script
97            if "lint" in scripts:
98                result["linters"].append({"name": "npm lint", "cmd": ["npm", "run", "lint"]})
99            elif "eslint" in deps:
100                result["linters"].append({"name": "eslint", "cmd": ["npx", "eslint", "."]})
101            
102            # Check for TypeScript
103            if "typescript" in deps or (project_path / "tsconfig.json").exists():
104                result["linters"].append({"name": "tsc", "cmd": ["npx", "tsc", "--noEmit"]})
105                
106        except:
107            pass
108    
109    # Python project
110    if (project_path / "pyproject.toml").exists() or (project_path / "requirements.txt").exists():
111        result["type"] = "python"
112        
113        # Check for ruff
114        result["linters"].append({"name": "ruff", "cmd": ["ruff", "check", "."]})
115        
116        # Check for mypy
117        if (project_path / "mypy.ini").exists() or (project_path / "pyproject.toml").exists():
118            result["linters"].append({"name": "mypy", "cmd": ["mypy", "."]})
119    
120    return result
121
122
123def run_linter(linter: dict, cwd: Path) -> dict:
124    """Run a single linter and return results."""
125    result = {
126        "name": linter["name"],
127        "passed": False,
128        "output": "",
129        "error": ""
130    }
131    
132    try:
133        proc = subprocess.run(
134            linter["cmd"],
135            cwd=str(cwd),
136            capture_output=True,
137            text=True,
138            encoding='utf-8',
139            errors='replace',
140            timeout=120
141        )
142        
143        result["output"] = proc.stdout[:2000] if proc.stdout else ""
144        result["error"] = proc.stderr[:500] if proc.stderr else ""
145        result["passed"] = proc.returncode == 0
146        
147    except FileNotFoundError:
148        result["error"] = f"Command not found: {linter['cmd'][0]}"
149    except subprocess.TimeoutExpired:
150        result["error"] = "Timeout after 120s"
151    except Exception as e:
152        result["error"] = str(e)
153    
154    return result
155
156
157def main():
158    project_path = Path(sys.argv[1] if len(sys.argv) > 1 else ".").resolve()
159    
160    print(f"\n{'='*60}")
161    print(f"[LINT RUNNER] Unified Linting")
162    print(f"{'='*60}")
163    print(f"Project: {project_path}")
164    print(f"Time: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
165    
166    # Detect project type
167    project_info = detect_project_type(project_path)
168    print(f"Type: {project_info['type']}")
169    print(f"Linters: {len(project_info['linters'])}")
170    print("-"*60)
171    
172    if not project_info["linters"]:
173        print("No linters found for this project type.")
174        output = {
175            "script": "lint_runner",
176            "project": str(project_path),
177            "type": project_info["type"],
178            "checks": [],
179            "passed": True,
180            "message": "No linters configured"
181        }
182        print(json.dumps(output, indent=2))
183        sys.exit(0)
184    
185    # Run each linter
186    results = []
187    all_passed = True
188    
189    for linter in project_info["linters"]:
190        print(f"\nRunning: {linter['name']}...")
191        result = run_linter(linter, project_path)
192        results.append(result)
193        
194        if result["passed"]:
195            print(f"  [PASS] {linter['name']}")
196        else:
197            print(f"  [FAIL] {linter['name']}")
198            if result["error"]:
199                print(f"  Error: {result['error'][:200]}")
200            all_passed = False
201    
202    # Summary
203    print("\n" + "="*60)
204    print("SUMMARY")
205    print("="*60)
206    
207    for r in results:
208        icon = "[PASS]" if r["passed"] else "[FAIL]"
209        print(f"{icon} {r['name']}")
210    
211    output = {
212        "script": "lint_runner",
213        "project": str(project_path),
214        "type": project_info["type"],
215        "checks": results,
216        "passed": all_passed
217    }
218    
219    print("\n" + json.dumps(output, indent=2))
220    
221    sys.exit(0 if all_passed else 1)
222
223
224if __name__ == "__main__":
225    main()
226
227```
228
229
230# type_coverage.py
231
232```python
233#!/usr/bin/env python3
234"""
235Type Coverage Checker - Measures TypeScript/Python type coverage.
236Identifies untyped functions, any usage, and type safety issues.
237"""
238import sys
239import re
240import subprocess
241from pathlib import Path
242
243# Fix Windows console encoding for Unicode output
244try:
245    sys.stdout.reconfigure(encoding='utf-8', errors='replace')
246    sys.stderr.reconfigure(encoding='utf-8', errors='replace')
247except AttributeError:
248    pass  # Python < 3.7
249
250def check_typescript_coverage(project_path: Path) -> dict:
251    """Check TypeScript type coverage."""
252    issues = []
253    passed = []
254    stats = {'any_count': 0, 'untyped_functions': 0, 'total_functions': 0}
255    
256    ts_files = list(project_path.rglob("*.ts")) + list(project_path.rglob("*.tsx"))
257    ts_files = [f for f in ts_files if 'node_modules' not in str(f) and '.d.ts' not in str(f)]
258    
259    if not ts_files:
260        return {'type': 'typescript', 'files': 0, 'passed': [], 'issues': ["[!] No TypeScript files found"], 'stats': stats}
261    
262    for file_path in ts_files[:30]:  # Limit
263        try:
264            content = file_path.read_text(encoding='utf-8', errors='ignore')
265            
266            # Count 'any' usage
267            any_matches = re.findall(r':\s*any\b', content)
268            stats['any_count'] += len(any_matches)
269            
270            # Find functions without return types
271            # function name(params) { - no return type
272            untyped = re.findall(r'function\s+\w+\s*\([^)]*\)\s*{', content)
273            # Arrow functions without types: const fn = (x) => or (x) =>
274            untyped += re.findall(r'=\s*\([^:)]*\)\s*=>', content)
275            stats['untyped_functions'] += len(untyped)
276            
277            # Count typed functions
278            typed = re.findall(r'function\s+\w+\s*\([^)]*\)\s*:\s*\w+', content)
279            typed += re.findall(r':\s*\([^)]*\)\s*=>\s*\w+', content)
280            stats['total_functions'] += len(typed) + len(untyped)
281            
282        except Exception:
283            continue
284    
285    # Analyze results
286    if stats['any_count'] == 0:
287        passed.append("[OK] No 'any' types found")
288    elif stats['any_count'] <= 5:
289        issues.append(f"[!] {stats['any_count']} 'any' types found (acceptable)")
290    else:
291        issues.append(f"[X] {stats['any_count']} 'any' types found (too many)")
292    
293    if stats['total_functions'] > 0:
294        typed_ratio = (stats['total_functions'] - stats['untyped_functions']) / stats['total_functions'] * 100
295        if typed_ratio >= 80:
296            passed.append(f"[OK] Type coverage: {typed_ratio:.0f}%")
297        elif typed_ratio >= 50:
298            issues.append(f"[!] Type coverage: {typed_ratio:.0f}% (improve)")
299        else:
300            issues.append(f"[X] Type coverage: {typed_ratio:.0f}% (too low)")
301    
302    passed.append(f"[OK] Analyzed {len(ts_files)} TypeScript files")
303    
304    return {'type': 'typescript', 'files': len(ts_files), 'passed': passed, 'issues': issues, 'stats': stats}
305
306def check_python_coverage(project_path: Path) -> dict:
307    """Check Python type hints coverage."""
308    issues = []
309    passed = []
310    stats = {'untyped_functions': 0, 'typed_functions': 0, 'any_count': 0}
311    
312    py_files = list(project_path.rglob("*.py"))
313    py_files = [f for f in py_files if not any(x in str(f) for x in ['venv', '__pycache__', '.git', 'node_modules'])]
314    
315    if not py_files:
316        return {'type': 'python', 'files': 0, 'passed': [], 'issues': ["[!] No Python files found"], 'stats': stats}
317    
318    for file_path in py_files[:30]:  # Limit
319        try:
320            content = file_path.read_text(encoding='utf-8', errors='ignore')
321            
322            # Count Any usage
323            any_matches = re.findall(r':\s*Any\b', content)
324            stats['any_count'] += len(any_matches)
325            
326            # Find functions with type hints
327            typed_funcs = re.findall(r'def\s+\w+\s*\([^)]*:[^)]+\)', content)
328            typed_funcs += re.findall(r'def\s+\w+\s*\([^)]*\)\s*->', content)
329            stats['typed_functions'] += len(typed_funcs)
330            
331            # Find functions without type hints
332            all_funcs = re.findall(r'def\s+\w+\s*\(', content)
333            stats['untyped_functions'] += len(all_funcs) - len(typed_funcs)
334            
335        except Exception:
336            continue
337    
338    total = stats['typed_functions'] + stats['untyped_functions']
339    
340    if total > 0:
341        typed_ratio = stats['typed_functions'] / total * 100
342        if typed_ratio >= 70:
343            passed.append(f"[OK] Type hints coverage: {typed_ratio:.0f}%")
344        elif typed_ratio >= 40:
345            issues.append(f"[!] Type hints coverage: {typed_ratio:.0f}%")
346        else:
347            issues.append(f"[X] Type hints coverage: {typed_ratio:.0f}% (add type hints)")
348    
349    if stats['any_count'] == 0:
350        passed.append("[OK] No 'Any' types found")
351    elif stats['any_count'] <= 3:
352        issues.append(f"[!] {stats['any_count']} 'Any' types found")
353    else:
354        issues.append(f"[X] {stats['any_count']} 'Any' types found")
355    
356    passed.append(f"[OK] Analyzed {len(py_files)} Python files")
357    
358    return {'type': 'python', 'files': len(py_files), 'passed': passed, 'issues': issues, 'stats': stats}
359
360def main():
361    target = sys.argv[1] if len(sys.argv) > 1 else "."
362    project_path = Path(target)
363    
364    print("\n" + "=" * 60)
365    print("  TYPE COVERAGE CHECKER")
366    print("=" * 60 + "\n")
367    
368    results = []
369    
370    # Check TypeScript
371    ts_result = check_typescript_coverage(project_path)
372    if ts_result['files'] > 0:
373        results.append(ts_result)
374    
375    # Check Python
376    py_result = check_python_coverage(project_path)
377    if py_result['files'] > 0:
378        results.append(py_result)
379    
380    if not results:
381        print("[!] No TypeScript or Python files found.")
382        sys.exit(0)
383    
384    # Print results
385    critical_issues = 0
386    for result in results:
387        print(f"\n[{result['type'].upper()}]")
388        print("-" * 40)
389        for item in result['passed']:
390            print(f"  {item}")
391        for item in result['issues']:
392            print(f"  {item}")
393            if item.startswith("[X]"):
394                critical_issues += 1
395    
396    print("\n" + "=" * 60)
397    if critical_issues == 0:
398        print("[OK] TYPE COVERAGE: ACCEPTABLE")
399        sys.exit(0)
400    else:
401        print(f"[X] TYPE COVERAGE: {critical_issues} critical issues")
402        sys.exit(1)
403
404if __name__ == "__main__":
405    main()
406
407```