Files
rea/src/domain/javascript/javascriptSymbolicBindings.test.ts
T
Ysopwnable 2c7583599b fix(javascript): preserve guarded factory fallthrough (#4341)
Preserve per-exit factory initialization and shared export facts.
Keep capture/caller boundaries, call-kind semantics and conditional
registration evidence consistent through CLI and MCP.

Fixes #3963
2026-10-16 13:10:49 +08:00

760 lines
26 KiB
TypeScript

import { expect, it, vi } from "vitest";
import * as t from "@babel/types";
import { parseJavaScriptSource } from "./javascriptSourceParser.js";
import { indexJavaScriptBindingsSteps } from "./javascriptSemanticAnalysis.js";
import {
completeSemanticSteps,
traverseJavaScriptAst,
} from "./javascriptSemanticTraversal.js";
import { JavaScriptSymbolicBindings } from "./javascriptSymbolicBindings.js";
import {
symbolicTruth,
symbolicVariants,
type SymbolicValue,
} from "./javascriptSymbolicValues.js";
const resolverFor = (source: string) => {
const parsed = parseJavaScriptSource(source);
if (parsed === null) throw new Error("symbolic source fixture did not parse");
const state = completeSemanticSteps(
indexJavaScriptBindingsSteps(parsed.program),
);
return {
parsed,
resolver: new JavaScriptSymbolicBindings(state, parsed.program),
};
};
/** Parse a fixture and collect the first argument of each inspect call. */
const inspectFixture = (
source: string,
evaluate: (
resolver: JavaScriptSymbolicBindings,
node: t.CallExpression,
) => SymbolicValue = (resolver, node) => resolver.evaluate(node.arguments[0]),
) => {
const { parsed, resolver } = resolverFor(source);
const values: SymbolicValue[] = [];
traverseJavaScriptAst(parsed.program, {
enter: (node) => {
if (
t.isCallExpression(node) &&
t.isIdentifier(node.callee, { name: "inspect" })
) {
values.push(evaluate(resolver, node));
}
},
});
return { parsed, resolver, values };
};
const inspectedValues = (source: string): SymbolicValue[] =>
inspectFixture(source).values;
const inspectOne = (source: string): SymbolicValue => {
const values = inspectedValues(source);
expect(values).toHaveLength(1);
const value = values[0];
if (value === undefined) throw new Error("inspect call result missing");
return value;
};
it("substitutes numeric modules, named exports and captured wrapper arguments", () => {
const { resolver, values } = inspectFixture(`(() => {
const modules = {
4(module) { module.exports = require("electron"); },
8(module, exports, load) {
const electron = load(4);
exports.sender = function(channel) { return (...args) => electron.ipcRenderer.send(channel,...args); };
},
12(module, exports, load) { const helpers = load(8); const api = {copy: helpers.sender("test:copy")}; inspect(api.copy); }
};
function load(id) { const m = {exports:{}}; modules[id](m,m.exports,load); return m.exports; }
load(12);
})();`);
expect(values).toHaveLength(1);
const value = values[0];
if (value === undefined) throw new Error("inspect call result missing");
const fn = symbolicVariants(value).find((v) => v.kind === "function");
expect(fn?.kind).toBe("function");
if (fn?.kind !== "function" || !t.isCallExpression(fn.node.body))
throw new Error("closure missing");
const env = resolver.bind(fn, []);
expect(resolver.evaluate(fn.node.body.callee, env)).toEqual({
kind: "external",
path: ["electron", "ipcRenderer", "send"],
});
expect(resolver.arguments(fn.node.body, env)[0]).toEqual({
kind: "literal",
value: "test:copy",
});
});
it("checks block lexical initializers against their containing function call", () => {
const { parsed, resolver } = resolverFor(`run();
function run() { { const value = require("electron"); inspect(value); } }`);
let resolved: SymbolicValue | undefined;
traverseJavaScriptAst(parsed.program, {
enter: (node) => {
if (
!t.isCallExpression(node) ||
!t.isIdentifier(node.callee, { name: "inspect" })
)
return;
const fn = parsed.program.body.find(
(statement): statement is t.FunctionDeclaration =>
t.isFunctionDeclaration(statement) && statement.id?.name === "run",
);
if (!fn) throw new Error("run declaration missing");
const call = parsed.program.body[0];
if (
!t.isExpressionStatement(call) ||
!t.isCallExpression(call.expression)
)
throw new Error("run invocation missing");
resolved = resolver.evaluate(
node.arguments[0],
resolver.bind(
{
kind: "function",
node: fn,
environment: { values: new Map() },
origins: [],
},
[],
call.expression,
),
);
},
});
expect(resolved).toEqual({ kind: "external", path: ["electron"] });
});
it("keeps same-factory reads before initialization unresolved", () => {
const { values, resolver } = inspectFixture(
`(() => {
const modules = { 9(module) { inspect(value); const value = require("electron"); } };
function load(id) { const m={exports:{}}; modules[id](m); return m.exports; }
load(9);
})();`,
(bindings, node) =>
bindings.evaluate(node.arguments[0], bindings.environmentAt(node)),
);
expect(values).toHaveLength(1);
const resolved = values[0];
if (resolved === undefined) throw new Error("inspect call result missing");
expect(resolved).toEqual({
kind: "unknown",
reason: "initializer not available at this read",
});
expect(resolver.unresolved).toBe(true);
});
it("invalidates a bootstrap module slot mutated through an alias", () => {
const values = inspectedValues(`(() => {
const modules = {
1(module) { module.exports = require("electron"); },
2(module, exports, load) { inspect(load(1)); inspect(load(3)); },
3(module) { module.exports = require("electron"); }
};
const table = modules;
function load(id) { const m = {exports:{}}; modules[id](m,m.exports,load); return m.exports; }
table[1] = function(module) { module.exports = require("other"); };
delete table[3];
load(2);
})();`);
expect(values).toHaveLength(2);
expect(values).toEqual([
expect.objectContaining({ kind: "unknown" }),
expect.objectContaining({ kind: "unknown" }),
]);
});
it.each([
[
"destructuring assignment",
`[table[1]] = [function(module) { module.exports = require("other"); }];`,
],
["for-of head", `for (table[1] of replacements) {}`],
[
"wrapped assignment",
`(table as never)[1] = function(module) { module.exports = require("other"); };`,
],
])(
"invalidates a bootstrap module slot overwritten through %s",
(_name, write) => {
const values = inspectedValues(`(() => {
const modules = {
1(module) { module.exports = require("electron"); },
2(module, exports, load) { inspect(load(1)); }
};
const table = modules;
function load(id) { const m = {exports:{}}; modules[id](m,m.exports,load); return m.exports; }
${write}
load(2);
})();`);
expect(values).toHaveLength(1);
expect(values[0]).toEqual(expect.objectContaining({ kind: "unknown" }));
},
);
it.each([
`const electron=require("electron"); const ipc=electron.ipcMain; ipc.handle=()=>null; inspect(electron.ipcMain.handle);`,
`require("electron").ipcMain.handle=()=>null; inspect(require("electron").ipcMain.handle);`,
`const electron=require("electron"); ({ipcMain:electron.ipcMain}=replacement); inspect(electron.ipcMain);`,
`const electron=require("electron"); for(electron.ipcMain of replacements){} inspect(electron.ipcMain);`,
])("invalidates visibly overwritten Electron members: %s", (source) => {
const values = inspectedValues(source);
expect(values).toHaveLength(1);
expect(values[0]?.kind).toBe("unknown");
});
it("does not substitute a lexical or var initializer before its source write", () => {
for (const source of [
`inspect(value); const value = require("electron");`,
`function run() { inspect(value); var value = require("electron"); }`,
]) {
const values = inspectedValues(source);
expect(values).toHaveLength(1);
const value = values[0];
if (value === undefined) throw new Error("inspect call result missing");
expect(value).toMatchObject({ kind: "unknown" });
}
});
it("keeps function declarations hoisted and checks closures at invocation", () => {
const inspected = inspectedValues(`
inspect(read);
function read() { return value; }
function localRead() {
const local = require("electron");
return local;
}
const closure = () => value;
const value = require("electron");
inspect(closure());
inspect(localRead());
`);
expect(inspected[0]).toMatchObject({ kind: "function" });
expect(inspected[1]).toEqual({ kind: "external", path: ["electron"] });
expect(inspected[2]).toEqual({ kind: "external", path: ["electron"] });
});
it("keeps a synchronous cross-function callback before initialization unresolved", () => {
const { parsed, resolver } = resolverFor(`function outer() {
const value = require("electron");
return use(() => value);
}
function use(callback) { return callback(); }
inspect(outer());`);
const outer = parsed.program.body.find(
(statement): statement is t.FunctionDeclaration =>
t.isFunctionDeclaration(statement) && statement.id?.name === "outer",
);
const outerCall = parsed.program.body.find(
(statement): statement is t.ExpressionStatement =>
t.isExpressionStatement(statement) &&
t.isCallExpression(statement.expression) &&
t.isIdentifier(statement.expression.callee, { name: "inspect" }),
);
if (!outer || !outerCall || !t.isCallExpression(outerCall.expression))
throw new Error("outer fixture missing");
const invocation = outer.body.body.find(
(statement): statement is t.ReturnStatement =>
t.isReturnStatement(statement),
);
if (!invocation || !t.isCallExpression(invocation.argument))
throw new Error("synchronous callback call missing");
const result = resolver.evaluate(
invocation.argument,
resolver.bind(
{
kind: "function",
node: outer,
environment: { values: new Map() },
origins: [],
},
[],
outerCall.expression,
),
);
expect(result).toMatchObject({ kind: "unknown" });
});
it("uses completed activation provenance for a returned closure", () => {
const result = inspectOne(`inspect(use(outer()));
function use(callback) { return callback(); }
function outer() {
const value = require("electron");
return () => value;
}`);
expect(result).toEqual({ kind: "external", path: ["electron"] });
});
it("does not treat statements after a returned closure as initialized", () => {
const result = inspectOne(`inspect(outer()());
function outer() {
return () => value;
const value = require("electron");
}`);
expect(result).toMatchObject({ kind: "unknown" });
});
it("does not treat statements after a returned module factory as initialized", () => {
const result = inspectOne(`(() => {
const modules = {
5(module, exports) {
exports.read = () => electron;
return;
const electron = require("electron");
},
9(module, exports, load) { const helper = load(5); inspect(helper.read()); }
};
function load(id) { const m={exports:{}}; modules[id](m,m.exports,load); return m.exports; }
load(9);
})();`);
expect(result).toMatchObject({ kind: "unknown" });
});
it("retains the guard on a factory's completed fallthrough activation", () => {
const value = inspectOne(`(() => {
const modules = {
5(module, exports) {
{ if (flag) return; }
const electron = require("electron");
exports.read = () => electron;
},
9(module, exports, load) { inspect(load(5).read()); }
};
function load(id) { const m={exports:{}}; modules[id](m,m.exports,load); return m.exports; }
load(9);
})();`);
const electron = symbolicVariants(value).filter(
(variant) => variant.kind === "external",
);
expect(electron).toHaveLength(1);
expect(electron[0]).toMatchObject({
path: ["electron"],
conditions: expect.arrayContaining([
{
node: expect.objectContaining({ type: "Identifier", name: "flag" }),
positive: false,
},
]),
});
expect(
symbolicVariants(value).some((variant) => variant.kind === "unknown"),
).toBe(true);
});
it("shares completed factory control flow across repeated loads within one analysis", () => {
const { parsed, resolver } = resolverFor(`(() => {
const modules = {
5(module, exports) { if (flag) return; exports.value = 42; },
9(module, exports, load) { ${"inspect(load(5));".repeat(64)} }
};
function load(id) { const m={exports:{}}; modules[id](m,m.exports,load); return m.exports; }
load(9);
})();`);
const evaluated = vi.spyOn(resolver, "evaluate");
const results: SymbolicValue[] = [];
traverseJavaScriptAst(parsed.program, {
enter: (node) => {
if (
t.isCallExpression(node) &&
t.isIdentifier(node.callee, { name: "inspect" })
)
results.push(resolver.evaluate(node.arguments[0]));
},
});
expect(results).toHaveLength(64);
expect(
results.every((result) =>
symbolicVariants(result).every(
(variant, index) => variant === symbolicVariants(results[0]!)[index],
),
),
).toBe(true);
expect(
evaluated.mock.calls.filter(([node]) =>
t.isIdentifier(node, { name: "flag" }),
),
).toHaveLength(1);
expect(resolver.truncated).toBe(false);
});
it("bounds predicate expansion across many conditional factory exits", () => {
const { resolver, values } = inspectFixture(`(() => {
const modules = {
5(module, exports) { ${"if (flag) return;".repeat(1_600)} exports.value = 42; },
9(module, exports, load) { inspect(load(5)); }
};
function load(id) { const m={exports:{}}; modules[id](m,m.exports,load); return m.exports; }
load(9);
})();`);
expect(resolver.truncated).toBe(true);
expect(values.flatMap(symbolicVariants)).toContainEqual(
expect.objectContaining({
kind: "unknown",
reason: expect.stringContaining("resource limit"),
}),
);
});
it("does not borrow an unrelated activation's namespace read boundary", () => {
const { parsed, resolver, values } = inspectFixture(`(() => {
const modules = {5(module,exports){exports.stop=true;inspect(exports);if(exports.stop)return;}};
function load(id){const m={exports:{}};modules[id](m,m.exports,load);return m.exports;}
load(5);
})();`);
const namespace = values[0];
if (namespace?.kind !== "namespace")
throw new Error("namespace fixture missing");
let read: t.MemberExpression | undefined;
traverseJavaScriptAst(parsed.program, {
enter: (node) => {
if (t.isIfStatement(node) && t.isMemberExpression(node.test))
read = node.test;
},
});
if (read === undefined) throw new Error("namespace read fixture missing");
expect(
resolver.property(namespace, "stop", {
node: read,
environment: namespace.environment,
}),
).toMatchObject({ kind: "literal", value: true });
expect(
resolver.property(namespace, "stop", {
node: read,
environment: { ...namespace.environment, activation: {} },
}),
).toMatchObject({
kind: "unknown",
reason: "module export read boundary is unavailable",
});
expect(resolver.truncated).toBe(false);
});
it("treats known symbolic records and loaders as truthy", () => {
expect(symbolicTruth({ kind: "record", values: new Map() })).toBe(true);
expect(symbolicTruth({ kind: "loader", modules: new Map() })).toBe(true);
expect(
symbolicTruth({
kind: "union",
values: [
{ kind: "record", values: new Map() },
{ kind: "loader", modules: new Map() },
],
}),
).toBe(true);
});
it("retains branch predicates on conditionally selected wrapper functions", () => {
const value = inspectOne(`
const selected = enabled
? () => navigator.clipboard.writeText("yes")
: () => navigator.clipboard.writeText("no");
inspect(selected);
`);
const functions = symbolicVariants(value).filter(
(candidate) => candidate.kind === "function",
);
expect(functions).toHaveLength(2);
expect(functions.map((fn) => fn.conditions?.[0]?.positive)).toEqual([
true,
false,
]);
});
it("retains fallthrough return guards after a one-sided early return", () => {
const value = inspectOne(`
function choose() {
if (enabled) return () => navigator.clipboard.writeText("early");
return () => navigator.clipboard.writeText("late");
}
inspect(choose());
`);
const functions = symbolicVariants(value).filter(
(candidate) => candidate.kind === "function",
);
expect(functions).toHaveLength(2);
expect(functions[0]).toMatchObject({ conditions: [{ positive: true }] });
expect(functions[1]).toMatchObject({ conditions: [{ positive: false }] });
});
it("composes fallthrough guards across successive conditional returns", () => {
const value = inspectOne(`
function choose() {
if (first) return () => navigator.clipboard.writeText("first");
if (second) return () => navigator.clipboard.writeText("second");
return () => navigator.clipboard.writeText("last");
}
inspect(choose());
`);
const functions = symbolicVariants(value).filter(
(candidate) => candidate.kind === "function",
);
expect(functions).toHaveLength(3);
expect(functions[0]?.conditions?.map(({ positive }) => positive)).toEqual([
true,
]);
expect(functions[1]?.conditions?.map(({ positive }) => positive)).toEqual([
false,
true,
]);
expect(functions[2]?.conditions?.map(({ positive }) => positive)).toEqual([
false,
false,
]);
});
it.each([
["while", 'inspect((() => { while (flag) { return "loop"; } })());'],
["for", 'inspect((() => { for (;;) { return "forever"; } })());'],
["try", 'inspect((() => { try { return "try"; } finally {} })());'],
[
"switch",
'inspect((() => { switch (x) { default: return "switch"; } })());',
],
])(
"keeps unsupported %s return control flow explicitly unknown",
(_name, source) => {
const value = inspectOne(source);
expect(
symbolicVariants(value).some((variant) => variant.kind === "unknown"),
).toBe(true);
expect(value).not.toEqual({ kind: "literal", value: undefined });
},
);
it("preserves later returns with unresolved conditions after unsupported return flow", () => {
const variants = symbolicVariants(
inspectOne(`inspect((() => {
while (flag) { return "inside"; }
return "after";
})());`),
);
expect(variants.some((variant) => variant.kind === "unknown")).toBe(true);
expect(variants).toContainEqual(
expect.objectContaining({
kind: "literal",
value: "after",
conditionUnresolved: true,
}),
);
});
it("does not treat loops without owned returns as unknown return flow", () => {
expect(
inspectOne(`inspect((() => {
while (flag) { work(); }
return "after";
})());`),
).toEqual({ kind: "literal", value: "after" });
});
it("does not treat nested-function returns as wrapper return flow", () => {
expect(
inspectOne(`inspect((() => {
try { function nested() { return "inner"; } }
catch {}
return "outer";
})());`),
).toEqual({ kind: "literal", value: "outer" });
});
it("marks return guards unresolved when a nested join cannot be represented", () => {
const value = inspectOne(`
function choose() {
if (enabled) {
if (other) return () => navigator.clipboard.writeText("early");
}
return () => navigator.clipboard.writeText("late");
}
inspect(choose());
`);
const functions = symbolicVariants(value).filter(
(candidate) => candidate.kind === "function",
);
expect(functions).toHaveLength(2);
expect(functions[1]).toMatchObject({ conditionUnresolved: true });
});
it.each([
[
"duplicate literal keys use the last value",
`const object = { value: "first", value: "last" }; inspect(object.value);`,
{ kind: "literal", value: "last" },
],
[
"a final accessor is unknown",
`const object = { value: "first", get value() { return "accessor"; } }; inspect(object.value);`,
{ kind: "unknown", reason: "accessor property" },
],
[
"a later literal overwrites an accessor",
`const object = { get value() { return "accessor"; }, value: "last" }; inspect(object.value);`,
{ kind: "literal", value: "last" },
],
[
"a later unknown computed key can overwrite an earlier value",
`const object = { value: "known", [dynamic]: "other" }; inspect(object.value);`,
{ kind: "unknown", reason: "dynamic property overwrite" },
],
[
"a later static key overrides an earlier unknown computed key",
`const object = { [dynamic]: "other", value: "last" }; inspect(object.value);`,
{ kind: "literal", value: "last" },
],
[
"a known spread can override an earlier static value",
`const replacement = { value: "spread" }; const object = { value: "first", ...replacement }; inspect(object.value);`,
{ kind: "literal", value: "spread" },
],
] as const)("resolves object properties when %s", (_case, source, expected) => {
expect(inspectOne(source)).toEqual(expected);
});
it("propagates branch guards through object properties and function calls", () => {
const inspected = inspectedValues(`
const direct = flag
? { action: () => navigator.clipboard.writeText("direct-yes") }
: { action: () => navigator.clipboard.writeText("direct-no") };
function choose() {
return flag
? () => navigator.clipboard.writeText("call-yes")
: () => navigator.clipboard.writeText("call-no");
}
const called = { action: choose() };
inspect(direct.action);
inspect(called.action);
`);
for (const value of inspected) {
const functions = symbolicVariants(value).filter(
(candidate) => candidate.kind === "function",
);
expect(functions).toHaveLength(2);
expect(functions.map((fn) => fn.conditions?.[0]?.positive)).toEqual([
true,
false,
]);
}
});
it("completes a loaded module past a very large array without stack overflow", () => {
const parsed = parseJavaScriptSource(`(() => {
const modules = {
4(module) {
module.exports = { table: [0] };
if (module) { return; }
return;
},
8(module, exports, load) { const dep = load(4); inspect(dep); }
};
function load(id) { const m = {exports:{}}; modules[id](m,m.exports,load); return m.exports; }
load(8);
})();`);
if (parsed === null) throw new Error("symbolic source fixture did not parse");
const arrays: t.ArrayExpression[] = [];
const bareReturns: t.ReturnStatement[] = [];
let inspectCall: t.CallExpression | undefined;
traverseJavaScriptAst(parsed.program, {
enter: (node) => {
if (t.isArrayExpression(node)) arrays.push(node);
if (t.isReturnStatement(node) && node.argument == null)
bareReturns.push(node);
if (
t.isCallExpression(node) &&
t.isIdentifier(node.callee, { name: "inspect" })
)
inspectCall = node;
},
});
for (const array of arrays)
array.elements = Array.from({ length: 1_000_000 }, () =>
t.numericLiteral(0),
);
const earliestReturn = bareReturns.toSorted(
(left, right) => (left.start ?? 0) - (right.start ?? 0),
)[0];
const inspected = inspectCall?.arguments[0];
if (earliestReturn === undefined || inspected === undefined)
throw new Error("symbolic source fixture is missing its probes");
const state = completeSemanticSteps(
indexJavaScriptBindingsSteps(parsed.program),
);
const resolver = new JavaScriptSymbolicBindings(state, parsed.program);
const namespace = symbolicVariants(resolver.evaluate(inspected)).find(
(variant) => variant.kind === "namespace",
);
if (namespace?.kind !== "namespace")
throw new Error("loaded module namespace missing");
expect(namespace.environment.completedAt).toBe(earliestReturn);
});
it("keeps async call objects truthy and unwraps only awaited results", () => {
const values = inspectedValues(`async function flag(){return false;}
inspect(flag()); inspect(!flag()); inspect(flag() ?? true);
inspect(await flag()); inspect(flag() || false);`);
expect(values[0]).toMatchObject({
kind: "promise",
value: { kind: "literal", value: false },
});
expect(values[1]).toMatchObject({ kind: "literal", value: false });
expect(values[2]?.kind).toBe("promise");
expect(values[3]).toMatchObject({ kind: "literal", value: false });
expect(values[4]?.kind).toBe("promise");
});
it("does not execute generator bodies to resolve initial calls", () => {
const { resolver, values } = inspectFixture(`function* flag(){return false;}
async function* asyncFlag(){return false;}
inspect(flag()); inspect(!flag()); inspect(asyncFlag()); inspect(flag() ?? false);`);
expect(values.map((value) => value.kind)).toEqual([
"iterator",
"literal",
"iterator",
"iterator",
]);
expect(values[1]).toMatchObject({ kind: "literal", value: false });
expect(resolver.unresolved).toBe(false);
});
it("preserves captured initialization through async and forwarding returns", () => {
const { resolver, values } = inspectFixture(`function outer(){
const electron=require("electron");
async function capture(){return ()=>electron.ipcMain;}
return capture();
}
async function forward(){return outer();}
inspect(await forward());`);
const fn = values[0];
expect(fn?.kind).toBe("function");
if (fn?.kind !== "function") throw new Error("awaited closure missing");
expect(resolver.evaluate(fn.node.body, resolver.bind(fn, []))).toMatchObject({
kind: "external",
path: ["electron", "ipcMain"],
});
expect(fn.origins.length).toBeGreaterThan(0);
});
it.each([
"function* value(){return false;}",
"async function value(){return false;}",
])(
"keeps opaque call-result member access explicitly unresolved: %s",
(declaration) => {
const { resolver, values } = inspectFixture(
`${declaration}inspect(value().next);`,
);
expect(values[0]).toMatchObject({
kind: "unknown",
reason: expect.stringContaining("members are not modeled"),
});
expect(resolver.unresolved).toBe(true);
},
);