Compare commits

...
Author SHA1 Message Date
Kaoru0822andClaude Opus 5.5 f5b01ef756 test(javascript): assert parameter-only request targets skip the member index
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 01:47:15 +00:00
Kaoru0822andClaude Opus 5.5 3ebab7ab45 perf(javascript): index member effects only for resolvable request targets
Static analysis built the whole-source member-mutation index whenever a file
had a pending fetch/XHR/WebSocket target. Only a target with a stable earlier
initializer can resolve, and that proof reads definition facts alone, so check
it first and build the index only when such a target needs its effects. Every
other request stays partial without the index.

On pdf.js's openjpeg_nowasm_fallback.js the static phase drops from 242 s and
4.2 GB RSS (heap exhaustion in the worker) to 0.6 s, so its static facts are
retained again. Static facts are byte-identical across 126 real files.

Refs: #2498

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 01:26:19 +00:00
3 changed files with 171 additions and 22 deletions

No files matched your search

@@ -1,6 +1,13 @@
import * as t from "@babel/types";
import { expect, it } from "vitest";
import { createJavaScriptRpcScan } from "./javascriptRpcEndpoints.js";
import { indexJavaScriptBindingsWithReceiversSteps } from "./javascriptSemanticAnalysis.js";
import { completeSemanticSteps } from "./javascriptSemanticTraversal.js";
import { parseJavaScriptSource } from "./javascriptSourceParser.js";
import { analyzeJavaScriptStaticSource } from "./javascriptStaticAnalysis.js";
import { finishPartialNetworkEndpointsSteps } from "./javascriptStaticAnalysisCalls.js";
import { createJavaScriptAnalysisAccumulator } from "./javascriptStaticAnalysisState.js";
const endpoints = (source: string) =>
analyzeJavaScriptStaticSource(source).endpoints.map((endpoint) => ({
@@ -10,6 +17,51 @@ const endpoints = (source: string) =>
resolution: endpoint.resolution,
}));
/** Finish every `fetch` call in source order and count member-index builds. */
const finishFetchCalls = (source: string) => {
const file = parseJavaScriptSource(source);
if (file === null) throw new TypeError("Fixture must parse");
const index = completeSemanticSteps(
indexJavaScriptBindingsWithReceiversSteps(file.program),
);
completeSemanticSteps(index.indexAmbientWritesSteps());
let memberIndexBuilds = 0;
const observed = {
...index,
*indexMemberMutationFactsSteps() {
memberIndexBuilds += 1;
yield* index.indexMemberMutationFactsSteps();
},
};
const context = {
source,
accumulator: createJavaScriptAnalysisAccumulator(source.length),
vitePreloadDependencyTable: null,
rpcScan: createJavaScriptRpcScan(
source,
index.receivers.rpcClientRoots,
observed,
),
};
t.traverseFast(file.program, (node) => {
if (
t.isCallExpression(node) &&
t.isIdentifier(node.callee, { name: "fetch" })
)
context.accumulator.pendingNetworkCalls.push({ node, name: "fetch" });
});
completeSemanticSteps(
finishPartialNetworkEndpointsSteps(context, index.bindings, observed),
);
return {
memberIndexBuilds,
endpoints: context.accumulator.endpoints.map(({ value }) => ({
value: value.value,
resolution: value.resolution,
})),
};
};
it("resolves unchanged lexical strings and object properties", () => {
expect(
endpoints(`
@@ -215,6 +267,67 @@ it("keeps an escaped URL object partial", () => {
});
});
it("checks member effects for stable targets beside parameter targets", () => {
expect(
endpoints(`
const load = (url) => fetch(url);
const target = "/items";
const socket = new URL("wss://old.example.test/socket");
configure(socket);
fetch(target);
fetch(socket);
`),
).toEqual([
{
kind: "network",
value: "url",
mechanism: "call:fetch",
resolution: "partial",
},
{
kind: "network",
value: "/items",
mechanism: "call:fetch",
resolution: undefined,
},
{
kind: "network",
value: "socket",
mechanism: "call:fetch",
resolution: "partial",
},
]);
});
it("builds member effects only when a pending target can resolve", () => {
expect(
finishFetchCalls(`
const load = (url) => fetch(url);
fetch(later);
const later = "/after";
`),
).toEqual({
memberIndexBuilds: 0,
endpoints: [
{ value: "url", resolution: "partial" },
{ value: "later", resolution: "partial" },
],
});
expect(
finishFetchCalls(`
const load = (url) => fetch(url);
const target = "/items";
fetch(target);
`),
).toEqual({
memberIndexBuilds: 1,
endpoints: [
{ value: "url", resolution: "partial" },
{ value: "/items", resolution: undefined },
],
});
});
it("retains dynamic targets on known global fetch receivers", () => {
expect(
endpoints(`
@@ -21,7 +21,7 @@ import {
import {
addReference,
addSourceMapDirectives,
finishPartialNetworkEndpoints,
finishPartialNetworkEndpointsSteps,
inspectCall,
inspectRouteProperty,
inspectRoleProperty,
@@ -99,10 +99,8 @@ export function* analyzeParsedJavaScriptStaticSourceSteps(
rpcScan: createJavaScriptRpcScan(source, receivers.rpcClientRoots, index),
};
yield* traverseStaticSourceSteps(file, findings, bindings);
if (accumulator.pendingNetworkCalls.length > 0) {
yield* index.indexMemberMutationFactsSteps();
finishPartialNetworkEndpoints(findings, bindings, index);
}
if (accumulator.pendingNetworkCalls.length > 0)
yield* finishPartialNetworkEndpointsSteps(findings, bindings, index);
accumulator.electronCallPaths = collectElectronWrappedCallPaths(
file.program,
bindings,
@@ -522,15 +522,44 @@ const isKnownNetworkRequest = (
};
/** Finish queued targets after shared member-mutation facts are indexed. */
export const finishPartialNetworkEndpoints = (
/**
* Only a target with a stable earlier initializer can resolve, and that proof
* reads definition facts alone. Build the whole-source mutation index only when
* such a target needs its effects; every other request stays partial.
*/
export function* finishPartialNetworkEndpointsSteps(
context: FindingContext,
bindings: JavaScriptSemanticAnalysisState,
index: JavaScriptBindingIndex,
): void => {
for (const { node, name } of context.accumulator.pendingNetworkCalls) {
const target = argumentNode(node.arguments[0]);
if (target === undefined) continue;
const resolved = resolvedRequestTarget(target, node, bindings, index);
): Generator<void, void> {
const pending = context.accumulator.pendingNetworkCalls.flatMap(
({ node, name }) => {
const target = argumentNode(node.arguments[0]);
if (target === undefined) return [];
const capture = requestTargetCapture(target, node);
return [
{
node,
name,
target,
capture,
resolvable: requestInitializerPrecedesSite(
capture.reference,
capture.site,
bindings,
index,
),
},
];
},
);
context.accumulator.pendingNetworkCalls.length = 0;
if (pending.some(({ resolvable }) => resolvable))
yield* index.indexMemberMutationFactsSteps();
for (const { node, name, target, capture, resolvable } of pending) {
const resolved = resolvable
? resolvedRequestTarget(target, capture, bindings, index)
: undefined;
if (resolved !== undefined)
addEndpoint(context, {
node,
@@ -540,11 +569,10 @@ export const finishPartialNetworkEndpoints = (
});
else addPartialNetworkEndpoint(context, node, name, target);
}
context.accumulator.pendingNetworkCalls.length = 0;
context.accumulator.endpoints.sort(
(left, right) => left.offset - right.offset,
);
};
}
const addPartialNetworkEndpoint = (
context: FindingContext,
@@ -561,24 +589,34 @@ const addPartialNetworkEndpoint = (
resolution: "partial",
});
const resolvedRequestTarget = (
interface RequestTargetCapture {
readonly reference: t.Node;
readonly site: t.Node;
}
const requestTargetCapture = (
node: t.Node,
site: t.Node,
bindings: JavaScriptSemanticAnalysisState,
index: JavaScriptBindingIndex,
): string | undefined => {
): RequestTargetCapture => {
const reference = requestReferenceNode(node);
// A toString result is selected before the enclosing request invocation.
const expression = unwrapJavaScriptExpression(node).node;
const capture = reference === expression ? site : expression;
if (referenceMayHaveChangedAtSite(reference, capture, index))
return undefined;
if (!requestInitializerPrecedesSite(reference, capture, bindings, index))
return { reference, site: reference === expression ? site : expression };
};
/** Resolve a target whose stable earlier initializer the caller has proved. */
const resolvedRequestTarget = (
node: t.Node,
capture: RequestTargetCapture,
bindings: JavaScriptSemanticAnalysisState,
index: JavaScriptBindingIndex,
): string | undefined => {
if (referenceMayHaveChangedAtSite(capture.reference, capture.site, index))
return undefined;
const evaluated = evaluateSemanticExpression(node, bindings);
if (evaluated.status === "literal" && typeof evaluated.value === "string")
return evaluated.value;
return urlObjectRequestTarget(node, capture, bindings, index);
return urlObjectRequestTarget(node, capture.site, bindings, index);
};
const requestInitializerPrecedesSite = (