Files
reverse-engineering-anything/src/domain/javascript/javascriptSemanticAnalysis.initialization_boundaries.test.ts
T

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");
});