#!/usr/bin/env python3 """Fast codebase scanner — finds errors using linters, tests, and type checkers. Claude Code runs this script itself via bash. It uses zero AI tokens because it's just running your project's existing tools and collecting the output. Usage (run by Claude Code, not by you): python scan.py [project_root] Outputs a structured report to stdout that Claude can read and act on. Auto-detects project type: Python, Node/TypeScript, Rust, Go. """ import subprocess import shutil import sys from pathlib import Path def run(cmd, cwd, timeout=120): """Run a command, return (exit_code, output).""" try: r = subprocess.run(cmd, cwd=cwd, capture_output=True, text=True, timeout=timeout) out = r.stdout if r.stderr: out += "\n" + r.stderr return r.returncode, out.strip() except subprocess.TimeoutExpired: return -1, f"TIMEOUT after {timeout}s: {' '.join(cmd)}" except FileNotFoundError: return -1, "" def extract_files(output, root): """Pull file paths from linter output like 'path/file.py:10: error'.""" files = set() for line in output.splitlines(): if ":" in line: candidate = line.split(":")[0].strip().lstrip("./") if candidate and (root / candidate).exists(): files.add(candidate) return sorted(files) def scan_python(root): """Run Python checks: pytest, ruff, mypy, syntax.""" sections = [] if not any((root / f).exists() for f in ["pyproject.toml", "setup.py", "requirements.txt"]): return sections # pytest for cmd in [["python", "-m", "pytest", "--tb=short", "-q"], ["pytest", "--tb=short", "-q"]]: code, out = run(cmd, root) if code == -1 and not out: continue if code != 0 and out: sections.append(("Test Failures", out)) break # ruff if shutil.which("ruff"): code, out = run(["ruff", "check", "."], root) if code != 0 and out: sections.append(("Lint Errors (ruff)", out)) # mypy if shutil.which("mypy"): code, out = run(["mypy", ".", "--no-error-summary"], root, timeout=180) if code != 0 and out: sections.append(("Type Errors (mypy)", out)) # Syntax errors syntax_errors = [] for py_file in list(root.rglob("*.py"))[:500]: rel = py_file.relative_to(root) if any(p in {".venv", "venv", "node_modules", "__pycache__", ".git", ".tox"} for p in rel.parts): continue code, out = run(["python", "-c", f"import ast; ast.parse(open('{py_file}').read())"], root) if code != 0: syntax_errors.append(f"{rel}: SyntaxError - {out.splitlines()[-1] if out else 'unknown'}") if syntax_errors: sections.append(("Syntax Errors", "\n".join(syntax_errors))) return sections def scan_node(root): """Run Node/TypeScript checks: test, eslint, tsc.""" sections = [] if not (root / "package.json").exists(): return sections pkg = "yarn" if (root / "yarn.lock").exists() else "npm" code, out = run([pkg, "test", "--", "--reporter=min"], root, timeout=180) if code != 0 and out: sections.append(("Test Failures", out)) if shutil.which("npx"): code, out = run(["npx", "eslint", ".", "--format=compact", "--no-warn-ignored"], root, timeout=120) if code != 0 and out: sections.append(("Lint Errors (eslint)", out)) if (root / "tsconfig.json").exists(): code, out = run(["npx", "tsc", "--noEmit"], root, timeout=180) if code != 0 and out: sections.append(("TypeScript Errors", out)) return sections def scan_rust(root): """Run Rust checks: cargo check, cargo test --no-run.""" sections = [] if not (root / "Cargo.toml").exists(): return sections code, out = run(["cargo", "check", "--message-format=short"], root, timeout=300) if code != 0 and out: sections.append(("Build Errors (cargo)", out)) code, out = run(["cargo", "test", "--no-run"], root, timeout=300) if code != 0 and out: sections.append(("Test Compilation Errors", out)) return sections def scan_go(root): """Run Go checks: build, vet, test.""" sections = [] if not (root / "go.mod").exists(): return sections code, out = run(["go", "build", "./..."], root, timeout=180) if code != 0 and out: sections.append(("Build Errors (go)", out)) code, out = run(["go", "vet", "./..."], root, timeout=120) if code != 0 and out: sections.append(("Vet Warnings (go)", out)) code, out = run(["go", "test", "-short", "-count=1", "./..."], root, timeout=180) if code != 0 and out: sections.append(("Test Failures (go)", out)) return sections def main(): root = Path(sys.argv[1] if len(sys.argv) > 1 else ".").resolve() if not root.is_dir(): print(f"Error: {root} is not a directory", file=sys.stderr) sys.exit(1) all_sections = [] all_sections.extend(scan_python(root)) all_sections.extend(scan_node(root)) all_sections.extend(scan_rust(root)) all_sections.extend(scan_go(root)) if not all_sections: print("SCAN COMPLETE: No errors found.") sys.exit(0) # Collect all files with issues all_files = set() for title, output in all_sections: all_files.update(extract_files(output, root)) # Print structured report print(f"SCAN COMPLETE: Found issues in {len(all_sections)} categories across {len(all_files)} files.\n") for title, output in all_sections: print(f"=== {title} ===") lines = output.splitlines() if len(lines) > 80: print("(truncated to last 80 lines)") print("\n".join(lines[-80:])) else: print(output) print() if all_files: print("=== Files with Issues ===") for f in sorted(all_files): print(f" {f}") if __name__ == "__main__": main()