#!/usr/bin/env python3 """ Static analysis for async/await issues in TypeScript/JavaScript. Catches common mistakes: - fetch() without await - .json() without await - Async function calls without await - Floating promises (promise not handled) """ import argparse import json import os import re import sys from pathlib import Path from typing import Dict, List, Tuple # ============================================================================ # Async Pattern Detection # ============================================================================ ASYNC_ISSUES = [ # fetch without await - but allow .then() chains { "pattern": r"(?\s*\{[^}]*fetch\s*\([^}]*\}\s*,", "severity": "MEDIUM", "message": "useEffect with fetch - check async handling", "fix": "Create inner async function: useEffect(() => { const load = async () => {...}; load(); }, [])" }, ] # Files/patterns to skip SKIP_PATTERNS = [ r"node_modules", r"\.next", r"dist", r"build", r"\.test\.", r"\.spec\.", r"__tests__", r"__mocks__", ] def should_skip_file(file_path: str) -> bool: """Check if file should be skipped.""" for pattern in SKIP_PATTERNS: if re.search(pattern, file_path): return True return False def analyze_file(file_path: Path) -> List[Dict]: """Analyze a single file for async issues.""" issues = [] try: content = file_path.read_text(encoding='utf-8') except Exception: return issues lines = content.split('\n') for line_num, line in enumerate(lines, 1): # Skip comments stripped = line.strip() if stripped.startswith('//') or stripped.startswith('*'): continue for rule in ASYNC_ISSUES: if re.search(rule["pattern"], line): # Additional context check - skip if line has .then or .catch nearby context = '\n'.join(lines[max(0, line_num-2):min(len(lines), line_num+2)]) if '.then(' in context and '.catch(' in context: continue issues.append({ "file": str(file_path), "line": line_num, "severity": rule["severity"], "message": rule["message"], "fix": rule["fix"], "code": line.strip()[:80] }) return issues def analyze_project(root_dir: str = ".") -> List[Dict]: """Analyze all TypeScript/JavaScript files in project.""" all_issues = [] extensions = [".ts", ".tsx", ".js", ".jsx"] root = Path(root_dir) for ext in extensions: for file_path in root.rglob(f"*{ext}"): if should_skip_file(str(file_path)): continue issues = analyze_file(file_path) all_issues.extend(issues) return all_issues # ============================================================================ # Output Formatting # ============================================================================ def format_text(issues: List[Dict]) -> str: """Format issues as readable text.""" if not issues: return "\n✅ No async/await issues found.\n" lines = [] lines.append("") lines.append("╔" + "═" * 70 + "╗") lines.append("║" + " ASYNC/AWAIT VERIFICATION".ljust(70) + "║") lines.append("╠" + "═" * 70 + "╣") high = [i for i in issues if i["severity"] == "HIGH"] medium = [i for i in issues if i["severity"] == "MEDIUM"] lines.append("║" + f" 🔴 High: {len(high)} issues".ljust(70) + "║") lines.append("║" + f" 🟡 Medium: {len(medium)} issues".ljust(70) + "║") if high: lines.append("╠" + "═" * 70 + "╣") lines.append("║" + " 🔴 HIGH SEVERITY".ljust(70) + "║") for issue in high: loc = f"{issue['file']}:{issue['line']}" lines.append("║" + f" {loc}".ljust(70) + "║") lines.append("║" + f" ❌ {issue['message']}".ljust(70) + "║") lines.append("║" + f" 💡 {issue['fix']}".ljust(70) + "║") code = issue['code'][:55] lines.append("║" + f" 📝 {code}".ljust(70) + "║") if medium: lines.append("╠" + "═" * 70 + "╣") lines.append("║" + " 🟡 MEDIUM SEVERITY".ljust(70) + "║") for issue in medium[:5]: # Limit display loc = f"{issue['file']}:{issue['line']}" lines.append("║" + f" {loc}".ljust(70) + "║") lines.append("║" + f" ⚠️ {issue['message']}".ljust(70) + "║") lines.append("╚" + "═" * 70 + "╝") return "\n".join(lines) def main(): parser = argparse.ArgumentParser(description="Check for async/await issues") parser.add_argument("--json", action="store_true", help="Output as JSON") parser.add_argument("--path", default=".", help="Project path to analyze") parser.add_argument("--strict", action="store_true", help="Fail on any issue") args = parser.parse_args() print("Scanning for async/await issues...") issues = analyze_project(args.path) if args.json: print(json.dumps({ "issues": issues, "summary": { "high": len([i for i in issues if i["severity"] == "HIGH"]), "medium": len([i for i in issues if i["severity"] == "MEDIUM"]), "total": len(issues) } }, indent=2)) else: print(format_text(issues)) # Exit codes high_count = len([i for i in issues if i["severity"] == "HIGH"]) if high_count > 0: sys.exit(1) # High severity issues found elif args.strict and issues: sys.exit(1) # Any issues in strict mode else: sys.exit(0) if __name__ == "__main__": main()