Something went wrong. Try again.
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140// The erase-mode judge: equivalence with ts-blank-space (docs/correctness.md, "The erase-mode contract").// Usage: node test/erase.mjs [substring | directory | file.jsonl]import fs from "node:fs";import path from "node:path";import ts from "typescript";import { blankSourceFile } from "../vendor/ts-blank-space/out/index.js";import { transformSync, engine } from "../src/index.js";import { cases } from "./corpus.mjs";
const arg = process.argv[2];const SK = ts.SyntaxKind;const ALLOWED = new Set([" ", ";", "(", ")"]);
// A directory holding a `.sweep-skip` marker (vendor/TypeScript's test suites, which get their own run) is left out of tree sweeps.function* walk(dir, top = true) { if (!top && fs.existsSync(path.join(dir, ".sweep-skip"))) return; for (const e of fs.readdirSync(dir, { withFileTypes: true })) { if (e.name.startsWith(".")) continue; const full = path.join(dir, e.name); if (e.isDirectory()) yield* walk(full, false); 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), { onError() {} })) out += piece; return out;}
let pass = 0;// Inputs the oracle itself cannot handle (TypeScript overflowing its stack on deep nesting) are not judged.let oracleFailed = 0;const failures = [];const tally = {};for (const { name, src, sf: parsed } of inputs()) { const sf = parsed ?? ts.createSourceFile("input.ts", src, ts.ScriptTarget.ESNext, false, ts.ScriptKind.TS); const wantErrors = []; let want; try { want = blankSourceFile(sf, (n) => wantErrors.push(n.getStart(sf))); jsAst(want); } catch { oracleFailed++; continue; } const gotErrors = []; let got; try { got = transformSync(src, { onError: (e) => gotErrors.push(e.start) }); } catch (e) { failures.push([name, "threw", e.message]); tally.threw = (tally.threw ?? 0) + 1; continue; } const problem = positional(src, got) ?? (() => { const w = jsAst(want); let g; try { g = jsAst(got); } catch (e) { return `TypeScript could not parse our output: ${e.message}`; } if (w.ok && !g.ok) return "JS parse failed where ts-blank-space's output parses"; if (w.ast !== g.ast) return "JS AST differs from ts-blank-space's output"; if (wantErrors.join() !== gotErrors.join()) return `errors want [${wantErrors}] got [${gotErrors}]`; if (engine().push) { let streamed; try { streamed = chunked(src); } catch (e) { return `streamed input threw: ${e.message}`; } if (streamed !== got) return "streamed output differs from whole-input output"; } return null; })(); if (!problem) { pass++; continue; } const kind = problem.split(" ").slice(0, 3).join(" "); tally[kind] = (tally[kind] ?? 0) + 1; let i = 0; while (i < want.length && got[i] === want[i]) i++; failures.push([name, problem, `near ${i}\n src : ${JSON.stringify(src.slice(Math.max(0, i - 40), i + 40))}\n want: ${JSON.stringify(want.slice(Math.max(0, i - 40), i + 40))}\n got : ${JSON.stringify(got.slice(Math.max(0, i - 40), i + 40))}`]);}// A baseline file lists known failures (one path per line): those don't fail the run, anything else does,// so the count can only go down. ERASE_BASELINE_WRITE=1 rewrites it from this run.const baselinePath = process.env.ERASE_BASELINE;if (baselinePath && process.env.ERASE_BASELINE_WRITE) fs.writeFileSync(baselinePath, failures.map(([n]) => path.relative(process.cwd(), n)).sort().join("\n") + "\n");const known = new Set(baselinePath && fs.existsSync(baselinePath) ? fs.readFileSync(baselinePath, "utf8").split("\n").filter(Boolean) : []);const isKnown = ([n]) => known.has(path.relative(process.cwd(), n));const fresh = failures.filter((f) => !isKnown(f));for (const [n, p, d] of fresh.slice(0, Number(process.env.SHOW ?? 15))) console.log(`FAIL ${n}: ${p}\n ${d}\n`);console.log(`erase: ${pass} pass, ${failures.length} fail`, failures.length ? JSON.stringify(tally) : "", oracleFailed ? `(${oracleFailed} skipped: oracle failed)` : "");if (known.size) { const failing = new Set(failures.map(([n]) => path.relative(process.cwd(), n))); const fixed = [...known].filter((n) => !failing.has(n)); console.log(`baseline: ${known.size} known, ${fresh.length} new, ${fixed.length} now passing${fixed.length ? " (shrink the baseline)" : ""}`);}process.exitCode = fresh.length ? 1 : 0;