mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-27 06:43:19 +08:00
1534 lines
52 KiB
TypeScript
1534 lines
52 KiB
TypeScript
/* Error-object behavior Node cannot oracle — scriptc-only.
|
||
*
|
||
* The differential corpus (1300–1306) covers everything observable in
|
||
* lockstep with Node: construction/fields/toString, subclasses, typed-catch
|
||
* narrowing, async rejections, runtime failures caught as instances. What
|
||
* lives here is the uncaught STDERR contract: Node prints a source excerpt
|
||
* and a stack trace (and its display shows the CONSTRUCTOR's name where it
|
||
* differs from .name); scriptc prints one `Uncaught <toString>` line —
|
||
* SEMANTICS.md divergence 11. These pins keep that line's shape exact.
|
||
*
|
||
* SCRIPTC_SAN=1 builds these with ASan + the RC audit like every harness lane.
|
||
*/
|
||
import { execFile } from "node:child_process";
|
||
import { createHash } from "node:crypto";
|
||
import { mkdirSync, writeFileSync } from "node:fs";
|
||
import { tmpdir } from "node:os";
|
||
import { join } from "node:path";
|
||
import { promisify } from "node:util";
|
||
import { describe, expect, test } from "vitest";
|
||
import { compile } from "@scriptc/compiler";
|
||
|
||
const execFileAsync = promisify(execFile);
|
||
const repoRoot = join(import.meta.dirname, "../..");
|
||
const cacheDir = join(repoRoot, "node_modules/.cache/scriptc-tests");
|
||
const sanitize = process.env["SCRIPTC_SAN"] === "1";
|
||
|
||
interface RunResult {
|
||
stdout: string;
|
||
stderr: string;
|
||
exitCode: number;
|
||
}
|
||
|
||
/** Compiles an inline program and runs the binary, tolerating nonzero exit.
|
||
* `ext` picks the frontend lane — "cjs" for JS-only shapes (deferred
|
||
* fences that tsc's arity families would reject in TS). `extraFiles`
|
||
* materialize beside the entry (multi-module shapes: the CJS link-error
|
||
* pins import sibling .cjs modules). */
|
||
async function compileAndRun(
|
||
name: string,
|
||
source: string,
|
||
ext = "ts",
|
||
extraFiles: Record<string, string> = {},
|
||
): Promise<RunResult> {
|
||
const key = createHash("sha256")
|
||
.update(source)
|
||
.update(JSON.stringify(extraFiles))
|
||
.update(sanitize ? "san" : "plain")
|
||
.digest("hex")
|
||
.slice(0, 16);
|
||
// Multi-module programs live OUTSIDE node_modules: the cache dir sits
|
||
// under it, where the resolver classifies sibling modules as npm files
|
||
// (correctly — for real packages) and the relative import stops
|
||
// resolving into the program.
|
||
const outDir =
|
||
Object.keys(extraFiles).length > 0
|
||
? join(tmpdir(), `scriptc-errors-${key}`)
|
||
: join(cacheDir, `errors-${key}`);
|
||
mkdirSync(outDir, { recursive: true });
|
||
const file = join(outDir, `${name}.${ext}`);
|
||
writeFileSync(file, source);
|
||
for (const [extraName, text] of Object.entries(extraFiles)) {
|
||
writeFileSync(join(outDir, extraName), text);
|
||
}
|
||
const result = await compile(file, {
|
||
outPath: join(outDir, name),
|
||
outDir,
|
||
sanitize,
|
||
backend: "llvm",
|
||
});
|
||
if (!result.ok) {
|
||
throw new Error(
|
||
"errors program failed to compile:\n" +
|
||
result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n"),
|
||
);
|
||
}
|
||
try {
|
||
const { stdout, stderr } = await execFileAsync(result.binaryPath, [], { encoding: "utf8" });
|
||
return { stdout, stderr: stripAsanFiberWarning(stderr), exitCode: 0 };
|
||
} catch (err) {
|
||
const e = err as { code?: unknown; stdout?: string; stderr?: string };
|
||
if (typeof e.code !== "number") throw err;
|
||
return {
|
||
stdout: e.stdout ?? "",
|
||
stderr: stripAsanFiberWarning(e.stderr ?? ""),
|
||
exitCode: e.code,
|
||
};
|
||
}
|
||
}
|
||
|
||
/** Linux ASan prints a once-per-process warning to stderr the first time a
|
||
* fiber swapcontext()s (the interceptor has no off switch; Apple's ASan
|
||
* never intercepts ucontext, so the macOS lanes never see it). Sanitizer
|
||
* diagnostic noise, never program output — dropped before any stderr
|
||
* expectation, the RC-audit-skip-notice pattern from differential.test.ts. */
|
||
function stripAsanFiberWarning(s: string): string {
|
||
return s.replace(
|
||
/^==\d+==WARNING: ASan doesn't fully support makecontext\/swapcontext.*\n/gm,
|
||
"",
|
||
);
|
||
}
|
||
|
||
describe(`uncaught error objects (scriptc-only${sanitize ? ", sanitized" : ""})`, () => {
|
||
test("an uncaught builtin error prints Uncaught name: message", async () => {
|
||
const r = await compileAndRun(
|
||
"uncaught-builtin",
|
||
`console.log("before");
|
||
throw new TypeError("boom");
|
||
`,
|
||
);
|
||
expect(r.exitCode).toBe(1);
|
||
expect(r.stdout).toBe("before\n");
|
||
expect(r.stderr).toBe("Uncaught TypeError: boom\n");
|
||
});
|
||
|
||
test("a subclass prints its name PROPERTY — 'Error' when never assigned", async () => {
|
||
// Node's uncaught display would show the constructor name (PlainSub)
|
||
// even though .name is "Error"; we print the property — divergence 11.
|
||
const r = await compileAndRun(
|
||
"uncaught-subclass",
|
||
`class PlainSub extends Error {}
|
||
throw new PlainSub("unnamed sub");
|
||
`,
|
||
);
|
||
expect(r.exitCode).toBe(1);
|
||
expect(r.stderr).toBe("Uncaught Error: unnamed sub\n");
|
||
});
|
||
|
||
test("toString's empty-half rules shape the line", async () => {
|
||
const empty = await compileAndRun("uncaught-empty", `throw new Error();\n`);
|
||
expect(empty.exitCode).toBe(1);
|
||
expect(empty.stderr).toBe("Uncaught Error\n");
|
||
|
||
const nameless = await compileAndRun(
|
||
"uncaught-nameless",
|
||
`const e = new Error("just the message");
|
||
e.name = "";
|
||
throw e;
|
||
`,
|
||
);
|
||
expect(nameless.exitCode).toBe(1);
|
||
expect(nameless.stderr).toBe("Uncaught just the message\n");
|
||
});
|
||
|
||
test("an uncaught checked-dynamic primitive prints its value, like Node", async () => {
|
||
// Node prints the thrown primitive itself (after its source excerpt);
|
||
// the value must never be lost behind a placeholder.
|
||
const text = await compileAndRun(
|
||
"uncaught-unknown-string",
|
||
`console.log("before");
|
||
const v: unknown = process.argv.length > 99 ? 0 : "plain";
|
||
throw v;
|
||
`,
|
||
);
|
||
expect(text.exitCode).toBe(1);
|
||
expect(text.stdout).toBe("before\n");
|
||
expect(text.stderr).toBe("Uncaught plain\n");
|
||
|
||
const num = await compileAndRun(
|
||
"uncaught-unknown-number",
|
||
`const v: unknown = process.argv.length > 99 ? "x" : 42;
|
||
throw v;
|
||
`,
|
||
);
|
||
expect(num.exitCode).toBe(1);
|
||
expect(num.stderr).toBe("Uncaught 42\n");
|
||
});
|
||
|
||
test("an unhandled ERROR rejection renders name: message too", async () => {
|
||
const r = await compileAndRun(
|
||
"unhandled-error-rejection",
|
||
`async function fail(): Promise<void> {
|
||
throw new RangeError("nobody awaits this");
|
||
}
|
||
fail();
|
||
console.log("sync done");
|
||
`,
|
||
);
|
||
expect(r.exitCode).toBe(1);
|
||
expect(r.stdout).toBe("sync done\n");
|
||
expect(r.stderr).toBe("Unhandled promise rejection: RangeError: nobody awaits this\n");
|
||
});
|
||
});
|
||
|
||
// `x as Sub` on a class value is a CHECKED downcast, and `x!` / `u as Arm`
|
||
// on a union value is a checked extraction: a stored value outside the
|
||
// target throws a catchable TypeError where Node's erasure would let the
|
||
// value flow on. The type and message are scriptc's contract (corpus 4432
|
||
// pins the Node-observable part: the failure is a TypeError raised before
|
||
// any later read). Plain operands are tested inline at the use and call
|
||
// operands through the interned helper; both must fail identically.
|
||
describe("checked class downcasts and extractions", () => {
|
||
test("failed downcasts throw the TypeError naming the target class", async () => {
|
||
const r = await compileAndRun(
|
||
"checked-downcast-messages",
|
||
`class Base {
|
||
kind = 0;
|
||
}
|
||
class Sub extends Base {
|
||
text = "sub";
|
||
}
|
||
class Other {
|
||
tag = "other";
|
||
}
|
||
function direct(b: Base): string {
|
||
return (b as Sub).text;
|
||
}
|
||
function optional(b: Base | undefined): string {
|
||
return (b as Sub).text;
|
||
}
|
||
function mixed(v: Base | Other): string {
|
||
return (v as Sub).text;
|
||
}
|
||
function make(b: Base | undefined): Base | undefined {
|
||
return b;
|
||
}
|
||
function called(b: Base | undefined): string {
|
||
return (make(b) as Sub).text;
|
||
}
|
||
function asserted(b: Base | undefined): number {
|
||
return b!.kind;
|
||
}
|
||
function assertedCall(b: Base | undefined): number {
|
||
return make(b)!.kind;
|
||
}
|
||
const runs: Array<() => unknown> = [
|
||
() => direct(new Sub()),
|
||
() => direct(new Base()),
|
||
() => optional(undefined),
|
||
() => optional(new Base()),
|
||
() => mixed(new Other()),
|
||
() => called(new Base()),
|
||
() => called(undefined),
|
||
() => asserted(new Base()),
|
||
() => asserted(undefined),
|
||
() => assertedCall(undefined),
|
||
];
|
||
for (const run of runs) {
|
||
try {
|
||
console.log("ok", run());
|
||
} catch (e) {
|
||
console.log(e instanceof TypeError, String(e));
|
||
}
|
||
}
|
||
direct(new Base());
|
||
`,
|
||
);
|
||
expect(r.stdout).toBe(
|
||
[
|
||
"ok sub",
|
||
"true TypeError: Value is not an instance of 'Sub'",
|
||
"true TypeError: Value is not an instance of 'Sub'",
|
||
"true TypeError: Value is not an instance of 'Sub'",
|
||
"true TypeError: Value is not an instance of 'Sub'",
|
||
"true TypeError: Value is not an instance of 'Sub'",
|
||
"true TypeError: Value is not an instance of 'Sub'",
|
||
"ok 0",
|
||
"true TypeError: undefined is not representable in the target union (a value narrowed or asserted past it still held it)",
|
||
"true TypeError: undefined is not representable in the target union (a value narrowed or asserted past it still held it)",
|
||
"",
|
||
].join("\n"),
|
||
);
|
||
expect(r.stderr).toBe("Uncaught TypeError: Value is not an instance of 'Sub'\n");
|
||
expect(r.exitCode).toBe(1);
|
||
});
|
||
});
|
||
|
||
// `e as C` on a catch binding is a CHECKED cast: a payload outside C's
|
||
// hierarchy throws a catchable TypeError where Node's erasure would read
|
||
// undefined off the value — dynCheck's documented trust-but-verify stance
|
||
// extended to exception payloads. This is exactly the case that cannot be
|
||
// a differential test; pinned so the divergence stays deliberate.
|
||
describe("checked catch-binding casts", () => {
|
||
test("a mismatched `as Error` throws the catchable TypeError", async () => {
|
||
const r = await compileAndRun(
|
||
"caught-check-mismatch",
|
||
`try {
|
||
throw "stringy";
|
||
} catch (err) {
|
||
try {
|
||
console.log((err as Error).message);
|
||
} catch (inner) {
|
||
if (inner instanceof TypeError) console.log("checked:", inner.message);
|
||
}
|
||
}
|
||
`,
|
||
);
|
||
expect(r.exitCode).toBe(0);
|
||
expect(r.stdout).toBe("checked: caught value is not an instance of Error (checked cast)\n");
|
||
expect(r.stderr).toBe("");
|
||
});
|
||
|
||
test("an uncaught mismatch propagates as the TypeError", async () => {
|
||
const r = await compileAndRun(
|
||
"caught-check-uncaught",
|
||
`try {
|
||
throw 42;
|
||
} catch (err) {
|
||
console.log((err as Error).message);
|
||
}
|
||
`,
|
||
);
|
||
expect(r.exitCode).toBe(1);
|
||
expect(r.stderr).toBe(
|
||
"Uncaught TypeError: caught value is not an instance of Error (checked cast)\n",
|
||
);
|
||
});
|
||
|
||
test("an EFFECTFUL surplus argument is a named deferred fence (JS lane)", async () => {
|
||
// Effect-free surplus args drop at compile time, JS-exact (corpus
|
||
// 1760 — the test-string-decoder writeSequences shape). An effectful
|
||
// one has no evaluation slot in the completed call, so instead of
|
||
// skipping it without a diagnostic the build defers a named SC1090 to the
|
||
// call site. JS-only: tsc's arity errors gate the TS lane.
|
||
const r = await compileAndRun(
|
||
"surplus-args-effectful",
|
||
`'use strict';
|
||
function two(a, b) {
|
||
return a + b;
|
||
}
|
||
function effectful() {
|
||
console.log('side effect JS would run');
|
||
return 0;
|
||
}
|
||
console.log(two(1, 2, effectful()));
|
||
`,
|
||
"cjs",
|
||
);
|
||
expect(r.exitCode).toBe(1);
|
||
expect(r.stdout).toBe("");
|
||
expect(r.stderr).toMatch(
|
||
/^Uncaught Error: surplus arguments with side effects .* are not supported yet \[SC1090 at .*surplus-args-effectful\.cjs:9\]\n$/,
|
||
);
|
||
});
|
||
|
||
test.each([
|
||
{
|
||
label: "Array.from",
|
||
name: "array-from-length-elements",
|
||
source: `const arr = Array.from({ length: 2 }, () => new Date(0));`,
|
||
},
|
||
{
|
||
label: "flatMap",
|
||
name: "flatmap-elements",
|
||
source: `const arr = [1].flatMap(() => new Date(0));`,
|
||
},
|
||
{
|
||
label: "tuple map",
|
||
name: "tuple-map-elements",
|
||
source: `/** @type {[number]} */
|
||
const tuple = [1];
|
||
const arr = tuple.map(() => new Date(0));`,
|
||
},
|
||
])("$label preserves checked callback results (JS lane)", async ({ name, source }) => {
|
||
const program = source + "\nconsole.log(arr.length, arr[0].getTime());\n";
|
||
const r = await compileAndRun(name, program, "js");
|
||
const reference = await execFileAsync(process.execPath, ["--eval", program], {
|
||
encoding: "utf8",
|
||
});
|
||
expect(r).toEqual({ stdout: reference.stdout, stderr: reference.stderr, exitCode: 0 });
|
||
});
|
||
|
||
test("extending a property-assigned class ABOVE its assignment is a named deferred fence (JS lane)", async () => {
|
||
// The property-assigned class-expression family (corpus 2032/2033
|
||
// pins what lowers): `exports.O = class extends exports.I {}` with
|
||
// the base assigned BELOW would extend undefined in Node (TypeError
|
||
// at the derived statement) — the source-order guard defers a named
|
||
// fence instead of pinning a base that doesn't exist yet.
|
||
const r = await compileAndRun(
|
||
"extends-above-assignment",
|
||
`'use strict';
|
||
exports.O = class extends exports.I {};
|
||
exports.I = class {};
|
||
console.log('unreached');
|
||
`,
|
||
"cjs",
|
||
);
|
||
expect(r.exitCode).toBe(1);
|
||
expect(r.stdout).toBe("");
|
||
expect(r.stderr).toMatch(
|
||
/^Uncaught Error: extending 'exports\.I' above the statement that assigns it .* \[SC1090 at .*extends-above-assignment\.cjs:2\]\n$/,
|
||
);
|
||
});
|
||
|
||
test("a REASSIGNED property-assigned class stays fenced instead of extending the wrong base (JS lane)", async () => {
|
||
// Two `exports.I = class …` assignments make the binding dynamic (the
|
||
// runtime base is whichever ran last). The second assignment's
|
||
// class-value flow fence fires first (nominal classval slots), and the
|
||
// extends resolver refuses to pin either class — the program fences
|
||
// instead of extending the first assignment's class.
|
||
const r = await compileAndRun(
|
||
"extends-reassigned-property",
|
||
`'use strict';
|
||
exports.I = class { a() { return 1; } };
|
||
exports.I = class { a() { return 2; } };
|
||
exports.O = class extends exports.I {};
|
||
console.log(new exports.O().a());
|
||
`,
|
||
"cjs",
|
||
);
|
||
expect(r.exitCode).toBe(1);
|
||
expect(r.stdout).toBe("");
|
||
expect(r.stderr).toMatch(
|
||
/^Uncaught Error: .* \[SC1090 at .*extends-reassigned-property\.cjs:\d+\]\n$/,
|
||
);
|
||
});
|
||
|
||
test("a non-callable constructor field shadows its method and throws at the call (JS lane)", async () => {
|
||
const r = await compileAndRun(
|
||
"leaf-shadowing-class-defers",
|
||
`'use strict';
|
||
console.log("before");
|
||
class X {
|
||
constructor() {
|
||
this.m = "not callable";
|
||
}
|
||
m() {
|
||
console.log("method");
|
||
}
|
||
}
|
||
const x = new X();
|
||
x.m();
|
||
console.log("unreachable");
|
||
`,
|
||
"cjs",
|
||
);
|
||
expect(r.exitCode).toBe(1);
|
||
expect(r.stdout).toBe("before\n");
|
||
expect(r.stderr).toBe("Uncaught TypeError: x.m is not a function\n");
|
||
});
|
||
|
||
test("a callable constructor field shadows an inherited method (JS lane)", async () => {
|
||
const r = await compileAndRun(
|
||
"derived-shadowing-class-defers",
|
||
`'use strict';
|
||
console.log("before");
|
||
class A {
|
||
foo() {
|
||
return 4;
|
||
}
|
||
}
|
||
class B extends A {
|
||
constructor() {
|
||
super();
|
||
this.foo = () => 3;
|
||
}
|
||
}
|
||
const i = new B();
|
||
console.log(i.foo());
|
||
`,
|
||
"cjs",
|
||
);
|
||
expect(r.exitCode).toBe(0);
|
||
expect(r.stdout).toBe("before\n3\n");
|
||
expect(r.stderr).toBe("");
|
||
});
|
||
|
||
test("destructuring a SURFACED builtin global's member is a named fence at the pattern (JS lane)", async () => {
|
||
// `const { max } = Math` would detach a member whose lowering is
|
||
// keyed to its receiver (Math.max) — the pattern fences by member
|
||
// and global name instead of binding a token whose calls fail late.
|
||
// Corpus 2558 pins the OPAQUE globals, where members DO bind tokens.
|
||
const r = await compileAndRun(
|
||
"destructure-surfaced-global",
|
||
`'use strict';
|
||
const { max } = Math;
|
||
console.log(max(1, 2));
|
||
`,
|
||
"cjs",
|
||
);
|
||
expect(r.exitCode).toBe(1);
|
||
expect(r.stdout).toBe("");
|
||
expect(r.stderr).toMatch(
|
||
/^Uncaught Error: destructuring the member 'max' of the builtin global 'Math' \(a detached member loses its receiver-keyed lowering — call it through the global\) is not supported yet \[SC1031 at .*destructure-surfaced-global\.cjs:2\]\n$/,
|
||
);
|
||
});
|
||
|
||
test("an OPAQUE global's destructured member binds a token; uses fence lazily per site (JS lane)", async () => {
|
||
// The node-suite webcrypto prologue: `const { subtle } =
|
||
// globalThis.crypto` binds the member identity token, so the program
|
||
// RUNS past the destructure (the eager pattern fence used to kill it
|
||
// at that line) and each use meets its own named fence.
|
||
const r = await compileAndRun(
|
||
"destructure-opaque-global",
|
||
`'use strict';
|
||
const { subtle } = globalThis.crypto;
|
||
console.log('bound', !!subtle);
|
||
subtle.digest('SHA-256');
|
||
`,
|
||
"cjs",
|
||
);
|
||
expect(r.exitCode).toBe(1);
|
||
expect(r.stdout).toBe("bound true\n");
|
||
expect(r.stderr).toMatch(/^Uncaught TypeError: subtle\.digest is not a function\n$/);
|
||
});
|
||
|
||
test("a 7000-level expression is a named deferred fence, not a stack overflow (JS lane)", async () => {
|
||
// The binderBinaryExpressionStress depth: an AST deep enough that the
|
||
// PREFLIGHT walks (the checker facade's prefetch sweep, the require
|
||
// scans) used to overflow the JS stack as a RangeError ICE before the
|
||
// expression lowering's 200-level SC1090 fence could fire. Every
|
||
// whole-file walk is iterative now (walkPreorder), so the JS lane
|
||
// compiles and defers the named fence to the statement — the TS lane's
|
||
// compile-time twin is the deep-nesting-preflight diagnostics fixture.
|
||
// Parens carry the depth (a real 6500-term chain spends minutes in
|
||
// per-node type queries; the walk that used to crash is shape-blind).
|
||
const r = await compileAndRun(
|
||
"deep-nesting-chain",
|
||
`'use strict';
|
||
let x = ${"(".repeat(7000)}1${")".repeat(7000)};
|
||
console.log('after');
|
||
`,
|
||
"cjs",
|
||
);
|
||
expect(r.exitCode).toBe(1);
|
||
expect(r.stdout).toBe("");
|
||
expect(r.stderr).toMatch(
|
||
/^Uncaught Error: expressions nested deeper than 200 levels are not supported yet \[SC1090 at .*deep-nesting-chain\.cjs:2\]\n$/,
|
||
);
|
||
});
|
||
|
||
test("process.on with a prototype-member event name fences by NAME", async () => {
|
||
// '__proto__' used to fall into the compiler's own signal table —
|
||
// `{ SIGINT: 2 }["__proto__"]` answered Object.prototype, which rode
|
||
// a numLit into the C as `[object Object]` (invalid C, cc.js death).
|
||
// The own() lookup makes it what every other unsupported process
|
||
// event is: a named deferred fence. Corpus 1761 pins the EMITTER
|
||
// side, where these names work and match Node byte-exactly.
|
||
const r = await compileAndRun(
|
||
"process-on-proto",
|
||
`'use strict';
|
||
process.on('__proto__', () => {});
|
||
`,
|
||
"cjs",
|
||
);
|
||
expect(r.exitCode).toBe(1);
|
||
expect(r.stdout).toBe("");
|
||
expect(r.stderr).toMatch(
|
||
/^Uncaught Error: 'process\.on\("__proto__", \.\.\.\)' is part of the standard library types but has no scriptc lowering yet \[SC2020 at .*process-on-proto\.cjs:2\]\n$/,
|
||
);
|
||
});
|
||
});
|
||
|
||
// Node's CJS named-import LINK error: a named import of an export the CJS
|
||
// lexer cannot detect kills the graph before ANY module evaluates, and
|
||
// scriptc compiles the program to exactly that startup throw. The corpus
|
||
// (1613/1616/1618/1619) pins stdout+exit differentially; what lives here
|
||
// is the MESSAGE — Node's exact SyntaxError text, both flavors, hint
|
||
// construction included — which the exit-1 stdout-only contract cannot see.
|
||
describe("CJS named-import link SyntaxError messages", () => {
|
||
test("an entry-level import gets Node's CommonJS hint form, ` as ` rewritten", async () => {
|
||
const r = await compileAndRun(
|
||
"cjs-link-flavored",
|
||
`import { a as z, vis } from './table.cjs';
|
||
console.log('never runs', z, vis);
|
||
`,
|
||
"mjs",
|
||
{ "table.cjs": "'use strict';\nconst v = 5;\nmodule.exports = { vis: v, a: 7 };\n" },
|
||
);
|
||
expect(r.exitCode).toBe(1);
|
||
expect(r.stdout).toBe("");
|
||
expect(r.stderr).toBe(
|
||
"Uncaught SyntaxError: Named export 'a' not found. The requested module './table.cjs' is a CommonJS module, which may not support all module.exports as named exports.\n" +
|
||
"CommonJS modules can always be imported via the default export, for example using:\n" +
|
||
"\n" +
|
||
"import pkg from './table.cjs';\n" +
|
||
"const { a: z, vis } = pkg;\n" +
|
||
"\n",
|
||
);
|
||
});
|
||
|
||
test("a failure below the entry keeps V8's generic wording", async () => {
|
||
// module_job.js rewrites the message only when the failing specifier
|
||
// is one of the ROOT module's own requests — mid.mjs's is not.
|
||
const r = await compileAndRun(
|
||
"cjs-link-generic",
|
||
`import { a } from './mid.mjs';
|
||
console.log('never runs', a);
|
||
`,
|
||
"mjs",
|
||
{
|
||
"mid.mjs": "export { a } from './table.cjs';\n",
|
||
"table.cjs": "'use strict';\nmodule.exports = { a: 7 };\n",
|
||
},
|
||
);
|
||
expect(r.exitCode).toBe(1);
|
||
expect(r.stdout).toBe("");
|
||
expect(r.stderr).toBe(
|
||
"Uncaught SyntaxError: The requested module './table.cjs' does not provide an export named 'a'\n",
|
||
);
|
||
});
|
||
});
|
||
|
||
// Node's strip-only TypeScript loader keeps ordinary named imports in the
|
||
// ESM request graph. If the requested declaration is type-only, Node refuses
|
||
// during linking even though the TypeScript checker accepts the import. The
|
||
// corpus pins stdout and exit; these assertions pin scriptc's exact uncaught
|
||
// SyntaxError message and the local-name ordering used to choose the first
|
||
// failure.
|
||
describe("native ESM type-import link SyntaxError messages", () => {
|
||
test("an unqualified interface import fails before any module evaluates", async () => {
|
||
const r = await compileAndRun(
|
||
"esm-type-link",
|
||
`import { value, Shape as MissingShape } from './types.ts';
|
||
console.log('never runs', value);
|
||
`,
|
||
"ts",
|
||
{
|
||
"types.ts": `export interface Shape { value: number }
|
||
export const value = 7;
|
||
console.log('types evaluated');
|
||
`,
|
||
},
|
||
);
|
||
expect(r.exitCode).toBe(1);
|
||
expect(r.stdout).toBe("");
|
||
expect(r.stderr).toBe(
|
||
"Uncaught SyntaxError: The requested module './types.ts' does not provide an export named 'Shape'\n",
|
||
);
|
||
});
|
||
|
||
test("a child re-export failure wins before the parent import is linked", async () => {
|
||
const r = await compileAndRun(
|
||
"esm-type-link-reexport",
|
||
`import { Zed as zed, Aa as aaa, value } from './middle.ts';
|
||
console.log('never runs', value);
|
||
`,
|
||
"ts",
|
||
{
|
||
"middle.ts": `export { Shape as Zed, Other as Aa, value } from './types.ts';
|
||
console.log('middle evaluated');
|
||
`,
|
||
"types.ts": `export interface Shape { value: number }
|
||
export type Other = string;
|
||
export const value = 9;
|
||
console.log('types evaluated');
|
||
`,
|
||
},
|
||
);
|
||
expect(r.exitCode).toBe(1);
|
||
expect(r.stdout).toBe("");
|
||
expect(r.stderr).toBe(
|
||
"Uncaught SyntaxError: The requested module './types.ts' does not provide an export named 'Shape'\n",
|
||
);
|
||
});
|
||
|
||
test("direct imports choose the first missing binding by local name", async () => {
|
||
const r = await compileAndRun(
|
||
"esm-type-link-order",
|
||
`import { Shape as zed, Other as aaa } from './types.ts';
|
||
console.log('never runs');
|
||
`,
|
||
"ts",
|
||
{
|
||
"types.ts": `export interface Shape { value: number }
|
||
export type Other = string;
|
||
`,
|
||
},
|
||
);
|
||
expect(r.exitCode).toBe(1);
|
||
expect(r.stdout).toBe("");
|
||
expect(r.stderr).toBe(
|
||
"Uncaught SyntaxError: The requested module './types.ts' does not provide an export named 'Other'\n",
|
||
);
|
||
});
|
||
|
||
test("native compilation retains the ESM link failure", async () => {
|
||
const r = await compileAndRun(
|
||
"esm-type-link-dynamic",
|
||
`// @dynamic
|
||
import { Shape as MissingShape } from './types.ts';
|
||
console.log('never runs');
|
||
`,
|
||
"ts",
|
||
{ "types.ts": "export interface Shape { value: number }\nconsole.log('types evaluated');\n" },
|
||
);
|
||
expect(r.exitCode).toBe(1);
|
||
expect(r.stdout).toBe("");
|
||
expect(r.stderr).toBe(
|
||
"Uncaught SyntaxError: The requested module './types.ts' does not provide an export named 'Shape'\n",
|
||
);
|
||
});
|
||
|
||
test("default interface exports satisfy Node's strip-only default link", async () => {
|
||
const r = await compileAndRun(
|
||
"esm-default-interface-link",
|
||
`import DefaultShape from './types.ts';
|
||
type LocalShape = DefaultShape;
|
||
console.log('ok');
|
||
`,
|
||
"ts",
|
||
{ "types.ts": "export default interface DefaultShape { value: number }\n" },
|
||
);
|
||
expect(r.exitCode).toBe(0);
|
||
expect(r.stdout).toBe("ok\n");
|
||
expect(r.stderr).toBe("");
|
||
});
|
||
|
||
test("explicit module packages do not expose default-interface placeholders", async () => {
|
||
const r = await compileAndRun(
|
||
"esm-default-interface-explicit-module",
|
||
`import DefaultShape from './types.ts';
|
||
type LocalShape = DefaultShape;
|
||
console.log('never runs');
|
||
`,
|
||
"ts",
|
||
{
|
||
"package.json": `{ "type": "module" }\n`,
|
||
"types.ts": "export default interface DefaultShape { value: number }\n",
|
||
},
|
||
);
|
||
expect(r.exitCode).toBe(1);
|
||
expect(r.stdout).toBe("");
|
||
expect(r.stderr).toBe(
|
||
"Uncaught SyntaxError: The requested module './types.ts' does not provide an export named 'default'\n",
|
||
);
|
||
});
|
||
|
||
test("typeless packages preserve default-interface placeholders through re-exports", async () => {
|
||
const r = await compileAndRun(
|
||
"esm-default-interface-reexport",
|
||
`import DefaultShape from './reexport.ts';
|
||
type LocalShape = DefaultShape;
|
||
console.log('ok');
|
||
`,
|
||
"ts",
|
||
{
|
||
"reexport.ts": `export { default } from './types.ts';\n`,
|
||
"types.ts": "export default interface DefaultShape { value: number }\n",
|
||
},
|
||
);
|
||
expect(r.exitCode).toBe(0);
|
||
expect(r.stdout).toBe("ok\n");
|
||
expect(r.stderr).toBe("");
|
||
});
|
||
|
||
test("a child native-ESM failure wins over a later parent CJS failure", async () => {
|
||
const r = await compileAndRun(
|
||
"esm-type-link-before-cjs",
|
||
`import './middle.ts';
|
||
import { missing } from './table.cjs';
|
||
console.log('never runs', missing);
|
||
`,
|
||
"ts",
|
||
{
|
||
"middle.ts": `import { Shape } from './types.ts';
|
||
console.log('middle never runs');
|
||
`,
|
||
"types.ts": "export interface Shape { value: number }\nconsole.log('types evaluated');\n",
|
||
"table.cjs": "module.exports = { missing: 7 };\n",
|
||
},
|
||
);
|
||
expect(r.exitCode).toBe(1);
|
||
expect(r.stdout).toBe("");
|
||
expect(r.stderr).toBe(
|
||
"Uncaught SyntaxError: The requested module './types.ts' does not provide an export named 'Shape'\n",
|
||
);
|
||
});
|
||
});
|
||
|
||
describe("checked-dynamic boundary behavior (scriptc-only)", () => {
|
||
test("a checked argument into a call through 'unknown' matches Node", async () => {
|
||
// `this` in a plain JS function is the checked-dynamic ambient
|
||
// receiver; a member CALL through it with an 'any'-typed argument
|
||
|
||
// 383(d)'s "one unbridgeable mix"). The crossing exists now
|
||
// (dynFromJsval, SEMANTICS 394): the argument wraps by reference, the
|
||
// statement compiles for real, and the unbound-`this` member read
|
||
// throws Node's OWN TypeError — the divergence this pin used to own
|
||
// is retired.
|
||
const r = await compileAndRun(
|
||
"checked-arg-dyn-call",
|
||
`// @dynamic
|
||
'use strict';
|
||
function register(item) {
|
||
try {
|
||
this.registry(item);
|
||
return "unreachable";
|
||
} catch (e) {
|
||
return "caught: " + e.message;
|
||
}
|
||
}
|
||
console.log(\`\${register(7)}\`);
|
||
`,
|
||
"mjs",
|
||
{ "tsconfig.json": '{"compilerOptions":{"strict":true,"noImplicitAny":false}}\n' },
|
||
);
|
||
expect(r.exitCode).toBe(0);
|
||
expect(r.stdout).toBe("caught: Cannot read properties of undefined (reading 'registry')\n");
|
||
expect(r.stderr).toBe("");
|
||
});
|
||
});
|
||
|
||
// A cipher name Node lists in crypto.getCiphers() but the runtime does not
|
||
// implement throws a coded SC2020 error naming the cipher when the name is
|
||
// computed; literal names are refused at compile time (node-types-fenced).
|
||
describe("unimplemented cipher names", () => {
|
||
test("a computed cipher name throws SC2020 naming the cipher", async () => {
|
||
const r = await compileAndRun(
|
||
"cipher-computed-name",
|
||
`import { createCipheriv } from "node:crypto";
|
||
const mode = ["c", "c", "m"].join("");
|
||
try {
|
||
createCipheriv("aes-128-" + mode, Buffer.alloc(16), Buffer.alloc(12));
|
||
} catch (error) {
|
||
const e = error as Error & { code?: string };
|
||
console.log(e.name, e.code, e.message);
|
||
}
|
||
try {
|
||
createCipheriv("aes-128-" + mode.slice(1), Buffer.alloc(16), Buffer.alloc(12));
|
||
} catch (error) {
|
||
const e = error as Error & { code?: string };
|
||
console.log(e.name, e.code, e.message);
|
||
}
|
||
`,
|
||
);
|
||
expect(r.exitCode).toBe(0);
|
||
expect(r.stdout).toBe(
|
||
"Error SC2020 The cipher 'aes-128-ccm' has no native implementation; the supported ciphers are " +
|
||
"aes-128-cbc, aes-192-cbc, aes-256-cbc, aes-128-ctr, aes-192-ctr, aes-256-ctr, aes-128-gcm, " +
|
||
"aes-192-gcm, aes-256-gcm and chacha20-poly1305\n" +
|
||
"Error ERR_CRYPTO_UNKNOWN_CIPHER Unknown cipher\n",
|
||
);
|
||
expect(r.stderr).toBe("");
|
||
});
|
||
});
|
||
|
||
// Node formats an unknown output encoding with %s, which prints a
|
||
// function's source text and calls a toString the encoding's class
|
||
// defines. The cipher runtime has neither, so both throw a coded SC2020
|
||
// error.
|
||
describe("cipher output encodings that print through source text or a class toString", () => {
|
||
test("update() and final() throw SC2020", async () => {
|
||
const r = await compileAndRun(
|
||
"cipher-encoding-class-to-string",
|
||
`import { createCipheriv } from "node:crypto";
|
||
class Encoding {
|
||
name: string;
|
||
constructor(name: string) {
|
||
this.name = name;
|
||
}
|
||
toString(): string {
|
||
return this.name;
|
||
}
|
||
}
|
||
const instance: any = new Encoding("nope");
|
||
const fn: any = () => "nope";
|
||
try {
|
||
createCipheriv("aes-128-ctr", Buffer.alloc(16), Buffer.alloc(16)).final(instance);
|
||
} catch (error) {
|
||
const e = error as Error & { code?: string };
|
||
console.log(e.name, e.code, e.message);
|
||
}
|
||
try {
|
||
createCipheriv("aes-128-ctr", Buffer.alloc(16), Buffer.alloc(16)).update("abc", "utf8", fn);
|
||
} catch (error) {
|
||
const e = error as Error & { code?: string };
|
||
console.log(e.name, e.code, e.message);
|
||
}
|
||
`,
|
||
);
|
||
expect(r.exitCode).toBe(0);
|
||
const message =
|
||
"Error SC2020 An output encoding that is a function or an instance of a class defining toString has no lowering; " +
|
||
"pass the encoding name as a string\n";
|
||
expect(r.stdout).toBe(message + message);
|
||
expect(r.stderr).toBe("");
|
||
});
|
||
});
|
||
|
||
// After a Hash's stream flushes its digest, Node copies the finished
|
||
// OpenSSL context, whose later updates and digests fail with OpenSSL
|
||
// error-queue codes. That copy throws a coded SC2020 error.
|
||
describe("copy() of a Hash whose stream has flushed", () => {
|
||
test("throws SC2020", async () => {
|
||
const r = await compileAndRun(
|
||
"hash-stream-copy-after-flush",
|
||
`import { createHash } from "node:crypto";
|
||
const hash = createHash("sha256");
|
||
hash.resume();
|
||
hash.on("finish", () => {
|
||
try {
|
||
hash.copy();
|
||
} catch (error) {
|
||
const e = error as Error & { code?: string };
|
||
console.log(e.name, e.code, e.message);
|
||
}
|
||
});
|
||
hash.end("abc");
|
||
`,
|
||
);
|
||
expect(r.exitCode).toBe(0);
|
||
expect(r.stdout).toBe(
|
||
"Error SC2020 copy() of a Hash whose stream has flushed its digest has no lowering\n",
|
||
);
|
||
expect(r.stderr).toBe("");
|
||
});
|
||
});
|
||
|
||
// A Hash or Hmac lets go of its stream once the stream is destroyed, so
|
||
// listeners that capture the value do not keep it alive. Using the value
|
||
// as a stream again throws a coded SC2020 error; Node keeps the destroyed
|
||
// stream.
|
||
describe("stream use of a Hash after its stream closed", () => {
|
||
test("throws SC2020", async () => {
|
||
const r = await compileAndRun(
|
||
"hash-stream-after-close",
|
||
`import { createHash } from "node:crypto";
|
||
const hash = createHash("sha256");
|
||
hash.resume();
|
||
hash.on("close", () => {
|
||
try {
|
||
hash.on("data", () => {});
|
||
} catch (error) {
|
||
const e = error as Error & { code?: string };
|
||
console.log(e.name, e.code, e.message);
|
||
}
|
||
console.log("digest", hash.digest("hex").slice(0, 8));
|
||
});
|
||
hash.end("abc");
|
||
`,
|
||
);
|
||
expect(r.exitCode).toBe(0);
|
||
expect(r.stdout).toBe(
|
||
"Error SC2020 Stream use of a Hash or Hmac after its stream closed has no lowering\n" +
|
||
"digest ba7816bf\n",
|
||
);
|
||
expect(r.stderr).toBe("");
|
||
});
|
||
});
|
||
|
||
// A Hash or Hmac held in an 'unknown' slot answers update(), digest() and
|
||
// Hash's copy(). Its other members throw a coded SC2020 error, and so do
|
||
// those methods called with another receiver.
|
||
describe("Hash and Hmac members through 'unknown'", () => {
|
||
test("throw SC2020 other than update, digest and copy", async () => {
|
||
const r = await compileAndRun(
|
||
"hash-unknown-members",
|
||
`const crypto = require("node:crypto");
|
||
function attempt(value, run) {
|
||
try {
|
||
run(value);
|
||
} catch (error) {
|
||
console.log(error.name, error.code, error.message);
|
||
}
|
||
}
|
||
const hash = [crypto.createHash("sha256")][0];
|
||
attempt(hash, (h) => h.on("data", () => {}));
|
||
attempt(hash, (h) => h.readable);
|
||
attempt([crypto.createHmac("sha256", "k")][0], (m) => m.write("x"));
|
||
attempt(hash, (h) => h.digest.call({}));
|
||
console.log(hash.update("abc").digest("hex").slice(0, 8));
|
||
`,
|
||
"cjs",
|
||
);
|
||
expect(r.exitCode).toBe(0);
|
||
expect(r.stdout).toBe(
|
||
"Error SC2020 Hash members other than update(), digest() and copy() have no lowering on an 'unknown' value; cast the value to Hash first\n".repeat(
|
||
2,
|
||
) +
|
||
"Error SC2020 Hmac members other than update() and digest() have no lowering on an 'unknown' value; cast the value to Hmac first\n" +
|
||
"Error SC2020 Hash and Hmac methods called with a receiver other than their own Hash or Hmac have no lowering\n" +
|
||
"ba7816bf\n",
|
||
);
|
||
expect(r.stderr).toBe("");
|
||
});
|
||
});
|
||
|
||
// Node 24.22 skips the chacha20-poly1305 tag check when neither
|
||
// setAuthTag() nor authTagLength was used, and returns unverified
|
||
// plaintext. scriptc keeps Node 24.15's behavior: final() fails.
|
||
describe("chacha20-poly1305 decryption without a tag", () => {
|
||
test("final() fails instead of returning unverified plaintext", async () => {
|
||
const r = await compileAndRun(
|
||
"cipher-chacha-no-tag",
|
||
`import { createCipheriv, createDecipheriv } from "node:crypto";
|
||
const key = Buffer.alloc(32, 1);
|
||
const iv = Buffer.alloc(12, 2);
|
||
const cipher = createCipheriv("chacha20-poly1305", key, iv);
|
||
const ciphertext = Buffer.concat([cipher.update("secret"), cipher.final()]);
|
||
for (const input of [ciphertext, Buffer.from("forged data"), Buffer.alloc(0)]) {
|
||
const decipher = createDecipheriv("chacha20-poly1305", key, iv);
|
||
const text = decipher.update(input);
|
||
try {
|
||
decipher.final();
|
||
console.log("accepted", text.toString("hex"));
|
||
} catch (error) {
|
||
const e = error as Error & { code?: string };
|
||
console.log(e.name, e.code, e.message);
|
||
}
|
||
}
|
||
`,
|
||
);
|
||
expect(r.exitCode).toBe(0);
|
||
expect(r.stdout).toBe(
|
||
"Error undefined Unsupported state or unable to authenticate data\n".repeat(3),
|
||
);
|
||
expect(r.stderr).toBe("");
|
||
});
|
||
});
|
||
|
||
// Function expressions the frontend lowers speculatively (the receiver of
|
||
// `.call`/`.apply`, a computed class base) keep their refused statements as
|
||
// runtime fences. They used to vanish: the capture buffer held the refusal,
|
||
// the candidate was kept, and the statement was simply missing from it.
|
||
describe("refused statements in speculatively lowered JavaScript functions", () => {
|
||
test.each([
|
||
[
|
||
"speculative-call-this",
|
||
`(function () {
|
||
var root = this;
|
||
console.log("before");
|
||
console.log(root === eval("this"));
|
||
console.log("after");
|
||
}).call(this);
|
||
`,
|
||
"before\n",
|
||
4,
|
||
],
|
||
[
|
||
"speculative-apply",
|
||
`(function () {
|
||
console.log("before");
|
||
console.log(typeof eval("1"));
|
||
}).apply(null, []);
|
||
`,
|
||
"before\n",
|
||
3,
|
||
],
|
||
[
|
||
"speculative-class-base",
|
||
`class Base {}
|
||
class Derived extends (function () {
|
||
console.log("before");
|
||
console.log(typeof eval("1"));
|
||
return Base;
|
||
})() {}
|
||
console.log(typeof Derived);
|
||
`,
|
||
"before\n",
|
||
4,
|
||
],
|
||
] as const)("%s", async (name, source, stdout, line) => {
|
||
const r = await compileAndRun(name, source, "cjs");
|
||
expect(r.exitCode).toBe(1);
|
||
expect(r.stdout).toBe(stdout);
|
||
expect(r.stderr).toMatch(
|
||
new RegExp(
|
||
`^Uncaught Error: values of type '\\(x: string\\) => any' cannot be compiled: .* \\[SC2009 at .*${name}\\.cjs:${line}\\]\\n$`,
|
||
),
|
||
);
|
||
});
|
||
});
|
||
|
||
describe("constructors returning objects (JS lane)", () => {
|
||
test("a returned plain object is a named deferred fence", async () => {
|
||
// Node's `new Box(2)` answers `{ size: 99 }`; a Box instance cannot
|
||
// hold that object, so the return refuses instead of answering `this`.
|
||
const r = await compileAndRun(
|
||
"constructor-returns-object",
|
||
`'use strict';
|
||
class Box {
|
||
constructor(size) {
|
||
this.size = size;
|
||
if (size > 1) return { size: 99 };
|
||
}
|
||
}
|
||
console.log(new Box(1).size);
|
||
console.log(new Box(2));
|
||
`,
|
||
"cjs",
|
||
);
|
||
expect(r.exitCode).toBe(1);
|
||
expect(r.stdout).toBe("1\n");
|
||
expect(r.stderr).toMatch(
|
||
/^Uncaught Error: returning an object other than an instance of the class from a constructor \(Node's 'new Box\(\)' answers the returned object, .*\) is not supported yet \[SC1090 at .*constructor-returns-object\.cjs:5\]\n$/,
|
||
);
|
||
});
|
||
|
||
test("an untyped returned value fences only when it holds a foreign object", async () => {
|
||
// Primitive results answer the constructed instance, as in Node; an
|
||
// object that is not a Wrap instance refuses.
|
||
const r = await compileAndRun(
|
||
"constructor-returns-untyped",
|
||
`'use strict';
|
||
class Wrap {
|
||
constructor(value) {
|
||
this.value = value;
|
||
return value;
|
||
}
|
||
}
|
||
console.log(new Wrap(1).value, new Wrap('s').value, new Wrap(null).value);
|
||
const w = new Wrap(0);
|
||
console.log(new Wrap(w) === w);
|
||
console.log(new Wrap({ a: 1 }));
|
||
`,
|
||
"cjs",
|
||
);
|
||
expect(r.exitCode).toBe(1);
|
||
expect(r.stdout).toBe("1 s null\ntrue\n");
|
||
expect(r.stderr).toMatch(
|
||
/^Uncaught Error: returning an object other than an instance of the class from a constructor .* is not supported yet \[SC1090 at .*constructor-returns-untyped\.cjs:5\]\n$/,
|
||
);
|
||
});
|
||
});
|
||
|
||
describe("module exports that read this, called through the module binding (JS lane)", () => {
|
||
test("a require binding's member call is a named deferred fence", async () => {
|
||
// Node passes the exports object as `this` to `m.hello()`; the direct
|
||
// call has none to pass, so it refuses instead of reading another one.
|
||
const r = await compileAndRun(
|
||
"require-member-this",
|
||
`'use strict';
|
||
const m = require('./lib.cjs');
|
||
console.log(m.plain(21));
|
||
console.log(m.hello());
|
||
`,
|
||
"cjs",
|
||
{
|
||
"lib.cjs": `'use strict';
|
||
exports.name = 'lib';
|
||
exports.hello = function () { return 'hi from ' + this.name; };
|
||
exports.plain = function (x) { return x * 2; };
|
||
`,
|
||
},
|
||
);
|
||
expect(r.exitCode).toBe(1);
|
||
expect(r.stdout).toBe("42\n");
|
||
expect(r.stderr).toMatch(
|
||
/^Uncaught Error: calling a module export that reads 'this' through its module binding \('hello': .*\) is not supported yet \[SC1090 at .*require-member-this\.cjs:4\]\n$/,
|
||
);
|
||
});
|
||
|
||
test("a namespace import's member call is a named deferred fence", async () => {
|
||
const r = await compileAndRun(
|
||
"namespace-member-this",
|
||
`import * as ns from './lib.mjs';
|
||
console.log(ns.plain(1));
|
||
console.log(ns.who());
|
||
`,
|
||
"mjs",
|
||
{
|
||
"lib.mjs": `export function who() { return typeof this; }
|
||
export function plain(x) { return x + 1; }
|
||
`,
|
||
},
|
||
);
|
||
expect(r.exitCode).toBe(1);
|
||
expect(r.stdout).toBe("2\n");
|
||
expect(r.stderr).toMatch(
|
||
/^Uncaught Error: calling a module export that reads 'this' through its module binding \('who': .*\) is not supported yet \[SC1090 at .*namespace-member-this\.mjs:3\]\n$/,
|
||
);
|
||
});
|
||
|
||
test("a namespace import's export that passes this on with apply or call compiles", async () => {
|
||
// Effect 4's provideService: `dual(...).apply(this, arguments)` hands the
|
||
// receiver to a function that `dual` returns, which reads it only to
|
||
// pass it on to an arrow, as here.
|
||
const r = await compileAndRun(
|
||
"namespace-member-forwards-this",
|
||
`import * as ns from './lib.mjs';
|
||
export const added = ns.provide(1, 2, 3);
|
||
console.log(added, ns.sum(3, 4));
|
||
`,
|
||
"mjs",
|
||
{
|
||
"lib.mjs": `const dual = function (arity, body) {
|
||
switch (arity) {
|
||
case 2:
|
||
return function (a, b) { return arguments.length >= 2 ? body(a, b) : (self) => body(self, a); };
|
||
default:
|
||
return function () { return body.apply(this, arguments); };
|
||
}
|
||
};
|
||
export const provide = function () { return dual(3, (a, b, c) => a + b + c).apply(this, arguments); };
|
||
const twice = function (a, b) { return (a + b) * 2; };
|
||
export const sum = function (a, b) { return twice.call(this, a, b); };
|
||
`,
|
||
},
|
||
);
|
||
expect(r).toEqual({ stdout: "6 14\n", stderr: "", exitCode: 0 });
|
||
});
|
||
|
||
test("a require binding's export that passes this on with apply compiles", async () => {
|
||
const r = await compileAndRun(
|
||
"require-member-forwards-this",
|
||
`'use strict';
|
||
const m = require('./lib.cjs');
|
||
console.log(m.scale(5));
|
||
`,
|
||
"cjs",
|
||
{
|
||
"lib.cjs": `'use strict';
|
||
const triple = function (x) { return x * 3; };
|
||
exports.scale = function () { return triple.apply(this, arguments); };
|
||
`,
|
||
},
|
||
);
|
||
expect(r).toEqual({ stdout: "15\n", stderr: "", exitCode: 0 });
|
||
});
|
||
|
||
test.each([
|
||
[
|
||
"a namespace import's export that passes this on to a function that reads it",
|
||
"namespace-member-forwards-to-reader",
|
||
"mjs",
|
||
`import * as ns from './lib.mjs';
|
||
console.log(ns.plain(1));
|
||
console.log(ns.viaCall('?'));
|
||
`,
|
||
{
|
||
"lib.mjs": `export const tag = 'ns';
|
||
function read(suffix) { return this.tag + suffix; }
|
||
export function viaCall(suffix) { return read.call(this, suffix); }
|
||
export function plain(x) { return x + 1; }
|
||
`,
|
||
},
|
||
"2\n",
|
||
"viaCall",
|
||
3,
|
||
],
|
||
[
|
||
"a namespace import's export that passes this on through a factory to a function that reads it",
|
||
"namespace-member-forwards-through-factory",
|
||
"mjs",
|
||
`import * as ns from './lib.mjs';
|
||
console.log(ns.plain(1));
|
||
console.log(ns.viaDual('?'));
|
||
`,
|
||
{
|
||
"lib.mjs": `export const tag = 'ns';
|
||
const dual = function (arity, body) {
|
||
switch (arity) {
|
||
case 2:
|
||
return function (a, b) { return arguments.length >= 2 ? body(a, b) : (self) => body(self, a); };
|
||
default:
|
||
return function () { return body.apply(this, arguments); };
|
||
}
|
||
};
|
||
export const viaDual = function () { return dual(9, function (s) { return this.tag + s; }).apply(this, arguments); };
|
||
export function plain(x) { return x + 1; }
|
||
`,
|
||
},
|
||
"2\n",
|
||
"viaDual",
|
||
3,
|
||
],
|
||
[
|
||
"a namespace import's export that passes this on through a factory that reassigns its parameter",
|
||
"namespace-member-forwards-reassigned",
|
||
"mjs",
|
||
`import * as ns from './lib.mjs';
|
||
console.log(ns.plain(1));
|
||
console.log(ns.viaWrap('?'));
|
||
`,
|
||
{
|
||
"lib.mjs": `export const tag = 'ns';
|
||
const wrap = function (body) {
|
||
body = function (s) { return this.tag + s; };
|
||
return function () { return body.apply(this, arguments); };
|
||
};
|
||
export const viaWrap = function () { return wrap((s) => s).apply(this, arguments); };
|
||
export function plain(x) { return x + 1; }
|
||
`,
|
||
},
|
||
"2\n",
|
||
"viaWrap",
|
||
3,
|
||
],
|
||
[
|
||
"a namespace import's export that passes this on through a factory that replaces its parameter as a destructuring",
|
||
"namespace-member-forwards-destructured",
|
||
"mjs",
|
||
`import * as ns from './lib.mjs';
|
||
console.log(ns.plain(1));
|
||
console.log(ns.viaWrap('?'));
|
||
`,
|
||
{
|
||
"lib.mjs": `export const tag = 'ns';
|
||
const wrap = function (body) {
|
||
[body] = [function (s) { return this.tag + s; }];
|
||
return function () { return body.apply(this, arguments); };
|
||
};
|
||
export const viaWrap = function () { return wrap((s) => s).apply(this, arguments); };
|
||
export function plain(x) { return x + 1; }
|
||
`,
|
||
},
|
||
"2\n",
|
||
"viaWrap",
|
||
3,
|
||
],
|
||
[
|
||
"a namespace import's export that passes this on through a factory that replaces its parameter as a object destructuring",
|
||
"namespace-member-forwards-object-destructured",
|
||
"mjs",
|
||
`import * as ns from './lib.mjs';
|
||
console.log(ns.plain(1));
|
||
console.log(ns.viaWrap('?'));
|
||
`,
|
||
{
|
||
"lib.mjs": `export const tag = 'ns';
|
||
const wrap = function (body) {
|
||
({ body } = { body: function (s) { return this.tag + s; } });
|
||
return function () { return body.apply(this, arguments); };
|
||
};
|
||
export const viaWrap = function () { return wrap((s) => s).apply(this, arguments); };
|
||
export function plain(x) { return x + 1; }
|
||
`,
|
||
},
|
||
"2\n",
|
||
"viaWrap",
|
||
3,
|
||
],
|
||
[
|
||
"a namespace import's export that passes this on through a factory that replaces its parameter as a for-of target",
|
||
"namespace-member-forwards-for-of",
|
||
"mjs",
|
||
`import * as ns from './lib.mjs';
|
||
console.log(ns.plain(1));
|
||
console.log(ns.viaWrap('?'));
|
||
`,
|
||
{
|
||
"lib.mjs": `export const tag = 'ns';
|
||
const wrap = function (body) {
|
||
for (body of [function (s) { return this.tag + s; }]);
|
||
return function () { return body.apply(this, arguments); };
|
||
};
|
||
export const viaWrap = function () { return wrap((s) => s).apply(this, arguments); };
|
||
export function plain(x) { return x + 1; }
|
||
`,
|
||
},
|
||
"2\n",
|
||
"viaWrap",
|
||
3,
|
||
],
|
||
[
|
||
"a require binding's export that passes this on to a function that reads it",
|
||
"require-member-forwards-to-reader",
|
||
"cjs",
|
||
`'use strict';
|
||
const m = require('./lib.cjs');
|
||
console.log(m.plain(21));
|
||
console.log(m.viaApply('!'));
|
||
`,
|
||
{
|
||
"lib.cjs": `'use strict';
|
||
exports.name = 'lib';
|
||
const read = function (suffix) { return this.name + suffix; };
|
||
exports.viaApply = function () { return read.apply(this, arguments); };
|
||
exports.plain = function (x) { return x * 2; };
|
||
`,
|
||
},
|
||
"42\n",
|
||
"viaApply",
|
||
4,
|
||
],
|
||
] as const)(
|
||
"%s is a named deferred fence",
|
||
async (_, name, ext, source, files, stdout, member, line) => {
|
||
// Node passes the module object on to `read`, which reads it; the
|
||
// direct call has none to pass.
|
||
const r = await compileAndRun(name, source, ext, files);
|
||
expect(r.exitCode).toBe(1);
|
||
expect(r.stdout).toBe(stdout);
|
||
expect(r.stderr).toMatch(
|
||
new RegExp(
|
||
`^Uncaught Error: calling a module export that reads 'this' through its module binding \\('${member}': .*\\) is not supported yet \\[SC1090 at .*${name}\\.${ext}:${line}\\]\\n$`,
|
||
),
|
||
);
|
||
},
|
||
);
|
||
|
||
test("an export that also reads this beside passing it on is a named deferred fence", async () => {
|
||
const r = await compileAndRun(
|
||
"namespace-member-forwards-and-reads-this",
|
||
`import * as ns from './lib.mjs';
|
||
console.log(ns.tag('x'));
|
||
`,
|
||
"mjs",
|
||
{
|
||
"lib.mjs": `const join = function (a, b) { return a + ':' + b; };
|
||
export function tag(x) { return join.call(this, x, typeof this); }
|
||
`,
|
||
},
|
||
);
|
||
expect(r.exitCode).toBe(1);
|
||
expect(r.stdout).toBe("");
|
||
expect(r.stderr).toMatch(
|
||
/^Uncaught Error: calling a module export that reads 'this' through its module binding \('tag': .*\) is not supported yet \[SC1090 at .*namespace-member-forwards-and-reads-this\.mjs:2\]\n$/,
|
||
);
|
||
});
|
||
});
|
||
|
||
// CommonJS forms around the module's exports that keep a refusal name the
|
||
// construct itself (they used to report an unlowerable `require` value, a
|
||
// "binding form with no lowering", or the ES-module `module` message).
|
||
describe("CommonJS export-object and inline require refusals", () => {
|
||
test.each([
|
||
[
|
||
"exports-as-argument",
|
||
`exports.x = 1;
|
||
function keys(o) {
|
||
return Object.keys(o);
|
||
}
|
||
console.log("before");
|
||
console.log(keys(exports));
|
||
`,
|
||
{},
|
||
"before\n",
|
||
6,
|
||
"'exports' as a value other than a member access, comparison, typeof or truthiness test \\(the compiled exports are a static table, not an object: read exports\\.<name>\\)",
|
||
],
|
||
[
|
||
"exports-rebinding",
|
||
`console.log("before");
|
||
exports = function hash(x) {
|
||
return x;
|
||
};
|
||
exports.extra = 1;
|
||
`,
|
||
{},
|
||
"before\n",
|
||
2,
|
||
"rebinding 'exports' without module\\.exports \\(exports = value\\)",
|
||
],
|
||
[
|
||
"require-computed",
|
||
`const name = "./lib.cjs";
|
||
console.log("before");
|
||
console.log(require(name).x);
|
||
`,
|
||
{ "lib.cjs": "exports.x = 1;\n" },
|
||
"before\n",
|
||
3,
|
||
"require\\(\\) with a computed specifier",
|
||
],
|
||
[
|
||
"exports-call-reads-this",
|
||
`exports.timeouts = function (n) {
|
||
return this.scale * n;
|
||
};
|
||
exports.scale = 2;
|
||
console.log("before");
|
||
console.log(exports.timeouts(3));
|
||
`,
|
||
{},
|
||
"before\n",
|
||
6,
|
||
"calling 'exports\\.timeouts' through the module's exports object when the function may read 'this'",
|
||
],
|
||
[
|
||
"require-table-as-value",
|
||
`function count(o) {
|
||
return Object.keys(o).length;
|
||
}
|
||
console.log("before");
|
||
console.log(count(require("./lib.cjs")));
|
||
`,
|
||
{ "lib.cjs": "exports.x = 1;\n" },
|
||
"before\n",
|
||
5,
|
||
"the exports object of '\\./lib\\.cjs' as a value",
|
||
],
|
||
] as const)("%s", async (name, source, extra, stdout, line, message) => {
|
||
const r = await compileAndRun(name, source, "cjs", extra);
|
||
expect(r.exitCode).toBe(1);
|
||
expect(r.stdout).toBe(stdout);
|
||
expect(r.stderr).toMatch(
|
||
new RegExp(`^Uncaught Error: ${message}.*\\[SC1090 at .*${name}\\.cjs:${line}\\]\\n$`),
|
||
);
|
||
});
|
||
});
|
||
|
||
// WebCrypto's native slice is HMAC importKey('raw'), sign and verify, and
|
||
// the SHA-1/SHA-2 digests. Algorithms Node recognizes outside that slice
|
||
// reject with a coded SC2020 Error instead of a Node-shaped success, and a
|
||
// JavaScript call with the 'jwk' format meets its named fence when it runs.
|
||
// Node's own rejections (unknown names, bad usages) are corpus-pinned
|
||
// (6292, 6294); these refusals have no Node twin, so they live here.
|
||
describe("WebCrypto refusals (JS lane)", () => {
|
||
test("recognized algorithms outside the HMAC slice reject with SC2020", async () => {
|
||
const r = await compileAndRun(
|
||
"webcrypto-unsupported-algorithms",
|
||
`const bytes = new TextEncoder().encode("secret");
|
||
async function attempt(label, run) {
|
||
try {
|
||
await run();
|
||
console.log(label, "resolved");
|
||
} catch (e) {
|
||
console.log(label, e.name, e.code, e.message);
|
||
}
|
||
}
|
||
await attempt("rsa", () =>
|
||
crypto.subtle.importKey("raw", bytes, { name: "RSA-PSS", hash: "SHA-256" }, false, ["verify"]),
|
||
);
|
||
const key = await crypto.subtle.importKey("raw", bytes, { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
|
||
await attempt("ecdsa", () => crypto.subtle.sign({ name: "ECDSA", hash: "SHA-256" }, key, bytes));
|
||
await attempt("sha3", () => crypto.subtle.digest("SHA3-256", bytes));
|
||
await attempt("sha3 hmac", () =>
|
||
crypto.subtle.importKey("raw", bytes, { name: "HMAC", hash: "SHA3-256" }, false, ["sign"]),
|
||
);
|
||
const format = JSON.parse('"jwk"');
|
||
await attempt("computed jwk", () =>
|
||
crypto.subtle.importKey(format, { kty: "oct", k: "c2VjcmV0" }, { name: "HMAC", hash: "SHA-256" }, false, ["sign"]),
|
||
);
|
||
`,
|
||
"mjs",
|
||
);
|
||
expect(r.exitCode).toBe(0);
|
||
expect(r.stderr).toBe("");
|
||
expect(r.stdout).toBe(
|
||
"rsa Error SC2020 crypto.subtle.importKey with the RSA-PSS algorithm is not supported yet\n" +
|
||
"ecdsa Error SC2020 crypto.subtle.sign with the ECDSA algorithm is not supported yet\n" +
|
||
"sha3 Error SC2020 crypto.subtle.digest with the SHA3-256 algorithm is not supported yet\n" +
|
||
"sha3 hmac Error SC2020 crypto.subtle.importKey with the SHA3-256 algorithm is not supported yet\n" +
|
||
"computed jwk Error SC2020 crypto.subtle.importKey with the 'jwk' key format is not supported yet\n",
|
||
);
|
||
});
|
||
|
||
test("a literal 'jwk' import is a named fence where it runs", async () => {
|
||
const r = await compileAndRun(
|
||
"webcrypto-jwk-literal",
|
||
`console.log("before");
|
||
await crypto.subtle.importKey("jwk", { kty: "oct", k: "c2VjcmV0" }, { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
|
||
`,
|
||
"mjs",
|
||
);
|
||
expect(r.exitCode).toBe(1);
|
||
expect(r.stdout).toBe("before\n");
|
||
expect(r.stderr).toMatch(
|
||
/^Uncaught Error: 'crypto\.subtle\.importKey with the 'jwk' key format' is part of the standard library types but has no scriptc lowering yet.*\n$/,
|
||
);
|
||
});
|
||
});
|