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.
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 newKaSingleCallexposes 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 resolveSymbol/resolveCall 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.resolveCall()— 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.
Old → new matrix
Result wrapper: KaCallInfo → KaCallResolutionAttempt
resolveToCall() returned a KaCallInfo; tryResolveCall() returns a KaCallResolutionAttempt. The hierarchies line up one-to-one, but the new one carries KaSingleCall<*, *> payloads instead of bare KaCall, and splits single-call attempts from multi-call ones (compound / for/ delegated property).
Old ( | New ( |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Multi-call attempts (KaForLoopCallResolutionAttempt, KaDelegatedPropertyCallResolutionAttempt, KaCompound*CallResolutionAttempt) expose call: KaMultiCall? plus per-step attempts: List<KaSingleCallResolutionAttempt>; the old KaCallInfo had no equivalent — these calls were resolved indirectly.
Call hierarchy: KaCall → KaSingleOrMultiCall
A successful resolution used to hand back a KaCall; now it hands back a KaSingleOrMultiCall. KaFunctionCall and KaVariableAccessCall keep their names but are now KaSingleCalls (so their data is inline — see below); compound, for-loop, and delegated-property calls are KaMultiCalls.
Old ( | New ( |
|---|---|
|
|
|
|
| same names, now |
(no first-class type) — |
|
(no first-class type) — callable references |
|
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 KaSingleOrMultiCall.
Old ( | New ( |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
Calls
Old call | New equivalent |
|---|---|
|
|
|
|
|
|
|
|
Inline access on a KaSingleCall
Every *PartiallyAppliedSymbol access becomes a direct field on the call.
Old call (legacy member on | New equivalent (inline on |
|---|---|
|
|
|
|
|
|
|
|
|
|
Subtypes you may have used
Legacy type / member | Replacement |
|---|---|
|
|
|
|
|
|
|
|
Compound calls (+=, ++, --, a[i] += v, by, for)
Old call | New equivalent |
|---|---|
|
|
|
|
|
|
|
|
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:
This pattern is the formal way to migrate a mainReference.resolveToSymbols() call site that needs the broadest possible result.
Old call | New equivalent |
|---|---|
|
|
|
|
|
|
|
|
Common migration patterns
The recipes below assume the opt-ins from the note at the top of this page are in scope.
resolveToCall
resolveCall() returns only the successfully resolved call — it is defined as tryResolveCall()?.successfulCall. 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 | On success | On an error call | New equivalent |
|---|---|---|---|
| the call |
|
|
| the call | the sole candidate of type |
|
| a one-call list | the candidate calls |
|
The common success-only case — a resolved single function call — collapses to one call, since resolveCall() over a KtCallElement already returns KaFunctionCall<*>?:
The candidate-tolerant singleFunctionCallOrNull() is not equivalent — resolveCall() drops the sole candidate of a failed call that singleFunctionCallOrNull() would have returned. Preserve that behavior with tryResolveCall():
If you only need the called symbol, skip the call entirely and use the specialized resolveSymbol():
KaPartiallyAppliedSymbol is gone, so partiallyAppliedSymbol.signature/.symbol chains flatten:
When the receiver is only statically a KtExpression/KtElement, cast to the marker interface and narrow the result:
The same cast covers other call subtypes. Variable access:
A member call resolved through successfulCallOrNull<KaCallableMemberCall<*, *>>():
Trap — iterating candidate calls. resolveCall() is null whenever resolution fails, which is exactly the case a .calls loop wants to inspect. Migrate .calls to tryResolveCall()?.calls, not resolveCall():
When a helper switched over a resolved KaCall, widen its parameter to KaSingleOrMultiCall (the type resolveCall() now returns) and drop the successfulCallOrNull<KaCall>() unwrapping at the call site. Deprecated subtype casts also disappear: as? KaSimpleFunctionCall becomes as? KaFunctionCall<*>.
resolveToCallCandidates
A rename plus a type rename: resolveToCallCandidates() → collectCallCandidates(), and the element type KaCallCandidateInfo → KaCallCandidate (the .candidate and .isInBestCandidates members are unchanged):
On a generic receiver, cast first:
collectCallCandidates() returns all overload-resolution candidates; resolveCall() 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:
A this expression resolves directly — no instanceReference.mainReference:
When the static type is only KtExpression, narrow to KtResolvable:
resolveToSymbols
resolveToSymbols() returned a collection. Both common reductions over it map to the single-result resolveSymbol(), but with a warning:
resolveSymbol() 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: resolveSymbol() returns null when there is more than one target. Note also that resolveSymbol() and resolveSymbols() 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 resolveSymbols() for ambiguity, or to the emulation recipe above (tryResolveCall()?.calls/tryResolveSymbols()?.symbols) for the full best-effort set, not resolveSymbol().
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:
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:
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.