mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-01 07:33:12 +08:00
- A function body or class static block treats its top-level function declarations as var-scoped: duplicates are legal in strict code, and a let/const/class of the same name is SC0006. - Strictness is the code around a node: a function declaration's own "use strict" no longer makes its declaring context strict. - Every part of a class is strict; object literal methods and accessors are strict only by their own prologue or their surroundings, and an arrow function's block body can carry "use strict". - The bare compareArray reads b.length, then a.length, and a.length again before every element pair, converting it as the upstream `i < a.length` does, so a string length compares its elements. - Tests that relied on the old handling of undeclared names now use constructs that are still refused or still compile: the installed-entry diagnostics fixture reports a syntax error (a read of an undeclared name is now Node's run-time ReferenceError), and static-objects-order's loops.js stores from its for-in head into `undefined` instead of an undeclared name (a store into an undeclared name refuses its package with SC1091).
141 lines
6.6 KiB
TypeScript
141 lines
6.6 KiB
TypeScript
/* Static-object specialization (shape-inference/static-objects.ts): the
|
|
* rewrites keep what the program evaluates and when its modules load. With
|
|
* the shape overlay on, the npm case static-objects-order (through its
|
|
* JavaScript consumer, js-main.cjs) keeps receivers with effects as
|
|
* written, keeps reads that may run before what they read exists, binds a
|
|
* module where the program loads it and only for reads that run after the
|
|
* binding, never adds a hoisted class to an exports literal that runs
|
|
* before the class's declaration, does not fold a literal's `__proto__`,
|
|
* keeps constructors whose methods store from for-in and for-of heads where
|
|
* strict mode throws, and prints what Node prints. A module in a require cycle through another
|
|
* module is not read through its exports from the cycle. The rewritten
|
|
* files are read from the overlay's debug output
|
|
* (SCRIPTC_DEBUG_SHAPE_OVERLAY). */
|
|
import { execFile } from "node:child_process";
|
|
import { mkdirSync } from "node:fs";
|
|
import { dirname, join } from "node:path";
|
|
import { promisify } from "node:util";
|
|
import { expect, test, vi } from "vitest";
|
|
import { compile } from "@scriptc/compiler";
|
|
|
|
const execFileAsync = promisify(execFile);
|
|
const repoRoot = join(import.meta.dirname, "../..");
|
|
const sanitize = process.env["SCRIPTC_SAN"] === "1";
|
|
|
|
/** Compiles `entry` with the overlay on; the result and the debug output. */
|
|
async function compileWithOverlay(entry: string, name: string) {
|
|
const outDir = join(repoRoot, "node_modules/.cache/scriptc-tests", name);
|
|
mkdirSync(outDir, { recursive: true });
|
|
// The debug output comes from the frontend, which a cached build skips.
|
|
const saved = process.env["SCRIPTC_DEBUG_SHAPE_OVERLAY"];
|
|
const savedCache = process.env["SCRIPTC_NO_CACHE"];
|
|
process.env["SCRIPTC_DEBUG_SHAPE_OVERLAY"] = "1";
|
|
process.env["SCRIPTC_NO_CACHE"] = "1";
|
|
let debug = "";
|
|
const write = vi.spyOn(process.stderr, "write").mockImplementation((chunk) => {
|
|
debug += String(chunk);
|
|
return true;
|
|
});
|
|
try {
|
|
const result = await compile(entry, {
|
|
outPath: join(outDir, "main"),
|
|
outDir,
|
|
sanitize,
|
|
shapeOverlay: true,
|
|
});
|
|
return { result, debug };
|
|
} finally {
|
|
write.mockRestore();
|
|
if (saved === undefined) delete process.env["SCRIPTC_DEBUG_SHAPE_OVERLAY"];
|
|
else process.env["SCRIPTC_DEBUG_SHAPE_OVERLAY"] = saved;
|
|
if (savedCache === undefined) delete process.env["SCRIPTC_NO_CACHE"];
|
|
else process.env["SCRIPTC_NO_CACHE"] = savedCache;
|
|
}
|
|
}
|
|
|
|
/** The text the rewrites give `file` in the debug output, if any. */
|
|
function rewrittenIfAny(debug: string, file: string): string | undefined {
|
|
const parts = debug.split(/^\[(?:static-objects|shape-overlay)\] /m);
|
|
return parts.find((p) => p.startsWith(`${file}\n`))?.slice(file.length + 1);
|
|
}
|
|
|
|
/** The text the rewrites give `file` in the debug output. */
|
|
function rewritten(debug: string, file: string): string {
|
|
const text = rewrittenIfAny(debug, file);
|
|
expect(text, `no rewrite of ${file}`).toBeDefined();
|
|
return text!;
|
|
}
|
|
|
|
test(
|
|
"receivers with effects, and exports literals before a class, stay as written",
|
|
{ timeout: 600_000 },
|
|
async () => {
|
|
const entry = join(repoRoot, "tests/fixtures/npm/cases/static-objects-order/js-main.cjs");
|
|
const { result, debug } = await compileWithOverlay(entry, "static-objects-order");
|
|
if (!result.ok)
|
|
throw new Error(result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n"));
|
|
const pkg = join(dirname(entry), "node_modules/static-objects-order");
|
|
expect(debug).toContain(
|
|
"kept constant read (reads++, ns.Kind).A: the receiver may have effects",
|
|
);
|
|
expect(debug).toContain(
|
|
"kept reference (reads++, ns.util).twice: the receiver may have effects",
|
|
);
|
|
expect(debug).toContain(
|
|
"kept table call (reads++, ns.ops)[op]: the table expression may have effects",
|
|
);
|
|
// vec.js assigns its exports literal before the constructor function.
|
|
// receivers.js reads ns.Late.X and calls ns.later() before they exist.
|
|
expect(debug).toContain(
|
|
"kept constant read ns.Late.X: it may run before the object is created",
|
|
);
|
|
expect(debug).toContain("kept reference ns.later: it may run before the member is stored");
|
|
// lazy-user.js's first mention of lazy.js is a conditional require: the
|
|
// binding follows the statement that loads the module each time it runs.
|
|
expect(rewritten(debug, join(pkg, "lazy-user.js"))).toMatch(
|
|
/var m = require\("\.\/lazy"\);\nconst __ns_mod_\w+ = require\("\.\/lazy"\);/,
|
|
);
|
|
// early-user.js calls a function in the statement that loads lazy.js:
|
|
// a binding after that statement is not initialized when it runs.
|
|
expect(rewrittenIfAny(debug, join(pkg, "early-user.js")) ?? "").not.toContain("__ns_mod_");
|
|
// coerce.js converts an object with a toString method between its
|
|
// constructor and the method: the conversion runs before the method exists.
|
|
expect(debug).toContain("code runs between the constructor and its methods");
|
|
expect(rewrittenIfAny(debug, join(pkg, "coerce.js")) ?? "").not.toContain(
|
|
"class __ns_shapes_Box",
|
|
);
|
|
// loops.js's methods store from for-in and for-of heads into the
|
|
// read-only global `undefined` and into a property of a string, which
|
|
// throws in a class body.
|
|
const loops = rewrittenIfAny(debug, join(pkg, "loops.js")) ?? "";
|
|
expect(loops).not.toContain("class __ns_shapes_Bag");
|
|
expect(loops).not.toContain("class __ns_shapes_Cell");
|
|
const vec = rewritten(debug, join(pkg, "vec.js"));
|
|
expect(vec).toContain("class __ns_geo_Vec");
|
|
expect(vec).toContain("module.exports = { geo: geo };");
|
|
const pending = execFileAsync(result.binaryPath, [], { cwd: dirname(entry), encoding: "utf8" });
|
|
pending.child.stdin?.end();
|
|
const { stdout } = await pending;
|
|
const node = await execFileAsync(process.execPath, [entry], {
|
|
cwd: dirname(entry),
|
|
encoding: "utf8",
|
|
});
|
|
expect(stdout).toBe(node.stdout);
|
|
},
|
|
);
|
|
|
|
test(
|
|
"a module in a require cycle is not read through its exports",
|
|
{ timeout: 600_000 },
|
|
async () => {
|
|
// cycle-pkg's cycle-d.js requires cycle-m.js, which requires cycle-r.js, which reads
|
|
// cycle-d's namespace while cycle-d is still loading. scriptc refuses the
|
|
// cycle itself (SC1016); the rewrite must not change what cycle-r reads.
|
|
const entry = join(repoRoot, "tests/fixtures/static-objects-cycle/main.cjs");
|
|
const { debug } = await compileWithOverlay(entry, "static-objects-cycle");
|
|
const text = rewritten(debug, join(dirname(entry), "node_modules/cycle-pkg/cycle-r.js"));
|
|
expect(text).toContain("typeof d.dd.f");
|
|
expect(text).not.toContain("__ns_mod_");
|
|
},
|
|
);
|