fix(javascript): bind factory export roles to parameter values

This commit is contained in:
morluto
2026-10-24 12:33:52 +08:00
parent e5965adb29
commit f774dc420b
3 changed files with 46 additions and 18 deletions
+4 -1
View File
@@ -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
@@ -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");
@@ -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");