fix(javascript): unify loader binding and mutation ownership

Model the CommonJS wrapper binding and query shared lexical identity, mutation, and evaluation timing at each call. Remove duplicate importer interpretation and preserve uncertainty for with scopes, prototypes, repeated writes, and opaque object escapes.

Validate generated fixtures against bounded Node oracles; no analyzed target execution is added.

Refs: #3982, #4072

Co-authored-by: Geronimo <varungore1@gmail.com>
This commit is contained in:
morlutoandGeronimo committed 2026-10-16 08:14:52 +08:00
1 parent b67baede2e
commit 1b9b725c4c
8 files changed
+1089 -295

No files matched your search

+273 -37
View File
@@ -1,4 +1,8 @@
import * as t from "@babel/types";
import {
semanticStaticPropertyKey,
unwrapJavaScriptExpression,
} from "./javascriptAstValues.js";
import type {
JavaScriptSemanticPropertyPath,
@@ -42,11 +46,29 @@ export interface JavaScriptSemanticMemberEffect {
readonly node: t.Node | undefined;
}
export type JavaScriptSemanticEffectTiming = "before" | "after" | "unknown";
export interface JavaScriptSemanticReferenceEffect {
readonly kind: "binding-write" | "member-write" | "escape" | "ambient-write";
readonly path?: JavaScriptSemanticPropertyPath;
readonly timing: JavaScriptSemanticEffectTiming;
}
/** Source-ordered effects relevant to one binding or projected member use. */
export interface JavaScriptSemanticReferenceEffectsAtSite {
readonly bindingId: string | null;
readonly resolution: JavaScriptSemanticReference["resolution"];
/** Whether alias-aware member and escape effects were collected for this source. */
readonly memberEffectsIndexed: boolean;
readonly effects: readonly JavaScriptSemanticReferenceEffect[];
}
export interface JavaScriptSemanticRequiredModuleEffect extends JavaScriptSemanticMemberEffect {
readonly kind: "write" | "escape";
}
export interface JavaScriptSemanticBindingState {
readonly hostOrigin?: "commonjs-wrapper";
readonly bindingId: string;
readonly scopeId: string;
readonly name: string;
@@ -72,6 +94,8 @@ export interface JavaScriptSemanticScopeState {
readonly location: JavaScriptSemanticScope["location"];
bindingsComplete: boolean;
readonly bindings: Map<string, JavaScriptSemanticBindingState>;
/** Object environment introduced by `with`, active only for its body. */
readonly dynamicWithStatement?: t.WithStatement;
}
/** Shared state for semantic collection and evaluation. */
@@ -89,6 +113,12 @@ export interface JavaScriptSemanticAnalysisState {
/** Scope boundaries and descendant scopes resolved on demand. */
readonly scopeByNode: WeakMap<t.Node, JavaScriptSemanticScopeState>;
readonly bindingsById: Map<string, JavaScriptSemanticBindingState>;
/** Resolved identifier writes retained at their exact source sites. */
readonly bindingWrites: Map<string, t.Node[]>;
/** Source evidence that Object.prototype may supply a `require` property. */
prototypeRequireMutationObserved: boolean | undefined;
/** Source evidence that Object.prototype may add inherited enumerable keys. */
prototypeEnumerationMayAffect: boolean | undefined;
/** Effects on a cached require exports object, independent of local projections. */
readonly requiredModuleEffects: Map<
string,
@@ -132,6 +162,12 @@ export interface JavaScriptBindingIndex {
readonly sourceType: t.Program["sourceType"];
readonly bindings: JavaScriptSemanticAnalysisState;
indexAmbientWritesSteps(): Generator<void, void>;
indexMemberMutationFactsSteps(): Generator<void, void>;
effectsForReferenceAt(
site: t.Node,
referenceNode: t.Node,
propertyPath?: JavaScriptSemanticPropertyPath,
): JavaScriptSemanticReferenceEffectsAtSite;
referencesSteps(): Generator<void, readonly JavaScriptSemanticReference[]>;
}
@@ -185,35 +221,18 @@ export const resolveSemanticBindingState = (
node: t.Node,
name: string,
): JavaScriptSemanticBindingState | undefined => {
let scope = semanticScopeForNode(state, node);
while (scope !== undefined) {
const binding = scope.bindings.get(name);
if (binding !== undefined) return binding;
if (!scope.bindingsComplete) return undefined;
scope =
scope.parentScopeId === null
? undefined
: state.scopesById.get(scope.parentScopeId);
}
return undefined;
const lookup = semanticBindingLookupAt(state, node, name);
return lookup.kind === "binding" ? lookup.binding : undefined;
};
/** Report whether an omitted binding scope blocks a trustworthy miss. */
/** Report whether an omitted or dynamic binding blocks a trustworthy miss. */
export const semanticResolutionBlocked = (
state: JavaScriptSemanticAnalysisState,
node: t.Node,
name: string,
): boolean => {
let scope = semanticScopeForNode(state, node);
while (scope !== undefined) {
if (scope.bindings.has(name)) return false;
if (!scope.bindingsComplete) return true;
scope =
scope.parentScopeId === null
? undefined
: state.scopesById.get(scope.parentScopeId);
}
return false;
const lookup = semanticBindingLookupAt(state, node, name);
return lookup.kind !== "binding" && lookup.kind !== "ambient";
};
const semanticScopeForNode = (
@@ -238,25 +257,232 @@ const semanticScopeForNode = (
return undefined;
};
/** Resolve one name starting from an explicitly selected lexical scope. */
type SemanticBindingLookup =
| {
readonly kind: "binding" | "uncertain-binding";
readonly binding: JavaScriptSemanticBindingState;
}
| { readonly kind: "ambient" | "uncertain-ambient" | "captured" | "unknown" };
/** Retain both a dynamic lookup's uncertainty and its possible outer write owner. */
export const semanticBindingLookupFromScope = (
scope: JavaScriptSemanticScopeState | undefined,
name: string,
state: JavaScriptSemanticAnalysisState,
): SemanticBindingLookup => {
let uncertain = false;
while (scope !== undefined) {
const binding = scope.bindings.get(name);
if (binding !== undefined)
return { kind: uncertain ? "uncertain-binding" : "binding", binding };
if (!scope.bindingsComplete) {
if (scope.dynamicWithStatement === undefined) return { kind: "unknown" };
const lookup = semanticWithRequireLookup(
state,
scope.dynamicWithStatement,
name,
);
if (lookup === "captures") return { kind: "captured" };
if (lookup === "unknown") uncertain = true;
}
scope =
scope.parentScopeId === null
? undefined
: state.scopesById.get(scope.parentScopeId);
}
return { kind: uncertain ? "uncertain-ambient" : "ambient" };
};
/** Return certain and possible binding ownership without discarding uncertainty. */
export const semanticBindingLookupAt = (
state: JavaScriptSemanticAnalysisState,
node: t.Node,
name: string,
): SemanticBindingLookup =>
semanticBindingLookupFromScope(
semanticScopeForNode(state, node),
name,
state,
);
/** Resolve a certain lexical binding from an explicitly selected scope. */
export const resolveSemanticBindingFromScope = (
scope: JavaScriptSemanticScopeState,
name: string,
state: JavaScriptSemanticAnalysisState,
): JavaScriptSemanticBindingState | undefined => {
let candidate: JavaScriptSemanticScopeState | undefined = scope;
while (candidate !== undefined) {
const binding = candidate.bindings.get(name);
if (binding !== undefined) return binding;
if (!candidate.bindingsComplete) return undefined;
candidate =
candidate.parentScopeId === null
? undefined
: state.scopesById.get(candidate.parentScopeId);
const lookup = semanticBindingLookupFromScope(scope, name, state);
return lookup.kind === "binding" ? lookup.binding : undefined;
};
type SemanticWithRequireLookup = "captures" | "skips" | "unknown";
const semanticUnscopablesKey = Symbol("semantic-unscopables-key");
/** Classify only source-visible own `require` entries in a literal with object. */
const semanticWithRequireLookup = (
state: JavaScriptSemanticAnalysisState,
statement: t.WithStatement,
name: string,
): SemanticWithRequireLookup => {
if (name !== "require") return "unknown";
const object = unwrapJavaScriptExpression(statement.object).node;
if (!t.isObjectExpression(object)) return "unknown";
let ownRequire = false;
let hasUnknownOwnProperties = false;
let prototypeIsNull = false;
let unknownPrototype = false;
for (const property of object.properties) {
if (t.isSpreadElement(property)) {
hasUnknownOwnProperties = true;
continue;
}
const key = semanticWithPropertyName(
property.key,
property.computed,
state,
);
if (key === "require") ownRequire = true;
else if (
key === "__proto__" &&
t.isObjectProperty(property) &&
!property.computed &&
!property.shorthand
) {
const prototype = unwrapJavaScriptExpression(property.value).node;
if (t.isNullLiteral(prototype)) prototypeIsNull = true;
else unknownPrototype = true;
} else if (key === null && property.computed) {
hasUnknownOwnProperties = true;
}
}
if (ownRequire) {
const unscopablesValue = finalSemanticWithSymbolProperty(object, state);
if (unscopablesValue === "unknown") return "unknown";
if (unscopablesValue === undefined)
return prototypeIsNull ||
(!unknownPrototype && state.prototypeRequireMutationObserved === false)
? "captures"
: "unknown";
const unscopables = unwrapJavaScriptExpression(unscopablesValue).node;
if (!t.isObjectExpression(unscopables))
return isSemanticWithPrimitive(unscopables) ? "captures" : "unknown";
const entry = finalSemanticWithStringProperty(unscopables, "require");
if (entry === "unknown") return "unknown";
if (entry === undefined) {
const prototype = semanticWithPrototypeKind(unscopables);
return prototype === "null" ||
(prototype === "ordinary" &&
state.prototypeRequireMutationObserved === false)
? "captures"
: "unknown";
}
const truthiness = semanticWithStaticTruthiness(entry);
return truthiness === true
? "skips"
: truthiness === false
? "captures"
: "unknown";
}
if (hasUnknownOwnProperties || unknownPrototype) return "unknown";
if (prototypeIsNull) return "skips";
return state.prototypeRequireMutationObserved === false ? "skips" : "unknown";
};
const semanticWithPropertyName = (
key: t.Node,
computed: boolean,
state: JavaScriptSemanticAnalysisState,
): string | typeof semanticUnscopablesKey | null => {
if (
computed &&
t.isMemberExpression(key) &&
!key.computed &&
t.isIdentifier(key.object, { name: "Symbol" }) &&
semanticStaticPropertyKey(key.property, false) === "unscopables" &&
resolveSemanticBindingState(state, key.object, "Symbol") === undefined &&
!semanticResolutionBlocked(state, key.object, "Symbol")
)
return semanticUnscopablesKey;
return semanticStaticPropertyKey(key, computed);
};
const finalSemanticWithSymbolProperty = (
object: t.ObjectExpression,
state: JavaScriptSemanticAnalysisState,
): t.Node | undefined | "unknown" => {
for (let index = object.properties.length - 1; index >= 0; index -= 1) {
const property = object.properties[index];
if (property === undefined || t.isSpreadElement(property)) return "unknown";
const name = semanticWithPropertyName(
property.key,
property.computed,
state,
);
if (name === semanticUnscopablesKey)
return t.isObjectProperty(property) ? property.value : "unknown";
if (name === null && property.computed) return "unknown";
}
return undefined;
};
const semanticWithPrototypeKind = (
object: t.ObjectExpression,
): "null" | "ordinary" | "other" => {
for (const property of object.properties) {
if (
t.isObjectProperty(property) &&
!property.computed &&
!property.shorthand &&
t.isIdentifier(property.key, { name: "__proto__" })
)
return t.isNullLiteral(unwrapJavaScriptExpression(property.value).node)
? "null"
: "other";
}
return "ordinary";
};
const finalSemanticWithStringProperty = (
object: t.ObjectExpression,
sought: string,
): t.Node | undefined | "unknown" => {
for (let index = object.properties.length - 1; index >= 0; index -= 1) {
const property = object.properties[index];
if (property === undefined || t.isSpreadElement(property)) return "unknown";
const name = semanticStaticPropertyKey(property.key, property.computed);
if (name === sought)
return t.isObjectProperty(property) ? property.value : "unknown";
if (name === null && property.computed) return "unknown";
}
return undefined;
};
const semanticWithStaticTruthiness = (node: t.Node): boolean | undefined => {
const value = unwrapJavaScriptExpression(node).node;
if (t.isBooleanLiteral(value)) return value.value;
if (t.isNullLiteral(value)) return false;
if (t.isStringLiteral(value)) return value.value.length > 0;
if (t.isNumericLiteral(value))
return value.value !== 0 && !Number.isNaN(value.value);
if (
t.isObjectExpression(value) ||
t.isArrayExpression(value) ||
t.isFunctionExpression(value) ||
t.isArrowFunctionExpression(value)
)
return true;
return undefined;
};
const isSemanticWithPrimitive = (node: t.Node): boolean =>
t.isNullLiteral(node) ||
t.isBooleanLiteral(node) ||
t.isStringLiteral(node) ||
t.isNumericLiteral(node) ||
t.isBigIntLiteral(node) ||
(t.isUnaryExpression(node, { operator: "-" }) &&
(t.isNumericLiteral(node.argument) || t.isBigIntLiteral(node.argument)));
/** Select the function/program owner for a var declaration. */
export const semanticVariableScope = (
scope: JavaScriptSemanticScopeState,
@@ -294,6 +520,16 @@ export const isUnmodifiedCommonJsWrapperBinding = (
name: string,
state: JavaScriptSemanticAnalysisState,
): boolean => {
const binding = commonJsWrapperBindingDeclaration(node, name, state);
return binding !== undefined && binding.initializers.length === 0;
};
/** Program `var` redeclarations retain wrapper identity until a source write. */
export const commonJsWrapperBindingDeclaration = (
node: t.Node,
name: string,
state: JavaScriptSemanticAnalysisState,
): JavaScriptSemanticBindingState | undefined => {
// Parser sourceType separates explicit modules from script/CommonJS source;
// specifier resolution mode does not establish that distinction.
if (
@@ -301,13 +537,13 @@ export const isUnmodifiedCommonJsWrapperBinding = (
!["module", "exports", "require"].includes(name) ||
!t.isIdentifier(node, { name })
)
return false;
return undefined;
const binding = resolveSemanticBindingState(state, node, name);
if (binding === undefined || binding.initializers.length !== 0) return false;
if (binding === undefined) return undefined;
const scope = state.scopesById.get(binding.scopeId);
return (
scope?.kind === "program" &&
return scope?.kind === "program" &&
binding.declarationKinds.size > 0 &&
[...binding.declarationKinds].every((kind) => kind === "var")
);
? binding
: undefined;
};