Files
reverse-engineering-anything/src/domain/javascript/javascriptSemanticCallResolution.test.ts
T
ShuaixinHuang e9fc8fb30c fix(javascript): preserve runtime path bytes and Electron ownership (#4941)
* fix(javascript): preserve literal percent runtime paths

* fix(javascript): ignore resolve prefixes before the final anchor

* fix(electron): retain application roots for loadFile declarations

* fix(electron): resolve loadFile from enclosing application package

* fix(electron): retain outer application root across package scopes

* fix(electron): derive application ownership from inventoried packages

* fix(electron): retain resolved package entrypoint ownership

* fix(electron): preserve competing application ownership

* fix(electron): retain invalid and omitted runtime ownership

* fix(electron): preserve omitted module ownership and runtime paths

* fix(electron): preserve proven roots before confining page paths

* fix(electron): retain possible owners behind missing imports

* fix(electron): distinguish absent imports and owned page candidates

* fix(electron): keep unavailable JSON out of owner gaps

* fix(electron): retain exact loader formats and default entry status

* fix(electron): retain unavailable entrypoint ownership candidates

* test(ghidra): preserve Windows Node probe prerequisites

* test(ghidra): assert cancellation during profile shutdown

* fix(electron): separate examined imports from frontend ownership

* fix(electron): retain unavailable directory entrypoint owners

* test(jeb): retain Linux ownership prerequisite in isolation

* fix(javascript): retain unchecked loader ownership and literal main paths

* fix(javascript): preserve runtime paths and call-site module aliases

* fix(javascript): bound module alias traversal and preserve projections

* fix(javascript): retain host module and nested artifact path boundaries

* fix(javascript): preserve ESM and archive module loader provenance

* test(javascript): distinguish unmapped containers from missing exports

* fix: retain default module aliases and Electron builtin imports

* fix: preserve host file URL and inward ASAR ownership

* fix: preserve CommonJS file URL classification in CI fixtures

* fix: retain bound module loaders and Windows entrypoint uncertainty

* fix: retain ASAR uncertainty and invalidate changed Electron app roots

* fix(javascript): retain ownership uncertainty across loader boundaries

* fix(javascript): preserve loader sources and bound alias queries

* fix(javascript): retain destructured loader sources

* fix(javascript): preserve loader uncertainty at resolution boundaries

* fix(javascript): preserve callable and package launch ownership

* fix(javascript): preserve captured values across member reads

* test(process): assert tagged filesystem cancellation failures

* fix(javascript): preserve inventory bases and invocation read timing

* fix(javascript): preserve ownership across indirect and unread loads

* test(javascript): order fixture directory creation on Windows

* test(javascript): share ordered filesystem fixture writer

* fix(javascript): unify loader provenance and package validation

* fix(javascript): preserve captured Node require loader values

* fix(javascript): preserve loader invocation authority

* fix(javascript): verify generic loader invocation authority

* fix(javascript): preserve conditional loader method alternatives

* fix(javascript): require proven factories for source association

* fix(javascript): honor replaced Electron app path setters

* fix(javascript): prove path and assignment source ownership

* fix(javascript): retain native delegate eligibility through shared writes

* fix(process): settle stale baseline errors after confirmed absence

* fix(javascript): preserve assignment and path boundary evidence

* fix(javascript): prove source-directory resolve anchors

* fix: preserve application path provenance and portable schemas

* fix(javascript): retain CommonJS exported setter effects

* fix: isolate Linux identity observations and retain native module exports

* fix(javascript): preserve published app and helper provenance

* fix(javascript): normalize typed stores and native path fixtures
2026-10-20 17:25:19 +08:00

355 lines
12 KiB
TypeScript

import * as t from "@babel/types";
import { expect, it, vi } from "vitest";
import {
analyzeJavaScriptSemantics,
indexJavaScriptBindingsSteps,
} from "./javascriptSemanticAnalysis.js";
import {
resolveLocalCallables,
type LocalCallableResolution,
} from "./javascriptSemanticCallResolution.js";
import { parseJavaScriptSource } from "./javascriptSourceParser.js";
import { createSemanticReturnedReferences } from "./javascriptSemanticReturnedReferences.js";
import { semanticBindingAtRead } from "./javascriptSemanticMutationInitializers.js";
import { traverseJavaScriptAst } from "./javascriptSemanticTraversal.js";
const indexed = (source: string) => {
const parsed = parseJavaScriptSource(source, "callback.js");
if (parsed === null) throw new Error("Expected valid callback fixture");
const steps = indexJavaScriptBindingsSteps(parsed.program);
let step = steps.next();
while (step.done !== true) step = steps.next();
const state = step.value;
const callableById = new Map(
state.callables.map((callable) => [callable.callableId, callable]),
);
const bindingCache = new WeakMap<t.Identifier, LocalCallableResolution>();
return {
state,
callableById,
bindingCache,
resolve: (node: t.Node) =>
resolveLocalCallables({
node,
state,
callableById,
bindingCache,
seenInitializers: new Set(),
}),
};
};
it.each([
"if(flag)({make}={make:replacement});",
"if(flag)[make]=[replacement];",
"({make}={make:replacement});",
"[make]=[replacement];",
])("does not prove the old callable after a projected write: %s", (write) => {
const analysis = analyzeJavaScriptSemantics(
`function original(){}function replacement(){}let make=original;make();${write}make();`,
);
const original = analysis.callables.find(({ name }) => name === "original");
expect(original).toBeDefined();
const [before, after] = analysis.callSites;
expect(before).toMatchObject({
resolution: "exact",
calleeCallableIds: [original?.callableId],
});
expect(after).toBeDefined();
expect(after?.resolution).not.toBe("exact");
expect(analysis.frontiers).toContainEqual(
expect.objectContaining({
kind: "dynamic-call",
location: after?.calleeLocation,
}),
);
});
it.each([
"if(flag)({make}={make:replacement});",
"if(flag)[make]=[replacement];",
])(
"retains known callable candidates without claiming completeness: %s",
(write) => {
const context = indexed(
`function original(){}function replacement(){}let make=original;${write}make();`,
);
const last = context.state.program.body.at(-1);
if (!t.isExpressionStatement(last) || !t.isCallExpression(last.expression))
throw new Error("Missing projected call");
const original = context.state.callables.find(
({ name }) => name === "original",
);
expect(original).toBeDefined();
expect(context.resolve(last.expression.callee)).toMatchObject({
complete: false,
callableIds: [original?.callableId],
});
},
);
it("retains selected callable keys without admitting an unrelated return", () => {
const context = indexed(`
const table = { x() { return {}; }, y() { return {}; }, z() { return module; } };
const make = flag ? table.x : table.y;
make();
`);
const last = context.state.program.body.at(-1);
if (!t.isExpressionStatement(last) || !t.isCallExpression(last.expression))
throw new Error("Missing selected call");
const returns = createSemanticReturnedReferences(context.state)(
last.expression.callee,
).references;
expect(returns).toHaveLength(2);
expect(returns.every(({ node }) => t.isObjectExpression(node))).toBe(true);
});
it("retains correlations between selected callable path segments", () => {
const context = indexed(`
const table = { a: { b() { return {}; }, d() { return module; } },
c: { d() { return {}; }, b() { return module; } } };
const make = flag ? table.a.b : table.c.d;
make();
`);
const last = context.state.program.body.at(-1);
if (!t.isExpressionStatement(last) || !t.isCallExpression(last.expression))
throw new Error("Missing selected call");
const returns = createSemanticReturnedReferences(context.state)(
last.expression.callee,
).references;
expect(returns).toHaveLength(2);
expect(returns.every(({ node }) => t.isObjectExpression(node))).toBe(true);
});
it("bounds shared partial callback alias expansion by AST size", () => {
const measure = (depth: number) => {
const context =
indexed(`function fallback(){return {member:1};} function choose(cb,flag){
if(flag)cb=fallback;
const a0=cb;
${Array.from({ length: depth }, (_, i) => `const a${i + 1}=flag?a${i}:a${i};`).join("")}
return a${depth}();
}`);
const choose = context.state.program.body[1];
if (!t.isFunctionDeclaration(choose)) throw new Error("Missing choose");
const last = choose.body.body.at(-1);
if (!t.isReturnStatement(last) || !t.isCallExpression(last.argument))
throw new Error("Missing call");
const reads = vi.spyOn(context.bindingCache, "get");
const result = context.resolve(last.argument.callee);
expect(result).toMatchObject({ complete: false });
expect(result.callableIds).toHaveLength(1);
const callReads = reads.mock.calls.length;
const lookups = context.state.scopes.map((scope) =>
vi.spyOn(scope.bindings, "get"),
);
try {
expect(
createSemanticReturnedReferences(context.state)(last.argument.callee)
.references,
).toHaveLength(1);
return {
callReads,
referenceReads: lookups.reduce(
(total, lookup) => total + lookup.mock.calls.length,
0,
),
};
} finally {
for (const lookup of lookups) lookup.mockRestore();
}
};
const small = measure(8);
const large = measure(12);
expect(small.callReads).toBeGreaterThan(0);
expect(large.callReads).toBeLessThan(3 * small.callReads);
expect(small.referenceReads).toBeGreaterThan(0);
expect(large.referenceReads).toBeLessThan(3 * small.referenceReads);
});
it.each([
["if(flag){cb=fallback;cb();}", true],
["if(flag)cb=fallback;cb();", false],
["cb ||= fallback;cb();", false],
] as const)(
"reports initializer coverage at the read: %s",
(body, expected) => {
const { state } = indexed(`function choose(cb,flag){${body}}`);
const binding = [...state.bindingsById.values()].find(
(binding) => binding.name === "cb",
);
if (binding === undefined) throw new Error("Missing cb binding");
let reads = 0;
traverseJavaScriptAst(state.program, {
enter(node) {
if (
t.isCallExpression(node) &&
t.isIdentifier(node.callee, { name: "cb" })
) {
reads++;
expect(
semanticBindingAtRead(binding, node.callee, state.parentsByNode)
.hasDefiniteInitializer,
).toBe(expected);
}
},
});
expect(reads).toBe(1);
},
);
it("does not reuse a later resolved callback at an earlier read", () => {
const context = indexed(
`function fallback(){} function choose(cb){cb();cb=fallback;cb();}`,
);
const choose = context.state.program.body[1];
if (!t.isFunctionDeclaration(choose)) throw new Error("Missing choose");
const calls = choose.body.body.flatMap((statement) =>
t.isExpressionStatement(statement) &&
t.isCallExpression(statement.expression)
? [statement.expression.callee]
: [],
);
const [before, after] = calls;
if (before === undefined || after === undefined)
throw new Error("Missing callback reads");
expect(context.resolve(after)).toMatchObject({ complete: true });
expect(context.resolve(before)).toMatchObject({
complete: false,
callableIds: [],
});
expect(context.resolve(after)).toMatchObject({ complete: true });
});
it.each([false, true])(
"bounds separate branch assignments with intrinsic cycle=%s",
(cyclic) => {
const measure = (depth: number) => {
const context = indexed(`function fallback(){return {};}
function choose(flag){let a0=${cyclic ? "flag?a0:{make:fallback}" : "{make:fallback}"};
${Array.from({ length: depth }, (_, i) => `let a${i + 1};if(flag)a${i + 1}=a${i};else a${i + 1}=a${i};`).join("")}
return a${depth}.make();}`);
const choose = context.state.program.body[1];
if (!t.isFunctionDeclaration(choose)) throw new Error("Missing choose");
const last = choose.body.body.at(-1);
if (!t.isReturnStatement(last) || !t.isCallExpression(last.argument))
throw new Error("Missing call");
const lookups = context.state.scopes.map((scope) =>
vi.spyOn(scope.bindings, "get"),
);
try {
expect(
createSemanticReturnedReferences(context.state)(last.argument.callee)
.references,
).toHaveLength(1);
return lookups.reduce(
(total, lookup) => total + lookup.mock.calls.length,
0,
);
} finally {
for (const lookup of lookups) lookup.mockRestore();
}
};
const small = measure(8),
large = measure(12);
expect(small).toBeGreaterThan(0);
expect(large).toBeLessThan(3 * small);
},
);
it.each(["||=", "??="])(
"retains definite assignment to an empty local: %s",
(operator) => {
const { state } = indexed(
`function choose(){let cb;cb ${operator} fallback;cb();}`,
);
const binding = [...state.bindingsById.values()].find(
(binding) => binding.name === "cb",
);
if (binding === undefined) throw new Error("Missing cb binding");
let reads = 0;
traverseJavaScriptAst(state.program, {
enter(node) {
if (
t.isCallExpression(node) &&
t.isIdentifier(node.callee, { name: "cb" })
) {
reads++;
expect(
semanticBindingAtRead(binding, node.callee, state.parentsByNode)
.hasDefiniteInitializer,
).toBe(true);
}
},
});
expect(reads).toBe(1);
},
);
it("does not memoize alias cycle cuts across entry paths", () => {
const context = indexed(
`function fallback(){} let a,b; function setA(){a=flag?b:fallback;} function setB(){b=a;} a(); b();`,
);
const callees = context.state.program.body.flatMap((statement) =>
t.isExpressionStatement(statement) &&
t.isCallExpression(statement.expression)
? [statement.expression.callee]
: [],
);
const fallback = context.state.callables.find(
(callable) => callable.name === "fallback",
);
if (fallback === undefined) throw new Error("Missing fallback");
for (const node of [...callees, ...callees].reverse()) {
const warm = context.resolve(node);
const cold = resolveLocalCallables({
node,
state: context.state,
callableById: context.callableById,
bindingCache: new WeakMap(),
seenInitializers: new Set(),
});
expect(warm).toEqual(cold);
expect(warm).toMatchObject({
complete: false,
cycleDependent: true,
callableIds: [fallback.callableId],
});
}
});
const meshLookups = (writes: number): number => {
const source = `function f(){} function g(){} let a=f,b=g;
function run(){${Array.from({ length: writes }, (_, index) =>
index % 2 === 0 ? "a=b;" : "b=a;",
).join("")} a();}`;
const context = indexed(source);
const run = context.state.program.body[3];
if (!t.isFunctionDeclaration(run)) throw new Error("Missing run");
const statement = run.body.body.at(-1);
if (
!t.isExpressionStatement(statement) ||
!t.isCallExpression(statement.expression)
)
throw new Error("Missing mesh call");
const lookups = vi.spyOn(context.bindingCache, "get");
const result = context.resolve(statement.expression.callee);
expect(result).toMatchObject({ complete: false, cycleDependent: true });
expect(
result.callableIds.map((id) => context.callableById.get(id)?.name).sort(),
).toEqual(["f", "g"]);
return lookups.mock.calls.length;
};
it("closes cycles at full-binding reads with bounded lookup growth", () => {
const six = meshLookups(6);
const eight = meshLookups(8);
const ten = meshLookups(10);
const twelve = meshLookups(12);
expect(six).toBeGreaterThan(0);
expect(eight).toBeLessThan(3 * six);
expect(ten).toBeLessThan(3 * eight);
expect(twelve).toBeLessThan(3 * ten);
});