// A copy of test/erase.mjs that never aborts: throws are tallied per input, with examples per class. // Usage: node judge.mjs [--list=path] (TYPESTRIP_ERASE_WASM selects the engine) import fs from "node:fs"; import path from "node:path"; import { createRequire } from "node:module"; const root = new URL("../..", import.meta.url).pathname.replace(/\/$/, ""); const ts = createRequire(root + "/package.json")("typescript"); process.env.TYPESTRIP_ERASE_WASM ??= "build/erase.wasm"; const { blankSourceFile } = await import(root + "/vendor/ts-blank-space/out/index.js"); const { transformSync } = await import(root + "/src/index.js"); const { cases } = await import(root + "/test/corpus.mjs"); const arg = process.argv[2]; const listPath = process.argv.find((a) => a.startsWith("--list="))?.slice(7); const SK = ts.SyntaxKind; const ALLOWED = new Set([" ", ";", "(", ")"]); function* walk(dir) { let entries; try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return; } for (const e of entries) { if (e.name.startsWith(".")) continue; const full = path.join(dir, e.name); if (e.isDirectory()) yield* walk(full); else if (/\.[cm]?ts$/.test(e.name) && !e.name.endsWith(".d.ts.map")) yield full; } } function* inputs() { if (arg && fs.existsSync(arg) && fs.statSync(arg).isDirectory()) { const seen = new Set(); for (const f of walk(arg)) { let src; try { src = fs.readFileSync(f, "utf8"); } catch { continue; } if (seen.has(src)) continue; seen.add(src); const sf = ts.createSourceFile("input.ts", src, ts.ScriptTarget.ESNext, false, ts.ScriptKind.TS); if (sf.parseDiagnostics.length === 0) yield { name: f, src, sf }; } } else if (arg?.endsWith(".jsonl")) { const lines = fs.readFileSync(arg, "utf8").split("\n").filter(Boolean); for (const [i, line] of lines.entries()) yield { name: `${path.basename(arg)}/${i}`, src: JSON.parse(line) }; } else { for (const c of cases({ only: arg })) yield c; } } function jsAst(code) { const sf = ts.createSourceFile("out.js", code, ts.ScriptTarget.ESNext, false, ts.ScriptKind.JS); const out = []; (function visit(n) { if (n.kind === SK.EmptyStatement || n.kind === SK.SemicolonClassElement) return; out.push(n.kind); if (ts.isIdentifier(n) || ts.isPrivateIdentifier(n) || ts.isLiteralExpression(n) || ts.isTemplateLiteralToken?.(n)) out.push(JSON.stringify(n.text)); ts.forEachChild(n, visit); out.push(-1); })(sf); return { ok: sf.parseDiagnostics.length === 0, ast: out.join(",") }; } function positional(src, out) { if (out.length !== src.length) return `length ${out.length} != input ${src.length}`; for (let i = 0; i < src.length; i++) { const a = src[i], b = out[i]; if (a === b) continue; if (a === "\n" || a === "\r") return `line terminator changed at ${i}`; if (!ALLOWED.has(b)) return `invented ${JSON.stringify(b)} at ${i}`; } return null; } function* parts(src) { let seed = src.length; for (let i = 0; i < src.length; ) { seed = (seed * 1103515245 + 12345) & 0x7fffffff; const n = 1 + (seed % 4096); yield src.slice(i, i + n); i += n; } } function chunked(src) { let out = ""; for (const piece of transformSync(parts(src))) out += piece; return out; } let pass = 0, n = 0; const failures = []; const tally = {}; const SHOW = Number(process.env.SHOW ?? 3); const t0 = performance.now(); for (const { name, src, sf: parsed } of inputs()) { n++; const sf = parsed ?? ts.createSourceFile("input.ts", src, ts.ScriptTarget.ESNext, false, ts.ScriptKind.TS); const wantErrors = []; let want; try { want = blankSourceFile(sf, (x) => wantErrors.push(x.getStart(sf))); } catch (e) { continue; } const gotErrors = []; let got; let problem = null; try { got = transformSync(src, { onError: (e) => gotErrors.push(e.start) }); } catch (e) { problem = "threw whole"; } if (!problem) { problem = positional(src, got); if (!problem) { const w = jsAst(want), g = jsAst(got); if (w.ok && !g.ok) problem = "JS parse failed where ts-blank-space's output parses"; else if (w.ast !== g.ast) problem = "JS AST differs from ts-blank-space's output"; else if (wantErrors.join() !== gotErrors.join()) problem = `errors want [${wantErrors}] got [${gotErrors}]`; else { try { if (chunked(src) !== got) problem = "streamed output differs from whole-input output"; } catch (e) { problem = "threw chunked"; } } } } if (!problem) { pass++; continue; } const kind = problem.startsWith("errors") ? "errors differ" : problem.split(" ").slice(0, 3).join(" "); tally[kind] = (tally[kind] ?? 0) + 1; let detail = ""; if (got !== undefined) { let i = 0; while (i < want.length && got[i] === want[i]) i++; detail = `near ${i}\n src : ${JSON.stringify(src.slice(Math.max(0, i - 50), i + 50))}\n want: ${JSON.stringify(want.slice(Math.max(0, i - 50), i + 50))}\n got : ${JSON.stringify(got.slice(Math.max(0, i - 50), i + 50))}`; } failures.push({ name, kind, problem, detail }); if ((tally[kind] ?? 0) <= SHOW) console.log(`FAIL ${name}: ${problem}\n ${detail}\n`); } console.log(`judge: ${pass} pass, ${failures.length} fail of ${n} in ${((performance.now() - t0) / 1000).toFixed(0)}s`, JSON.stringify(tally)); if (listPath) fs.writeFileSync(listPath, failures.map((f) => `${f.kind}\t${f.name}\t${f.problem}`).join("\n") + "\n");