refactor(javascript): break the semantic analysis import cycle (#8063)

* refactor(javascript): break the semantic analysis import cycle

Split shared semantic facts out of the analysis hub so projectors of
effects, evaluation, and identity no longer import one another in a cycle.

* refactor(javascript): share the import specifier reader

Read static imports in one module used by the boundary checker and both
cycle guards, so those rules cannot drift apart.

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
This commit is contained in:
morlutoandCursor Agent authored and GitHub committed 2026-10-30 06:07:49 +08:00
1 parent 940d71a45a
commit 249b90c5e3
33 files changed
+1043 -772

No files matched your search

@@ -11,11 +11,13 @@ import {
semanticStaticPropertyKey,
unwrapJavaScriptExpression,
} from "./javascriptAstValues.js";
import { semanticScopeId } from "./javascriptSemanticState.js";
import type {
JavaScriptSemanticBindingState,
JavaScriptSemanticAnalysisState,
JavaScriptSemanticEffectTiming,
import {
resolveSemanticBindingState,
semanticResolutionBlocked,
semanticScopeId,
type JavaScriptSemanticBindingState,
type JavaScriptSemanticAnalysisState,
type JavaScriptSemanticEffectTiming,
} from "./javascriptSemanticState.js";
interface StatementPosition {
@@ -1207,3 +1209,100 @@ export const semanticEscapeFollowsCapture = (
positionPrecedes(capturePosition, escapePosition)
);
};
/** Explicit wrapper facts and program `var` redeclarations retain their lexical identity. */
export const commonJsWrapperBindingDeclaration = (
node: t.Node,
name: string,
state: JavaScriptSemanticAnalysisState,
): JavaScriptSemanticBindingState | undefined => {
if (
!["module", "exports", "require", "__dirname", "__filename"].includes(
name,
) ||
!t.isIdentifier(node, { name })
)
return undefined;
const binding = resolveSemanticBindingState(state, node, name);
if (binding === undefined) return undefined;
const scope = state.scopesById.get(binding.scopeId);
if (
scope?.kind !== "program" ||
![...binding.declarationKinds].every((kind) => kind === "var")
)
return undefined;
// The index owns explicit wrapper parameters, including ones with no source
// redeclaration. Babel's module classification does not invalidate that fact.
if (binding.hostOrigin === "commonjs-wrapper") return binding;
// Without an explicit host fact, preserve the legacy script-var inference.
return state.program.sourceType === "script" &&
binding.declarationKinds.size > 0
? binding
: undefined;
};
/** Retained value and pattern stores share one source-ordered write boundary. */
export const semanticBindingHasPriorValueWrite = (
site: t.Node,
binding: JavaScriptSemanticBindingState,
state: JavaScriptSemanticAnalysisState,
): boolean => {
if (binding.definitions.some(({ kind }) => kind === "function")) return true;
const writes = state.bindingWrites.get(binding.bindingId) ?? [];
const read = semanticBindingAtRead(binding, site, state.parentsByNode);
const retained =
read.status === "ordered"
? new Set([
...read.binding.initializers,
...read.binding.referenceInitializers,
])
: undefined;
const representedWrites = new Set<t.Node>();
const retainedWrites = new Set<t.Node>();
for (const source of [
...binding.initializers,
...binding.referenceInitializers,
]) {
let owner: t.Node | undefined = source.node;
while (owner !== undefined && !writes.includes(owner))
owner = state.parentsByNode.get(owner);
if (owner !== undefined) {
representedWrites.add(owner);
if (retained === undefined || retained.has(source))
retainedWrites.add(owner);
} else if (
(retained === undefined || retained.has(source)) &&
semanticEffectTiming(site, source.node, state) !== "after"
)
return true;
}
return writes.some(
(write) =>
(!representedWrites.has(write) || retainedWrites.has(write)) &&
semanticEffectTiming(site, write, state) !== "after",
);
};
/** A program-level `var` redeclaration still denotes its untouched CJS wrapper parameter. */
export const isUnmodifiedCommonJsWrapperBinding = (
node: t.Node,
name: string,
state: JavaScriptSemanticAnalysisState,
): boolean => {
const binding = commonJsWrapperBindingDeclaration(node, name, state);
return (
binding !== undefined &&
!semanticBindingHasPriorValueWrite(node, binding, state)
);
};
/** Identify a global name only when no lexical or dynamic environment shadows it. */
export const isUnshadowedGlobal = (
node: t.Node,
state: JavaScriptSemanticAnalysisState,
name: string,
): boolean =>
t.isIdentifier(node, { name }) &&
(resolveSemanticBindingState(state, node, name) === undefined
? !semanticResolutionBlocked(state, node, name)
: isUnmodifiedCommonJsWrapperBinding(node, name, state));