mirror of
https://github.com/morluto/rea
synced 2026-10-27 06:33:32 -04:00
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:
@@ -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";
|
||||
|
||||
Reference in New Issue
Block a user