From 8b2f8514c50e469c3add8ce9ba7ebebef888b14f Mon Sep 17 00:00:00 2001 From: morluto <76467478+morluto@users.noreply.github.com> Date: Sun, 11 Oct 2026 07:30:52 +0800 Subject: [PATCH] fix(javascript): ignore bare CommonJS exports rebinding Assigning a new value to the exports alias does not replace module.exports. Stop inventing a default export link and definite return-shape changes for that assignment. Validation: 22 semantic structure source tests and 49 compiled CLI/MCP acceptance tests passed. The new regression checks Node require returns an empty export object for both versions and both adapters report missing defaults without definite changes. --- .../javascriptSemanticProjection.ts | 2 +- .../exportedFunctionBindingsCli.test.ts | 25 +++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/src/domain/javascript/javascriptSemanticProjection.ts b/src/domain/javascript/javascriptSemanticProjection.ts index a5577bf975..f52d4495f3 100644 --- a/src/domain/javascript/javascriptSemanticProjection.ts +++ b/src/domain/javascript/javascriptSemanticProjection.ts @@ -641,7 +641,7 @@ const commonJsExportName = ( node: t.Node, state: JavaScriptSemanticAnalysisState, ): string | undefined => { - if (isUnshadowedGlobal(node, state, "exports")) return "default"; + // Rebinding the CommonJS `exports` alias does not replace `module.exports`. if (!t.isMemberExpression(node) && !t.isOptionalMemberExpression(node)) return undefined; const key = semanticStaticPropertyKey(node.property, node.computed); diff --git a/tests/acceptance/applications/exportedFunctionBindingsCli.test.ts b/tests/acceptance/applications/exportedFunctionBindingsCli.test.ts index f32f9e5e2e..bb2b10c34b 100644 --- a/tests/acceptance/applications/exportedFunctionBindingsCli.test.ts +++ b/tests/acceptance/applications/exportedFunctionBindingsCli.test.ts @@ -1,5 +1,6 @@ import { execFile } from "node:child_process"; import { mkdir, writeFile } from "node:fs/promises"; +import { createRequire } from "node:module"; import { join, resolve } from "node:path"; import { promisify } from "node:util"; @@ -13,6 +14,7 @@ import { javaScriptExportShapeComparisonResultSchema } from "../../../src/domain import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; const execute = promisify(execFile); +const requireFixture = createRequire(import.meta.url); const comparisonEnvelope = z.object({ normalized_result: javaScriptExportShapeComparisonResultSchema, }); @@ -111,6 +113,28 @@ describe("exported function binding resolution through the CLI", () => { expectUncertainComparison(cli); expect(await compareThroughMcp(fixture.input)).toEqual(cli); }); + + it("does not treat rebinding the CommonJS exports alias as a default export", async () => { + const fixture = await analyzeVersions( + (count) => + `exports = function parse() { return { kind: "result", count: ${String(count)} }; };`, + "default", + "app.cjs", + ); + + // This is a test-owned fixture, so Node's actual CommonJS loader is the + // oracle for what the application exports. + expect(requireFixture(fixture.leftPath)).toEqual({}); + expect(requireFixture(fixture.rightPath)).toEqual({}); + + const cli = await compareThroughCli(fixture); + expect(cli).toMatchObject({ + left: { status: "missing" }, + right: { status: "missing" }, + summary: { added: 0, removed: 0, changed: 0 }, + }); + expect(await compareThroughMcp(fixture.input)).toEqual(cli); + }); }); describe("stable exported function bindings through the CLI", () => { @@ -588,6 +612,7 @@ const compareThroughMcp = async (input: Record) => { arguments: input, }); expect(response.isError).not.toBe(true); + await client.ping(); return comparisonEnvelope.parse(response.structuredContent) .normalized_result; } finally {