perf(javascript): analyze member chains without recursive copying (#932)

* fix(javascript): traverse deep semantic member chains iteratively

* fix(javascript): preserve deep initializer and pattern analysis
This commit is contained in:
morluto authored and GitHub committed 2026-10-07 19:09:25 +08:00
1 parent e6ba9c3af3
commit dcde958450
7 files changed
+287 -116

No files matched your search

@@ -157,13 +157,12 @@ const nativeRequire = (
):
| { readonly specifier: string; readonly member: string | null }
| undefined => {
if (t.isMemberExpression(node) || t.isOptionalMemberExpression(node)) {
const nested = t.isNode(node.object)
? nativeRequire(node.object)
: undefined;
if (nested === undefined) return undefined;
const member = semanticStaticPropertyName(node.property, node.computed);
return { ...nested, member: member === "" ? nested.member : member };
let member: string | null = null;
while (t.isMemberExpression(node) || t.isOptionalMemberExpression(node)) {
if (!t.isNode(node.object)) return undefined;
const property = semanticStaticPropertyName(node.property, node.computed);
if (member === null && property !== "") member = property;
node = node.object;
}
if (!t.isCallExpression(node)) return undefined;
const name = calleeName(node.callee);
@@ -175,7 +174,7 @@ const nativeRequire = (
return undefined;
const specifier = stringValue(argumentNode(node.arguments[0]));
return specifier !== undefined && isNativeSpecifier(specifier, "require")
? { specifier, member: null }
? { specifier, member }
: undefined;
};
@@ -0,0 +1,154 @@
import * as t from "@babel/types";
import { expect, it } from "vitest";
import { analyzeJavaScriptSemantics } from "./javascriptSemanticAnalysis.js";
import { onlyBinding, origin } from "./javascriptSemanticAnalysis.fixture.js";
import { parseJavaScriptSource } from "./javascriptSourceParser.js";
import { calleeName } from "./javascriptStaticAnalysisHelpers.js";
import { analyzeJavaScriptStaticSource } from "./javascriptStaticAnalysis.js";
const memberChain = ".next".repeat(12_000);
it("retains native addon observations through a deep member initializer", () => {
const analysis = analyzeJavaScriptStaticSource(
`const value = require("./addon.node")${memberChain}.last[""];`,
);
expect(analysis.electron.native_addon_bindings).toEqual([
expect.objectContaining({ specifier: "./addon.node", members: ["last"] }),
]);
expect(analysis.parse_error_count).toBe(0);
});
it("keeps a deep non-native initializer outside native addon observations", () => {
const analysis = analyzeJavaScriptStaticSource(
`const value = root${memberChain}.last;`,
);
expect(analysis.electron.native_addon_bindings).toEqual([]);
expect(analysis.parse_status).toBe("complete");
});
it("retains unknown value diagnostics and references after a deep member initializer", () => {
const ir = analyzeJavaScriptSemantics(
`const value = root${memberChain}.last;`,
);
expect(ir.coverage).toEqual({ status: "complete", omittedCount: 0 });
expect(onlyBinding(ir, "value")).toMatchObject({
value: { status: "unknown", reason: "Cannot project last from unknown." },
provenance: { status: "unknown", reason: "Unbound identifier root." },
});
expect(ir.references.map(({ name, role }) => ({ name, role }))).toEqual([
{ name: "root", role: "read" },
]);
});
it.each([
`const value = require("./dependency.js")${memberChain}[""].last;`,
`const dependency = require("./dependency.js"); const value = dependency${memberChain}[""].last;`,
`const value = require("./dependency.js")?.next${memberChain}[""].last;`,
])("preserves the full literal module path (case %#)", (source) => {
const ir = analyzeJavaScriptSemantics(source);
const path = origin(onlyBinding(ir, "value"));
expect(path.specifier).toBe("./dependency.js");
expect(path.importedPath).toEqual([
...(source.includes("?.next") ? ["next"] : []),
...Array.from({ length: 12_000 }, () => "next"),
"",
"last",
]);
expect(ir.coverage.status).toBe("complete");
});
it("retains a deep CommonJS re-export path without inventing dynamic module links", () => {
const ir = analyzeJavaScriptSemantics(`
module.exports.result = require("./dependency.js")${memberChain}.last;
const dynamic = require("./other.js")${memberChain}[key].last;
`);
expect(ir.moduleLinks).toEqual([
expect.objectContaining({
kind: "commonjs-export",
specifier: "./dependency.js",
importedName: "last",
exportedName: "result",
}),
]);
expect(onlyBinding(ir, "dynamic").provenance).toMatchObject({
status: "unknown",
reason: "Dynamic provenance member.",
});
});
it("invalidates a deep mutation while retaining unrelated literal properties", () => {
const ir = analyzeJavaScriptSemantics(`
const root = { untouched: "retained" };
root${memberChain}.last = 1;
const value = root.untouched;
`);
expect(onlyBinding(ir, "root").value).toEqual({
status: "object",
properties: [
{ name: "untouched", value: { status: "literal", value: "retained" } },
],
unknownProperties: true,
omittedProperties: null,
});
expect(onlyBinding(ir, "value").value).toEqual({
status: "literal",
value: "retained",
});
});
it("retains the complete location and unresolved target of a deep call", () => {
const callee = `root${memberChain}.last`;
const ir = analyzeJavaScriptSemantics(`${callee}();`);
expect(ir.callSites).toEqual([
expect.objectContaining({
kind: "call",
resolution: "unresolved",
calleeCallableIds: [],
calleeLocation: {
start: { line: 1, column: 0 },
end: { line: 1, column: callee.length },
},
}),
]);
});
it("retains deep default values without confusing their reads with parameter bindings", () => {
const ir = analyzeJavaScriptSemantics(
`export function inspect(value = root${memberChain}.last) { return value; }`,
);
expect(ir.references.map(({ name, role }) => ({ name, role }))).toEqual([
{ name: "root", role: "read" },
{ name: "value", role: "read" },
]);
expect(ir.coverage.status).toBe("complete");
});
it("keeps a shadowed require local through a deep member initializer", () => {
const ir = analyzeJavaScriptSemantics(
`function inspect(require) { const value = require("./dependency.js")${memberChain}.last; return value; }`,
);
expect(ir.moduleLinks).toEqual([]);
expect(onlyBinding(ir, "value").provenance.origins).toEqual([]);
});
it.each([
["root.first.last()", "root.first.last"],
['root[""].last()', "root..last"],
['("")[""].last()', "last"],
['("")[""]()', ""],
["root[0].last()", "root.0.last"],
["root[key].last()", "root.[computed@5].last"],
["root?.first.last()", "root.first.last"],
[`root${memberChain}.last()`, `root${memberChain}.last`],
])("preserves exact callee syntax (case %#)", (source, expected) => {
const file = parseJavaScriptSource(source);
const statement = file?.program.body[0];
if (
!t.isExpressionStatement(statement) ||
(!t.isCallExpression(statement.expression) &&
!t.isOptionalCallExpression(statement.expression))
)
throw new Error("Expected parsed call expression");
expect(calleeName(statement.expression.callee)).toBe(expected);
});
@@ -39,11 +39,18 @@ export const collectSemanticMemberMutations = (
return;
}
if (t.isMemberExpression(node) || t.isOptionalMemberExpression(node)) {
markValue(
node.object,
[semanticStaticPropertyKey(node.property, node.computed), ...path],
bindings,
);
const members: (string | null)[] = [];
let current: t.Node = node;
while (
t.isMemberExpression(current) ||
t.isOptionalMemberExpression(current)
) {
members.push(
semanticStaticPropertyKey(current.property, current.computed),
);
current = current.object;
}
markValue(current, [...members.reverse(), ...path], bindings);
return;
}
for (const value of referencedValues(node, path))
@@ -1,7 +1,6 @@
import * as t from "@babel/types";
import type {
JavaScriptModuleOrigin,
JavaScriptSemanticBinding,
JavaScriptSemanticModuleLink,
JavaScriptSemanticReference,
@@ -14,6 +13,7 @@ import {
type JavaScriptSemanticAnalysisState,
type JavaScriptSemanticScopeState,
} from "./javascriptSemanticState.js";
import { semanticRequireOrigin } from "./javascriptSemanticRequireOrigin.js";
import { compareCodePoints } from "../canonicalOrdering.js";
import { traverseJavaScriptAst } from "./javascriptSemanticTraversal.js";
import {
@@ -100,12 +100,16 @@ export const collectSemanticReferences = (
): JavaScriptSemanticReference[] => {
const output: JavaScriptSemanticReference[] = [];
const seen = new Set<string>();
const patterns: t.Node[] = [];
traverseJavaScriptAst(program, {
enter: (node, parent, readAncestors) => {
if (isPatternNode(node)) patterns.push(node);
if (
!t.isIdentifier(node) ||
parent === null ||
(name !== undefined && node.name !== name)
(name !== undefined && node.name !== name) ||
(isNonReferenceKey(node, parent) &&
!patterns.some((pattern) => bindsIdentifier(pattern, parent, node)))
)
return;
const role = semanticIdentifierRole(node, parent, readAncestors());
@@ -138,6 +142,9 @@ export const collectSemanticReferences = (
});
}
},
exit: (node) => {
if (isPatternNode(node)) patterns.pop();
},
});
return output;
};
@@ -174,40 +181,6 @@ export const immutableSemanticBindings = (
}))
.sort((left, right) => compareCodePoints(left.bindingId, right.bindingId));
/** Recover a literal require origin plus an optional member chain. */
const semanticModuleOrigin = (
node: t.Node | null | undefined,
projection: readonly (string | number | null)[],
state: JavaScriptSemanticAnalysisState,
): JavaScriptModuleOrigin | undefined => {
if (t.isMemberExpression(node) || t.isOptionalMemberExpression(node)) {
const nested = t.isNode(node.object)
? semanticModuleOrigin(node.object, projection, state)
: undefined;
const member = semanticStaticPropertyKey(node.property, node.computed);
return nested === undefined || member === null
? undefined
: { ...nested, importedPath: [...nested.importedPath, member] };
}
if (
!t.isCallExpression(node) ||
!t.isIdentifier(node.callee, { name: "require" })
)
return undefined;
if (
resolveSemanticBindingState(state, node.callee, "require") !== undefined ||
semanticResolutionBlocked(state, node.callee, "require")
)
return undefined;
const specifier = stringValue(node.arguments[0]);
return specifier === undefined || projection.includes(null)
? undefined
: {
specifier,
importedPath: projection.map((segment) => String(segment)),
};
};
export {
semanticStaticPropertyKey,
semanticStaticPropertyName,
@@ -318,7 +291,7 @@ const collectRequireLink = (
node: t.VariableDeclarator,
state: JavaScriptSemanticAnalysisState,
): void => {
const origin = semanticModuleOrigin(node.init, [], state);
const origin = semanticRequireOrigin(node.init, state);
if (origin === undefined) return;
for (const binding of requirePatternBindings(node.id, origin.importedPath))
addModuleLink(state, {
@@ -364,7 +337,7 @@ const collectCommonJsExport = (
): void => {
const exportedName = commonJsExportName(node.left, state);
if (exportedName === undefined) return;
const origin = semanticModuleOrigin(node.right, [], state);
const origin = semanticRequireOrigin(node.right, state);
addModuleLink(state, {
kind: "commonjs-export",
specifier: origin?.specifier ?? null,
@@ -0,0 +1,38 @@
import * as t from "@babel/types";
import type { JavaScriptModuleOrigin } from "./javascriptSemanticIr.js";
import {
resolveSemanticBindingState,
semanticResolutionBlocked,
type JavaScriptSemanticAnalysisState,
} from "./javascriptSemanticState.js";
import { semanticStaticPropertyKey } from "./javascriptAstValues.js";
import { stringValue } from "./javascriptStaticAnalysisHelpers.js";
/** Recover an unshadowed literal require origin and its exact member path. */
export const semanticRequireOrigin = (
node: t.Node | null | undefined,
state: JavaScriptSemanticAnalysisState,
): JavaScriptModuleOrigin | undefined => {
const members: string[] = [];
while (t.isMemberExpression(node) || t.isOptionalMemberExpression(node)) {
const member = semanticStaticPropertyKey(node.property, node.computed);
if (member === null || !t.isNode(node.object)) return undefined;
members.push(member);
node = node.object;
}
if (
!t.isCallExpression(node) ||
!t.isIdentifier(node.callee, { name: "require" })
)
return undefined;
if (
resolveSemanticBindingState(state, node.callee, "require") !== undefined ||
semanticResolutionBlocked(state, node.callee, "require")
)
return undefined;
const specifier = stringValue(node.arguments[0]);
return specifier === undefined
? undefined
: { specifier, importedPath: members.reverse() };
};
@@ -4,17 +4,15 @@ import { invalidateSemanticMutationPath } from "./javascriptSemanticMutationValu
import type {
JavaScriptBindingProvenance,
JavaScriptModuleOrigin,
JavaScriptSemanticProperty,
JavaScriptSemanticValue,
} from "./javascriptSemanticIr.js";
import {
resolveSemanticBindingState,
semanticResolutionBlocked,
type JavaScriptSemanticAnalysisState,
type JavaScriptSemanticBindingState,
} from "./javascriptSemanticState.js";
import { stringValue } from "./javascriptStaticAnalysisHelpers.js";
import { semanticRequireOrigin } from "./javascriptSemanticRequireOrigin.js";
import { compareCodePoints } from "../canonicalOrdering.js";
import { semanticStaticPropertyKey } from "./javascriptAstValues.js";
import {
@@ -253,15 +251,28 @@ const evaluateMember = (
node: t.MemberExpression | t.OptionalMemberExpression,
context: EvaluationContext,
): JavaScriptSemanticValue => {
if (!t.isNode(node.object))
return { status: "unknown", reason: "Unsupported member base." };
const key =
semanticStaticPropertyKey(node.property, node.computed) ?? undefined;
if (key === undefined)
return { status: "unknown", reason: "Dynamic member key." };
return projectValue(evaluateExpression(node.object, nestedContext(context)), [
key,
]);
const keys: string[] = [];
let current: t.Node = node;
let value: JavaScriptSemanticValue | undefined;
while (
t.isMemberExpression(current) ||
t.isOptionalMemberExpression(current)
) {
if (!t.isNode(current.object)) {
value = { status: "unknown", reason: "Unsupported member base." };
break;
}
const key = semanticStaticPropertyKey(current.property, current.computed);
if (key === null) {
value = { status: "unknown", reason: "Dynamic member key." };
break;
}
keys.push(key);
current = current.object;
}
value ??= evaluateExpression(current, nestedContext(context));
for (const key of keys.reverse()) value = projectValue(value, [key]);
return value;
};
const evaluateAddition = (
@@ -425,7 +436,7 @@ const provenanceForExpression = (
node: t.Node,
context: EvaluationContext,
): JavaScriptBindingProvenance => {
const required = requireOrigin(node, context.state);
const required = semanticRequireOrigin(node, context.state);
if (required !== undefined) return semanticOriginsProvenance([required]);
if (t.isIdentifier(node)) {
const binding = resolveSemanticBindingState(context.state, node, node.name);
@@ -437,25 +448,37 @@ const provenanceForExpression = (
: provenanceForBinding(binding, nestedContext(context));
}
if (t.isMemberExpression(node) || t.isOptionalMemberExpression(node)) {
if (!t.isNode(node.object))
return semanticUnresolvedProvenance(
"unknown",
"Unsupported member base.",
const members: string[] = [];
let current: t.Node = node;
while (
t.isMemberExpression(current) ||
t.isOptionalMemberExpression(current)
) {
if (!t.isNode(current.object))
return semanticUnresolvedProvenance(
"unknown",
"Unsupported member base.",
);
const member = semanticStaticPropertyKey(
current.property,
current.computed,
);
const member =
semanticStaticPropertyKey(node.property, node.computed) ?? undefined;
if (typeof member !== "string")
return semanticUnresolvedProvenance(
"unknown",
"Dynamic provenance member.",
);
const base = provenanceForExpression(node.object, nestedContext(context));
if (member === null)
return semanticUnresolvedProvenance(
"unknown",
"Dynamic provenance member.",
);
members.push(member);
current = current.object;
}
const base = provenanceForExpression(current, nestedContext(context));
const path = members.reverse();
return base.status !== "module"
? base
: semanticOriginsProvenance(
base.origins.map((origin) => ({
...origin,
importedPath: [...origin.importedPath, member],
importedPath: [...origin.importedPath, ...path],
})),
);
}
@@ -480,33 +503,6 @@ const provenanceForExpression = (
return semanticLocalProvenance();
};
const requireOrigin = (
node: t.Node,
state: JavaScriptSemanticAnalysisState,
): JavaScriptModuleOrigin | undefined => {
if (t.isMemberExpression(node) || t.isOptionalMemberExpression(node)) {
if (!t.isNode(node.object)) return undefined;
const nested = requireOrigin(node.object, state);
const member =
semanticStaticPropertyKey(node.property, node.computed) ?? undefined;
return nested === undefined || typeof member !== "string"
? undefined
: { ...nested, importedPath: [...nested.importedPath, member] };
}
if (
!t.isCallExpression(node) ||
!t.isIdentifier(node.callee, { name: "require" })
)
return undefined;
if (
resolveSemanticBindingState(state, node.callee, "require") !== undefined ||
semanticResolutionBlocked(state, node.callee, "require")
)
return undefined;
const specifier = stringValue(node.arguments[0]);
return specifier === undefined ? undefined : { specifier, importedPath: [] };
};
const mergeValues = (
values: readonly JavaScriptSemanticValue[],
): JavaScriptSemanticValue => {
@@ -520,19 +520,23 @@ export const storageKind = (
};
/** Produce a dotted callee name from member syntax. */
export const calleeName = (node: t.Node): string => calleeNameAt(node);
const calleeNameAt = (node: t.Node): string => {
if (t.isIdentifier(node)) return node.name;
if (t.isImport(node)) return "import";
if (t.isMemberExpression(node) || t.isOptionalMemberExpression(node)) {
const object = t.isNode(node.object) ? calleeNameAt(node.object) : "";
// A dynamic key commits no name: `target[method]()` must not be reported as
// `target.method`, which would fabricate a runtime call shape.
const property = memberPropertyName(node);
return object === "" ? property : `${object}.${property}`;
export const calleeName = (node: t.Node): string => {
const segments: string[] = [];
while (t.isMemberExpression(node) || t.isOptionalMemberExpression(node)) {
// Dynamic keys retain their syntax identity instead of inventing a name.
segments.push(memberPropertyName(node));
if (!t.isNode(node.object)) break;
node = node.object;
}
return "";
const base = t.isIdentifier(node)
? node.name
: t.isImport(node)
? "import"
: "";
segments.push(base);
segments.reverse();
const first = segments.findIndex((segment) => segment !== "");
return first === -1 ? "" : segments.slice(first).join(".");
};
/** Read a member property only when its syntax commits an exact name. */