mirror of
https://github.com/morluto/rea.git
synced 2026-10-27 18:05:45 +00:00
251 lines
7.9 KiB
TypeScript
251 lines
7.9 KiB
TypeScript
import { expect, it } from "vitest";
|
|
import { analyzeParsedJavaScriptSemantics } from "./javascriptSemanticAnalysis.js";
|
|
import {
|
|
onlyCallable,
|
|
onlyBinding,
|
|
} from "./javascriptSemanticAnalysis.fixture.js";
|
|
import { parseJavaScriptSource } from "./javascriptSourceParser.js";
|
|
|
|
const analyze = (source: string, path = "fixture.cjs") => {
|
|
const file = parseJavaScriptSource(source, path);
|
|
if (file === null) throw new Error("Expected parsed fixture");
|
|
return analyzeParsedJavaScriptSemantics(file);
|
|
};
|
|
const returned = (body: string) =>
|
|
onlyCallable(analyze(`exports.read = function read() { ${body} };`), "read")
|
|
.returnSites[0]?.value;
|
|
|
|
it.each([
|
|
"return value; var value = 1;",
|
|
"var snapshot = value; var value = 1; return snapshot;",
|
|
])("selects declaration-only bindings at their actual read: %s", (body) => {
|
|
expect(returned(body)?.status).toBe("unknown");
|
|
});
|
|
|
|
it.each([
|
|
"do { break; var value = 1; } while (false); return value;",
|
|
"do { if (flag) break; var value = 1; } while (false); return value;",
|
|
"do { continue; var value = 1; } while (false); return value;",
|
|
"label: { break label; var value = 1; } return value;",
|
|
])(
|
|
"does not treat an initializer skipped by abrupt completion as definite: %s",
|
|
(body) => {
|
|
expect(returned(body)?.status).toBe("unknown");
|
|
},
|
|
);
|
|
|
|
it.each([
|
|
"var value = 1; return value;",
|
|
"do { var value = 1; break; } while (false); return value;",
|
|
"do { for (;;) { break; } var value = 1; } while (false); return value;",
|
|
"label: { break label; } var value = 1; return value;",
|
|
"function unused() { return; } var value = 1; return value;",
|
|
])(
|
|
"preserves initializers that an earlier local exit does not skip: %s",
|
|
(body) => {
|
|
expect(returned(body)).toEqual({ status: "literal", value: 1 });
|
|
},
|
|
);
|
|
|
|
it.each([
|
|
"var value = 1; { function value() {} } exports.read = function read() { return value; };",
|
|
"exports.read = function read() { var value = 1; if (flag) { function value() {} } return value; };",
|
|
])(
|
|
"indexes the legacy block-function write on its outer owner: %s",
|
|
(source) => {
|
|
expect(
|
|
onlyCallable(analyze(source), "read").returnSites[0]?.value.status,
|
|
).not.toBe("literal");
|
|
},
|
|
);
|
|
|
|
it.each([
|
|
[
|
|
"fixture.cjs",
|
|
'"use strict"; var value = 1; { function value() {} } exports.read = function read() { return value; };',
|
|
],
|
|
[
|
|
"fixture.cjs",
|
|
"let value = 1; { function value() {} } exports.read = function read() { return value; };",
|
|
],
|
|
[
|
|
"fixture.mjs",
|
|
"var value = 1; { function value() {} } export function read() { return value; }",
|
|
],
|
|
])(
|
|
"does not apply legacy writes across a strict or lexical boundary: %s",
|
|
(path, source) => {
|
|
expect(
|
|
onlyCallable(analyze(source, path), "read").returnSites[0]?.value,
|
|
).toEqual({ status: "literal", value: 1 });
|
|
},
|
|
);
|
|
|
|
it("assigns a catch-shadowed var initializer to the active lexical binding", () => {
|
|
const ir = analyze(`
|
|
function target() { return "target"; }
|
|
try { throw 0; } catch (handler) { var handler = target; handler(); }
|
|
`);
|
|
const target = onlyCallable(ir, "target");
|
|
expect(ir.callSites).toContainEqual(
|
|
expect.objectContaining({
|
|
resolution: "exact",
|
|
calleeCallableIds: [target.callableId],
|
|
}),
|
|
);
|
|
const hoisted = ir.bindings.find(
|
|
({ name, kind }) => name === "handler" && kind === "variable",
|
|
);
|
|
expect(hoisted?.value.status).toBe("unknown");
|
|
});
|
|
|
|
it("keeps ordinary arguments local while arrows retain the outer binding", () => {
|
|
const ir = analyze(`
|
|
var arguments = { 0: "OUTER" };
|
|
function ordinary() { const selected = arguments[0]; return selected; }
|
|
const arrow = () => arguments[0];
|
|
`);
|
|
expect(onlyBinding(ir, "selected").value.status).toBe("unknown");
|
|
expect(onlyCallable(ir, "ordinary").returnSites[0]?.value.status).toBe(
|
|
"unknown",
|
|
);
|
|
expect(onlyCallable(ir, "arrow").returnSites[0]?.value).toEqual({
|
|
status: "literal",
|
|
value: "OUTER",
|
|
});
|
|
const outer = onlyBinding(ir, "arguments");
|
|
expect(
|
|
ir.closureCaptures.some(
|
|
({ bindingId, callableId }) =>
|
|
bindingId === outer.bindingId &&
|
|
callableId === onlyCallable(ir, "ordinary").callableId,
|
|
),
|
|
).toBe(false);
|
|
});
|
|
|
|
it.each([
|
|
["object", "{ handler }", "{ handler: state }"],
|
|
["array", "[handler]", "[state]"],
|
|
])(
|
|
"retains mutations through catch-shadowed %s var aliases",
|
|
(_shape, pattern, value) => {
|
|
for (const shadowed of [true, false]) {
|
|
const ir = analyze(`
|
|
exports.read = function read() {
|
|
const state = { value: 1 };
|
|
try { throw 0; } catch (${shadowed ? "handler" : "error"}) {
|
|
var ${pattern} = ${value};
|
|
handler.value = 2;
|
|
}
|
|
return state.value;
|
|
};
|
|
`);
|
|
expect(ir.coverage.status).toBe("complete");
|
|
expect(onlyCallable(ir, "read").returnSites[0]?.value).toEqual({
|
|
status: "unknown",
|
|
reason: "Own property value is unknown-coverage.",
|
|
});
|
|
if (shadowed)
|
|
expect(
|
|
ir.bindings.find(
|
|
({ name, kind }) => name === "handler" && kind === "variable",
|
|
)?.value.status,
|
|
).toBe("unknown");
|
|
}
|
|
},
|
|
);
|
|
|
|
it("does not resolve Promise sources through a future initializer", () => {
|
|
const early = analyze(
|
|
`
|
|
try { task.then(work); } catch (error) {}
|
|
const task = Promise.resolve(1);
|
|
`,
|
|
"fixture.mjs",
|
|
);
|
|
expect(
|
|
early.promiseOperations.find(({ method }) => method === "then"),
|
|
).toMatchObject({ sourceResolution: "unresolved", sourcePromiseIds: [] });
|
|
const initialized = analyze(
|
|
"const task = Promise.resolve(1); task.then(work);",
|
|
"fixture.mjs",
|
|
);
|
|
expect(
|
|
initialized.promiseOperations.find(({ method }) => method === "then"),
|
|
).toMatchObject({
|
|
sourceResolution: "complete",
|
|
sourcePromiseIds: [expect.any(String)],
|
|
});
|
|
});
|
|
|
|
it.each([
|
|
"globalThis.Promise = { resolve() { return 17; } };",
|
|
"Promise.resolve = () => 17;",
|
|
])(
|
|
"requires the selected intrinsic Promise identity after ambient writes: %s",
|
|
(write) => {
|
|
const ir = analyze(
|
|
`${write} const task = Promise.resolve(1);`,
|
|
"fixture.mjs",
|
|
);
|
|
expect(
|
|
ir.promiseOperations.filter(({ method }) => method === "resolve"),
|
|
).toEqual([]);
|
|
expect(
|
|
analyze("const task = Promise.resolve(1);", "fixture.mjs")
|
|
.promiseOperations,
|
|
).toContainEqual(
|
|
expect.objectContaining({
|
|
method: "resolve",
|
|
sourceResolution: "complete",
|
|
}),
|
|
);
|
|
},
|
|
);
|
|
|
|
it("retains caught reference identity without tainting inert throws or later replacements", () => {
|
|
const cases = [
|
|
["caught.value = 2;", "unknown"],
|
|
["mutate(caught);", "unknown"],
|
|
["", "literal"],
|
|
["caught = {}; caught.value = 2;", "literal"],
|
|
] as const;
|
|
for (const [body, status] of cases)
|
|
expect(
|
|
returned(
|
|
`const state = { value: 1 }; try { throw state; } catch (caught) { ${body} } return state.value;`,
|
|
)?.status,
|
|
).toBe(status);
|
|
});
|
|
|
|
it("owns a class declaration's self reference separately from its outer variable", () => {
|
|
const ir = analyze(
|
|
`
|
|
class Service { static create() { return new Service(); } }
|
|
const Original = Service;
|
|
Service = function Replacement() {};
|
|
`,
|
|
"fixture.mjs",
|
|
);
|
|
const bindings = ir.bindings.filter(({ name }) => name === "Service");
|
|
expect(bindings).toHaveLength(2);
|
|
const create = onlyCallable(ir, "create");
|
|
expect(
|
|
ir.closureCaptures.some(
|
|
({ callableId, bindingId }) =>
|
|
callableId === create.callableId &&
|
|
bindingId === bindings.find(({ mutable }) => mutable)?.bindingId,
|
|
),
|
|
).toBe(false);
|
|
expect(
|
|
ir.callSites.find(
|
|
({ callerCallableId }) => callerCallableId === create.callableId,
|
|
)?.resolution,
|
|
).toBe("exact");
|
|
});
|
|
|
|
it("does not initialize a class self name during computed-key evaluation", () => {
|
|
const ir = analyze(`class Service { [Service()]() {} }`, "fixture.mjs");
|
|
expect(ir.callSites[0]?.resolution).not.toBe("exact");
|
|
});
|