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; readonly seenInitializers: ReadonlySet; readonly bindingCache: WeakMap; } /** Inputs needed to resolve one local call target. */ export interface ResolveLocalCallablesInput { readonly node: t.Node; readonly state: JavaScriptSemanticAnalysisState; readonly callableById: ReadonlyMap; readonly seenInitializers: ReadonlySet; readonly bindingCache: WeakMap; } /** 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);