From f774dc420b3befe2b8e68971b4116da2d62fc800 Mon Sep 17 00:00:00 2001 From: morluto <76467478+morluto@users.noreply.github.com> Date: Sat, 24 Oct 2026 12:33:52 +0800 Subject: [PATCH] fix(javascript): bind factory export roles to parameter values --- docs/javascript-artifact-reconstruction.md | 5 ++- .../javascript/javascriptAstFingerprint.ts | 21 +++++++++- .../webpackRenamedExportsCli.test.ts | 38 +++++++++++-------- 3 files changed, 46 insertions(+), 18 deletions(-) diff --git a/docs/javascript-artifact-reconstruction.md b/docs/javascript-artifact-reconstruction.md index 5727eef481..413dd696e6 100644 --- a/docs/javascript-artifact-reconstruction.md +++ b/docs/javascript-artifact-reconstruction.md @@ -313,7 +313,10 @@ Selected bounded text is then parsed as inert data to recover: - local HTML script entrypoints; - Webpack and Rspack chunk registrations and module factories represented as AST literals, plus esbuild `__commonJS` module tables; recovered esbuild - CommonJS exports use its factory's `exports`-then-`module` parameter order; + CommonJS exports use its factory's `exports`-then-`module` parameter order. + Export receivers and loader helpers retain those calling-convention roles only + while the binding at the read still holds its original parameter value; + replacing a parameter does not give the replacement object that role; - static imports, dynamic imports, CommonJS `require` calls, workers, and service workers (only `new Worker(new URL(path, import.meta.url))` resolves relative to the calling module; string worker and service-worker paths use diff --git a/src/domain/javascript/javascriptAstFingerprint.ts b/src/domain/javascript/javascriptAstFingerprint.ts index 6b76eb124a..9efcc803a7 100644 --- a/src/domain/javascript/javascriptAstFingerprint.ts +++ b/src/domain/javascript/javascriptAstFingerprint.ts @@ -2,6 +2,7 @@ import { createHash } from "node:crypto"; import * as t from "@babel/types"; +import { semanticBindingAtRead } from "./javascriptSemanticMutationInitializers.js"; import { isUnmodifiedGlobalReference } from "./javascriptSemanticAmbientWrites.js"; import { traverseJavaScriptAst } from "./javascriptSemanticTraversal.js"; @@ -60,7 +61,25 @@ export const collectJavaScriptExports = ( return (reference: t.Node | null): boolean => { if (!t.isIdentifier(reference)) return false; if (roles === undefined) return reference.name === fallback; - return id !== undefined && bindingId(reference) === id; + if (id === undefined || bindings === undefined) return false; + const binding = resolveSemanticBindingState( + bindings, + reference, + reference.name, + ); + if (binding?.bindingId !== id) return false; + // The calling convention owns the parameter's entry value, not every + // later value assigned to the same lexical binding. + const read = semanticBindingAtRead( + binding, + reference, + bindings.parentsByNode, + ); + return ( + read.status === "ordered" && + read.binding.initializers.length === 0 && + read.binding.referenceInitializers.length === 0 + ); }; }; const isExportsAlias = identity("exports", "exports"); diff --git a/tests/acceptance/applications/webpackRenamedExportsCli.test.ts b/tests/acceptance/applications/webpackRenamedExportsCli.test.ts index 33a1bd56c4..6b5693ea99 100644 --- a/tests/acceptance/applications/webpackRenamedExportsCli.test.ts +++ b/tests/acceptance/applications/webpackRenamedExportsCli.test.ts @@ -3,14 +3,13 @@ import { join } from "node:path"; import { expect } from "vitest"; -import { parseEvidence } from "../../../src/domain/evidence.js"; -import { javascriptApplicationAnalysisResultSchema } from "../../../src/domain/javascript/javascriptApplicationAnalysis.js"; import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; import { cliTest } from "../../support/cli/cliFixture.js"; +import { analyzeJavaScriptDirectory } from "../../support/cli/javascriptApplication.js"; -cliTest( - "reports Webpack exports attached to renamed factory parameters", - async ({ cli }) => { +cliTest.for(["CLI", "stdio MCP"] as const)( + "reports Webpack exports attached to original renamed factory parameter values through %s", + async (surface, { cli }) => { const root = await createTestTempDirectory("rea-webpack-renamed-exports-"); await mkdir(join(root, "src")); await writeFile( @@ -21,20 +20,24 @@ cliTest( Object.defineProperty(__webpack_exports__, "delta", { enumerable: true, get() { return 3; } }); __webpack_require__.d(__webpack_exports__.nested, { nested_target: () => 0 }); __webpack_exports__ = { rebound: () => 0 }; + __webpack_exports__.ghost = 4; + Object.defineProperty(__webpack_exports__, "definedGhost", {value: 5}); + __webpack_require__.d(__webpack_exports__, { declaredGhost: () => 6 }); + module.exports.moduleBefore = 7; + module = {exports: {}}; + module.exports.moduleGhost = 8; __webpack_require__.d({}, { unrelated: () => 0 }); { const exports = {}; __webpack_require__.d(exports, { shadowed_exports: () => 5 }); } }, - 23(m, e, r) { r.d(e, { alpha: () => 1 }); } + 23(m, e, r) { + r.d(e, { alpha: () => 1 }); + r = {d() {}}; + r.d(e, { loaderGhost: () => 2 }); + e.afterLoaderReplacement = 3; + } }]);`, ); - const response = await cli.run({ - arguments: ["analyze-javascript-application", root, "--format", "json"], - environment: { REA_LOG_LEVEL: "silent" }, - }); - expect(response.exitCode).toBe(0); - const result = javascriptApplicationAnalysisResultSchema.parse( - parseEvidence(response.json).normalized_result, - ); + const result = await analyzeJavaScriptDirectory(surface, cli, root); const modules = result.graph.nodes .filter(({ kind }) => kind === "javascript-module") .map(({ observations }) => observations[0]?.properties); @@ -42,9 +45,12 @@ cliTest( expect.arrayContaining([ expect.objectContaining({ module_key: "17", - exports: ["alpha", "beta.gamma", "delta"], + exports: ["alpha", "beta.gamma", "delta", "moduleBefore"], + }), + expect.objectContaining({ + module_key: "23", + exports: ["afterLoaderReplacement", "alpha"], }), - expect.objectContaining({ module_key: "23", exports: ["alpha"] }), ]), ); expect(result.graph.coverage.status).toBe("complete");