Files
rea/src/domain/javascript/javascriptSemanticState.ts
T
9b3e59804d fix(javascript): prove namespace interop before attributing Electron APIs (#1857)
* fix(javascript): prove Electron module namespace conversion

* refactor(javascript): share AST child field metadata

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
Co-authored-by: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com>

* fix(javascript): retain destructured intrinsic writes

Share one pattern-target walker across lexical definitions, member mutation
tracking and Electron identity writes. Keep overwritten globals unresolved
without treating property keys or shadowed locals as global replacements.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
Co-authored-by: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com>

* fix(javascript): preserve intrinsic projections and fresh target identity

Resolve known mutators through static binding projections and prove stable
identifier chains to fresh literals before widening unknown mutation targets.
Keep reassigned, uninitialized and member-projected targets conservative.
Cover both review regressions through source tests and inert CLI/MCP fixtures.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
Co-authored-by: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com>

* fix(javascript): retain fresh function and class target identity

Recognize stable local function, undecorated class and RegExp targets before
widening intrinsic mutations. Keep decorated classes opaque because their
binding value can be replaced, and preserve class-initialization writes.
Cover fresh targets and replacement decorators in inert CLI/MCP fixtures.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
Co-authored-by: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com>

* 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.

* fix(javascript): respect hoisted fresh function declarations

Function declarations provide fresh targets before their textual position. Retain initialization-order checks for variable and class values, including aliases. Cover the reviewed regression with source controls and the public CLI/MCP fixture.

* fix(javascript): preserve proven interop result projections

* fix(javascript): retain stable declarations and bound intrinsic writes

Keep repeated variable declarations with one unconditional initializer while
rejecting assignments, conditional initialization and multiple values.
Normalize known intrinsic calls, aliases and bound argument prefixes at the
semantic owner, retaining opaque targets without tainting unused bindings or
fresh local objects. Cover both regressions through source and CLI/MCP tests.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
Co-authored-by: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com>

* fix(javascript): retain bound intrinsic mutation effects

* fix(javascript): preserve bound receivers for intrinsic invocation

* fix(javascript): prove generic binding and invocation effects

* fix(javascript): retain Reflect set receiver effects

---------

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
Co-authored-by: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com>
Co-authored-by: morluto <76467478+morluto@users.noreply.github.com>
2026-10-12 01:06:33 +08:00

229 lines
8.4 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.
* Repeated variable declarations without another initializer do not write it.
* 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 &&
((definition?.kind === "variable" &&
binding.definitions.every(({ kind }) => kind === "variable")) ||
(binding.definitions.length === 1 &&
(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);