#!/usr/bin/env python3 """Fix TS6133 unused variable/import errors reported by tsc.""" import re import subprocess import sys from pathlib import Path ROOT = Path(__file__).resolve().parent.parent def run_tsc() -> list[str]: result = subprocess.run( [str(ROOT / "node_modules/.bin/tsc"), "--noEmit"], cwd=ROOT, capture_output=True, text=True, ) lines = (result.stdout + result.stderr).splitlines() return [l for l in lines if "error TS6133" in l or "error TS6198" in l] def parse_error(line: str) -> tuple[str, int, str, str] | None: m = re.match( r"^(src/[^:]+)\((\d+),(\d+)\): error TS(6133|6198): '([^']+)'", line, ) if not m: return None path, row, col, code, name = m.groups() return path, int(row), int(col), name, code def fix_file(path: Path, row: int, name: str, code: str) -> bool: lines = path.read_text().splitlines(keepends=True) if row < 1 or row > len(lines): return False idx = row - 1 line = lines[idx] if code == "6198": # All destructured elements unused — prefix each binding with _ new_line = re.sub(r"\{([^}]+)\}", lambda m: "{" + ", ".join( (p.strip() if p.strip().startswith("_") else "_" + p.strip().split(":")[0].strip() + (":" + ":".join(p.strip().split(":")[1:]) if ":" in p.strip() else "")) for p in m.group(1).split(",") ) + "}", line, count=1) if new_line != line: lines[idx] = new_line path.write_text("".join(lines)) return True return False # Unused import — remove named import or whole import line if re.search(rf"\bimport\b.*\b{re.escape(name)}\b", line): if re.match(r"import\s+\{" , line): names = re.search(r"\{([^}]+)\}", line) if names: parts = [p.strip() for p in names.group(1).split(",")] kept = [] for p in parts: base = p.split(" as ")[0].strip() if base != name: kept.append(p) if not kept: lines[idx] = "" path.write_text("".join(lines)) return True new_import = re.sub(r"\{[^}]+\}", "{" + ", ".join(kept) + "}", line) lines[idx] = new_import path.write_text("".join(lines)) return True if re.match(rf"import\s+{re.escape(name)}\s", line) or re.match(rf"import\s+\*\s+as\s+{re.escape(name)}\s", line): lines[idx] = "" path.write_text("".join(lines)) return True # Parameter or local — prefix with underscore patterns = [ (rf"\(({re.escape(name)})\:", rf"(_{name}:"), (rf",\s*{re.escape(name)}\:", rf", _{name}:"), (rf"\(\s*{re.escape(name)}\s*\)", rf"(_{name})"), (rf"\(\s*{re.escape(name)}\s*,", rf"(_{name},"), (rf",\s*{re.escape(name)}\s*\)", rf", _{name})"), (rf"\b(const|let)\s+{re.escape(name)}\b", rf"\1 _{name}"), (rf"\basync\s+{re.escape(name)}\b", rf"async _{name}"), (rf"function\s+{re.escape(name)}\b", rf"function _{name}"), ] new_line = line for pat, repl in patterns: new_line2 = re.sub(pat, repl, new_line) if new_line2 != new_line: new_line = new_line2 break else: # fallback: word boundary prefix in destructuring { name } new_line = re.sub(rf"\{{([^}}]*)\b{re.escape(name)}\b", rf"{{\1_{name}", line, count=1) if new_line == line: new_line = re.sub(rf"\b{re.escape(name)}\b", f"_{name}", line, count=1) if new_line != line: lines[idx] = new_line path.write_text("".join(lines)) return True return False def main() -> int: fixed = 0 for _ in range(5): errors = run_tsc() if not errors: break changed = False seen: set[tuple] = set() for err in errors: parsed = parse_error(err) if not parsed: continue rel, row, col, name, code = parsed key = (rel, row, name) if key in seen: continue seen.add(key) path = ROOT / rel if fix_file(path, row, name, code): fixed += 1 changed = True if not changed: break print(f"Fixed {fixed} TS6133/6198 issues") remaining = run_tsc() print(f"Remaining TS6133/6198: {len(remaining)}") return 0 if __name__ == "__main__": sys.exit(main())