Files
scriptc/packages/compiler/src/frontend/program.test.ts
T
Chris Tate cc75597869 Shorten the CI critical path (#605)
* Shorten the CI critical path

* Bound Effect runner memory and avoid repeated type checks
2026-10-01 22:18:40 -05:00

221 lines
9.3 KiB
TypeScript

import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { expect, test, vi } from "vitest";
import { checkPreflight, isNodeEsmFile, loadProgram, makeCycleAdmission, type CycleEdge } from "./program-node.js";
import * as ts from "./ts7/ast.js";
import { AstNode } from "./ts7/ast-node.js";
import { SemanticChecker } from "./ts7/semantic-checker.js";
test("program-root discovery does not fetch types for unrelated string calls", () => {
const dir = mkdtempSync(join(tmpdir(), "scriptc-root-queries-"));
const entry = join(dir, "main.ts");
writeFileSync(join(dir, "package.json"), '{"type":"module"}');
writeFileSync(entry, 'function label(text: string) { return text; } console.log(label("ready"));\n');
const types = vi.spyOn(SemanticChecker.prototype, "getTypeOfSymbol");
const load = loadProgram(entry);
try {
expect(types).not.toHaveBeenCalled();
expect(checkPreflight(load)).toEqual([]);
} finally {
types.mockRestore();
load.dispose();
rmSync(dir, { recursive: true, force: true });
}
});
test("ambiguous module classification bounds ancestor work on deep expressions", () => {
const directory = mkdtempSync(join(process.platform === "win32" ? tmpdir() : "/tmp", "scriptc-module-depth-"));
const entry = join(directory, "main.js");
const depth = 1_000;
writeFileSync(join(directory, "package.json"), "{}");
writeFileSync(entry, Array.from({ length: depth }, () => "1").join(" + ") + ";\n");
const load = loadProgram(entry);
try {
const parents = vi.spyOn(AstNode.prototype, "parent", "get");
try {
expect(isNodeEsmFile(load.entry)).toBe(false);
expect(parents.mock.calls.length).toBeLessThan(depth * 4);
} finally { parents.mockRestore(); }
} finally {
load.dispose();
rmSync(directory, { recursive: true, force: true });
}
});
function requireOrderDiagnostics(source: string, dependency = "exports.value = 'ready';\n") {
const dir = mkdtempSync(join(tmpdir(), "scriptc-require-order-"));
const entry = join(dir, "main.cjs");
writeFileSync(entry, source);
writeFileSync(join(dir, "dep.cjs"), dependency);
const load = loadProgram(entry);
try {
return checkPreflight(load);
} finally {
load.dispose();
rmSync(dir, { recursive: true, force: true });
}
}
test.for([
"exports.read = read;",
"module.exports.renamed = read;",
"module.exports = read;",
])("a hoisted function export before require is safe: %s", (publish) => {
expect(requireOrderDiagnostics(`
'use strict';
Object.defineProperty(exports, '__esModule', { value: true });
${publish}
const dep = require('./dep.cjs');
function read() { return helper(); }
function helper() { return dep.value; }
`)).toEqual([]);
});
test.for([
"exports.a = exports.b = void 0;",
"exports.a = undefined; exports.b = 'literal'; exports.c = 1;",
"exports.first = exports.second = read;",
])("literal and chained export prologues do not invoke functions: %s", (prefix) => {
expect(requireOrderDiagnostics(`
Object.defineProperty(exports, '__esModule', { value: true });
${prefix}
exports.read = read;
const dep = require('./dep.cjs');
function read() { return dep.value; }
`)).toEqual([]);
});
test.for([
["direct call", "read();"],
["transitive call", "helper(); function helper() { return read(); }"],
["callback escape", "[1].map(read);"],
["export call", "exports.read();"],
["export alias call", "const alias = exports; alias.read();"],
["computed export name", "exports[read()] = read;"],
["export initializer call", "exports.value = read();"],
["class static initializer", "class Early { static value = read(); }"],
["extra descriptor effect", "Object.defineProperty(exports, '__esModule', { value: true, enumerable: read() });"],
["void initializer call", "exports.value = void read();"],
["prototype mutation", "exports.__proto__ = read;"],
["read-only marker", "Object.defineProperty(exports, '__esModule', { value: true }); exports.__esModule = read;"],
["replacement export property", "module.exports = read; module.exports.name = read;"],
["non-export assignment chain", "const box = {}; exports.alias = box.read = read;"],
])("require still refuses an early read through %s", ([, early]) => {
const diagnostics = requireOrderDiagnostics(`
exports.read = read;
${early}
const dep = require('./dep.cjs');
function read() { return dep.value; }
`);
expect(diagnostics.some((diag) => diag.code === "SC1013" && diag.message.includes("binding 'dep'")))
.toBe(true);
});
test("publishing a require binding itself still reads it before initialization", () => {
const diagnostics = requireOrderDiagnostics(`
exports.read = read;
const { read } = require('./dep.cjs');
`);
expect(diagnostics.some((diag) => diag.code === "SC1013" && diag.message.includes("binding 'read'")))
.toBe(true);
});
test.for([
"var Object = null; Object.defineProperty(exports, '__esModule', { value: true }); exports.read = read;",
"var exports = null; exports.read = read;",
"var module = null; module.exports = read;",
])("source bindings cannot masquerade as a CommonJS prologue: %s", (prefix) => {
const diagnostics = requireOrderDiagnostics(`
${prefix}
const dep = require('./dep.cjs');
function read() { return dep.value; }
`);
expect(diagnostics.some((diag) => diag.code === "SC1013" && diag.message.includes("binding 'dep'")))
.toBe(true);
});
test("an earlier declarator can call an export before the require initializes", () => {
const diagnostics = requireOrderDiagnostics(`
exports.read = read;
const before = read(), dep = require('./dep.cjs');
function read() { return dep.value; }
`);
expect(diagnostics.some((diag) => diag.code === "SC1013" && diag.message.includes("binding 'dep'")))
.toBe(true);
});
test("a later declarator can call an export after the require initializes", () => {
expect(requireOrderDiagnostics(`
exports.read = read;
const dep = require('./dep.cjs'), after = read();
function read() { return dep.value; }
`)).toEqual([]);
});
test("function publication does not admit CommonJS cycles", () => {
const diagnostics = requireOrderDiagnostics(`
exports.read = read;
const dep = require('./dep.cjs');
function read() { return dep.value; }
`, "const main = require('./main.cjs'); exports.value = main.read();\n");
expect(diagnostics.some((diag) => diag.code === "SC1016")).toBe(true);
});
test.for([
['export function read() { return first + alias; }', null],
['export { first, alias }; export function read() { return 1; }', null],
['type Value = typeof alias; export function read(first: number = 1) { return first; }', null],
['class Box { value = alias; } export function read(value = first) { return value; }', null],
['const value = alias; export function read() { return value; }', "alias"],
['class Box { static value = first; } export function read() { return 1; }', "first"],
['class Box { [alias] = 1; } export function read() { return 1; }', "alias"],
])("cycle binding indexes preserve initialization safety: %s", ([body, refused]) => {
const directory = mkdtempSync(join(process.platform === "win32" ? tmpdir() : "/tmp", "scriptc-cycle-bindings-"));
const entry = join(directory, "main.ts");
writeFileSync(entry, 'import { read } from "./peer.ts"; export const first = 1; export const second = 2; export function run() { return read(); }');
writeFileSync(join(directory, "peer.ts"), 'import { first, second as alias } from "./main.ts";\n' + body);
const load = loadProgram(entry);
try {
const cycles = checkPreflight(load).filter((diagnostic) => diagnostic.code === "SC1016");
if (refused === null) expect(cycles).toEqual([]);
else {
expect(cycles).toHaveLength(1);
expect(cycles[0]!.message).toContain(`binding '${refused}' is read`);
}
} finally {
load.dispose();
rmSync(directory, { recursive: true, force: true });
}
});
test("repeated cycle edges reuse a file's binding index", () => {
const directory = mkdtempSync(join(process.platform === "win32" ? tmpdir() : "/tmp", "scriptc-cycle-index-"));
const entry = join(directory, "main.ts");
writeFileSync(entry, 'import { read } from "./peer.ts"; export const first = 1; export const second = 2; export function run() { return read(); }');
writeFileSync(join(directory, "peer.ts"), 'import { first } from "./main.ts"; import { second } from "./main.ts"; export function read() { return first + second; }');
const load = loadProgram(entry);
try {
const main = load.entry;
const peer = load.program.getSourceFile(join(directory, "peer.ts"))!;
const mainImports = main.statements.filter(ts.isImportDeclaration);
const peerImports = peer.statements.filter(ts.isImportDeclaration);
const edges = new Map<ts.SourceFile, CycleEdge[]>([
[main, mainImports.map((stmt) => ({ dep: peer, stmt }))],
[peer, peerImports.map((stmt) => ({ dep: main, stmt }))],
]);
const admit = makeCycleAdmission(load.program, (file) => edges.get(file) ?? []);
expect(admit(peer, edges.get(peer)![0]!)).toBeNull();
// The first edge may materialize/check the file. A later binding must
// inspect its indexed references without another full-file walk.
const walk = vi.spyOn(peer, "forEachChild");
try {
expect(admit(peer, edges.get(peer)![1]!)).toBeNull();
expect(walk).not.toHaveBeenCalled();
} finally { walk.mockRestore(); }
} finally {
load.dispose();
rmSync(directory, { recursive: true, force: true });
}
});