From dcde9584508830a5d99ac5e2032f539563c69002 Mon Sep 17 00:00:00 2001 From: morluto <76467478+morluto@users.noreply.github.com> Date: Wed, 7 Oct 2026 19:09:25 +0800 Subject: [PATCH] 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 --- .../electronStaticAnalysisNative.ts | 15 +- ...criptSemanticAnalysis.member_depth.test.ts | 154 ++++++++++++++++++ .../javascriptSemanticMemberMutations.ts | 17 +- .../javascriptSemanticProjection.ts | 49 ++---- .../javascriptSemanticRequireOrigin.ts | 38 +++++ .../javascript/javascriptSemanticValues.ts | 102 ++++++------ .../javascriptStaticAnalysisHelpers.ts | 28 ++-- 7 files changed, 287 insertions(+), 116 deletions(-) create mode 100644 src/domain/javascript/javascriptSemanticAnalysis.member_depth.test.ts create mode 100644 src/domain/javascript/javascriptSemanticRequireOrigin.ts diff --git a/src/domain/javascript/electronStaticAnalysisNative.ts b/src/domain/javascript/electronStaticAnalysisNative.ts index e292596dc5..77353fe78c 100644 --- a/src/domain/javascript/electronStaticAnalysisNative.ts +++ b/src/domain/javascript/electronStaticAnalysisNative.ts @@ -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; }; diff --git a/src/domain/javascript/javascriptSemanticAnalysis.member_depth.test.ts b/src/domain/javascript/javascriptSemanticAnalysis.member_depth.test.ts new file mode 100644 index 0000000000..9d27e0ed5d --- /dev/null +++ b/src/domain/javascript/javascriptSemanticAnalysis.member_depth.test.ts @@ -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); +}); diff --git a/src/domain/javascript/javascriptSemanticMemberMutations.ts b/src/domain/javascript/javascriptSemanticMemberMutations.ts index e168b20468..325e481693 100644 --- a/src/domain/javascript/javascriptSemanticMemberMutations.ts +++ b/src/domain/javascript/javascriptSemanticMemberMutations.ts @@ -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)) diff --git a/src/domain/javascript/javascriptSemanticProjection.ts b/src/domain/javascript/javascriptSemanticProjection.ts index 9dcc8a7ad1..740b9b0a75 100644 --- a/src/domain/javascript/javascriptSemanticProjection.ts +++ b/src/domain/javascript/javascriptSemanticProjection.ts @@ -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(); + 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, diff --git a/src/domain/javascript/javascriptSemanticRequireOrigin.ts b/src/domain/javascript/javascriptSemanticRequireOrigin.ts new file mode 100644 index 0000000000..e0bf540460 --- /dev/null +++ b/src/domain/javascript/javascriptSemanticRequireOrigin.ts @@ -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() }; +}; diff --git a/src/domain/javascript/javascriptSemanticValues.ts b/src/domain/javascript/javascriptSemanticValues.ts index 78fc72c1c6..034297db55 100644 --- a/src/domain/javascript/javascriptSemanticValues.ts +++ b/src/domain/javascript/javascriptSemanticValues.ts @@ -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 => { diff --git a/src/domain/javascript/javascriptStaticAnalysisHelpers.ts b/src/domain/javascript/javascriptStaticAnalysisHelpers.ts index 382feff613..b86e78b675 100644 --- a/src/domain/javascript/javascriptStaticAnalysisHelpers.ts +++ b/src/domain/javascript/javascriptStaticAnalysisHelpers.ts @@ -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. */