Skip to content

Fundamentals

This page covers a few fundamental concepts and rules of the Analysis API. Please read it thoroughly.

Kotlin PSI

The Kotlin compiler exposes a Kotlin abstract syntax tree, built on top of the PSI API, a part of IntelliJ IDEA. For some languages, such as Java, the PSI acts both as a syntax tree and as a source of semantic information. In Kotlin, though, these concepts are clearly separated.

Below is a simplified tree of the Kotlin PSI hierarchy.

graph TD
  KtElement
  KtElement --> KtFile
  KtElement --> KtTypeElement
  KtElement --> KtExpression
  KtExpression --> KtDeclaration
  KtDeclaration --> KtClassOrObject
  KtDeclaration --> KtFunction
  KtDeclaration --> KtVariableDeclaration
  KtExpression --> KtOperationExpression
  KtOperationExpression --> KtUnaryExpression
  KtOperationExpression --> KtBinaryExpression
  KtExpression --> KtCallExpression
  KtExpression --> KtBlockExpression

KtElement is the root type of the hierarchy. KtDeclaration and KtExpression are two notable subtypes of it. KtDeclaration itself is an expression, which makes it possible to have local classes and functions.

The Kotlin PSI does not have a strict separation between statements and expressions. There is, however, a KtStatementExpression marker interface that annotates statement-like constructs.

The Analysis API is implemented on top of the Kotlin PSI, mostly as a set of extension functions and properties, providing access to semantic information. For example, to get an expression type, there is a ktExpression.expressionType extension property. Or, to get resolved call information, one should use ktCallExpression.resolveSuccessfulCall() (References and Calls).

KaSession

KaSession is the entry point for interacting with the Analysis API. It provides access to various components and utilities needed for analyzing code in Kotlin.

Each KaSession is associated with a specific module and provides analysis results from the perspective of that module. In other words, a KaSession only sees declarations from the owning (use-site) module and from all its dependencies, both direct and transitive.

To get a KaSession, use the analyze {} function, passing a KaModule or some PsiElement from that module:

@RequiresReadLock
fun perform(element: PsiElement) {
    analyze(element) {
        // Use the 'KaSession' within this block
    }
}

The KaSession is available as a context parameter within the lambda block. The session is valid only within this block, and it should not be stored or accessed outside of it.

The analyze {} call is only available inside read actions. You can use the @RequiresReadLock annotation to specify that the method must be called from a read action. In later parts of the documentation, the annotation is not added for clarity.

KaLifetimeOwner

KaSession and most entities retrieved from it (symbols, types, etc.) are valid only within the same read action and the analyze block in which they were created. All such entities have the KaLifetimeOwner supertype.

It is crucial to avoid caching KaLifetimeOwners for an arbitrary time, including storing them in properties of long-living classes or in a static context. Doing so will likely lead to severe memory leaks as these entities hold the whole underlying resolution session. Always retrieve and use them within the analyze block or pass them as parameters to functions that require them.

If you need to extract parts of the resolution logic to a separate function, prefer passing the KaSession as an extension or ordinary parameter, instead of keeping it in some shared context class.

fun perform(element: KtElement) {
    analyze(element) {
        if (check(element)) {
            modify(element)
        }
    }
}

// Here we pass the session as a context parameter
context(session: KaSession)
fun check(element: KtElement) { ... }

fun modify(element: KtElement) { ... }

Inside the analyze {} block, KaSession is also available as a useSiteSession property:

fun perform(element: KtElement) {
    analyze(element) {
        check(useSiteSession, element)
    }
}

// Here we pass the session as an ordinary parameter
fun check(useSiteSession: KaSession, element: KtElement) { ... }

If you need to pass a KaSymbol to a different analysis session, use KaSymbolPointers. Unlike symbols, pointers do not capture the analysis session, so they can be freely passed around or cached:

fun perform(call: KtCallExpression) {
    val pointer = resolveSymbolPointer(call) ?: return
    processCallTarget(call, pointer)
}

fun resolveSymbolPointer(
    call: KtCallExpression,
): KaSymbolPointer<KaCallableSymbol>? = analyze(call) {
    val symbol = call.resolveSuccessfulSymbol()
    symbol?.createPointer() // Create the pointer
}

fun processCallTarget(
    ktContext: KtElement,
    pointer: KaSymbolPointer<KaCallableSymbol>,
) {
    analyze(ktContext) {
        val symbol = pointer.restoreSymbol() ?: return
        println(symbol.callableId) // Use the restored symbol
    }
}

Note

You can also create pointers for KaTypes with KaType.createPointer() and restore them in an analysis session using KaTypePointer.restore().