Files
rea/src/domain/javascript/javascriptSemanticState.ts
T
morluto 548a4334c4 fix(javascript): preserve intrinsic alias capture timing
Validate each alias read after initialization and retain the original function capture across copies. Share declaration initializer facts with RPC and Electron analysis, including class declarations used for fresh-target identity. Accept single-statement control blocks while keeping lexical roles and added operations checked. Cover the capture regressions through inert source and public CLI/MCP workflows.
2026-10-11 23:53:55 +08:00

228 lines
8.2 KiB
TypeScript

import * as t from "@babel/types";
import type { JavaScriptSemanticPropertyPath } from "./javascriptSemanticPropertyPaths.js";
import type {
JavaScriptModuleOrigin,
JavaScriptSemanticDefinition,
JavaScriptSemanticModuleLink,
JavaScriptSemanticCallable,
JavaScriptSemanticScope,
} from "./javascriptSemanticIr.js";
/** Internal initializer plus a destructuring/member projection. */
interface JavaScriptSemanticInitializer {
readonly node: t.Node;
readonly projection: readonly (string | number | null)[];
readonly entryBody?: t.BlockStatement;
}
/** Additional references from destructuring copies and possible defaults. */
interface JavaScriptSemanticReferenceInitializer extends JavaScriptSemanticInitializer {
readonly copyKind?: "object-rest" | "array-rest";
readonly copyProjectionOffset?: number;
readonly copyExcludedKeys?: readonly string[];
readonly copyStartIndex?: number;
readonly fallbackSources?: readonly JavaScriptSemanticInitializer[];
readonly requiredSources?: readonly JavaScriptSemanticInitializer[];
}
/** Mutable binding state used only while constructing the immutable IR. */
export interface JavaScriptSemanticBindingState {
readonly bindingId: string;
readonly scopeId: string;
readonly name: string;
kind: JavaScriptSemanticDefinition["kind"];
mutable: boolean;
readonly mutatedPaths: JavaScriptSemanticPropertyPath[];
readonly escapedPaths: {
readonly path: JavaScriptSemanticPropertyPath;
readonly node: t.Node | undefined;
}[];
readonly definitions: JavaScriptSemanticDefinition[];
readonly initializers: JavaScriptSemanticInitializer[];
readonly referenceInitializers: JavaScriptSemanticReferenceInitializer[];
readonly directOrigins: JavaScriptModuleOrigin[];
}
/** Mutable lexical-scope state used only during AST recovery. */
export interface JavaScriptSemanticScopeState {
readonly scopeId: string;
readonly parentScopeId: string | null;
readonly kind: JavaScriptSemanticScope["kind"];
readonly location: JavaScriptSemanticScope["location"];
bindingsComplete: boolean;
readonly bindings: Map<string, JavaScriptSemanticBindingState>;
}
/** Shared state for semantic collection and evaluation. */
export interface JavaScriptSemanticAnalysisState {
readonly parentsByNode: WeakMap<t.Node, t.Node>;
readonly scopes: JavaScriptSemanticScopeState[];
readonly scopesById: Map<string, JavaScriptSemanticScopeState>;
/** Scope boundaries and descendant scopes resolved on demand. */
readonly scopeByNode: WeakMap<t.Node, JavaScriptSemanticScopeState>;
readonly bindingsById: Map<string, JavaScriptSemanticBindingState>;
readonly callables: JavaScriptSemanticCallable[];
readonly callableNodesById: Map<string, t.Node>;
readonly moduleLinks: JavaScriptSemanticModuleLink[];
/** Exact calls already represented by binding or dependency links. */
readonly linkedRequireCalls: WeakSet<t.CallExpression>;
readonly moduleLinkBindings: WeakMap<
JavaScriptSemanticModuleLink,
JavaScriptSemanticBindingState
>;
readonly conditionalInitializers: WeakSet<t.Node>;
}
/**
* Return one unconditional source initializer for a declared lexical binding.
* This establishes a stable source fact only; it does not prove immutability or
* that the declaration has executed at a particular program point.
*/
export const semanticStableInitializer = (
binding: JavaScriptSemanticBindingState | undefined,
state: JavaScriptSemanticAnalysisState,
): JavaScriptSemanticBindingState["initializers"][number] | undefined => {
const [initializer] = binding?.initializers ?? [];
const [definition] = binding?.definitions ?? [];
return binding !== undefined &&
binding.definitions.length === 1 &&
(definition?.kind === "variable" ||
definition?.kind === "function" ||
definition?.kind === "class") &&
binding.initializers.length === 1 &&
initializer !== undefined &&
!state.conditionalInitializers.has(initializer.node)
? initializer
: undefined;
};
/** Return the active scope from a non-empty construction stack. */
export const currentSemanticScope = (
stack: readonly JavaScriptSemanticScopeState[],
): JavaScriptSemanticScopeState => {
const scope = stack.at(-1);
if (scope === undefined) throw new TypeError("Missing semantic scope");
return scope;
};
/** Create a stable source-position identity for one lexical scope. */
export const semanticScopeId = (
kind: JavaScriptSemanticScope["kind"],
node: t.Node,
): string =>
`scope:${kind}:${String(node.start ?? -1)}:${String(node.end ?? -1)}`;
/** Create the canonical identity used by one binding in a lexical scope. */
export const semanticBindingId = (scopeId: string, name: string): string =>
`${scopeId}:binding:${encodeURIComponent(name)}`;
/** Resolve one lexical name from the scope containing a node. */
export const resolveSemanticBindingState = (
state: JavaScriptSemanticAnalysisState,
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;
};
/** Report whether an omitted binding scope 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 semanticScopeForNode = (
state: JavaScriptSemanticAnalysisState,
node: t.Node,
): JavaScriptSemanticScopeState | undefined => {
let candidate: t.Node | undefined = node;
while (candidate !== undefined) {
const scope = state.scopeByNode.get(candidate);
if (scope !== undefined) {
// Repeated references in a deep expression share this ancestry. Cache
// the traversed path without eagerly indexing nodes never resolved.
let descendant: t.Node | undefined = node;
while (descendant !== undefined && descendant !== candidate) {
state.scopeByNode.set(descendant, scope);
descendant = state.parentsByNode.get(descendant);
}
return scope;
}
candidate = state.parentsByNode.get(candidate);
}
return undefined;
};
/** Resolve one name starting from an explicitly selected lexical 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);
}
return undefined;
};
/** Select the function/program owner for a var declaration. */
export const semanticVariableScope = (
scope: JavaScriptSemanticScopeState,
parent: t.Node | null,
state: JavaScriptSemanticAnalysisState,
): JavaScriptSemanticScopeState => {
if (!(parent !== null && t.isVariableDeclaration(parent, { kind: "var" })))
return scope;
let candidate = scope;
while (candidate.kind === "block" || candidate.kind === "catch") {
const outer =
candidate.parentScopeId === null
? undefined
: state.scopesById.get(candidate.parentScopeId);
if (outer === undefined) break;
candidate = outer;
}
return candidate;
};
/** 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);