Call resolution answers "how is this expression executed at this site — which callable, applied to which receivers, type arguments, and value arguments?". Read Resolution Fundamentals first if you have not.
Entry points
For a given resolvable PSI element, prefer the specialized form — an extension on the concrete PSI type with a precisely typed return value:
val callForFunction: KaFunctionCall<*>? =
callElement.resolveCall()
val callForAnnotation: KaAnnotationCall? =
annotationEntry.resolveCall()
val callForArray: KaFunctionCall<KaNamedFunctionSymbol>? =
arrayAccess.resolveCall()
val callForRef: KaCallableReferenceCall<*, *>? =
callableReference.resolveCall()
val loop: KaForLoopCall? = forExpression.resolveCall()
val delegate: KaDelegatedPropertyCall? =
propertyDelegate.resolveCall()
When the PSI type is unknown, fall back to the generic form on KtResolvableCall via a safe cast:
val call: KaSingleOrMultiCall? =
(element as? KtResolvableCall)?.resolveCall()
The result is a KaSingleOrMultiCall? — sealed into two branches:
KaSingleCall describes a single resolved callable applied at this site. This is what you get for ordinary function calls, property accesses, callable references, annotation entries, supertype calls, and so on.
KaMultiCall describes a compound or desugared call that involves several sub-calls. This is what you get for for loops, delegated properties, compound assignments (+=, ++, --), and compound array access (a[i] += v).
The type system tells you which branch you are on. Many specializations narrow the return further — for instance KtForExpression.resolveCall(): KaForLoopCall? already commits to the multi-call branch.
Working with a KaSingleCall
Every KaSingleCall<S, C> exposes the call-site context inline — no partiallyAppliedSymbol wrapper to drill through:
val call: KaSingleCall<*, *> = TODO()
val signature = call.signature // callable signature
val symbol = call.symbol // signature.symbol
val dispatch = call.dispatchReceiver // member receiver
val extension = call.extensionReceiver // extension receiver
val contexts = call.contextArguments // KEEP-367 ctx params
val typeArgs = call.typeArgumentsMapping // inferred + explicit
Concrete subtypes add their own fields. KaFunctionCall<S> adds the argument mappings:
val call: KaFunctionCall<*> = callElement.resolveCall() ?: return
val valueArgs = call.valueArgumentMapping // value arguments
val contextArgs = call.contextArgumentMapping // KEEP-0448 ctx args
val combined = call.combinedArgumentMapping // values + ctx
KaVariableAccessCall adds the access kind: call.kind is a KaVariableAccessCall.Kind.Read or KaVariableAccessCall.Kind.Write — the latter exposes value: KtExpression?, the right-hand side of the assignment. The boolean call.isContextSensitive indicates whether context-sensitive resolution was used.
KaCallableReferenceCall is the cleanest example of the new shape: it extends KaSingleCall<S, C> only — no legacy base. A callable reference does not invoke the callable, so there is no valueArgumentMapping and no read/write kind — only the signature, bound receivers, and type arguments.
Use call is KaImplicitInvokeCall to detect implicit f("...") invocations on values with a functional type. The legacy KaSimpleFunctionCall.isImplicitInvoke boolean is deprecated.
KtNameReferenceExpression on a constructor reference
The call counterpart of a name reference can return a different symbol from the symbol counterpart. For MyClass() the name MyClass literally points to the class (resolveSymbol() returns the KaClassLikeSymbol), but the call wraps the constructor that is actually invoked:
class MyClass
val c = MyClass()
// ^^^^^^^ resolveCall() -> KaFunctionCall<...>
// ^^^^^^^ resolveSymbol() -> class `MyClass`
Pick the one that matches your question: who is invoked here → resolveCall(); what does the name literally mean → resolveSymbol().
Compound and desugared calls
Some Kotlin constructs desugar into several operator calls. The Analysis API exposes the assembled multi-call and the individual sub-calls through dedicated KaMultiCall subtypes. The names of the sub-calls match what the language spec calls them.
for loops
for (item in list) {
println(item)
}
A for loop desugars into iterator(), hasNext(), and next(). The call is a KaForLoopCall:
val loop = forExpression.resolveCall() ?: return
val iter: KaFunctionCall<KaNamedFunctionSymbol> = loop.iteratorCall
val hasNext: KaFunctionCall<KaNamedFunctionSymbol> = loop.hasNextCall
val next: KaFunctionCall<KaNamedFunctionSymbol> = loop.nextCall
The richer form is KtForExpression.tryResolveCall(): KaForLoopCallResolutionAttempt?, which exposes each sub-call attempt separately — if one of iterator()/hasNext()/next() fails to resolve, the others remain available.
Delegated properties
val name: String by lazy { "John" }
A delegated property desugars into getValue(), optionally setValue() (for var), and optionally provideDelegate(). The call is a KaDelegatedPropertyCall:
val delegate = propertyDelegate.resolveCall() ?: return
// KaFunctionCall<KaNamedFunctionSymbol>
val getter = delegate.valueGetterCall
// null for `val`
val setter = delegate.valueSetterCall
// null if not applicable
val provide = delegate.provideDelegateCall
The matching attempt type is KaDelegatedPropertyCallResolutionAttempt.
Compound variable access (+=, ++, --)
var i = 0
i += 1
i++
A compound assignment or unary increment on a variable resolves to a KaCompoundVariableAccessCall:
val call: KaCompoundVariableAccessCall = TODO()
// the variable read/write
val variable: KaVariableAccessCall = call.variableCall
// the `plus` / `inc` / ...
val operator: KaFunctionCall<KaNamedFunctionSymbol> =
call.operationCall
when (val op = call.compoundOperation) {
is KaCompoundAssignOperation -> {
// PLUS_ASSIGN, MINUS_ASSIGN, TIMES_ASSIGN,
// DIV_ASSIGN, REM_ASSIGN
op.kind
op.operand // right-hand-side expression
}
is KaCompoundUnaryOperation -> {
op.kind // INC or DEC
op.precedence // PREFIX or POSTFIX
}
}
Compound array access (a[i] += v)
m["a"] += "b"
The most elaborate compound is the array form, represented by KaCompoundArrayAccessCall. It involves both get and set, plus the operator function:
val call: KaCompoundArrayAccessCall = TODO()
val getter: KaFunctionCall<KaNamedFunctionSymbol> =
call.getterCall
val setter: KaFunctionCall<KaNamedFunctionSymbol> =
call.setterCall
val operator: KaFunctionCall<KaNamedFunctionSymbol> =
call.operationCall
val indices: List<KtExpression> = call.indexArguments
The matching attempt is KaCompoundArrayAccessCallResolutionAttempt with getterCallAttempt, operationCallAttempt, and setterCallAttempt.
A flat view of the sub-calls
Every KaMultiCall also exposes calls: List<KaSingleCall<*, *>>. If you do not care which sub-call is which, this is the simplest accessor:
val flat: List<KaSingleCall<*, *>> = (call as KaMultiCall).calls
Plain form vs try form
Use resolveCall() when you only need a valid result; resolution failure becomes null. Use tryResolveCall() when you want the diagnostic and the candidate calls the compiler considered. The richer form returns a KaCallResolutionAttempt — see KaCallResolutionAttempt for the full hierarchy and helpers.
val attempt = callElement.tryResolveCall() ?: return
// single resolved call, or null
val resolved = attempt.successfulCall
// success: one call; error: candidate calls
val everything = attempt.calls
For compound expressions, the multi-attempt types (KaForLoopCallResolutionAttempt, KaDelegatedPropertyCallResolutionAttempt, KaCompoundVariableAccessCallResolutionAttempt, KaCompoundArrayAccessCallResolutionAttempt) keep each sub-call attempt addressable, so you can still inspect the sub-calls that did resolve when another sub-call failed.