mirror of
https://github.com/morluto/rea.git
synced 2026-10-27 18:05:45 +00:00
Electron's utilityProcess.fork(modulePath[, args][, options]) treats a
non-array object in the second argument as the options and ignores the
third. The static extractor only read the third argument, so
`fork("./worker.js", { serviceName })` lost the declared name while
reporting exact, complete coverage, and an object in the second slot let
an ignored third argument supply the name.
Move the optional-argv options selection that child_process already used
into the shared expression-shape owner and reuse it for utility
processes. Unproven argv shapes, spreads and identifier options remain
unknown with partial coverage; nullish options or a nullish serviceName
declare no name, matching Electron's runtime checks.
Fixes #6208
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
200 lines
11 KiB
TypeScript
200 lines
11 KiB
TypeScript
import { expect, it } from "vitest";
|
|
import * as t from "@babel/types";
|
|
|
|
import { analyzeJavaScriptStaticSource } from "./javascriptStaticAnalysis.js";
|
|
import { inspectCall } from "./javascriptStaticAnalysisCalls.js";
|
|
import { createJavaScriptAnalysisAccumulator } from "./javascriptStaticAnalysisState.js";
|
|
import { indexJavaScriptBindingsWithReceiversSteps } from "./javascriptSemanticAnalysis.js";
|
|
import { parseJavaScriptSource } from "./javascriptSourceParser.js";
|
|
import { createJavaScriptRpcScan } from "./javascriptRpcEndpoints.js";
|
|
import { createSemanticExpressionShapeResolver } from "./javascriptSemanticExpressionShapes.js";
|
|
import {
|
|
completeSemanticSteps,
|
|
traverseJavaScriptAst,
|
|
} from "./javascriptSemanticTraversal.js";
|
|
|
|
it("does not classify a local Map lookup as a network endpoint", () => {
|
|
const analysis = analyzeJavaScriptStaticSource(`
|
|
const records = new Map([["/accounts", { name: "Alice" }]]);
|
|
console.log(records.get("/accounts"));
|
|
fetch("/network");
|
|
`);
|
|
expect(analysis.endpoints).toMatchObject([
|
|
{ kind: "network", value: "/network", mechanism: "call:fetch" },
|
|
]);
|
|
expect(analysis.endpoints[0]?.interpretation).toBeUndefined();
|
|
});
|
|
|
|
it.each([
|
|
'const records = new Map(); const alias = records; alias.get("/accounts"); records.delete("/accounts"); alias.get("/accounts")',
|
|
'new Map().get("/accounts")',
|
|
'new Map().delete("https://example.test/accounts")',
|
|
'const records = new Map(); const alias = records; alias.get("/accounts")',
|
|
'const records = new globalThis.Map(); records?.["get"]("/accounts")',
|
|
'function inspect() { const records = new Map(); return records.get("/accounts"); }',
|
|
'const inspect = () => { const records = new Map(); const alias = records; return alias.delete("/accounts"); }',
|
|
'const browser = window; browser.open("about:blank"); new Map().get("/accounts")',
|
|
'const box = {}; box.value = { name: "Alice" }; inspect(box); new Map().get("/accounts")',
|
|
'patch(() => ({ name: "Alice" })); new Map().get("/accounts")',
|
|
'const box = {}; box.prototype = Map.prototype; new Map().get("/accounts")',
|
|
'const box = {}; const alias = box; alias.prototype = Map.prototype; inspect({}); new Map().get("/accounts")',
|
|
'const box = {}; box.make = () => Map.prototype; new Map().get("/accounts")',
|
|
'let box = {}; const first = box; box = {}; box.p = Map.prototype; patch(first); new Map().get("/accounts")',
|
|
'const box = {}; box.a = {}; box.a.p = Map.prototype; box.safe = {}; patch(box.safe); new Map().get("/accounts")',
|
|
'const records = new Map(); records.get = fetch; new Map().get("/accounts")',
|
|
'const holder = { get prototype() { return Map.prototype; } }; new Map().get("/accounts")',
|
|
'const holder = { get selected() { return {}; }, get other() { return Map.prototype; } }; holder.selected.get = fetch; new Map().get("/accounts")',
|
|
'function Map() {} new Map().constructor.prototype.get = fetch; globalThis.Map.prototype; new globalThis.Map().get("/accounts")',
|
|
'const factory = { make() { return {}; } }; factory.make = () => ({}); patch(factory.make()); new Map().get("/accounts")',
|
|
])("omits a proven Map operation: %s", (source) => {
|
|
expect(analyzeJavaScriptStaticSource(source).endpoints).toEqual([]);
|
|
});
|
|
|
|
it.each([
|
|
'client.get("/accounts")',
|
|
'Object.getPrototypeOf = () => Map.prototype; Object.getPrototypeOf({}).get = fetch; new Map().get("/accounts")',
|
|
'Reflect.getPrototypeOf = () => Map.prototype; Reflect.getPrototypeOf({}).get = fetch; new Map().get("/accounts")',
|
|
'const records = new Map(); records.get = request; records.get("/accounts")',
|
|
'let records = new Map(); records = client; records.get("/accounts")',
|
|
'function inspect(Map) { new Map().get("/accounts"); }',
|
|
'Map.prototype.get = request; const records = new Map(); records.get("/accounts")',
|
|
'const records = new Map(); records.__proto__ = { get: fetch }; records.get("/accounts")',
|
|
'const records = new Map(); const alias = records; alias.__proto__ = { get: fetch }; records.get("/accounts")',
|
|
'const records = new Map(); records["__proto__"] = { delete: fetch }; records.delete("/accounts")',
|
|
'function inspect() { records.get("/accounts"); const records = new Map(); }',
|
|
'const alias = records; const records = new Map(); alias.get("/accounts")',
|
|
'function inspect() { const alias = records; const records = new Map(); alias.get("/accounts"); }',
|
|
'function inspect() { const records = new Map(); records.get = fetch; records.get("/accounts"); }',
|
|
'patch(Map.prototype); const records = new Map(); records.get("/accounts")',
|
|
'const prototype = Map.prototype; patch(prototype); new Map().get("/accounts")',
|
|
'patch(Map); new Map().get("/accounts")',
|
|
'new Patch(Map.prototype); new Map().get("/accounts")',
|
|
'Map.prototype.patch = function () { this.get = fetch; }; Map.prototype.patch(); new Map().get("/accounts")',
|
|
'Map.patch(); new Map().get("/accounts")',
|
|
'globalThis.patch = function () { this.Map.prototype.get = fetch; }; globalThis.patch(); new Map().get("/accounts")',
|
|
'window.open = function () { this.Map.prototype.get = fetch; }; window.open(); new Map().get("/accounts")',
|
|
'function invoke() { window.open(); } window.open = patch; invoke(); new Map().get("/accounts")',
|
|
'patch`prototype ${Map.prototype}`; new Map().get("/accounts")',
|
|
'const box = { prototype: Map.prototype }; patch(box); new Map().get("/accounts")',
|
|
'patch((() => Map.prototype)()); new Map().get("/accounts")',
|
|
'const prototype = (() => Map.prototype)(); prototype.get = fetch; new Map().get("/accounts")',
|
|
'const box = {}; box.prototype = Map.prototype; box.prototype.get = fetch; new Map().get("/accounts")',
|
|
'const box = {}; box.p = (anything(), Map.prototype); patch(box); new Map().get("/accounts")',
|
|
'window.prototype = Map.prototype; new Map().get("/accounts")',
|
|
'const box = new ExternalBox(); box.prototype = Map.prototype; new Map().get("/accounts")',
|
|
'function expose(box) { box.prototype = Map.prototype; } new Map().get("/accounts")',
|
|
'const factory = { make() { return Map.prototype; } }; factory.make().get = fetch; new Map().get("/accounts")',
|
|
'new Map().__proto__.get = fetch; new Map().get("/accounts")',
|
|
'new Map().constructor.prototype.get = fetch; new Map().get("/accounts")',
|
|
'Object.getPrototypeOf(new Map()).constructor.prototype.get = fetch; new Map().get("/accounts")',
|
|
'const holder = { get prototype() { return Map.prototype; } }; holder.prototype.get = fetch; new Map().get("/accounts")',
|
|
'const holder = { get ["prototype"]() { return { nested: Map.prototype }; } }; holder.prototype.nested.get = fetch; new Map().get("/accounts")',
|
|
'const holder = { get prototype() { return Map.prototype; } }; patch(holder); new Map().get("/accounts")',
|
|
'function store() { box.child.prototype = Map.prototype; } const box = {}; const inner = {}; box.child = inner; store(); patch(inner); new Map().get("/accounts")',
|
|
'function store() { box.child.next.prototype = Map.prototype; } const box = {}; const middle = {}; const inner = {}; middle.next = inner; box.child = middle; store(); patch(inner); new Map().get("/accounts")',
|
|
'const records = new Map(); Object.getPrototypeOf(records).get = fetch; new Map().get("/accounts")',
|
|
'const getPrototype = Reflect.getPrototypeOf; getPrototype(new Map()).get = fetch; new Map().get("/accounts")',
|
|
'patch(() => Map.prototype); new Map().get("/accounts")',
|
|
'const factory = { make() { return {}; } }; factory.make = () => Map.prototype; patch(factory.make()); new Map().get("/accounts")',
|
|
'function make() { return { prototype: Map.prototype }; } patch(make()); new Map().get("/accounts")',
|
|
'const factory = { prototype() { return Map.prototype; } }; patch(factory.prototype()); new Map().get("/accounts")',
|
|
'patch([Map.prototype]); new Map().get("/accounts")',
|
|
'Object.setPrototypeOf(Map.prototype, custom); new Map().get("/accounts")',
|
|
])("keeps unresolved lookup semantics as a candidate: %s", (source) => {
|
|
expect(analyzeJavaScriptStaticSource(source).endpoints).toMatchObject([
|
|
{ kind: "network", value: "/accounts", interpretation: "candidate" },
|
|
]);
|
|
});
|
|
|
|
it.each([
|
|
"records.delete = fetch;",
|
|
"patch(records);",
|
|
"Map.prototype.delete = fetch;",
|
|
"const alias = records; alias.__proto__ = { delete: fetch };",
|
|
])("retains genuine changes between Map operations: %s", (mutation) => {
|
|
const analysis = analyzeJavaScriptStaticSource(
|
|
`const records = new Map(); records.get('/before'); ${mutation} records.delete('/after');`,
|
|
);
|
|
expect(analysis.endpoints).toContainEqual(
|
|
expect.objectContaining({ value: "/after", interpretation: "candidate" }),
|
|
);
|
|
});
|
|
|
|
it.each(["get", "delete"])(
|
|
"does not assume an unchanged constructor adder for Map.%s",
|
|
(method) => {
|
|
const analysis = analyzeJavaScriptStaticSource(`
|
|
Map.prototype.set = function() { this.${method} = fetch; return this; };
|
|
new Map([['x', 1]]).${method}('/accounts');
|
|
`);
|
|
expect(analysis.endpoints).toContainEqual(
|
|
expect.objectContaining({
|
|
value: "/accounts",
|
|
interpretation: "candidate",
|
|
}),
|
|
);
|
|
},
|
|
);
|
|
|
|
it.each([
|
|
"box.prototype = Map.prototype;",
|
|
'const alias = box; const prototype = Map.prototype; alias["prototype"] = prototype;',
|
|
"({ value: box.prototype } = { value: Map.prototype });",
|
|
"({ value: box.prototype = Map.prototype } = {});",
|
|
"for (box.prototype of [Map.prototype]) {}",
|
|
])(
|
|
"retains prototype identities stored after container initialization: %s",
|
|
(store) => {
|
|
const source = `const box = {}; ${store}
|
|
function patch(b) { b.prototype.get = fetch; }
|
|
patch(box); new Map().get("/accounts");`;
|
|
expect(analyzeJavaScriptStaticSource(source).endpoints).toMatchObject([
|
|
{ kind: "network", value: "/accounts", interpretation: "candidate" },
|
|
]);
|
|
},
|
|
);
|
|
|
|
it.each([
|
|
['new Headers().get("content-type")', 0],
|
|
['new URLSearchParams().delete("q")', 0],
|
|
['new Map().get("/accounts")', 0],
|
|
['new Map().delete("/accounts")', 0],
|
|
['client.get("/accounts")', 0],
|
|
['const client = createClient(); client.get("/accounts")', 0],
|
|
['const records = new Map(); records.get("/accounts")', 1],
|
|
] as const)(
|
|
"requests mutation facts only for unresolved local receivers: %s",
|
|
(source, pending) => {
|
|
const file = parseJavaScriptSource(source);
|
|
if (file === null) throw new Error("Expected parsed source");
|
|
const index = completeSemanticSteps(
|
|
indexJavaScriptBindingsWithReceiversSteps(file.program),
|
|
);
|
|
completeSemanticSteps(index.indexAmbientWritesSteps());
|
|
const accumulator = createJavaScriptAnalysisAccumulator(source.length);
|
|
const context = {
|
|
source,
|
|
accumulator,
|
|
vitePreloadDependencyTable: null,
|
|
openReceiverFacts: index.receivers.openReceivers,
|
|
rpcScan: createJavaScriptRpcScan(
|
|
source,
|
|
index.receivers.rpcClientRoots,
|
|
index,
|
|
),
|
|
expressionShapes: createSemanticExpressionShapeResolver(index.bindings),
|
|
};
|
|
traverseJavaScriptAst(file.program, {
|
|
enter(node) {
|
|
if (
|
|
t.isCallExpression(node) ||
|
|
t.isOptionalCallExpression(node) ||
|
|
t.isNewExpression(node)
|
|
)
|
|
inspectCall(source, node, context, index.bindings);
|
|
},
|
|
});
|
|
expect(accumulator.pendingNetworkCalls).toHaveLength(pending);
|
|
},
|
|
);
|