fix(javascript): track public CommonJS export receiver lifetimes (#3567)

* fix(javascript): track public CommonJS export receiver lifetimes

* fix(javascript): retain exports across uncertain identity effects
This commit is contained in:
鸡哥
2026-10-14 11:46:14 -07:00
committed by GitHub
parent 357996cc89
commit 75104040df
10 changed files with 502 additions and 71 deletions
@@ -9,7 +9,6 @@ import type {
import {
resolveSemanticBindingState,
semanticResolutionBlocked,
isUnshadowedGlobal,
type JavaScriptSemanticAnalysisState,
type JavaScriptSemanticScopeState,
} from "./javascriptSemanticState.js";
@@ -38,6 +37,10 @@ import {
isTypeOnlyImportDeclaration,
isTypeOnlyImportSpecifier,
} from "./javascriptModuleDeclaration.js";
import {
commonJsExportName,
type CommonJsExportReceiverEffect,
} from "./javascriptCommonJsExportReceivers.js";
interface CollectCallableInput {
readonly node: t.Node;
@@ -78,6 +81,10 @@ export const collectSemanticCallable = (input: CollectCallableInput): void => {
export const collectSemanticModuleLink = (
node: t.Node,
state: JavaScriptSemanticAnalysisState,
commonJsReceivers: ReadonlyMap<
t.AssignmentExpression,
CommonJsExportReceiverEffect
>,
): void => {
if (t.isTSImportEqualsDeclaration(node)) collectImportEquals(node, state);
else if (t.isImportDeclaration(node)) collectImports(node, state);
@@ -110,7 +117,8 @@ export const collectSemanticModuleLink = (
node,
);
else if (t.isVariableDeclarator(node)) collectRequireLink(node, state);
else if (t.isAssignmentExpression(node)) collectCommonJsExport(node, state);
else if (t.isAssignmentExpression(node))
collectCommonJsExport(node, state, commonJsReceivers.get(node));
else if (t.isCallExpression(node)) collectUnboundRequireLink(node, state);
};
@@ -469,12 +477,10 @@ const requirePatternBindings = (
const collectCommonJsExport = (
node: t.AssignmentExpression,
state: JavaScriptSemanticAnalysisState,
receiverEffect: CommonJsExportReceiverEffect | undefined,
): void => {
const exportedName = commonJsExportName(node.left, state);
if (exportedName === undefined) return;
const receiverEffect = isCommonJsModuleExports(node.left, state)
? null
: commonJsExportReceiverEffect(node.right, state);
if (receiverEffect === "detached") return;
// Simple assignment chains yield their rightmost assigned value. Compound
// and logical writes also depend on the previous value, so remain unknown.
@@ -501,40 +507,6 @@ const collectCommonJsExport = (
if (required !== undefined) state.linkedRequireCalls.add(required.call);
};
/**
* An outer property assignment captures its receiver before evaluating the
* RHS. When that chain assigns a freshly created identity to `module.exports`,
* the outer write targets the old exports object and cannot define an export
* on the value ultimately returned by the CommonJS module. Reassignments from
* existing bindings may preserve identity, so this guard leaves them as
* export candidates.
*/
const commonJsExportReceiverEffect = (
node: t.Node,
state: JavaScriptSemanticAnalysisState,
): "detached" | "uncertain" | null => {
let value = node;
while (t.isAssignmentExpression(value, { operator: "=" })) {
if (isCommonJsModuleExports(value.left, state))
return isFreshAssignmentResult(value.right) ? "detached" : "uncertain";
value = value.right;
}
return null;
};
const isFreshAssignmentResult = (node: t.Node): boolean => {
let value = node;
while (t.isAssignmentExpression(value, { operator: "=" }))
value = value.right;
return (
t.isArrowFunctionExpression(value) ||
t.isFunctionExpression(value) ||
t.isClassExpression(value) ||
t.isObjectExpression(value) ||
t.isArrayExpression(value)
);
};
const addModuleLink = (
state: JavaScriptSemanticAnalysisState,
link: Omit<JavaScriptSemanticModuleLink, "callableId"> & {
@@ -831,32 +803,3 @@ const defaultExportLocalName = (
? value.id.name
: null;
};
const commonJsExportName = (
node: t.Node,
state: JavaScriptSemanticAnalysisState,
): string | undefined => {
// 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);
// A computed nonliteral key is unknown, but the empty string is a real exact
// CommonJS export name and must not collapse into the wildcard/default.
if (isUnshadowedGlobal(node.object, state, "exports"))
return key === null ? "*" : key;
if (
t.isMemberExpression(node.object) &&
isCommonJsModuleExports(node.object, state)
)
return key === null ? "*" : key;
if (isCommonJsModuleExports(node, state)) return "default";
return undefined;
};
const isCommonJsModuleExports = (
node: t.Node,
state: JavaScriptSemanticAnalysisState,
): boolean =>
(t.isMemberExpression(node) || t.isOptionalMemberExpression(node)) &&
isUnshadowedGlobal(node.object, state, "module") &&
semanticStaticPropertyKey(node.property, node.computed) === "exports";