mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-26 06:34:20 +08:00
1988 lines
71 KiB
TypeScript
1988 lines
71 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.each([
|
||
["an argument", "make(Upper, { objectMode: true });"],
|
||
["a spread argument", "spread(Upper, [{ objectMode: true }]);"],
|
||
])(
|
||
"%s through an untyped factory to an inherited stream constructor is a named deferred fence (JS lane)",
|
||
async (_, call) => {
|
||
// The constructor a ctor-less stream subclass inherits takes no
|
||
// arguments here; typed construction refuses them at compile time,
|
||
// and construction from untyped code refuses them at run time
|
||
// instead of constructing without them.
|
||
const r = await compileAndRun(
|
||
`inherited-stream-ctor-${call.startsWith("make") ? "args" : "spread"}`,
|
||
`'use strict';
|
||
const { Transform } = require('node:stream');
|
||
class Upper extends Transform {}
|
||
function make(F, options) { return new F(options); }
|
||
function bare(F) { return new F(); }
|
||
function spread(F, args) { return new F(...args); }
|
||
console.log('bare', bare(Upper) instanceof Upper, spread(Upper, []) instanceof Upper);
|
||
${call}
|
||
`,
|
||
"cjs",
|
||
);
|
||
expect(r.exitCode).toBe(1);
|
||
expect(r.stdout).toBe("bare true true\n");
|
||
expect(r.stderr).toMatch(
|
||
/^Uncaught Error: new Upper with arguments through an inherited stream constructor .* is not supported yet \[SC1090 at .*inherited-stream-ctor-\w+\.cjs:3\]\n$/,
|
||
);
|
||
},
|
||
);
|
||
|
||
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, aes-192 and aes-256 in cbc, ctr, gcm, ecb, cfb, cfb8, cfb1 and ofb modes, des-ede3 in " +
|
||
"cbc, ecb, cfb, cfb8, cfb1 and ofb modes, des-ede in cbc, ecb, cfb and ofb modes, chacha20 and " +
|
||
"chacha20-poly1305\n" +
|
||
"Error ERR_CRYPTO_UNKNOWN_CIPHER Unknown cipher\n",
|
||
);
|
||
expect(r.stderr).toBe("");
|
||
});
|
||
});
|
||
|
||
// new tls.TLSSocket reads a runtime options record: the client mode, whose
|
||
// handshake Node never starts, and members without a lowering throw the
|
||
// fence, as do sockets it cannot take over (a second wrap, and
|
||
// tls.connect({ socket }) over a socket that is still dialing).
|
||
describe("tls.TLSSocket runtime fences", () => {
|
||
test("client mode, unlowered members and sockets that cannot be taken over throw", async () => {
|
||
const r = await compileAndRun(
|
||
"tls-socket-fences",
|
||
`import { readFileSync } from "node:fs";
|
||
import * as net from "node:net";
|
||
import * as tls from "node:tls";
|
||
const certs = "tests/fixtures/server/certs";
|
||
const cert = readFileSync(\`\${certs}/localhost.pem\`);
|
||
const key = readFileSync(\`\${certs}/localhost-key.pem\`);
|
||
function report(label: string, run: () => void): void {
|
||
try {
|
||
run();
|
||
console.log(label, "no error");
|
||
} catch (error) {
|
||
console.log(label, (error as Error).message);
|
||
}
|
||
}
|
||
const client: Record<string, unknown> = { cert, key };
|
||
const session: Record<string, unknown> = { isServer: true, cert, key, session: Buffer.alloc(1) };
|
||
const server: Record<string, unknown> = { isServer: true, cert, key };
|
||
const listener = net.createServer((raw) => {
|
||
report("client mode", () => new tls.TLSSocket(raw, client));
|
||
report("session", () => new tls.TLSSocket(raw, session));
|
||
const wrapped = new tls.TLSSocket(raw, server);
|
||
report("twice", () => new tls.TLSSocket(raw, server));
|
||
wrapped.destroy();
|
||
listener.close();
|
||
});
|
||
listener.listen(0, () => {
|
||
const c = net.connect((listener.address() as net.AddressInfo).port, "localhost");
|
||
report("dialing", () => tls.connect({ socket: c }));
|
||
c.on("error", () => {});
|
||
});
|
||
`,
|
||
);
|
||
expect(r.stderr).toBe("");
|
||
expect(r.stdout).toBe(
|
||
"dialing tls.connect over a socket that is not connected, or is already wrapped, is not supported yet\n" +
|
||
"client mode new tls.TLSSocket option 'isServer' has no static lowering — Node starts no handshake for a client-mode TLSSocket; tls.connect({ socket }) runs the client handshake over an open socket\n" +
|
||
"session new tls.TLSSocket option 'session' has no static lowering — isServer, cert, key, secureContext, ALPNProtocols, minVersion and maxVersion are the implemented members\n" +
|
||
"twice new tls.TLSSocket over a socket that is not connected, or is already wrapped, is not supported yet\n",
|
||
);
|
||
});
|
||
});
|
||
|
||
// With a secureContext, the TLSSocket's own options still apply: a truthy
|
||
// requestCert, which asks for client certificates this runtime does not
|
||
// verify, and SNICallback throw the fence instead of being dropped.
|
||
describe("tls.TLSSocket socket options beside a secureContext", () => {
|
||
test("requestCert and SNICallback throw the fence", async () => {
|
||
const r = await compileAndRun(
|
||
"tls-socket-context-fences",
|
||
`import { readFileSync } from "node:fs";
|
||
import * as net from "node:net";
|
||
import * as tls from "node:tls";
|
||
const certs = "tests/fixtures/server/certs";
|
||
const cert = readFileSync(\`\${certs}/localhost.pem\`);
|
||
const key = readFileSync(\`\${certs}/localhost-key.pem\`);
|
||
const context = tls.createSecureContext({ cert, key });
|
||
function report(label: string, run: () => void): void {
|
||
try {
|
||
run();
|
||
console.log(label, "no error");
|
||
} catch (error) {
|
||
console.log(label, (error as Error).message);
|
||
}
|
||
}
|
||
const listener = net.createServer((raw) => {
|
||
report("requestCert", () => new tls.TLSSocket(raw, { isServer: true, secureContext: context, requestCert: true, rejectUnauthorized: true }));
|
||
report("SNICallback", () => new tls.TLSSocket(raw, { isServer: true, secureContext: context, SNICallback: (name: string, cb: (err: Error | null, ctx?: tls.SecureContext) => void) => cb(null, context) }));
|
||
report("requestCert false", () => new tls.TLSSocket(raw, { isServer: true, secureContext: context, requestCert: false, rejectUnauthorized: true }).destroy());
|
||
listener.close();
|
||
});
|
||
listener.listen(0, () => {
|
||
const c = net.connect((listener.address() as net.AddressInfo).port, "localhost");
|
||
c.on("error", () => {});
|
||
c.on("close", () => console.log("client closed"));
|
||
});
|
||
`,
|
||
);
|
||
expect(r.stderr).toBe("");
|
||
expect(r.stdout).toBe(
|
||
"requestCert new tls.TLSSocket option 'requestCert' has no static lowering — client-certificate handshakes are not modeled (SEMANTICS.md divergence 55) — requestCert stays at Node's false default\n" +
|
||
"SNICallback new tls.TLSSocket option 'SNICallback' has no static lowering — isServer, cert, key, secureContext, ALPNProtocols, minVersion and maxVersion are the implemented members\n" +
|
||
"requestCert false no error\n" +
|
||
"client closed\n",
|
||
);
|
||
});
|
||
});
|
||
|
||
// A curve Node lists in crypto.getCurves() but the runtime does not
|
||
// implement throws a coded SC2020 error naming the curve when the name is
|
||
// computed; literal names are refused at compile time (node-types-fenced).
|
||
describe("unimplemented ECDH curves", () => {
|
||
test("a computed curve name throws SC2020 naming the curve", async () => {
|
||
const r = await compileAndRun(
|
||
"ecdh-computed-curve",
|
||
`import { createECDH, ECDH } from "node:crypto";
|
||
const size = ["1", "6", "3"].join("");
|
||
function report(error: unknown): void {
|
||
const e = error as Error & { code?: string };
|
||
console.log(e.name, e.code, e.message);
|
||
}
|
||
try {
|
||
createECDH("sect" + size + "k1");
|
||
} catch (error) {
|
||
report(error);
|
||
}
|
||
try {
|
||
ECDH.convertKey(Buffer.from([0]), "sect" + size + "k1");
|
||
} catch (error) {
|
||
report(error);
|
||
}
|
||
try {
|
||
createECDH("sect" + size + "x1");
|
||
} catch (error) {
|
||
report(error);
|
||
}
|
||
`,
|
||
);
|
||
expect(r.exitCode).toBe(0);
|
||
const supported =
|
||
"has no native implementation; the supported curves are prime192v1, secp224r1, prime256v1, " +
|
||
"secp384r1, secp521r1, secp192k1, secp224k1, secp256k1, brainpoolP256r1, brainpoolP384r1 and " +
|
||
"brainpoolP512r1";
|
||
expect(r.stdout).toBe(
|
||
`Error SC2020 The curve 'sect163k1' ${supported}\n` +
|
||
`Error SC2020 The curve 'sect163k1' ${supported}\n` +
|
||
"TypeError ERR_CRYPTO_INVALID_CURVE Invalid EC curve name\n",
|
||
);
|
||
expect(r.stderr).toBe("");
|
||
});
|
||
|
||
test("ECDH property reads and writes throw SC2020", async () => {
|
||
const r = await compileAndRun(
|
||
"ecdh-properties",
|
||
`const crypto = require("node:crypto");
|
||
const ecdh = crypto.createECDH("prime256v1");
|
||
for (const run of [() => ecdh.getPublicKey.name, () => { ecdh.custom = 1; }]) {
|
||
try {
|
||
run();
|
||
} catch (e) {
|
||
console.log(e.name, e.code);
|
||
}
|
||
}
|
||
`,
|
||
"cjs",
|
||
);
|
||
expect(r.exitCode).toBe(0);
|
||
expect(r.stdout).toBe("Error SC2020\nError SC2020\n");
|
||
});
|
||
});
|
||
|
||
// DiffieHellman primes past mbedTLS's 8192-bit limit throw a coded SC2020
|
||
// error, as do property reads other than verifyError, property writes, and
|
||
// Object.prototype method calls on an untyped DiffieHellman.
|
||
describe("DiffieHellman runtime refusals", () => {
|
||
test("primes longer than 8192 bits throw SC2020", async () => {
|
||
const r = await compileAndRun(
|
||
"dh-long-prime",
|
||
`import { createDiffieHellman } from "node:crypto";
|
||
const prime = Buffer.alloc(1025, 0xff);
|
||
for (const run of [() => createDiffieHellman(prime), () => createDiffieHellman(8193)]) {
|
||
try {
|
||
run();
|
||
} catch (error) {
|
||
const e = error as Error & { code?: string };
|
||
console.log(e.name, e.code, e.message);
|
||
}
|
||
}
|
||
console.log(createDiffieHellman(Buffer.alloc(1024, 0xff)).verifyError);
|
||
`,
|
||
);
|
||
expect(r.exitCode).toBe(0);
|
||
const message = "DiffieHellman primes longer than 8192 bits have no native implementation";
|
||
expect(r.stdout).toBe(`Error SC2020 ${message}\nError SC2020 ${message}\n1\n`);
|
||
expect(r.stderr).toBe("");
|
||
});
|
||
|
||
test("DiffieHellman property reads, writes and Object.prototype calls throw SC2020", async () => {
|
||
const r = await compileAndRun(
|
||
"dh-properties",
|
||
`const crypto = require("node:crypto");
|
||
const dh = crypto.getDiffieHellman("modp1");
|
||
console.log(dh.verifyError);
|
||
for (const run of [() => dh.getPrime.name, () => { dh.verifyError = 1; }, () => dh.hasOwnProperty("verifyError")]) {
|
||
try {
|
||
run();
|
||
} catch (e) {
|
||
console.log(e.name, e.code);
|
||
}
|
||
}
|
||
`,
|
||
"cjs",
|
||
);
|
||
expect(r.exitCode).toBe(0);
|
||
expect(r.stdout).toBe("0\nError SC2020\nError SC2020\nError SC2020\n");
|
||
});
|
||
});
|
||
|
||
// 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");
|
||
process.on("__proto__", () => root);
|
||
console.log("after");
|
||
}).call(this);
|
||
`,
|
||
"before\n",
|
||
4,
|
||
],
|
||
[
|
||
"speculative-apply",
|
||
`(function () {
|
||
console.log("before");
|
||
process.on("__proto__", () => {});
|
||
}).apply(null, []);
|
||
`,
|
||
"before\n",
|
||
3,
|
||
],
|
||
[
|
||
"speculative-class-base",
|
||
`class Base {}
|
||
class Derived extends (function () {
|
||
console.log("before");
|
||
process.on("__proto__", () => {});
|
||
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: 'process\\.on\\("__proto__", \\.\\.\\.\\)' is part of the standard library types but has no scriptc lowering yet \\[SC2020 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$`),
|
||
);
|
||
});
|
||
});
|
||
|
||
describe("builtin constructor values (JS lane)", () => {
|
||
test("members without a lowering stay refused instead of reading undefined", async () => {
|
||
// Builtin constructors read as interned function values that model only
|
||
// name, length and constructor. Every other static member or prototype
|
||
// read (direct, bracketed, `in`, through an alias or globalThis), and
|
||
// reflection on or redefinition of their members, must stay a located
|
||
// SC2020/SC1090 refusal, never an undefined or otherwise wrong value.
|
||
// Test262 lost 769 refusals to undefined reads before this was pinned.
|
||
const reads = [
|
||
["Map.prototype", "Map.prototype"],
|
||
["Date.prototype", "Date.prototype"],
|
||
["RegExp.prototype", "RegExp.prototype"],
|
||
["Set.prototype", "Set.prototype"],
|
||
["WeakMap.prototype", "WeakMap.prototype"],
|
||
["WeakSet.prototype", "WeakSet.prototype"],
|
||
["Date.prototype.getYear", "Date.prototype.getYear"],
|
||
["Map.prototype.constructor", "Map.prototype.constructor"],
|
||
["Object.isFrozen", "Object.isFrozen"],
|
||
[
|
||
"getOwnPropertyDescriptor(Map.prototype)",
|
||
'Object.getOwnPropertyDescriptor(Map.prototype, "size")',
|
||
],
|
||
["isExtensible(Date.prototype)", "Object.isExtensible(Date.prototype)"],
|
||
["Map['prototype']", 'Map["prototype"]'],
|
||
["'prototype' in Map", '"prototype" in Map'],
|
||
["globalThis.Map.prototype", "globalThis.Map.prototype"],
|
||
["Map alias prototype", "MapAlias.prototype"],
|
||
["Date alias prototype", "DateAlias.prototype"],
|
||
["Object alias isFrozen", "ObjectAlias.isFrozen"],
|
||
["Object.hasOwn(Object)", 'Object.hasOwn(Object, "hasOwn")'],
|
||
["getOwnPropertyNames(Date)", "Object.getOwnPropertyNames(Date)"],
|
||
["defineProperty(Promise)", 'Object.defineProperty(Promise, "resolve", { value: 1 })'],
|
||
["hasOwnProperty.call(Map)", 'Object.prototype.hasOwnProperty.call(Map, "groupBy")'],
|
||
] as const;
|
||
const source = `"use strict";
|
||
const MapAlias = Map;
|
||
const DateAlias = Date;
|
||
const { Object: ObjectAlias } = globalThis;
|
||
function report(label, e) {
|
||
const m = /\\[(SC\\d{4}) at [^\\]]*:(\\d+)\\]$/.exec(e.message);
|
||
console.log(label, m ? "refused " + m[1] + " line " + m[2] : "threw " + e.message);
|
||
}
|
||
${reads.map(([label, read]) => `try { const value = ${read}; console.log(${JSON.stringify(label)}, "read", typeof value); } catch (e) { report(${JSON.stringify(label)}, e); }`).join("\n")}
|
||
console.log(Date.length, RegExp.length, Promise.length, Object.length, Map.length, Set.length);
|
||
console.log(Date.name, Map.name, Map.constructor === Function, Object.constructor === Function);
|
||
const promisePrototype = Promise.prototype;
|
||
console.log(typeof promisePrototype, typeof promisePrototype.then, promisePrototype.constructor === Promise);
|
||
`;
|
||
const r = await compileAndRun("builtin-constructor-members", source, "cjs");
|
||
expect(r.stderr).toBe("");
|
||
expect(r.exitCode).toBe(0);
|
||
const lines = r.stdout.trimEnd().split("\n");
|
||
expect(lines.slice(0, reads.length)).toEqual(
|
||
reads.map(([label], i) =>
|
||
expect.stringMatching(
|
||
new RegExp(
|
||
`^${label.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")} refused SC(2020|1090) line ${i + 9}$`,
|
||
),
|
||
),
|
||
),
|
||
);
|
||
// Promise.prototype reads as an object holding the runtime's methods.
|
||
expect(lines.slice(reads.length)).toEqual([
|
||
"7 2 1 1 0 0",
|
||
"Date Map true true",
|
||
"object function true",
|
||
]);
|
||
});
|
||
});
|
||
|
||
// WebCrypto's native slice is crypto.subtle's secret-key methods and its
|
||
// ECDSA, Ed25519 and RSA algorithms. Algorithms Node recognizes outside
|
||
// that slice reject with a coded SC2020
|
||
// Error instead of a Node-shaped success, and a JavaScript call of a
|
||
// method outside it 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 lowered ones 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);
|
||
}
|
||
}
|
||
const key = await crypto.subtle.importKey("raw", bytes, { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
|
||
await attempt("ed448", () => crypto.subtle.sign({ name: "Ed448" }, key, bytes));
|
||
await attempt("ecdsa with an hmac key", () => 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 aes = await crypto.subtle.generateKey({ name: "AES-GCM", length: 128 }, false, ["encrypt"]);
|
||
await attempt("aes-ocb", () => crypto.subtle.encrypt({ name: "AES-OCB", iv: new Uint8Array(12) }, aes, bytes));
|
||
await attempt("ecdh bits", () => crypto.subtle.deriveBits({ name: "ECDH" }, key, 256));
|
||
await attempt("rsa-oaep wrap", () => crypto.subtle.wrapKey("raw", key, key, { name: "RSA-OAEP" }));
|
||
const pair = await crypto.subtle.generateKey({ name: "ECDSA", namedCurve: "P-256" }, true, ["sign", "verify"]);
|
||
const kw = await crypto.subtle.generateKey({ name: "AES-KW", length: 128 }, false, ["wrapKey", "unwrapKey"]);
|
||
await attempt("wrap an ecdsa key", () => crypto.subtle.wrapKey("spki", pair.publicKey, kw, "AES-KW"));
|
||
await attempt("wrap with an ecdsa key", () => crypto.subtle.wrapKey("raw", aes, pair.privateKey, "AES-KW"));
|
||
await attempt("unwrap an ecdsa key", () =>
|
||
crypto.subtle.unwrapKey("spki", new Uint8Array(24), kw, "AES-KW", { name: "ECDSA", namedCurve: "P-256" }, true, [
|
||
"verify",
|
||
]),
|
||
);
|
||
`,
|
||
"mjs",
|
||
);
|
||
expect(r.exitCode).toBe(0);
|
||
expect(r.stderr).toBe("");
|
||
expect(r.stdout).toBe(
|
||
"ed448 Error SC2020 crypto.subtle.sign with the Ed448 algorithm is not supported yet\n" +
|
||
"ecdsa with an hmac key InvalidAccessError 15 Key algorithm mismatch\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" +
|
||
"aes-ocb Error SC2020 crypto.subtle.encrypt with the AES-OCB algorithm is not supported yet\n" +
|
||
"ecdh bits Error SC2020 crypto.subtle.deriveBits with the ECDH algorithm is not supported yet\n" +
|
||
"rsa-oaep wrap Error SC2020 crypto.subtle.wrapKey with the RSA-OAEP algorithm is not supported yet\n" +
|
||
"wrap an ecdsa key Error SC2020 crypto.subtle.wrapKey with an ECDSA, Ed25519 or RSA key is not supported yet\n" +
|
||
"wrap with an ecdsa key InvalidAccessError 15 Key algorithm mismatch\n" +
|
||
"unwrap an ecdsa key Error SC2020 crypto.subtle.unwrapKey with the ECDSA algorithm is not supported yet\n",
|
||
);
|
||
});
|
||
|
||
test("a method outside the slice is a named fence where it runs", async () => {
|
||
const r = await compileAndRun(
|
||
"webcrypto-get-public-key",
|
||
`console.log("before");
|
||
const key = await crypto.subtle.generateKey({ name: "HMAC", hash: "SHA-256" }, true, ["sign"]);
|
||
await crypto.subtle.getPublicKey(key, ["verify"]);
|
||
`,
|
||
"mjs",
|
||
);
|
||
expect(r.exitCode).toBe(1);
|
||
expect(r.stdout).toBe("before\n");
|
||
expect(r.stderr).toMatch(
|
||
/^Uncaught Error: 'crypto\.subtle\.getPublicKey' is part of the standard library types but has no scriptc lowering yet.*\n$/,
|
||
);
|
||
});
|
||
});
|
||
|
||
// The Ed25519 arithmetic (scr_ed25519.c) links only into programs whose
|
||
// string literals name Ed25519. Without it, an Ed25519 public key still
|
||
// imports and exports, while private-key parsing, signing and verification
|
||
// reject with a coded SC2020 Error where Node answers; there is no Node
|
||
// twin, so this lives here. The corpus (7080, 7081) pins the linked path.
|
||
describe("Ed25519 without the Ed25519 unit (JS lane)", () => {
|
||
test("a program that never names Ed25519 refuses its arithmetic with SC2020", async () => {
|
||
const r = await compileAndRun(
|
||
"ed25519-unit-not-linked",
|
||
`import * as c from "node:crypto";
|
||
const spki = Buffer.from("302a300506032b6570032100d75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a", "hex");
|
||
const pkcs8 = Buffer.from("302e020100300506032b6570042204209d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60", "hex");
|
||
const pub = c.createPublicKey({ key: spki, format: "der", type: "spki" });
|
||
console.log(pub.asymmetricKeyType, pub.export({ format: "jwk" }).x);
|
||
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("pkcs8", () => c.createPrivateKey({ key: pkcs8, format: "der", type: "pkcs8" }));
|
||
await attempt("verify", () => c.verify(null, Buffer.from("x"), pub, Buffer.alloc(64)));
|
||
// ECDSA links the WebCrypto key unit; the Ed25519 name is built at run time.
|
||
const ecdsa = await crypto.subtle.generateKey({ name: "ECDSA", namedCurve: "P-256" }, false, ["sign"]);
|
||
console.log(ecdsa.privateKey.type);
|
||
const name = ["Ed", "25519"].join("");
|
||
await attempt("generateKey", () => crypto.subtle.generateKey(name, false, ["sign", "verify"]));
|
||
await attempt("importKey", () => crypto.subtle.importKey("pkcs8", pkcs8, name, false, ["sign"]));
|
||
const raw = await crypto.subtle.importKey("raw", spki.subarray(12), name, false, ["verify"]);
|
||
await attempt("subtle verify", () => crypto.subtle.verify(name, raw, new Uint8Array(64), new Uint8Array(1)));
|
||
`,
|
||
"mjs",
|
||
);
|
||
const refusal =
|
||
"Error SC2020 Ed25519 keys are supported when a string literal in the program names 'ed25519'";
|
||
expect(r.exitCode).toBe(0);
|
||
expect(r.stderr).toBe("");
|
||
expect(r.stdout).toBe(
|
||
"ed25519 11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo\n" +
|
||
`pkcs8 ${refusal}\n` +
|
||
`verify ${refusal}\n` +
|
||
"private\n" +
|
||
`generateKey ${refusal}\n` +
|
||
`importKey ${refusal}\n` +
|
||
`subtle verify ${refusal}\n`,
|
||
);
|
||
});
|
||
});
|
||
|
||
describe("inspecting error subclasses with properties created at run time (JS lane)", () => {
|
||
test("an instance whose constructor created a property is a named fence", async () => {
|
||
// Node prints `[Conditional [Error]: m] { extra: 'x' }`. The property
|
||
// lives in the instance's property bag, which the error rendering does
|
||
// not read, so the instance refuses instead of printing a partial
|
||
// object. One without such a property prints as Node prints it.
|
||
const r = await compileAndRun(
|
||
"error-subclass-runtime-properties",
|
||
`import * as util from "node:util";
|
||
class Conditional extends Error {
|
||
constructor(message, extra) {
|
||
super(message);
|
||
if (extra) this.extra = extra;
|
||
}
|
||
}
|
||
const plain = new Conditional("plain");
|
||
plain.stack = "";
|
||
console.log(util.inspect(plain));
|
||
const extra = new Conditional("extra", "x");
|
||
extra.stack = "";
|
||
console.log(util.inspect(extra));
|
||
`,
|
||
"mjs",
|
||
);
|
||
expect(r.exitCode).toBe(1);
|
||
expect(r.stdout).toBe("[Conditional [Error]: plain]\n");
|
||
expect(r.stderr).toMatch(
|
||
/^Uncaught Error: inspecting a 'Conditional' error with properties created at run time is not supported yet \[SC2020[^\]]*\]\n$/,
|
||
);
|
||
});
|
||
});
|
||
|
||
describe("elements on Array.prototype", () => {
|
||
// Holes of native arrays read undefined without consulting
|
||
// Array.prototype, so storing an element there refuses instead of
|
||
// letting later hole reads diverge from Node. Named members stay
|
||
// writable.
|
||
test("an index write is a named fence (TS lane)", async () => {
|
||
const r = await compileAndRun(
|
||
"array-prototype-element-ts",
|
||
`const proto = Array.prototype as unknown as Record<string, unknown>;
|
||
proto.label = "named";
|
||
console.log(proto.label);
|
||
(Array.prototype as unknown as Record<number, unknown>)[1] = "proto";
|
||
console.log("unreachable");
|
||
`,
|
||
);
|
||
expect(r.exitCode).toBe(1);
|
||
expect(r.stdout).toBe("named\n");
|
||
expect(r.stderr).toMatch(
|
||
/^Uncaught Error: Elements on Array\.prototype are not supported yet \[SC2020[^\]]*\]\n$/,
|
||
);
|
||
});
|
||
|
||
test("an index write and a mutating method are named fences (JS lane)", async () => {
|
||
const write = await compileAndRun(
|
||
"array-prototype-element-js",
|
||
`Array.prototype[0] = "proto";
|
||
console.log("unreachable");
|
||
`,
|
||
"cjs",
|
||
);
|
||
expect(write.exitCode).toBe(1);
|
||
expect(write.stdout).toBe("");
|
||
expect(write.stderr).toMatch(
|
||
/^Uncaught Error: Elements on Array\.prototype are not supported yet \[SC2020[^\]]*\]\n$/,
|
||
);
|
||
const push = await compileAndRun(
|
||
"array-prototype-push-js",
|
||
`Array.prototype.push("proto");
|
||
console.log("unreachable");
|
||
`,
|
||
"cjs",
|
||
);
|
||
expect(push.exitCode).toBe(1);
|
||
expect(push.stdout).toBe("");
|
||
expect(push.stderr).toMatch(
|
||
/^Uncaught Error: Elements on Array\.prototype are not supported yet \[SC2020[^\]]*\]\n$/,
|
||
);
|
||
});
|
||
});
|