Files
rea/src/domain/javascript/javascriptSemanticCallResolution.ts
T
Ysopwnable 42fc6c53ae fix(javascript): preserve incoming callback target uncertainty (#4243)
Preserve incoming callback uncertainty and resolve proven preceding writes.
Share read-position and initializer identity facts with returned-reference
analysis; cache completed projected work with actual cycle dependencies.

Fixes #3880
2026-10-16 10:09:22 +08:00

262 lines
9.0 KiB
TypeScript

import * as t from "@babel/types";
import type {
JavaScriptSemanticArgumentFlow,
JavaScriptSemanticCallable,
} from "./javascriptSemanticIr.js";
import { compareUnicodeCodePoints } from "../unicodeCodePointOrder.js";
import { semanticCallableIdForNode } from "./javascriptSemanticProjection.js";
import { semanticBindingAtRead } from "./javascriptSemanticMutationInitializers.js";
import {
unwrapJavaScriptExpression,
runtimeFunctionParameters,
} from "./javascriptAstValues.js";
import {
resolveSemanticBindingState,
semanticStableInitializer,
type JavaScriptSemanticAnalysisState,
type JavaScriptSemanticBindingState,
type JavaScriptSemanticInitializer,
} from "./javascriptSemanticState.js";
import { range } from "./javascriptStaticAnalysisHelpers.js";
/** Resolution of one call target within the currently analyzed source. */
export interface LocalCallableResolution {
readonly callableIds: readonly string[];
readonly complete: boolean;
readonly reason: string;
/** A cycle cut depends on the active alias path and cannot be memoized. */
readonly cycleDependent?: true;
}
interface CallResolutionContext {
readonly state: JavaScriptSemanticAnalysisState;
readonly callableById: ReadonlyMap<string, JavaScriptSemanticCallable>;
readonly seenInitializers: ReadonlySet<JavaScriptSemanticInitializer>;
readonly bindingCache: WeakMap<t.Identifier, LocalCallableResolution>;
}
/** Inputs needed to resolve one local call target. */
export interface ResolveLocalCallablesInput {
readonly node: t.Node;
readonly state: JavaScriptSemanticAnalysisState;
readonly callableById: ReadonlyMap<string, JavaScriptSemanticCallable>;
readonly seenInitializers: ReadonlySet<JavaScriptSemanticInitializer>;
readonly bindingCache: WeakMap<t.Identifier, LocalCallableResolution>;
}
/** Resolve local callable candidates without following dynamic properties. */
export const resolveLocalCallables = (
input: ResolveLocalCallablesInput,
): LocalCallableResolution =>
resolveCallables(input.node, {
state: input.state,
callableById: input.callableById,
seenInitializers: input.seenInitializers,
bindingCache: input.bindingCache,
});
const resolveCallables = (
node: t.Node,
context: CallResolutionContext,
): LocalCallableResolution => {
// `unwrapJavaScriptExpression` strips all transparent layers iteratively,
// so a single recursion step suffices (no per-layer stack growth).
const unwrapped = unwrapJavaScriptExpression(node).node;
if (unwrapped !== node) return resolveCallables(unwrapped, context);
const direct = semanticCallableIdForNode(node);
if (direct !== null && context.callableById.has(direct))
return {
callableIds: [direct],
complete: true,
reason: "Direct local callable.",
};
if (t.isIdentifier(node)) return resolveIdentifier(node, context);
if (t.isConditionalExpression(node) || t.isLogicalExpression(node))
return resolveAlternatives(
t.isConditionalExpression(node) ? node.consequent : node.left,
t.isConditionalExpression(node) ? node.alternate : node.right,
context,
);
return {
callableIds: [],
complete: false,
reason: `Unsupported ${node.type} call target.`,
};
};
const resolveIdentifier = (
node: t.Identifier,
context: CallResolutionContext,
): LocalCallableResolution => {
const cached = context.bindingCache.get(node);
if (cached !== undefined) return cached;
const binding = resolveSemanticBindingState(context.state, node, node.name);
if (binding === undefined)
return {
callableIds: [],
complete: false,
reason: `Unresolved call target ${node.name}.`,
};
const read = semanticBindingAtRead(
binding,
node,
context.state.parentsByNode,
);
const candidates = resolveBindingCallables(read.binding, context);
// Knowing every local candidate does not prove that any write replaced the
// incoming value, or that a write in another activation precedes this read.
const covered =
read.hasDefiniteInitializer ||
semanticStableInitializer(binding, context.state) !== undefined;
const resolution =
candidates.complete && !covered
? {
...candidates,
complete: false,
reason: `Call target ${binding.name} may retain an incoming value or an unordered assignment.`,
}
: candidates;
// Key by the immutable read, not binding identity: before/after snapshots
// differ. Cache cycle-independent partial results too, keeping alias
// diamonds linear even when their shared leaf has an unknown incoming value.
if (!resolution.cycleDependent) context.bindingCache.set(node, resolution);
return resolution;
};
const resolveAlternatives = (
left: t.Node,
right: t.Node,
context: CallResolutionContext,
): LocalCallableResolution => {
const candidates = [left, right].map((candidate) =>
resolveCallables(candidate, context),
);
return {
callableIds: uniqueCallableIds(
candidates.flatMap(({ callableIds }) => callableIds),
),
complete: candidates.every(({ complete }) => complete),
reason: "Conditional or logical call target has multiple candidates.",
...(candidates.some((candidate) => candidate.cycleDependent)
? { cycleDependent: true as const }
: {}),
};
};
const resolveBindingCallables = (
binding: JavaScriptSemanticBindingState,
context: CallResolutionContext,
): LocalCallableResolution => {
if (binding.initializers.length === 0)
return missingBindingInitializer(binding);
const candidates = binding.initializers.map(
(initializer): LocalCallableResolution => {
if (context.seenInitializers.has(initializer))
return {
callableIds: [],
complete: false,
cycleDependent: true,
reason: `Call target initializer cycle at ${binding.name}.`,
};
return initializer.projection.length === 0
? resolveCallables(initializer.node, {
...context,
seenInitializers: new Set([
...context.seenInitializers,
initializer,
]),
})
: {
callableIds: [],
complete: false,
reason: `Projected call target ${binding.name} is unresolved.`,
};
},
);
const callableIds = uniqueCallableIds(
candidates.flatMap((candidate) => candidate.callableIds),
);
const resolution = {
callableIds,
complete: candidates.every(({ complete }) => complete),
reason:
callableIds.length === 1 && candidates.every(({ complete }) => complete)
? "Unique local callable target."
: `Call target ${binding.name} has ambiguous or unsupported assignments.`,
...(candidates.some((candidate) => candidate.cycleDependent)
? { cycleDependent: true as const }
: {}),
};
return resolution;
};
const missingBindingInitializer = (
binding: JavaScriptSemanticBindingState,
): LocalCallableResolution => ({
callableIds: [],
complete: false,
reason:
binding.kind === "import"
? `Call target ${binding.name} is imported from outside this source.`
: `Call target ${binding.name} has no local initializer.`,
});
/** Find all bindings introduced by one positional parameter pattern. */
export const parameterBindings = (
callable: JavaScriptSemanticCallable,
index: number,
state: JavaScriptSemanticAnalysisState,
): JavaScriptSemanticBindingState[] => {
if (callable.bodyScopeId === null) return [];
const scope = state.scopesById.get(callable.bodyScopeId);
if (scope === undefined) return [];
return [...scope.bindings.values()]
.filter(
(binding) =>
binding.kind === "parameter" &&
binding.definitions.some(
({ kind, location }) =>
kind === "parameter" &&
parameterAcceptsArgument(callable, location, index, state),
),
)
.sort((left, right) =>
compareUnicodeCodePoints(left.bindingId, right.bindingId),
);
};
const parameterAcceptsArgument = (
callable: JavaScriptSemanticCallable,
location: JavaScriptSemanticArgumentFlow["parameterLocation"],
argumentIndex: number,
state: JavaScriptSemanticAnalysisState,
): boolean => {
const node = state.callableNodesById.get(callable.callableId);
if (!t.isFunction(node)) return false;
const parameters = runtimeFunctionParameters(node);
const parameterIndex = parameters.findIndex((parameter) =>
rangeContains(range(parameter), location),
);
if (parameterIndex === argumentIndex) return true;
return (
parameterIndex >= 0 &&
parameterIndex < argumentIndex &&
t.isRestElement(parameters[parameterIndex])
);
};
const rangeContains = (
outer: JavaScriptSemanticArgumentFlow["parameterLocation"],
inner: JavaScriptSemanticArgumentFlow["parameterLocation"],
): boolean =>
(outer.start.line < inner.start.line ||
(outer.start.line === inner.start.line &&
outer.start.column <= inner.start.column)) &&
(outer.end.line > inner.end.line ||
(outer.end.line === inner.end.line &&
outer.end.column >= inner.end.column));
const uniqueCallableIds = (values: readonly string[]): string[] =>
[...new Set(values)].sort(compareUnicodeCodePoints);