Migrating from the Legacy Resolution API¶
The Analysis API resolution surface was redesigned as part of KT-66039. This guide maps the old API to the new one. If you are not yet familiar with the new shape, read Resolution Fundamentals first — this guide assumes you know the old API.
Tip
Snippets on this page assume an analyze { } block (or context(_: KaSession)) is in scope and that
@OptIn(KaExperimentalApi::class) is applied somewhere up the call chain. Snippets that name the
KtResolvable / KtResolvableCall marker interfaces additionally require @OptIn(KtExperimentalApi::class),
since those interfaces themselves are annotated @KtExperimentalApi.
Why the API changed¶
Five user-facing pains motivated the redesign:
KtReferencewas a PSI/IDE infrastructure leak. It belongs to IntelliJ infrastructure, yet the Analysis API required reaching forelement.mainReference.resolveTo...(), smuggling a syntax-layer concept into the semantic contract.- Discoverability suffered. To resolve via the Analysis API, you first had to know to call
.mainReferenceon aKtElementand then invokeresolveTo...()on the resultingKtReference. Nothing about the API surface advertised this idiom, so callers who did not already know it fell back to the more familiarPsiReference.resolve()instead. - Result types were under-specified.
resolveToSymbol()returnedKaSymbol?everywhere;resolveToCall()returned aKaCallInfo?over a genericKaCall. Callers had toas?-cast to anything specific. KaPartiallyAppliedSymbolwas a useless wrapper. Receivers, signature, and type-argument mapping had to be unwrapped through an extra layer. The newKaSimpleCallexposes all of this inline; the new compound calls expose named sub-call accessors (variableCall,getterCall,setterCall,operationCall).KtElement.resolveToCallaccepted anyKtElement. Nothing in the type told you whether resolution would succeed or what it would return.KtReferencefilled the gap for everythingresolveToCallcould not handle — so callers had to maintain two parallel resolution mechanisms.
The new API replaces both KtReference-based resolution and KtElement.resolveToCall with a single, type-driven
surface: specialized resolveSuccessfulSymbol / resolveSuccessfulCall methods on concrete PSI types, plus the marker
interfaces KtResolvable and KtResolvableCall for the generic case.
Migration rules¶
Two rules cover almost all sites:
- Use the specialized method on the concrete PSI type whenever possible. If you know the element is a
KtCallElement, writecallElement.resolveSuccessfulCall()— you get back aKaFunctionCall<*>?directly. Each specialization narrows the return type, so type checks and casts you used to write disappear. - When the PSI type is genuinely unknown, narrow with a safe
as?cast. To check whether the element implementsKtResolvableorKtResolvableCall.
// Specialized form (preferred)
val call: KaFunctionCall<*>? = callElement.resolveSuccessfulCall()
// Generic form (only when the element type is unknown)
val call: KaSimpleOrMultiCall? =
(element as? KtResolvableCall)?.resolveSuccessfulCall()
Old → new matrix¶
Result wrapper: KaCallInfo → KaCallResolutionAttempt¶
resolveToCall() returned a KaCallInfo; tryResolveCall() returns a KaCallResolutionAttempt. The new one carries
KaSimpleCall<*, *> payloads instead of bare KaCall, and splits simple-call attempts from multi-call ones
(compound / for / delegated property).
Warning
The hierarchies do not line up one-to-one on the failure side. KaErrorCallInfo covered every failure, while its
counterpart KaSimpleCallResolutionError is a leaf under KaSimpleCallResolutionAttempt: a failed
KaMultiCallResolutionAttempt is not an instance of it. So a literal translation of
as? KaErrorCallInfo / is KaErrorCallInfo silently stops reporting failures of compound calls.
Use attempt.isSuccessful and attempt.errors, which answer for every attempt kind:
Old (KaCallInfo) |
New (KaCallResolutionAttempt) |
|---|---|
KaCallInfo |
KaCallResolutionAttempt (KaSimpleCallResolutionAttempt / KaMultiCallResolutionAttempt) |
KaSuccessCallInfo |
KaSimpleCallResolutionSuccess |
KaSuccessCallInfo.call: KaCall |
KaSimpleCallResolutionSuccess.call: KaSimpleCall<*, *> (or the attempt.successful: KaSimpleOrMultiCall? extension) |
KaErrorCallInfo |
KaSimpleCallResolutionError for a simple call; a failed KaMultiCallResolutionAttempt for a compound one. attempt.errors covers both |
KaErrorCallInfo.candidateCalls: List<KaCall> |
KaSimpleCallResolutionError.candidateCalls: List<KaSimpleCall<*, *>> |
KaErrorCallInfo.diagnostic |
KaSimpleCallResolutionError.diagnostic |
KaCallInfo.calls: List<KaCall> |
KaCallResolutionAttempt.calls: List<KaSimpleOrMultiCall> |
KaCallInfo.singleCallOrNull<T>() / successfulCallOrNull<T>() |
attempt.single / attempt.successful, narrowed with .simple / .function / .variable / .constructor |
Multi-call attempts (KaForLoopCallResolutionAttempt, KaDelegatedPropertyCallResolutionAttempt,
KaCompound*CallResolutionAttempt) expose call: KaMultiCall? plus per-step
simpleAttempts: List<KaSimpleCallResolutionAttempt>; the old KaCallInfo had no equivalent — these calls were
resolved indirectly.
Call hierarchy: KaCall → KaSimpleOrMultiCall¶
A successful resolution used to hand back a KaCall; now it hands back a KaSimpleOrMultiCall. KaFunctionCall and
KaVariableAccessCall keep their names but are now KaSimpleCalls (so their data is inline — see below);
compound, for-loop, and delegated-property calls are KaMultiCalls.
Old (KaCall) |
New (KaSimpleOrMultiCall) |
|---|---|
KaCall |
KaSimpleOrMultiCall |
KaCallableMemberCall<S, C> |
KaSimpleCall<S, C> (its partiallyAppliedSymbol.* members are now inline — see below) |
KaCompoundVariableAccessCall / KaCompoundArrayAccessCall |
same names, now KaMultiCalls (.calls: List<KaSimpleCall<*, *>>) |
(no first-class type) — for-loop / delegated property |
KaForLoopCall / KaDelegatedPropertyCall (KaMultiCalls) |
| (no first-class type) — callable references | KaCallableReferenceCall (KaSimpleCall) |
Candidate hierarchy: KaCallCandidateInfo → KaCallCandidate¶
collectCallCandidates() (was resolveToCallCandidates()) returns KaCallCandidates instead of KaCallCandidateInfo
s. The hierarchy is identical; only the names change and candidate widens from KaCall to KaSimpleOrMultiCall.
Old (KaCallCandidateInfo) |
New (KaCallCandidate) |
|---|---|
KaCallCandidateInfo |
KaCallCandidate |
KaCallCandidateInfo.candidate: KaCall |
KaCallCandidate.candidate: KaSimpleOrMultiCall |
KaCallCandidateInfo.isInBestCandidates |
KaCallCandidate.isInBestCandidates (unchanged) |
KaApplicableCallCandidateInfo |
KaApplicableCallCandidate |
KaInapplicableCallCandidateInfo |
KaInapplicableCallCandidate |
KaInapplicableCallCandidateInfo.diagnostic |
KaInapplicableCallCandidate.diagnostic |
Calls¶
| Old call | New equivalent |
|---|---|
KtElement.resolveToCall(): KaCallInfo? |
KtResolvableCall.tryResolveCall(): KaCallResolutionAttempt?, but resolveSuccessfulCall() is preferred and more convenient in most cases. |
KaCallInfo.{successfulFunctionCallOrNull, successfulVariableAccessCall, successfulConstructorCallOrNull}() |
attempt.successful?.function / ?.variable / ?.constructor, but in most cases KaCallResolutionAttempt is not needed. Call resolveSuccessfulCall() directly. |
KaCallInfo.singleCallOrNull<T>() / singleFunctionCallOrNull() / ... |
attempt.single, narrowed with .simple / .function / .variable / .constructor. |
KtElement.resolveToCallCandidates(): List<KaCallCandidateInfo> |
KtResolvableCall.collectCallCandidates(): List<KaCallCandidate>. |
Tip
resolveSuccessfulCall() vs tryResolveCall() vs collectCallCandidates(). resolveSuccessfulCall() returns
only the successfully resolved (most-specific) call, or null. To inspect the candidate calls of an unresolved or
ambiguous call (the old KaCallInfo.calls), use tryResolveCall()?.calls. To inspect all overload-resolution
candidates (the old resolveToCallCandidates()), use collectCallCandidates().
Tip
The result of tryResolveCall and resolveToCall are equivalent in all cases (tryResolveCall may carry
more detailed diagnostics on an error call).
Inline access on a KaSimpleCall¶
Every *PartiallyAppliedSymbol access becomes a direct field on the call.
Old call (legacy member on KaCallableMemberCall) |
New equivalent (inline on KaSimpleCall) |
|---|---|
call.partiallyAppliedSymbol.signature |
call.signature |
call.partiallyAppliedSymbol.symbol |
call.symbol |
call.partiallyAppliedSymbol.dispatchReceiver |
call.dispatchReceiver |
call.partiallyAppliedSymbol.extensionReceiver |
call.extensionReceiver |
call.partiallyAppliedSymbol.contextArguments |
call.contextArguments |
Subtypes you may have used¶
| Legacy type / member | Replacement |
|---|---|
KaSimpleFunctionCall |
KaFunctionCall<*> |
KaSimpleFunctionCall.isImplicitInvoke |
call is KaImplicitInvokeCall |
KaSimpleVariableAccessCall |
KaVariableAccessCall |
KaSimpleVariableAccess / KaSimpleVariableAccess.Read / .Write |
KaVariableAccessCall.Kind / KaVariableAccessCall.Kind.Read / KaVariableAccessCall.Kind.Write |
Compound calls (+=, ++, --, a[i] += v, by, for)¶
| Old call | New equivalent |
|---|---|
KaCompoundVariableAccessCall.variablePartiallyAppliedSymbol |
variableCall: KaVariableAccessCall |
KaCompoundArrayAccessCall.getPartiallyAppliedSymbol |
getterCall: KaFunctionCall<KaNamedFunctionSymbol> |
KaCompoundArrayAccessCall.setPartiallyAppliedSymbol |
setterCall: KaFunctionCall<KaNamedFunctionSymbol> |
KaCompoundOperation.operationPartiallyAppliedSymbol |
operationCall: KaFunctionCall<KaNamedFunctionSymbol> (also exposed directly on KaCompoundAccessCall.operationCall). |
For-loops and delegated properties did not have a separate API in the old world; they were either resolved indirectly or
required custom code. The new API exposes them as first-class multi-calls:
KaForLoopCall and KaDelegatedPropertyCall, with matching attempt types KaForLoopCallResolutionAttempt
and KaDelegatedPropertyCallResolutionAttempt.
Symbol resolution¶
Emulating mainReference.resolveToSymbols()¶
For navigation, find-usages, and similar features, the old mainReference.resolveToSymbols() returned every symbol the
compiler considered — a kitchen-sink that included results from both call resolution (precise targets) and plain
symbol resolution. The new API does not bundle these two into a single call, but the recipe is short:
@OptIn(KaExperimentalApi::class, KtExperimentalApi::class)
context(session: KaSession)
fun resolveToSymbols(element: KtElement): Collection<KaSymbol> {
// 1. Prefer symbols extracted from a resolved call.
// Call resolution is more precise: a constructor call,
// for instance, points at the constructor, not the class.
//
// `KtOperationReferenceExpression` is excluded on purpose. Its call resolution
// forwards to the parent expression and would also surface intermediate reads
// (e.g. the `get` of a compound `a[i] += v`), whereas the legacy
// `resolveToSymbols()` returned only the symbols the operator itself contributes
// (`plus`, `set`). Skipping the call step lets it fall through to
// `tryResolveSymbols()` below, which preserves that behavior.
val callSymbols = (element as? KtResolvableCall)?.takeUnless { it is KtOperationReferenceExpression }
?.tryResolveCall()
?.calls
?.flatMap(KaSimpleOrMultiCall::symbols)
?.takeUnless(List<KaSymbol>::isEmpty)
if (callSymbols != null) return callSymbols
// 2. Fall back to plain symbol resolution.
return (element as? KtResolvable)
?.tryResolveSymbols()
?.symbols
.orEmpty()
}
This pattern is the formal way to migrate a mainReference.resolveToSymbols() call site that needs the broadest
possible result.
| Old call | New equivalent |
|---|---|
expr.mainReference.resolveToSymbol() |
expr.resolveSuccessfulSymbol() – specialized form on the concrete PSI type. KtResolvable.resolveSuccessfulSymbol(): KaSymbol? – generic fallback. |
expr.mainReference.resolveToSymbols() |
(expr as? KtResolvable)?.resolveSuccessfulSymbols() – the generic KtResolvable.resolveSuccessfulSymbols(): Collection<KaSymbol> (there is no per-PSI-type specialization for the plural form). |
KtReference.isImplicitReferenceToCompanion() |
(element as? KtSimpleNameExpression)?.isImplicitReferenceToCompanion == true |
KtReference.usesContextSensitiveResolution |
(element as? KtSimpleNameExpression)?.usesContextSensitiveResolution == true |
Tip
A resolved symbol is not always the call target. For most elements the symbol from
resolveSuccessfulSymbol() and the symbol behind resolveSuccessfulCall() coincide. They can diverge for
KtNameReferenceExpression, KtOperationReferenceExpression, and KtEnumEntrySuperclassReferenceExpression:
resolveSuccessfulSymbol / resolveSuccessfulSymbols prefer the exact referenced symbol, while
resolveSuccessfulCall may describe the enclosing call. Choose the one that matches what you need (see
Common migration patterns below).
resolveSuccessfulSymbol() returns a non-null result only for a single, unambiguous, successfully resolved
target, so on valid code it matches the old resolveToSymbols().singleOrNull(). Two warnings. The old
resolveToSymbols().firstOrNull() silently picked one of several ambiguous results — for that breadth
migrates to resolveSuccessfulSymbols(), not resolveSuccessfulSymbol(). And both resolveSuccessfulSymbol()
and resolveSuccessfulSymbols() expose only successfulSymbols, whereas the legacy resolveToSymbols() also
surfaced candidate symbols on red code; to keep that best-effort behavior, use tryResolveSymbols()?.symbols
(the emulation recipe above).
Common migration patterns¶
Tip
Pick the specialized overload first. On a concrete PSI type (KtCallElement, KtNameReferenceExpression,
KtArrayAccessExpression, ...) the resolveSuccessfulCall / resolveSuccessfulSymbol overload needs no cast and
returns a narrower type. Only when your receiver is statically KtElement / KtExpression do cast to the marker
interface (as? KtResolvableCall / as? KtResolvable) and use the generic form.
The recipes below assume the opt-ins from the note at the top of this page are in scope.
resolveToCall¶
resolveSuccessfulCall() returns only the successfully resolved call — it is defined as
tryResolveCall()?.successful. It is therefore the faithful replacement for the successful* reductions, not
the single* ones: the single* helpers read KaCallInfo.calls, which for an error call is the candidate list, so
they also return the sole candidate of an unresolved call. Classify the old reduction before migrating:
Old reduction over KaCallInfo |
On success | On an error call | New equivalent |
|---|---|---|---|
successfulFunctionCallOrNull() / successfulVariableAccessCall() / successfulCallOrNull<T>() |
the call | null |
resolveSuccessfulCall() (add ?.function / ?.variable on a generic receiver) |
singleFunctionCallOrNull() / singleVariableAccessCall() / singleCallOrNull<T>() |
the call | the sole candidate of type T, else null |
tryResolveCall()?.single?.function (or ?.variable) |
KaCallInfo.calls |
a one-call list | the candidate calls | tryResolveCall()?.calls |
The common success-only case — a resolved single function call — collapses to one call, since
resolveSuccessfulCall() over a KtCallElement already returns KaFunctionCall<*>?:
// Old (success-only)
val call = callExpression.resolveToCall()
?.successfulFunctionCallOrNull()
?: return
// New
val call = callExpression.resolveSuccessfulCall() ?: return
The candidate-tolerant singleFunctionCallOrNull() is not equivalent — resolveSuccessfulCall() drops the
sole candidate of a failed call that singleFunctionCallOrNull() would have returned. Preserve that behavior with
tryResolveCall():
// Old (candidate-tolerant)
val call = callExpression.resolveToCall()
?.singleFunctionCallOrNull()
?: return
// New
val call = callExpression.tryResolveCall()
?.single
?.function
?: return
Tip
Drop red code or match it? Migrate to resolveSuccessfulCall() when an unresolved or ambiguous call should be
silently dropped (typical for inspections that must not produce false positives); migrate to tryResolveCall() when
you want a best-effort match on red code (typical for navigation and find-usages). This is the same distinction as the
iterating candidate calls trap below.
If you only need the called symbol, skip the call entirely and use the specialized resolveSuccessfulSymbol():
// Old
val symbol = callExpression.resolveToCall()
?.successfulFunctionCallOrNull()
?.symbol
?: return
// New
val symbol = callExpression.resolveSuccessfulSymbol() ?: return
KaPartiallyAppliedSymbol is gone, so partiallyAppliedSymbol.signature / .symbol chains flatten:
// Old
val id = callExpression.resolveToCall()
?.successfulFunctionCallOrNull()
?.partiallyAppliedSymbol?.signature?.callableId
// New
val id = callExpression.resolveSuccessfulCall()?.symbol?.callableId
// or: callExpression.resolveSuccessfulSymbol()?.callableId
When the receiver is only statically a KtExpression / KtElement, cast to the marker interface and narrow the result:
// Old
val call = expression.resolveToCall()?.successfulFunctionCallOrNull()
// New
val call = (expression as? KtResolvableCall)?.resolveSuccessfulCall()?.function
The same cast covers other call shapes. Variable access:
// Old
val access = expression.resolveToCall()?.successfulVariableAccessCall()
// New
val access = (expression as? KtResolvableCall)?.resolveSuccessfulCall()?.variable
A member call resolved through successfulCallOrNull<KaCallableMemberCall<*, *>>():
// Old
val symbol = expression.resolveToCall()
?.successfulCallOrNull<KaCallableMemberCall<*, *>>()
?.partiallyAppliedSymbol?.symbol
// New
val symbol = (expression as? KtResolvableCall)?.resolveSuccessfulCall()?.simple?.symbol
Trap — iterating candidate calls. resolveSuccessfulCall() is null whenever resolution fails, which is
exactly the case a .calls loop wants to inspect. Migrate .calls to tryResolveCall()?.calls, not
resolveSuccessfulCall():
// Old
for (call in call.resolveToCall()?.calls.orEmpty()) {
// ...
}
// New
for (call in call.tryResolveCall()?.calls.orEmpty()) {
// ...
}
When a helper switched over a resolved KaCall, widen its parameter to KaSimpleOrMultiCall (the type
resolveSuccessfulCall() now returns) and drop the successfulCallOrNull<KaCall>() unwrapping at the call site.
Deprecated subtype casts also disappear: as? KaSimpleFunctionCall becomes the function property.
resolveToCallCandidates¶
A rename plus a type rename: resolveToCallCandidates() → collectCallCandidates(), and the element type
KaCallCandidateInfo → KaCallCandidate (the .candidate and .isInBestCandidates members are unchanged):
// Old
fun handle(candidates: List<KaCallCandidateInfo>) {
}
handle(callExpression.resolveToCallCandidates())
// New
fun handle(candidates: List<KaCallCandidate>) {
}
handle(callExpression.collectCallCandidates())
On a generic receiver, cast first:
// Old
val first = expression.resolveToCallCandidates()
.firstOrNull()
?.candidate as? KaFunctionCall<*>
// New
val candidates = (expression as? KtResolvableCall)?.collectCallCandidates()
val first = candidates?.firstOrNull()?.candidate as? KaFunctionCall<*>
collectCallCandidates() returns all overload-resolution candidates; resolveSuccessfulCall() returns only the
single most-specific result. Use candidates when you need to reason about ambiguity, the resolved call otherwise.
resolveToSymbol¶
Resolve on the element, not on its mainReference. The specialized overload returns a narrower symbol type:
// Old
val symbol = operationReference.mainReference.resolveToSymbol() as? KaFunctionSymbol
?: return
// New
val symbol = operationReference.resolveSuccessfulSymbol() as? KaFunctionSymbol
?: return
A this expression resolves directly — no instanceReference.mainReference:
// Old
val symbol = thisExpression.instanceReference
.mainReference
.resolveToSymbol()
// New
val symbol = thisExpression.resolveSuccessfulSymbol()
When the static type is only KtExpression, narrow to KtResolvable:
// Old
val symbol = expression.mainReference?.resolveToSymbol() as? KaValueParameterSymbol
?: return
// New
val symbol = (expression as? KtResolvable)?.resolveSuccessfulSymbol() as? KaValueParameterSymbol
?: return
Tip
Precision trap — the called declaration vs. the referenced symbol. Old code often resolved the callee name
reference of a call: callExpression.calleeExpression?.mainReference?.resolveToSymbol(). Resolving the call element
instead returns the called declaration — the constructor of a constructor call, the invoke of an implicit
invoke call — whereas resolving the name reference returns the referenced symbol (the class, the property).
These genuinely differ:
// Old: resolves the callee reference
val symbol = callExpression.calleeExpression
?.mainReference
?.resolveToSymbol() as? KaConstructorSymbol
// New: resolves the call element -> the constructor itself
val symbol = callExpression.resolveSuccessfulSymbol() as? KaConstructorSymbol
Migrate to the call element when you want the called function; keep
(reference as? KtResolvable)?.resolveSuccessfulSymbol() on the name reference when you want the exact referenced
symbol.
resolveToSymbols¶
resolveToSymbols() returned a collection. Both common reductions over it map to the single-result
resolveSuccessfulSymbol(), but with a warning:
// Old (exact, single target)
val symbol = element.mainReference.resolveToSymbols().singleOrNull()
// New
val symbol = element.resolveSuccessfulSymbol()
// or: (element as? KtResolvable)?.resolveSuccessfulSymbol()
// Old (tolerated ambiguity, picked one)
val symbol = element.mainReference.resolveToSymbols().firstOrNull()
// New (returns null on more than one target!)
val symbol = element.resolveSuccessfulSymbol()
resolveSuccessfulSymbol() is an exact match for .singleOrNull() on valid code — both yield a result only for
a single, unambiguous target. It is not a faithful match for .firstOrNull(), which silently chose one of several
ambiguous symbols: resolveSuccessfulSymbol() returns null when there is more than one target. Note also that
resolveSuccessfulSymbol() and resolveSuccessfulSymbols() return only successful targets, whereas the old
resolveToSymbols() also exposed candidate symbols on invalid code. If a call site genuinely needs every candidate
— ambiguous or red — migrate to resolveSuccessfulSymbols() for ambiguity, or to the emulation recipe above
(tryResolveCall()?.calls / tryResolveSymbols()?.symbols) for the full best-effort set, not
resolveSuccessfulSymbol().
What about KtReference?¶
Kotlin references have become part of the IntelliJ IDEA Kotlin plugin. The Analysis API no longer uses them.
The extensions KtReference.resolveToSymbols() and KtReference.resolveToSymbol() remain available as a
backward-compatibility surface; they are not annotated @Deprecated outright, but the modern flow does not pass through
KtReference at all. Resolve directly on the KtElement instead, using the specialized method or a safe as?
narrowing to KtResolvable.
Two KtReference extensions are explicitly deprecated and have moved to KtSimpleNameExpression:
KtReference.isImplicitReferenceToCompanion()→(element as? KtSimpleNameExpression)?.isImplicitReferenceToCompanion == true.KtReference.usesContextSensitiveResolution→(element as? KtSimpleNameExpression)?.usesContextSensitiveResolution == true.
Opting in¶
All entry points described here are annotated @KaExperimentalApi. You must opt in at the call site:
@OptIn(KaExperimentalApi::class)
fun myUsage(element: KtCallElement) = analyze(element) {
element.resolveSuccessfulCall()
}
When a snippet names the KtResolvable / KtResolvableCall marker interfaces directly (the generic fallback form), add
the second opt-in — the markers themselves are annotated @KtExperimentalApi:
@OptIn(KaExperimentalApi::class, KtExperimentalApi::class)
fun myUsage(element: KtElement) = analyze(element) {
(element as? KtResolvableCall)?.resolveSuccessfulCall()
}
This is the only deliberate friction during migration: the rest of the surface is designed to require no manual cast, no
wrapper unwrapping, and no parallel KtReference fallback.