fix(javascript): preserve lexical binding and default export capture semantics

Resolve CommonJS wrapper bindings from the parser-owned source type. Capture default identifier export values at the export statement while preserving live named bindings.

Refs: #3261, #3297

Co-authored-by: hhang <169027046+xu91102@users.noreply.github.com>
This commit is contained in:
morlutoandhhang committed 2026-10-14 17:49:13 +08:00
1 parent 322506d0c0
commit dae1e55381
6 files changed
+111 -16

No files matched your search

@@ -28,6 +28,7 @@ import { evaluateSemanticExpression } from "./javascriptSemanticValues.js";
import { range } from "./javascriptStaticAnalysisHelpers.js";
import { semanticReturnCoverage } from "./javascriptSemanticCoverage.js";
import { semanticCallableResultExpressions } from "./javascriptSemanticCallableResults.js";
import { semanticReadInitializers } from "./javascriptSemanticMutationInitializers.js";
interface ReturnExpression {
readonly node: t.Node | null;
@@ -104,15 +105,18 @@ export const resolveSemanticModuleCallables = (
if (link.kind === "type-import") return { ...link, callableId: null };
// Named exports follow their lexical binding, including every assignment.
// A declaration's ID or a callable's display name cannot prove that link.
if (link.localName === null)
const captured = state.defaultExportExpressions.get(link);
if (link.localName === null && captured === undefined)
return link.callableId !== null && callableIds.has(link.callableId)
? link
: { ...link, callableId: null };
const binding = state.moduleLinkBindings.get(link);
const resolved =
binding === undefined
? []
: callableIdsForBinding(binding, state, callableIds, new Set());
captured !== undefined
? callableIdsForNode(captured, state, callableIds, new Set(), true)
: binding === undefined
? []
: callableIdsForBinding(binding, state, callableIds, new Set());
const provenance =
binding === undefined || link.specifier !== null
? undefined
@@ -139,21 +143,29 @@ const callableIdsForBinding = (
state: JavaScriptSemanticAnalysisState,
admitted: ReadonlySet<string>,
seen: ReadonlySet<string>,
read?: t.Node,
): string[] => {
// A single initializer does not prove an assignment executes or replace
// the unknown incoming value of a parameter/catch binding.
if (
seen.has(binding.bindingId) ||
binding.initializers.length !== 1 ||
(read === undefined && binding.initializers.length !== 1) ||
binding.definitions.some(
({ kind }) =>
kind === "assignment" || kind === "parameter" || kind === "catch",
(read === undefined && kind === "assignment") ||
kind === "parameter" ||
kind === "catch",
)
)
return [];
const initializer = binding.initializers[0];
const selected =
read === undefined
? binding
: semanticReadInitializers(binding, read, state.parentsByNode);
const initializer = selected.initializers[0];
if (
initializer === undefined ||
selected.initializers.length !== 1 ||
initializer.projection.length > 0 ||
state.conditionallySkippedExpressions.has(initializer.node)
)
@@ -163,6 +175,7 @@ const callableIdsForBinding = (
state,
admitted,
new Set([...seen, binding.bindingId]),
read !== undefined,
);
};
@@ -171,17 +184,24 @@ const callableIdsForNode = (
state: JavaScriptSemanticAnalysisState,
admitted: ReadonlySet<string>,
seen: ReadonlySet<string>,
snapshot = false,
): string[] => {
const unwrapped = unwrapJavaScriptExpression(node).node;
if (unwrapped !== node)
return callableIdsForNode(unwrapped, state, admitted, seen);
return callableIdsForNode(unwrapped, state, admitted, seen, snapshot);
const direct = semanticCallableIdForNode(node);
if (direct !== null && admitted.has(direct)) return [direct];
if (t.isIdentifier(node)) {
const binding = resolveSemanticBindingState(state, node, node.name);
return binding === undefined
? []
: callableIdsForBinding(binding, state, admitted, seen);
: callableIdsForBinding(
binding,
state,
admitted,
seen,
snapshot ? node : undefined,
);
}
return [];
};