mirror of
https://github.com/morluto/rea.git
synced 2026-10-27 18:05:45 +00:00
fix(javascript): preserve initializer execution and lexical ownership
This commit is contained in:
1 parent
5cbcd58550
commit
fba4faf88a
8 files changed
+488
-41
No files matched your search
@@ -0,0 +1,213 @@
|
||||
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("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");
|
||||
});
|
||||
Reference in new issue
Block a user