import * as t from "@babel/types"; import type { JavaScriptSemanticArgumentFlow, JavaScriptSemanticCallable, } from "./javascriptSemanticIr.js"; import { compareUnicodeCodePoints } from "../unicodeCodePointOrder.js"; import { semanticCallableIdForNode } from "./javascriptSemanticProjection.js"; import { unwrapJavaScriptExpression } from "./javascriptAstValues.js"; import { resolveSemanticBindingState, type JavaScriptSemanticAnalysisState, type JavaScriptSemanticBindingState, } 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; } interface CallResolutionContext { readonly state: JavaScriptSemanticAnalysisState; readonly callableById: ReadonlyMap; readonly seenBindings: ReadonlySet; readonly bindingCache: Map; } /** Inputs needed to resolve one local call target. */ export interface ResolveLocalCallablesInput { readonly node: t.Node; readonly state: JavaScriptSemanticAnalysisState; readonly callableById: ReadonlyMap; readonly seenBindings: ReadonlySet; readonly bindingCache: Map; } /** Resolve local callable candidates without following dynamic properties. */ export const resolveLocalCallables = ( input: ResolveLocalCallablesInput, ): LocalCallableResolution => resolveCallables(input.node, { state: input.state, callableById: input.callableById, seenBindings: input.seenBindings, 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 binding = resolveSemanticBindingState(context.state, node, node.name); return binding === undefined ? { callableIds: [], complete: false, reason: `Unresolved call target ${node.name}.`, } : resolveBindingCallables(binding, context); }; 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.", }; }; const resolveBindingCallables = ( binding: JavaScriptSemanticBindingState, context: CallResolutionContext, ): LocalCallableResolution => { const cached = context.bindingCache.get(binding.bindingId); if (cached !== undefined) return cached; if (context.seenBindings.has(binding.bindingId)) return { callableIds: [], complete: false, reason: `Call target alias cycle at ${binding.name}.`, }; if (binding.initializers.length === 0) return missingBindingInitializer(binding); const candidateContext = { ...context, seenBindings: new Set([...context.seenBindings, binding.bindingId]), }; const candidates = binding.initializers.map(({ node, projection }) => projection.length === 0 ? resolveCallables(node, candidateContext) : { 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.`, }; if (resolution.complete) context.bindingCache.set(binding.bindingId, resolution); 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 parameterIndex = node.params.findIndex((parameter) => rangeContains(range(parameter), location), ); if (parameterIndex === argumentIndex) return true; return ( parameterIndex >= 0 && parameterIndex < argumentIndex && t.isRestElement(node.params[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);