From 348c65c7ece1aee280f9431d995996b444e7549d Mon Sep 17 00:00:00 2001 From: morluto <76467478+morluto@users.noreply.github.com> Date: Wed, 7 Oct 2026 19:51:43 +0800 Subject: [PATCH] fix(javascript): handle deep static member paths (#939) --- .../javascript/javascriptAstFingerprint.ts | 34 +++++++--- ...ptStaticAnalysis.member_path_depth.test.ts | 63 +++++++++++++++++++ .../javascriptStaticAnalysisHelpers.ts | 43 +++++++++---- 3 files changed, 119 insertions(+), 21 deletions(-) create mode 100644 src/domain/javascript/javascriptStaticAnalysis.member_path_depth.test.ts diff --git a/src/domain/javascript/javascriptAstFingerprint.ts b/src/domain/javascript/javascriptAstFingerprint.ts index 00bbaefba0..fd12f6435b 100644 --- a/src/domain/javascript/javascriptAstFingerprint.ts +++ b/src/domain/javascript/javascriptAstFingerprint.ts @@ -119,14 +119,30 @@ const addExport = (output: Set, name: string): void => { }; const memberPath = (node: t.Node | null | undefined): string => { - if (node === undefined || node === null) return ""; - if (t.isIdentifier(node) || t.isPrivateName(node)) - return t.isIdentifier(node) ? node.name : ""; - if (t.isThisExpression(node)) return "this"; - if (t.isMemberExpression(node) || t.isOptionalMemberExpression(node)) { - const object = t.isNode(node.object) ? memberPath(node.object) : ""; - const property = propertyName(node.property); - return object === "" ? property : `${object}.${property}`; + const properties: string[] = []; + let current = node; + while ( + current !== undefined && + current !== null && + (t.isMemberExpression(current) || t.isOptionalMemberExpression(current)) + ) { + properties.push(propertyName(current.property)); + current = t.isNode(current.object) ? current.object : undefined; } - return ""; + + const root = + current !== undefined && t.isIdentifier(current) + ? current.name + : current !== undefined && t.isThisExpression(current) + ? "this" + : ""; + if (properties.length === 0) return root; + + properties.reverse(); + const path = root === "" ? [] : [root]; + for (const property of properties) { + if (path.length === 0 && property === "") continue; + path.push(property); + } + return path.join("."); }; diff --git a/src/domain/javascript/javascriptStaticAnalysis.member_path_depth.test.ts b/src/domain/javascript/javascriptStaticAnalysis.member_path_depth.test.ts new file mode 100644 index 0000000000..6e426c7c68 --- /dev/null +++ b/src/domain/javascript/javascriptStaticAnalysis.member_path_depth.test.ts @@ -0,0 +1,63 @@ +import { expect, it } from "vitest"; + +import { collectJavaScriptExports } from "./javascriptAstFingerprint.js"; +import { analyzeJavaScriptStaticSource } from "./javascriptStaticAnalysis.js"; +import { parseJavaScriptSource } from "./javascriptSourceParser.js"; + +const memberChain = ".next".repeat(12_000); + +it("collects a 12,000-member CommonJS export path without recursion", () => { + const file = parseJavaScriptSource(`exports${memberChain}.last = 1;`); + if (file === null) throw new Error("Expected valid JavaScript"); + expect(collectJavaScriptExports(file).values).toEqual([ + `${"next.".repeat(12_000)}last`, + ]); +}); + +it("preserves empty, dynamic, this, and private member path behavior", () => { + const file = parseJavaScriptSource(` + exports[""].value = 1; + exports[""] = 0; + exports[key].value = 2; + this.exports.ignored = 3; + class Example { #private = 0; read() { this.#private = 1; } } + `); + if (file === null) throw new Error("Expected valid JavaScript"); + expect(collectJavaScriptExports(file).values).toEqual([ + "", + ".value", + "key.value", + ]); +}); + +it("keeps left-first and outer-member-first bundler runtime matches", () => { + const source = ` + (globalThis.webpackChunkLeft || globalThis.webpackChunkRight).push([[1], { 1: function() {} }]); + globalThis.webpackChunkOuter.webpackChunkInner.push([[2], { 2: function() {} }]); + `; + const analysis = analyzeJavaScriptStaticSource(source); + expect( + analysis.bundler_registrations.find(({ chunk_keys }) => + chunk_keys.includes("1"), + )?.runtime, + ).toBe("webpackChunkLeft"); + expect( + analysis.bundler_registrations.find(({ chunk_keys }) => + chunk_keys.includes("2"), + )?.runtime, + ).toBe("webpackChunkInner"); +}); + +it("analyzes a 12,000-member bundler runtime chain without recursion", () => { + const source = `globalThis.webpackChunkApp${memberChain}.push([[1], { 1: function(module) { module.exports.ok = 42; } }]);`; + const analysis = analyzeJavaScriptStaticSource(source); + expect(analysis.parse_status).toBe("complete"); + expect(analysis.parse_error_count).toBe(0); + expect(analysis.bundler_registrations).toHaveLength(1); + expect(analysis.bundler_registrations[0]).toMatchObject({ + bundler: "webpack", + runtime: "webpackChunkApp", + chunk_keys: ["1"], + modules: [expect.objectContaining({ exports: ["ok"] })], + }); +}); diff --git a/src/domain/javascript/javascriptStaticAnalysisHelpers.ts b/src/domain/javascript/javascriptStaticAnalysisHelpers.ts index b86e78b675..a84d1dd03d 100644 --- a/src/domain/javascript/javascriptStaticAnalysisHelpers.ts +++ b/src/domain/javascript/javascriptStaticAnalysisHelpers.ts @@ -73,19 +73,38 @@ export const chunkRuntime = (call: t.CallExpression): string | undefined => { }; const findChunkRuntime = (node: t.Node): string | undefined => { - if (t.isIdentifier(node) && /(?:webpack|rspack)Chunk/iu.test(node.name)) - return node.name; - if (t.isMemberExpression(node) || t.isOptionalMemberExpression(node)) { - const property = semanticStaticPropertyName(node.property, node.computed); - if (/(?:webpack|rspack)Chunk/iu.test(property)) return property; - return t.isNode(node.object) ? findChunkRuntime(node.object) : undefined; + const pending: t.Node[] = [node]; + while (pending.length > 0) { + const current = pending.pop(); + if (current === undefined) continue; + if ( + t.isIdentifier(current) && + /(?:webpack|rspack)Chunk/iu.test(current.name) + ) + return current.name; + if ( + t.isMemberExpression(current) || + t.isOptionalMemberExpression(current) + ) { + const property = semanticStaticPropertyName( + current.property, + current.computed, + ); + if (/(?:webpack|rspack)Chunk/iu.test(property)) return property; + if (t.isNode(current.object)) pending.push(current.object); + continue; + } + if (t.isAssignmentExpression(current)) { + pending.push(current.right, current.left); + continue; + } + if (t.isLogicalExpression(current) || t.isBinaryExpression(current)) { + pending.push(current.right, current.left); + continue; + } + if (t.isParenthesizedExpression(current) || t.isTSAsExpression(current)) + pending.push(current.expression); } - if (t.isAssignmentExpression(node)) - return findChunkRuntime(node.left) ?? findChunkRuntime(node.right); - if (t.isLogicalExpression(node) || t.isBinaryExpression(node)) - return findChunkRuntime(node.left) ?? findChunkRuntime(node.right); - if (t.isParenthesizedExpression(node) || t.isTSAsExpression(node)) - return findChunkRuntime(node.expression); return undefined; };