kotlin Documentation

The Kotlin Programming Language.

RAW Doc

Docker & Self-Hosted Deployment Verified OCI

Deploy kotlin on any server, homelab, or cloud container platform.

1-Click Terminal Command
bash (docker run)
docker run -d --name kotlin --restart always jetbrains/kotlin:latest

Technical Documentation: JetBrains/kotlin

ℹ️ Provenance: Multi-Tier Ingestion Β· CodeWiki Reference

Analysis Docs Contribution Guide Index (`analysis/docs/contribution-guide/index.md`)

Kotlin Analysis API Development Guidelines

This series of guides provides comprehensive instructions for developing and maintaining the Kotlin Analysis API. They are intended for:

  • JetBrains developers working on the Analysis API core, implementations, and platform integrations
  • External contributors submitting patches and improvements to the Analysis API
  • Tool developers who want to understand the design philosophy and best practices behind the API

The guidelines cover API design principles, implementation patterns, naming conventions, and evolution strategies that help maintain
consistency and quality across the codebase.

What is the Analysis API?

The Kotlin Analysis API is a library for analyzing Kotlin code at the semantic level. It provides a structured way to query information
about Kotlin code, including symbols, types, and semantic relationships between them. Built on top of Kotlin's
PSI syntax trees, the API offers a clean abstraction over the compiler's internal
representations, making it accessible both for IDE plugin makers and command-line tool developers.

Analysis starts with the analyze entry point, which opens a KaSession as a context parameter so that the API's endpoints are available
inside the block.

Check the Analysis API documentation for more information.

Guidelines

**[API Development Guide](api-development.md)**

This document covers principles and practices for developing new Analysis API endpoints, including design patterns, naming conventions,
documentation standards, and requirements for API implementations.

**[API Evolution Guide](api-evolution.md)**

This document covers the lifetime of individual Analysis API endpoints from introduction to deprecation.

**[Endpoint Card](endpoint-card.md)**

A template for systematic analysis of existing API endpoints to support refactoring, enhancement, or deprecation decisions.


Analysis Docs Design Documents README (`analysis/docs/design-documents/README.md`)

Design Documents

This folder contains design documents that have already been implemented. Design documents represent a historical snapshot. They are not
updated when details in the architecture are changed.

When adding a design document, make sure that references to internal details are removed or rephrased.

General

Project Structure


Analysis Docs Contribution Guide Api Development (`analysis/docs/contribution-guide/api-development.md`)

API Development

This guide covers principles and practices for developing new Analysis API endpoints, including design patterns, implementation guidelines,
and documentation standards.

For information about the Kotlin Analysis API itself, check the Analysis API documentation.
For information about evolving existing APIs, see the API Evolution Guide.

[!NOTE]
These guidelines apply to both Analysis API (Ka* types) and Kotlin PSI (Kt* types).
While examples primarily use Analysis API, the same principles govern PSI development.
PSI-specific considerations are noted where they differ.

Important Disclaimer

Not all existing parts of the Analysis API strictly follow every guideline in this document.
There are several reasons for these deviations:

  • Historical reasons
    Some APIs were designed before these guidelines were established and cannot be changed without breaking compatibility.
  • Performance optimizations
    Certain implementation details may violate ideal design principles to achieve necessary performance characteristics.
  • Platform requirements
    Integration with IntelliJ IDEA and other platforms sometimes requires compromises in the API design.
  • Experimental evolution
    Some unstable APIs intentionally explore alternative approaches before settling on final designs.

When working with existing code, strive to follow these guidelines for new APIs while respecting the constraints of existing
implementations. During refactoring, gradually align older code with these guidelines where possible without breaking compatibility.

Core Design Principles

[!TIP]
Build APIs that solve real problems elegantly, prioritize Kotlin idioms, and maintain consistency with language concepts.

When designing or refactoring any part of the Analysis API, the following fundamental principles guide our decisions.

Solve Real Problems

Start by understanding how developers currently tackle the problem you're addressing. Research existing use cases and identify common
patterns in workarounds. This ensures your API addresses actual needs rather than theoretical scenarios.
Use the endpoint card when working with complicated endpoints.

Design for Common Patterns

Provide simple, intuitive solutions for frequent use cases while offering sophisticated APIs for complex scenarios.
You can apply different stability guarantees to each tier.

kotlin
// Simple API for common cases: get just the target symbol
expression.mainReference.resolveToSymbol()

// Advanced API for complex scenarios: get all information about the call
expression.resolveToCall()?.singleFunctionCallOrNull()?.argumentMapping

Dogfood Your API

Test your API design by implementing it in real scenarios.
This reveals usability issues and missing functionality early.
If you find yourself writing wrapper functions or complex workarounds, revisit the design.

Embrace Kotlin Idioms

Design APIs that feel natural in Kotlin code.
Use Kotlin's language features to create convenient and safe endpoints.

kotlin
// Leverage extension functions for better readability
val KaClassSymbol.allSuperTypes: Sequence<KaType>       // Good
fun allSuperTypes(symbol: KaClassSymbol): Sequence<KaType>  // Less idiomatic

// Use Kotlin function types instead of Java interfaces
fun processTypes(filter: (KaType) -> Boolean)  // Good
fun processTypes(filter: Predicate<KaType>)    // Less idiomatic

Expose Semantic Concepts

The Analysis API reflects semantic meaning, not syntactic structure (for which there is the Kotlin PSI).
If you still need to expose syntactic information, use clear naming conventions, e.g., isMarkedThing or sourceThing.

kotlin
// Checking semantic visibility (considering all modifiers and context)
val KaSymbol.visibility: KaSymbolVisibility

// Checking the presence of a syntactic marker for nullable types (e.g., 'T?')
val KaType.isMarkedNullable: Boolean  // Not just 'isNullable'

Use Established Patterns

Align with widely recognized conventions from Kotlin stdlib and the ecosystem.

kotlin
// Reuse standard interfaces instead of custom wrappers where appropriate
interface KaAnnotationList : List<KaAnnotation>, KaLifetimeOwner {
    operator fun contains(classId: ClassId): Boolean
    operator fun get(classId: ClassId): List<KaAnnotation>
}

// Use conventional method names
interface KaType {
    override fun toString(): String  // Not 'debugString()' or 'asString()'
}

Use Familiar Concepts

Align terminology and concepts with official Kotlin documentation, language specification,
and KEEPs. Avoid direct mirroring compiler internals if they don't match user mental models.

API Guidelines

[!TIP]
APIs require attention to detail and forward-thinking design. Carefully design entity hierarchies, check whether endpoints are
evolution-friendly, and provide comprehensive documentation together with tests.

Enable Explicit API Mode

Always enable explicit API mode to ensure all public
declarations have explicit visibility modifiers and return types. This prevents accidental API surface expansion.

kotlin
abstract class KaDefinitelyNotNullType : KaType {  // Visibility must be specified
    final override val isNullable  // Return type must be specified
        get() = withValidityAssertion { KaTypeNullability.NON_NULLABLE }
}

[!NOTE]
To keep examples clean and focused, the public modifier is omitted in most code snippets.

Maintain Binary Compatibility

Use the binary-compatibility-validator tool to catch breaking changes.
The API must remain binary-compatible across minor versions.

Avoid experimental language features

Stick to stable Kotlin language features in generally available Analysis API endpoints.
Experimental features may change syntax or semantics between Kotlin versions, requiring API clients to opt into unstable language features,
or may be removed completely.

When exposing an experimental feature for early adopters, always annotate affected declarations with an
opt-in annotation.

Avoid Compiler-Specific Concepts

Don't expose internal compiler representations directly.
Create abstractions that have predictable backward compatibility and make sense to API users.

kotlin
// Bad: Exposing internal enum from compiler
interface KaConstantValue {
    val constantValueKind: ConstantValueKind  // Internal compiler enum can be changed at any time
}

// Good: API-specific abstraction
sealed interface KaConstantValue {
    val value: Any?

    interface IntValue : KaConstantValue {
        override val value: Int
    }
    interface StringValue : KaConstantValue {
        override val value: String
    }
    // ...
}

Class and Interface Design

Prefer Interfaces to Classes

Interfaces provide more flexibility for implementations and have better binary compatibility characteristics than classes.
Use abstract classes only when you need to share implementation details or state.

kotlin
// Preferred: Interface allows for context-tailored implementations
interface KaType : KaLifetimeOwner, KaAnnotated

// Avoid unless needed: Class restricts implementation
abstract class KaType : KaLifetimeOwner, KaAnnotated

Avoid Sophisticated Hierarchies Unless They Add Clear Value

Complex inheritance structures increase cognitive load and maintenance burden.
Consider alternative approaches before creating deep hierarchies.

Avoid marker interfaces created solely for member sharing.
Rule of thumb β€” ask yourself, "Would it make sense for clients to pass instances of this type around"?
If not, the interface might be unnecessary.

kotlin
// Good: Clear, purposeful hierarchy
interface KaSymbol
interface KaDeclarationSymbol : KaSymbol {
    val modality: KaSymbolModality
    val visibility: KaSymbolVisibility
    val isExpect: Boolean
    val isActual: Boolean
}

// Bad: Unnecessary complexity
interface KaSymbol
interface KaSymbolWithModality : KaSymbol {
    val modality: KaSymbolModality
}
interface KaSymbolWithVisibility : KaSymbol {
    val visibility: KaSymbolVisibility
}
interface KaMultiplatformSymbol : KaSymbol {
    val isExpect: Boolean
    val isActual: Boolean
}
interface KaDeclarationSymbol : KaSymbolWithModality, KaSymbolWithVisibility, KaMultiplatformSymbol

Avoid Mixing Conceptual Hierarchies

Keep different concepts separate in your type hierarchies.
Prefer composition to inheritance.

Such as, a class symbol should not also be a scope, even if it contains declarations:

kotlin
// Bad: Mixing concepts
interface KaClassSymbol : KaSymbol, KaScope

// Good: Separate concepts with clear relationships
// Even better: move out the scope API to a session component
interface KaClassSymbol : KaSymbol {
    val staticScope: KaScope
    val memberScope: KaScope
}

Use Appropriate Suffixes for Containment Interfaces

When you do need interfaces for sharing members, use clear suffixes that indicate the relationship:

kotlin
// "Owner" for ownership relations
interface KaContextParameterOwnerSymbol : KaFunctionSymbol {
    val contextParameters: List<KaContextParameterSymbol>
}

// "Container" for weaker containment relations  
interface KaDeclarationContainerSymbol : KaSymbol {
    val declarations: Sequence<KaDeclarationSymbol>
}

Think Twice Before Introducing Sealed Classes or Enums

Sealed classes and enums create strong API commitments because you cannot add new subtypes without breaking compatibility (exhaustive
when). Consider your evolution needs carefully.

kotlin
// Good: Sealed class with escape hatch for evolution
sealed class KaAnnotationValue {
    data class IntValue(val value: Int) : KaAnnotationValue()
    data class StringValue(val value: String) : KaAnnotationValue()

    // A private subclass forces 'else' branches in client code
    private class UnknownValue : KaAnnotationValue()
}

// Alternative: Statics for more flexibility (and no exhaustive 'when')
class KaSeverity private constructor(val name: String) {
    companion object {
        val ERROR: KaSeverity = KaSeverity("ERROR")
        val WARNING: KaSeverity = KaSeverity("WARNING")
    }
}

Choose Sealed Class Placement Consciously

For simpler hierarchies, prefer placing inheritors inside a sealed class or interface.
This way, users can immediately see all subtypes at a glance, and subtypes can have simpler names.

Note that with the
emerging context-sensitive resolution
feature, specifying outer class names won't be needed in many cases.

kotlin
sealed class KaSeverity {
    object Error : KaSeverity()
    object Warning : KaSeverity()
    private object Unknown : KaSeverity()  // Forces 'else' branches in exhaustiveness checks
}

When you have two or more inheritance levels in a sealed hierarchy, or when individual classes are meant to be passed around independently,
implement them as top-level classes:

kotlin
sealed class KaClassifierSymbol : KaDeclarationSymbol()
sealed class KaClassSymbol : KaClassifierSymbol()
abstract class KaNamedClassSymbol : KaClassSymbol()
abstract class KaAnonymousObjectSymbol : KaClassSymbol()
abstract class KaTypeAliasSymbol : KaClassifierSymbol()

Use `@SubclassOptInRequired` for Non-Extendable Types

If an interface or class isn't intended to be subclassed by clients, make this explicit:

kotlin
// Clients aren't supposed to create their session implementations
@SubclassOptInRequired(KaImplementationDetail::class)
interface KaSession

Use `@KaSpi` for Extendable Types

If you expect users to create subtypes of your class or interface,
mark the declaration with @KaSpi annotation and provide detailed instructions on not only how to use
each of the members, as well as how to implement them correctly.

Use @KaSpiExtensionPoint annotation to explicitly mark members of the SPI that are designed only for extension, not for direct use.

The Kotlin PSI uses @KtSpi and @KtSpiExtensionPoint for the same purpose.

ExtensionPointName can be used to inject client implementations. Implementation notes:

  • The KDoc of the SPI should mention the extension point name (the name that is used to register the EP)
  • @KaSpiExtensionPoint should be used to annotate all members of the extension point class

Do not mix API and SPI (Service Provider Interface) in one type. It is better to have a clear separation
between types that are supposed to be used by clients and types that are supposed to be used by the Analysis API platform itself.
This would help prevent the potential evolution of API/SPI from introducing breaking changes.

Check Whether Your Entity Needs to be a `KaLifetimeOwner`

Entities containing symbols or types typically
require lifetime management.
Consider how your entity will be used outside analysis sessions.

kotlin
// Needs lifetime management: Contains symbols
interface KaCallInfo : KaLifetimeOwner {
    val targetFunction: KaFunctionSymbol  // Symbol requires lifetime
}

// Doesn't need lifetime management: Pure data
sealed interface KaConstantValue {
    val value: Any?
    val sourcePsi: KtElement?

    fun render(): String
}

Avoid Data Classes in Public APIs

Data classes aren't designed
for API evolution. They expose implementation details through generated methods and make compatibility harder to maintain.

kotlin
// Bad: Data class in public API
data class KaCallInfo(
    val targetFunction: KaFunctionSymbol,
    val arguments: List<KaArgument>
)

// Good: Interface with explicit contract
interface KaCallInfo {
    val targetFunction: KaFunctionSymbol
    val arguments: List<KaArgument>
}

Document Equality Semantics

If your type overrides equals() and hashCode(), clearly specify the equality contract.

Ensure the contract confirms to the Java requirements for
Object.equals() and
Object.hashCode(). If conformance is
impossible, document the reasons.

kotlin
/**
 * [KaType.equals] implements *structural type equality*, which may not match
 * with the usual intuition of type equality. Structural equality
 * is favored for `equals` because it is fast and predictable, and
 * additionally it allows constructing a hash code.
 *
 * For semantic type comparisons, [semanticallyEquals] should be used, as it
 * implements the equality rules defined by the type system.
 */
interface KaType

Property and Function Design

[!TIP]
Design properties and functions that are intuitive, evolution-friendly, and follow Kotlin conventions. Pay attention to parameter
names, return types, and error handling.

Choose Between Properties and Functions Based on Semantics

In many languages, properties are often used for simple operations that don't involve expensive computation, or when the result is already
known, while functions represent more complex operations. However, as the Analysis API features on-demand code analysis, it's rarely
possible to predict the amount of computation required for a given endpoint.

So instead, choose between properties and functions based on the endpoint semantic, and aesthetic of its usages:

  • Use functions when you consume parameters. While in such cases there's typically no choice in the first place, think of whether an
    extension property will look more elegant.
  • Prefer functions for actions – an endpoint that performs some work conceptually (e.g., type creation) or has side effects.
  • Prefer properties for semantical attributes that users just get or set. E.g., KtClassOrObject.symbol means
    "get a symbol that represents the given class". The user doesn't care whether the symbol is newly computed or taken from the cache.
kotlin
// Property: No user-visible side effects
val KaClassSymbol.isData: Boolean

// Function: May involve computation or have parameters
fun KtElement.resolveToCall(): KaCallInfo?
fun KaClassSymbol.isSubClassOf(superClass: KaClassSymbol): Boolean

Provide Convenience Properties for Simple Cases

If you have a function endpoint with a parameter which is often omitted (for instance, by providing a trivial value, such as null, an
empty collection, or a true-returning lambda), consider adding a convenience property with the same name:

kotlin
val KaScope.callables: Sequence<KaCallableSymbol>
fun KaScope.callables(nameFilter: (Name) -> Boolean): Sequence<KaCallableSymbol>

Avoid Unsafe Defaults

On incorrect or incomplete input, the Kotlin compiler may return a partial resolution result. Never assume a reference or a call can
always be resolved.

Don't expose an API relying on unsafe assumptions:

kotlin
// Bad: Throws on analysis failure
fun KtCallExpression.resolveSymbol(): KaFunctionSymbol  // Throws if resolution fails

// Good: Returns 'null' on analysis failure
fun KtCallExpression.resolveSymbol(): KaFunctionSymbol?

Avoid Exceptions for Non-Exceptional Cases

Use appropriate return types to indicate absence or failure:

kotlin
// Bad: Throwing for missing elements
fun getClass(classId: ClassId): KaClassSymbol  // Throws if not found

// Good: Null for not found
fun findClass(classId: ClassId): KaClassSymbol?

// Good: Result or a custom class for operations that can fail with details
interface KaCompilerFacility {
    fun compile(file: KtFile): KaCompilationResult
}

Throwing exceptions is appropriate for:

  • Internal compiler consistency problems when the Analysis API can do nothing, and continuing would corrupt the state
  • Invalid API usage such as malformed parameters
kotlin
// Good: Exception on API misuse
val KtFunction.symbol: KaFunctionSymbol
    get() = withValidityCheck {
        check(this.isValid)
        ...
    }

Review Parameter Names Carefully

Parameter names cannot be changed without breaking source compatibility:

kotlin
// If you ship this
fun findClass(className: Name): KaClassSymbol?

// You won't be able to change to this without breaking clients
fun findClass(name: Name): KaClassSymbol?  // Breaks named parameters!

Avoid Default Parameters in Stable APIs

Default parameters break binary compatibility when changed. Use overloads instead:

kotlin
// Bad: Adding/changing defaults breaks binary compatibility
fun findFunctions(name: Name, scope: KaScope? = null): Sequence<KaFunctionSymbol>

// Good: Explicit overloads
fun findFunctions(name: Name): Sequence<KaFunctionSymbol>
fun findFunctions(name: Name, scope: KaScope): Sequence<KaFunctionSymbol>

While @JvmOverloads may look like a solution, it still generates the $default function, compatibility of which breaks on signature
changes. E.g., for the following function:

kotlin
@JvmOverloads 
fun test(i: Int = 0, s: String = "") {}

The following set of JVM methods is generated:

java
public final class FacadeKt {
    public static final void test(int, java.lang.String);
    public static void test$default(int, java.lang.String, int, java.lang.Object);
    public static final void test(int);
    public static final void test();
}

Now, if the test() function gets a new parameter, the old $default will disappear.

Ensure Consistent Behavior Across Overloads

Overloaded or similar methods should behave predictably:

kotlin
interface KaSymbolProvider {
    // Bad: No common patterns
    fun findClass(classId: ClassId): KaClassSymbol?
    fun findTypeAlias(fqName: FqName): Result<KaTypeAliasSymbol>
    fun findFunction(classId: ClassId?, name: Name): KaFunctionSymbol?

    // Good: All "find" methods return 'null' for not found
    fun findClass(classId: ClassId): KaClassSymbol?
    fun findTypeAlias(classId: ClassId): KaTypeAliasSymbol?
    fun findFunction(callableId: CallableId): KaFunctionSymbol?
}

Use Overloaded Extensions

Reduce cognitive load by providing a single entry point with specialized overloads:

kotlin
// Base extension for all declarations
val KtDeclaration.symbol: KaDeclarationSymbol

// Specialized versions with more specific return types
val KtClass.symbol: KaClassSymbol
val KtNamedFunction.symbol: KaFunctionSymbol
val KtProperty.symbol: KaPropertySymbol

// Users only need to remember ".symbol"

Choose Return Types Thoughtfully

Select return types that provide flexibility for implementations and users.
Avoid choosing the List unconsciously – consider also Sequence, Collection or Iterable.

kotlin
// Good: Sequence allows lazy evaluation (and lazy implementation!)
val KaType.allSupertypes: Sequence<KaType>

// Document iteration capabilities
/**
 * Returns a sequence of all supertypes of the given [KaType].
 * 
 * Supertypes may be traversed lazily.
 * The sequence can be iterated multiple times.
 */
val KaType.allSupertypes: Sequence<KaType>

// For small, always-computed collections, 'List' is fine
interface KaFunctionSymbol {
    val valueParameters: List<KaValueParameterSymbol>
}

At the same time, avoid using Sequences "just in case" – they are more heavyweight. Also, they are prone to scoping issues, as you cannot
know when the sequence will be traversed. Use them when it can make the implementation significantly more efficient.

Design Collection Operations as Extensions

Make APIs more fluent by extending collection types:

kotlin
// Good: Extension on a collection (or 'Iterable' in this particular case)
val Iterable<KaType>.commonSupertype: KaType?
get() = computeCommonSupertype(this)

// Usage is clear with collections
val supertype = listOf(stringType, intType).commonSupertype

// When collection literals come to Kotlin, this will look even nicer
val supertype = [stringType, intType].commonSupertype

// Less idiomatic: Standalone function
fun commonSupertype(types: List<KaType>): KaType?

Provide Helper Extensions on Wider Types

Provide nullable helpers for members of subtypes that are used extremely often:

kotlin
// Calling a popular 'symbol' property requires an explicit cast
interface KaType : KaLifetimeOwner, KaAnnotated
interface KaClassType : KaType {
    val symbol: KaClassLikeSymbol
}

// Now users can easily get a symbol when it's available
val KaType.symbol: KaClassLikeSymbol?
get() = when (this) {
    is KaClassType -> symbol
    else -> null
}

This approach is particularly valuable when you expect users to mostly work with base types.
Avoid implementing helpers as default interface methods – those are still virtual and can be overridden by clients.

Use Operators Judiciously

Reserve operators for their conventional meanings:

kotlin
// Good: Conventional operator usage
interface KaScope {
    operator fun contains(name: Name): Boolean
    operator fun get(name: Name): KaSymbol?
}

// Avoid: Unconventional operators outside DSLs
operator fun KaType.plus(other: KaType): KaType  // What does this mean?

Only Use Infix Functions in DSLs

Infix functions are rarely used in ordinary Kotlin code. Although the syntax might feel tempting, reserve it for DSLs:

kotlin
// Bad: Nice-looking in theory, awkward in practice
infix fun KaType.isSubtypeOf(supertype: KaType)
myType isSubtypeOf serializableType

// Good: Keep it simple
fun KaType.isSubtypeOf(supertype: KaType)
myType.isSubtypeOf(serializableType)

Consider Contracts for Smart Casts

Help the compiler with smart casts using contracts:

kotlin
inline fun KaSession.buildSubstitutor(build: KaSubstitutorBuilder.() -> Unit): KaSubstitutor {
    contract {
        callsInPlace(build, InvocationKind.EXACTLY_ONCE)
    }
    return createSubstitutor(KaSubstitutorBuilder(token).apply(build).mappings)
}

Handle File-Level Declarations Properly

When adding top-level functions or properties, consider the JVM facade name:

kotlin
// File: KaTypeUtils.kt
@file:JvmName("KaTypes")  // Better than "KaTypeUtilsKt"

fun KaType.isSubtypeOf(other: KaType): Boolean
val KaType.isNullable: Boolean

Avoid `Object.clone()`

The clone() method is fundamentally broken in Java. It performs shallow copying, bypasses constructors, and requires manual casting.
Offer explicit copying methods instead:

kotlin
// Bad: Using clone()
interface KaAnnotation : Cloneable

// Good: Explicit copy with clear semantics
interface KaAnnotation {
    /**
     * Creates a deep copy of this annotation with all its values.
     * The copy is independent and can be safely modified.
     */
    fun copy(): KaAnnotation
}

Mind the Inlines

[!TIP]
Inline functions and constants are copied into client code at compile time.
Changes to their implementation won't affect existing compiled code.

Understand Inlining Implications

When you use const val or inline fun, the implementation is copied directly into client bytecode. This means:

kotlin
// If clients compile against version 1.0
const val DEFAULT_TIMEOUT = 1000

inline fun performOperation(action: () -> Unit, timeout: Int = DEFAULT_TIMEOUT) {
    ...
}

// Changing DEFAULT_TIMEOUT to 2000 in version 1.1 won't affect already-compiled clients!
// They will continue using 1000 until recompiled

Think Twice Before Using Inline Functions

Inline functions have limited evolution possibilities and expose implementation details:

kotlin
// Bad: Inline function limits future evolution
inline fun <reified T : KaSymbol> KaModule.topLevelSymbolsOfType(): Sequence<T>

// Good: Regular function allows internal changes
fun <T : KaSymbol> KaModule.topLevelSymbolsOfType(klass: KClass<T>): Sequence<T>

// Convenience: You can still provide an inline function with a trivial, delegating body
inline fun <reified T : KaSymbol> KaModule.topLevelSymbolsOfType(): Sequence<T> =
    topLevelSymbolsOfType(T::class)

// Even better: let the users filter symbols by themselves 
// or provide distinct functions for specific symbols
val KaModule.topLevelSymbols: Sequence<KaSymbol>

Design Extensible Inline Functions

For critical inline functions like analyze, embed hooks for future functionality:

kotlin
inline fun <R> analyze(
    useSiteElement: PsiElement,
    action: context(KaSession) () -> R,
): R {
    // Current implementation
    val session = createSession(useSiteElement)

    // Hook for future additions without breaking compatibility
    beforeAnalysis?.invoke(session)

    return try {
        action(session)
    } finally {
        // Future cleanup hook
        afterAnalysis?.invoke(session)
    }
}

Be prepared for suspension! Unless the lambda parameter of an inline function is marked with crossinline, or a lambda runs inside a
synchronized block, a suspend call may happen inside the passed lambda, and your function body may never be fully executed.

Java-Kotlin Interoperability (PSI)

The PSI module contains both Java and Kotlin files. Converting Java to Kotlin is not always straightforward and in some cases even impossible:

  • @JvmName unavailable in interfaces β€” in some cases it is impossible to convert Java methods to Kotlin properties in a binary-compatible way since @JvmName cannot be used to fix potential clashes.
  • Platform type handling β€” IntelliJ Platform APIs use Java types extensively; Kotlin's null-safety interop requires careful handling.
    • The classic example is PsiElement.getParent() returning PsiElement!. After conversion to Kotlin it becomes either PsiElement? or PsiElement – both of them are breaking changes.
      A workaround is to delegate the implementation to a Java method and keep the return type implicit.
  • Binary compatibility β€” PSI classes are widely used; the binary and source compatibility must be preserved as much as possible.

Consult with PSI maintainers before converting Java classes to Kotlin.

Endpoint Architecture

The public API surface is exposed as top-level endpoints: context(session: KaSession) functions and properties grouped by capability
into domain packages (types, symbols, scopes, expressions, …). The endpoint accepts the analysis session as a context parameter,
so it is accessible within the analyze function without polluting the dispatch receiver's scope:

kotlin
// types/typeRelations.kt
context(session: KaSession)
public fun KaType.isSubtypeOf(supertype: KaType): Boolean = session.withValidityAssertion {
    @OptIn(KaImplementationDetail::class)
    internals.typeRelationChecker.isSubtypeOf(this, supertype)
}

KaSession itself is a clean marker β€” interface KaSession : KaLifetimeOwner. The implementation is hidden behind an internal facade,
KaInternals (@KaImplementationDetail), which an endpoint reaches through the module-private internals cast.
Clients only ever see the endpoint; the proxy it delegates to is an implementation detail.

Every top-level endpoint wraps its body in session.withValidityAssertion { … }. Performing the
lifetime check once at the endpoint keeps it in a single place, so the KaInternals proxy implementations don't
have to repeat it.

Find the Right Package Before Adding an Endpoint

Before introducing a new domain package, verify that your functionality doesn't belong in an existing one. Place the endpoint next to
related capabilities and reuse the existing capability-named file (for example, add a type-relation endpoint to types/typeRelations.kt)
rather than creating a parallel home for it:

kotlin
// Type inheritance is part of `types` β€” add it next to the other type-relation endpoints,
// don't create a new package for it.
context(session: KaSession)
public val KaType.directSupertypes: Sequence<KaType>
    get() = session.withValidityAssertion {
        internals.typeProvider.directSupertypes(this)
    }

Retiring the legacy mechanism. Endpoints used to be members of KaSessionComponent interfaces mixed into KaSession, paired with
auto-generated @KaContextParameterApi context-parameter bridges. That mechanism is being phased out β€” do not add new KaSessionComponent
supertypes or @KaContextParameterApi bridges. New endpoints are plain context(session: KaSession) declarations as shown above.

AnalysisApiLegacySurfaceDumpTest enforces this: it freezes the public declarations under components/ and KaSession.kt, so any new
declaration added to the legacy surface fails the build.

Documentation Standards

[!TIP]
Write clear, comprehensive KDocs covering happy paths, edge cases, also providing examples.
Mention contracts, exceptions, and nullability behavior.

Your documentation should answer:

  • What is the endpoint's purpose?
  • What are the use-cases?
  • What are the pre-conditions and post-conditions?
  • What does it normally return?
    • If it returns a Collection or similar, is the item order specified (or at least stable)?
    • If it returns a Sequence, can the sequence be iterated over more than once?
    • In which cases does it return null or empty collections?
  • When does it throw exceptions?
  • What happens with invalid or edge-case inputs?

When including code examples in documentation, ensure they demonstrate real-world scenarios that help users understand practical usage.
Examples should be copy-paste ready, correctly handle all necessary cases, and follow proper Kotlin code style.

Use meaningful names that reflect the domain or are just more pleasant to see, rather than generic placeholders.
Avoid random variable names like a, x, or temp.
The often-used Foo placeholder may be acceptable when discussing declarations in general terms.

kotlin
/**
 * The function symbol for the original Java getter method.
 *
 * #### Example:
 *
 * ```
 * public class JavaClass {
 *     private int field;
 *
 *     public int getField() {
 *         return field;
 *     }
 * }
 * ```
 *
 * In the synthetic property for `field`, [javaGetterSymbol] is the function symbol for `getField`.
 */
val javaGetterSymbol: KaNamedFunctionSymbol

Structure documentation comments

KDoc is not just a custom comment syntax – it supports Markdown tags.
Inside longer documentation comments, use Markdown headers (###) for individual comment sections.

kotlin
/**
 * The abbreviated type for this expanded [KaType], or `null` if this type has not been expanded from an abbreviated type or the
 * abbreviated type cannot be resolved. [...]
 *
 * ### Resolvability
 *
 * Even when this [KaType] is an expansion, the abbreviated type may be `null` if it is not resolvable from this type's use-site module.
 * This can occur when the abbreviated type from a module `M1` was expanded at some declaration `D` in module `M2`, and the use-site
 * module uses `D`, but only has a dependency on `M2`. Then the type alias of `M1` remains unresolved and [abbreviation] is `null`.
 *
 * ### Type arguments and nested abbreviated types
 *
 * The type arguments of an abbreviated type are not converted to abbreviated types automatically. That is, if a type argument is a type
 * expansion, its [abbreviation] doesn't automatically replace the expanded type. [...]
 *
 * ### Transitive expansion
 *
 * Types are always expanded to their final form. That is, if we have a chain of type alias expansions, the [KaType] only represents the
 * final expanded type, and its [abbreviation] the initial type alias application. [...}]
 */
val abbreviation: KaUsualClassType?

Use the documentation viewer in IntelliJ IDEA (F1 by default) to check how your documentation looks like for the users.

Document properties and functions differently

As explained in the Choose between properties and functions based on semantics
section, function and property endpoints are different. The difference should be reflected in the documentation.

For functions, explain what the action does:

kotlin
/**
 * Creates a new [KaType] based on the given type with the updated nullability specified by [isMarkedNullable].
 */
fun KaType.withNullability(isMarkedNullable: Boolean): KaType

@param and @return tags can be omitted if the user can get the same information from a more natural description at the beginning
of the KDoc. However, add them if there are special cases that need explanation, or to make the description more concise so understanding
the general goal of a function is easier.

For properties, just explain what the user gets (or sets) when accessing the property. Avoid verbs like "returns", "computes" or similar:

kotlin
/**
 * A [KaScope] containing the top-level declarations (such as classes, functions and properties) in the given [KaFileSymbol].
 */
public val KaFileSymbol.fileScope: KaScope

Test Your Documentation's Completeness

Try to "hack" your own KDoc. Look for ambiguities or missing information. Don't leave users guessing about special cases:

kotlin
/**
 * The compile-time constant initializer for the given property, if available.
 */
val KaPropertySymbol.compileTimeInitializer: KaConstantValue?

// Questions that should be answered:
// - Is the expression evaluated or just parsed?
// - What happens if the property is not `const`?
// - What happens if the initializer isn't a compile-time constant, but it still can be evaluated?
// - Does the property work for non-Kotlin (e.g., Java) declarations, including `KaSyntheticJavaPropertySymbol`?

Always Proofread Documentation

Before committing:

  1. Run documentation through a grammar checker (Grammarly, LanguageTool, etc.)
  2. Have someone else review for clarity
  3. Verify examples compile and work correctly

Update External Documentation

After adding or modifying APIs:

  1. Update the documentation website at https://github.com/Kotlin/analysis-api
  2. Update migration guides if deprecating existing APIs

Java Files in PSI

For Java files in the PSI module, apply the same documentation principles using JavaDoc syntax.
Use @param, @return, and @see tags as the JavaDoc equivalents of KDoc's parameter documentation.

Naming Conventions

Use Common Prefixes for Top-Level Class-Like Declarations

API Prefix Examples
Analysis API Ka KaSession, KaSymbol, KaType
Kotlin PSI Kt KtElement, KtExpression, KtDeclaration
KDoc PSI KDoc KDocTag, KDocSection

Use Common Property and Parameter Names

Entity naming in the Analysis API differs on whether an entity is referred from inside the same entity hierarchy or from another place.
Such as, accessing a getter symbol of a KaPropertySymbol is different from accessing a KaPropertySymbol corresponding to a KtProperty.

For cross-references inside a single entity hierarchy, prefer simpler names to avoid verbosity:

kotlin
interface KaTypeParameterOwnerSymbol : KaSymbol {
    val typeParameters: List<KaTypeParameterSymbol>  // Not 'typeParameterSymbols'
}

Across domain boundaries, prefer more descriptive names:

  • KaSymbols
    • symbol (not declaration!) for an arbitrary symbol and when no disambiguation is needed (almost always)
    • functionSymbol for a KaFunctionSymbol and its subtypes
    • propertySymbol for a KaPropertySymbol and its subtypes
    • classSymbol for a KaClassSymbol
    • fileSymbol for a KaFileSymbol
    • etc.
  • KaTypes
    • type for an arbitrary type
    • classType, functionType and others when referring specifically to a function, class, or type (rarely needed)
  • PsiElements
    • psi for a PsiElement, or for a KtElement
    • declaration for a KtDeclaration or its subtypes (when no disambiguation is needed)
    • expression for a KtExpression or its subtypes
      • callExpression (not call!) for KtCallExpression
    • typeReference (not type!) for KtTypeReference
    • script for a KtScript

Rule of thumb: is it clear for the user that they get an entity of a different API domain?

  • PSI ↔ Analysis API
  • KaSymbol ↔ KaType

For example:

kotlin
sealed class KaClassType : KaType {
    // From `KaType` to `KaSymbol`
    val symbol: KaClassLikeSymbol
}

// From `KaSymbol` to `KaTye`
val KaClassifierSymbol.defaultType: KaType

Avoid Unnecessary `ka` and `kt` Prefixes

Don't use ka and kt prefixes for callable names unless absolutely necessary to resolve conflicts:

kotlin
// Good: Clean names
val type: KaType
val annotations: List<KaAnnotation>
val declaration: KtDeclaration

// Bad: Unnecessary prefixes
val kaType: KaType
val kaAnnotations: List<KaAnnotation>
val ktDeclaration: KtDeclaration

// Might work, but think of alternatives (e.g., `KaModuleProvider.find(element)`)
fun PsiElement.findKaModule(): KaModule?

However, if you handle both API layers in a single place (e.g., you map the Kotlin PSI to FIR), prefer adding prefixes at least for
non-primary entities:

kotlin
fun findDeclaration(file: FirFile, ktDeclaration: KtDeclaration): FirDeclaration

Check for Potential Name Clashes

Check for both syntactic and semantic naming conflicts within the API and with external libraries.
Before introducing new names, verify they don't clash with existing functionality or create confusion:

kotlin
// Bad: Clash with 'kotlin.Array'
sealed class KaAnnotationValue {
    class Array : KaAnnotationValue()
}

// Good: A distinct name
sealed class KaAnnotationValue {
    class ArrayValue : KaAnnotationValue()
}

Avoid Getter-Like Functions

Avoid get prefixes unless they carry special meaning. Choose verbs that reflect the operation's nature.
Function names should clearly indicate their behavior and return characteristics:

kotlin
// Bad: Unnecessary 'get' prefix
fun getSupertypes(): List<KaType>
fun KtElement.getDiagnostics(filter: KaDiagnosticCheckerFilter): List<KaDiagnosticWithPsi<*>>
fun getClass(classId: ClassId): KaClassSymbol?

// Good: Direct naming
val supertypes: List<KaType>
fun KtElement.diagnostics(filter: KaDiagnosticCheckerFilter): List<KaDiagnosticWithPsi<*>>
fun findClass(classId: ClassId): KaClassSymbol?  // 'find' indicates nullable result

// Good: Use appropriate verbs for actions
fun buildClassType(classId: ClassId): KaType
fun KaType.render(): String
fun KtExpression.evaluate(): KaConstantValue?

Avoid Redundant Naming

Don't repeat type information or parameter details in names unless ambiguity takes place. Keep names concise and meaningful.

kotlin
// Bad: Redundant type information
val annotationList: List<KaAnnotation>
fun findClassByClassId(classId: ClassId)

// Good: Clean, descriptive names
val annotations: List<KaAnnotation>
fun findClass(classId: ClassId)

Choose Precise Boolean Property Prefixes

Use prefixes that clearly convey the intent and distinguish between different boolean concepts.
Boolean property names should indicate what they're checking and when the check is performed:

kotlin
// Bad: Ambiguous meaning
val nullable: Boolean
val overridden: Boolean
val inline: Boolean

// Good: Clear intent with appropriate prefixes
val isNullable: Boolean          // Current state
val hasTypeParameters: Boolean   // Existence check
val canBeOverridden: Boolean     // Capability check
val shouldBeInlined: Boolean     // Recommendation

// Good: Distinguish between temporal states
val shouldBeEvaluated: Boolean   // Future action recommended
val willBeEvaluated: Boolean     // Future action will happen
val isEvaluated: Boolean         // Past/current state

Check that the names of flags are grammatically correct, and they mean exactly what they say.

Mind a difference between an actor and an object. E.g., shouldResolve is a property of the resolver itself, while for a declaration the
correct flag name is shouldBeResolved.

Name Sealed Classes Appropriately

Use simple names for nested sealed class implementations, also dropping the Ka prefix.
Reserve complex names for top-level classes.

kotlin
// Good: Simple names for nested implementations
sealed class KaConstantValue {
    class StringValue : KaConstantValue()  // Without 'Value', will clash with 'kotlin.String'
    class IntValue : KaConstantValue()
    class CharValue : KaConstantValue()
}

// Good: Descriptive names for top-level sealed implementations
sealed class KaClassifierSymbol : KaDeclarationSymbol()
sealed class KaClassSymbol : KaClassifierSymbol()
abstract class KaTypeAliasSymbol : KaClassifierSymbol()

Use Consistent Terminology

Use established single-word forms for compound terms commonly used in the programming language industry.

  • supertype and subtype (not superType or subType)
  • builtin (not builtIn)
  • uint (not uInt, but UInt when in PascalCase)

Guard API Endpoints with Annotations

The Analysis API serves various projects with different stability requirements.
To prevent accidental usage of unstable APIs or usage-tailored functionality, the Analysis API uses a set of opt-in annotations.

Placement

Opt-in requirements are propagated through the lexical scope to nested declarations. Hence, it is generally sufficient to annotate the
top-level declaration.

However, note that the binary compatibility checker does not propagate opt-in annotations from outer classes.
If the outer class is annotated, add the same opt-in annotation to all nested classes as well.

Experimental API Markers

  • @KaExperimentalApi
    • Marks user-facing APIs that are experimental.
      These APIs are intended for public consumption but may change or be removed without deprecation cycles.

API Layer Markers

  • @KaPlatformInterface
    • Marks APIs intended for Analysis API implementations and platforms.
      These APIs define contracts between the core API and platform implementations, are neither stable nor intended for end-user
      consumption.
    • All public endpoints in the analysis-api-platform-interface module should be annotated with this marker according to the placement
      rules above. This ensures that clients have to opt in when they accidentally or automatically depend on the platform interface.

Internal API Markers

These APIs are intended to only be used in a specific project, or a group of projects.

  • @KaNonPublicApi
    • Marks APIs used within JetBrains projects but not intended for external consumption.
      These APIs have relaxed compatibility guarantees and may change without deprecation cycles.
  • @KaIdeApi
    • Marks APIs designed for the Kotlin IntelliJ plugin.
      These APIs are implemented in the Analysis API for efficiency reasons but are too specific for general use and have no compatibility
      guarantees outside the IDE context.
  • @KaImplementationDetail
    • Marks APIs that are implementation details of the Analysis API itself.
      These APIs exist on the public surface due to architectural constraints but should never be used outside Analysis API implementation
      modules.
    • The KaInternals facade and its per-capability proxies are the canonical example: they back the public endpoints (which delegate via
      internals) but carry no compatibility guarantees themselves.

Unstable Dependency Markers

These APIs are intended for all users but depend on unstable language features.

  • @KaContextParameterApi
    • Marks APIs that use context parameters, an experimental Kotlin language feature.
    • It is not applied to standard public endpoints: they use context(session: KaSession) as their canonical form without an opt-in.
      The annotation only remains on the legacy auto-generated bridges that are being retired (see Endpoint Architecture).

Kotlin PSI Markers

The Kotlin PSI has its own set of markers, most of which carry the same meaning as their Analysis API counterparts:

Analysis API Kotlin PSI
@KaExperimentalApi @KtExperimentalApi
@KaNonPublicApi @KtNonPublicApi
@KaIdeApi @KtIdeApi
@KaImplementationDetail @KtImplementationDetail
@KaPlatformInterface @KtPlatformInterface
@KaSpi @KtSpi
@KaSpiExtensionPoint @KtSpiExtensionPoint

Two markers do not map one-to-one:

  • @KtPlatformInterface is parallel in name only. The Kotlin PSI has no platform layer of its own, so instead of a set of components that a
    platform has to implement, the marker denotes the contract with the Analysis API engine and its platform implementations: stub building,
    decompilation, element type registration, and indexing.
  • @KtPsiInconsistencyHandling has no Analysis API counterpart. It marks declarations that are meant to work with possibly inconsistent PSI,
    which the Kotlin parser cannot produce but PSI modification can.

Adding New Annotations

When existing annotations don't cover your use case, consider adding new opt-in annotations.

  1. Identify the specific audience
    Who should use this API and under what constraints?
  2. Define stability guarantees
    What compatibility promises can you give? How are they unique?
  3. Identify the scope.
    If the annotation is only applicable to a very narrow number of cases, using a more generic one (e.g., @KaExperimentalApi) with a
    dedicated comment is preferred.
  4. Write comprehensive documentation
    Explain the purpose, intended audience, and compatibility guarantees

Experimental API

Experimental APIs have relaxed evolution rules, but they still should maintain quality standards.
Write proper documentation and cover the endpoints with tests. When applicable, explain why the API is kept experimental.

Remember – an experimental API becomes stable someday. Avoid breaking your first clients without a strong reason.

All experimental API must be marked with the @KaExperimentalApi annotation.

For detailed information about the lifecycle of experimental APIs and their transition to stable status, see
the API Evolution Guide.

API Implementation

[!TIP]
API Implementations don't need to provide API compatibility guarantees. However, you should prevent their accidental usage outside
the implementation module where possible. Use the fail-fast approach – check input values and throw consistency exceptions when necessary.

Use `internal` by Default

Implementation declarations should be internal unless they need to be part of the public API surface (like KaFirDiagnostic).
Minimize accidental exposure of implementation details.

If you cannot make the declaration internal, mark it with @KaImplementationDetail.

Lifetime Management

All API implementations must validate lifetime ownership and fail fast on invalid access.

Lifetime safety is critical for preventing memory leaks and ensuring correctness:

kotlin
internal class KaFirNamedClassSymbol(
    override val firSymbol: FirClassSymbol,
    override val analysisSession: KaFirSession
) : KaNamedClassSymbol {
    override val name: Name
        // Either use 'withValidityAssertion' (preferred) or 'assertIsValidAndAccessible()'
        get() = withValidityAssertion {
            firSymbol.name
        }

    override val typeParameters: List<KaTypeParameterSymbol>
        get() = withValidityAssertion {
            firSymbol.typeParameters.map { it.toKaSymbol() }
        }
}

For endpoints consuming PsiElements, use withPsiValidityAssertion() instead:

kotlin
override val KtFile.symbol: KaFileSymbol
    get() = withPsiValidityAssertion {
        KaFirFileSymbol(this, analysisSession)
    }

Remember to guard properties you declare in the primary constructor:

kotlin
class KaScopeWithKindImpl(
    private val backingScope: KaScope,
    private val backingKind: KaScopeKind,
) : KaScopeWithKind {
    override val scope: KaScope get() = withValidityAssertion { backingScope }
    override val kind: KaScopeKind get() = withValidityAssertion { backingKind }
}

For equals(), hashCode() and toString() implementations, avoid validation as these methods are often used in uncontrolled contexts.

Handle Errors Appropriately

Fail fast with clear messages, use appropriate exception types, and document error conditions.

Robust error handling improves developer experience. Use checkWithAttachment, requireWithAttachment, and errorWithAttachment, passing
the relevant information. Never pass sensitive information such as code snippets, or names from source code (e.g., names of individual
classes or functions) in the exception message directly.

kotlin
private fun calculateLazyBodyForResultProperty(firProperty: FirProperty, designation: FirDesignation) {
    val newInitializer = revive<FirAnonymousInitializer>(designation)
    val body = newInitializer.body
    requireWithAttachment(body != null, { "${FirAnonymousInitializer::class.simpleName} without body" }) {
        withFirDesignationEntry("designation", designation)
        withFirEntry("initializer", newInitializer)
    }

    ...
}

Document throwing behavior:

kotlin
/**
 * @throws IllegalArgumentException if [name] is a special name like <init>
 */
fun findFunctions(name: Name): Sequence<KaFunctionSymbol> {
    require(!name.isSpecial) { "Special names are not supported: $name" }
    // ...
}

Further Reading


Analysis Docs Contribution Guide Api Evolution (`analysis/docs/contribution-guide/api-evolution.md`)

API Evolution

This guide covers the lifecycle of Analysis API endpoints from introduction to deprecation, including stability transitions and migration
strategies. For guidance on designing new APIs, see the API Development Guide.

Components of the Analysis API

The Analysis API consists of several interconnected components, each serving a specific purpose:

  • The Kotlin PSI (source, guidelines)

    • Foundation layer providing syntax tree representation through KtElement
      hierarchy
    • Uses Kt prefix (compared to Ka for Analysis API)
    • Has own stability annotations mirroring the Analysis API ones: @KtExperimentalApi, @KtImplementationDetail, @KtNonPublicApi,
      @KtIdeApi, @KtPlatformInterface, @KtSpi, and @KtSpiExtensionPoint
    • Key entities: KtFile,
      KtDeclaration,
      and KtExpression
  • Analysis API Surface (source)

    • User-facing layer of the Analysis API. Maps the Kotlin PSI to its semantic representation
    • Accessed through the analyze entry point, which opens a KaSession as a context parameter; capabilities are exposed as top-level
      context(session: KaSession) endpoints
    • Key entities: KaSession,
      KaSymbol, KaType
  • Platform Interface (source)

    • Abstraction layer between the API and its execution environments
    • Defines how the API interacts with project structure and file systems
    • Key entities: KotlinPlatformComponent,
      KaEngineService
  • Analysis API Standalone (source)

    • Command-line implementation for using the API outside of IDEs
    • Provides project structure management that IDEs typically handle automatically
  • Implementations

    • Analysis API implementations
      • K2 implementation, based on the new K2 compiler frontend (source)
    • Platform interface implementations
      • Kotlin IntelliJ IDEA plugin (source)
      • Analysis API Standalone (source)
    • PSI Implementation, including stubs (source)
  • Light Classes (source)

    • A Java view for Kotlin declarations designed mainly for Java interoperability
    • Currently, also used in UAST (technical debt)
  • Low-level API (source)

    • K2-specific infrastructure for on-demand and incremental code analysis
    • Not directly accessible to end users

API Stability Categories

Every part of the Analysis API falls under one of these stability categories:

  • Stable API

    • Strong source and binary backward compatibility
    • Changes follow strict deprecation cycles
    • Applies to
      • Analysis API Surface (excluding declarations with opt-ins)
  • Unstable API

    • May evolve without deprecation warnings
    • Changes may follow deprecation cycles, depending on the API usage
    • Applies to
      • Platform Interface
      • Standalone API
      • Declarations annotated with @KaExperimentalApi, @KaNonPublicApi, @KaIdeApi
      • Kotlin PSI is formally not yet stable despite its practical stability
  • Implementation Details

    • No compatibility guarantees between versions
    • Reserved for internal use only
    • Applies to
      • Implementation of the Analysis API
      • Platform Interface implementations
      • Low-level API
      • Light Classes (unless explicitly exposed in the Analysis API Surface)
      • PSI Reference implementations
      • Declarations annotated with @KaImplementationDetail

[!NOTE]
PSI uses parallel annotations: @KtExperimentalApi, @KtImplementationDetail, @KtNonPublicApi, @KtIdeApi, @KtSpi, and
@KtSpiExtensionPoint.
@KtPlatformInterface is parallel in name only: since the Kotlin PSI has no platform layer of its own, it marks the contract with the
Analysis API engine and its platform implementations.
The @KtPsiInconsistencyHandling annotation marks code handling inconsistent PSI states (no Analysis API equivalent).
See Kotlin PSI Markers for the full mapping.

Adding New APIs

New APIs follow a three-phase lifecycle designed to prevent premature stabilization while gathering real-world feedback.
This process protects both API designers and users from the cost of breaking changes.

1. Introduction Phase

Goal: Ship the API to early adopters while explicitly communicating its experimental nature.

  • Mark with @KaExperimentalApi
    This annotation serves as both a compiler warning and documentation that the API may change.
    Users must explicitly opt-in, ensuring they understand the risks.

  • Provide comprehensive documentation and tests
    Experimental doesn't mean untested. Include edge cases, error conditions, and usage examples.
    This helps early adopters use the API correctly and provides valuable feedback.

  • Follow the design principles outlined in the API Development Guide
    Experimental APIs should still adhere to design principles.
    Fixing design issues later is more expensive than getting them right initially.

2. Validation Phase

Goal: Confirm the API design through real usage and identify necessary refinements.

  • Gather usage feedback
    Monitor how developers actually use the API versus intended usage.
    Look for common workarounds, frequent questions, or misuse patterns that suggest design issues.

  • Ensure idiomatic usage patterns emerge
    The API should feel natural to Kotlin developers.
    If users consistently need helper functions or complex setup code, consider incorporating these patterns into the API itself.

  • Verify edge case handling
    Real-world usage often reveals edge cases not covered in initial testing.
    Ensure the API gracefully handles malformed input, incomplete analysis results, and platform-specific variations.

3. Stabilization Phase

Goal: Ensure the API is ready for long-term compatibility commitments.

Before removing @KaExperimentalApi, verify that:

  • API is actively used with positive feedback
    Look for evidence that the API solves real problems efficiently.
    Lack of usage may indicate the API isn't needed anymore.

  • Naming is consistent and self-explanatory
    Names should align with Analysis API conventions and be understandable without extensive documentation.
    Consider whether parameter names can be improved based on usage patterns.

  • No anticipated design changes
    The team should have high confidence that the API design will remain stable.
    Major changes after stabilization require the full deprecation process.

Deprecating APIs

Deprecation follows a careful multi-release cycle with clear migration paths.
The process balances giving users time to migrate while preventing indefinite maintenance of obsolete APIs.

1. Initial Deprecation

Goal: Signal the intent to remove the API while providing clear migration guidance.

  • Add the @Deprecated(WARNING) annotation
    This generates compiler warnings that alert users to the deprecation without breaking builds.
    The warning level ensures existing code continues to compile while encouraging migration.

  • Explain alternatives in the deprecation message and KDoc
    Provide guidance on replacement APIs. Include code examples when the migration is non-trivial.
    If no direct replacement exists, explain the recommended approach.

  • Update all usages in Kotlin and IntelliJ repositories
    Test the migration by updating internal usage.
    This serves as real-world examples for external users and ensures the migration path is practical.

  • Document migration in COMPATIBILITY.md
    Mention the deprecation in the compatibility document for smoother communication with the Analysis API users.

2. `ERROR` Advancement

Goal: Prevent new usage while maintaining both source and binary compatibility for existing code.

  • Ensure there's at least one major release with WARNING
    This gives users a full release cycle to discover and plan their migration.

  • Advance the deprecation level to ERROR
    This prevents new usage while allowing existing code to compile with explicit suppression.
    Users can still build their code but must acknowledge they're using deprecated APIs that will be removed shortly.

3. Removal or Hiding

Goal: Complete the deprecation process while maintaining ecosystem compatibility.

  • Ensure there's at least one major release with ERROR
    This provides a final grace period for users who need additional time to migrate complex codebases.

  • Choose between complete removal and @Deprecated(HIDDEN)
    Annotating by @Deprecated(HIDDEN) might be useful when the API is still needed for compatibility with already built JARs.


Analysis Docs Contribution Guide Endpoint Card (`analysis/docs/contribution-guide/endpoint-card.md`)

Analysis API Endpoint Card

Purpose

The endpoint card is a structured analysis template for evaluating existing Analysis API endpoints. It helps contributors systematically
assess API endpoints from multiple perspectives: clarity, usage patterns, design quality, and potential improvements. Use this card when
analyzing an endpoint for refactoring, enhancement, or deprecation to ensure thorough evaluation of its current state and identify areas for
improvement.

Endpoint Traits

Description

Provide a clear, concise description of what the endpoint does in 1–2 sentences. Focus on the primary functionality and expected behavior.
If describing the endpoint's purpose is challenging, or it requires multiple sentences, this may indicate the method has too many
responsibilities and could benefit from decomposition.

Example:

Checks if a symbol is visible in the given context.

Understandability

Evaluate how easily developers can understand the endpoint's purpose and behavior without diving into implementation details.

  • Documentation: Is there a comprehensive KDoc that explains the purpose, parameters, return values, and any important behavior or edge
    cases?
  • Naming: Do the function name, parameter names, and type annotations clearly convey the intended meaning and usage?
  • Clarity: Can a developer understand what the endpoint does just by reading its signature and documentation?

Examples:

For javaGetterName, there is no KDoc. The extension always returns some name, even if the property doesn’t have a getter. It’s unclear
how the method works for expect declarations or for non-JVM platforms. It’s unclear whether it checks the @JvmName annotation or
handles bytecode mangling for inline classes.

The function name analyseImports() is too generic. It does not suggest that the method is part of the import optimizer implementation
and is not designed for general use.

Usages

Where is the endpoint used?

Identify where and how frequently the endpoint is used across different contexts.
The scale of usage is more important than exact counts and helps prioritize design work.

Usage contexts to check:

  • Kotlin IntelliJ IDEA plugin
  • External plugins (use the Find External Usages action)
  • KSP
  • Dokka
  • Swift Export
  • Other clients

Questions to consider:

  • How many usages are there? (approximate count is enough)
  • Is the method suitable for both the IDE and Standalone scenarios?
  • Are there any usage spikes in particular modules or contexts?

Are there common patterns?

Look for repetitive code patterns that appear consistently across different usages of the endpoint.
Common patterns may indicate missing convenience methods, unclear API design, or opportunities for improvement.

Questions to consider:

  • Do developers frequently write similar boilerplate code before or after calling this endpoint?
  • Are there common type checks, null checks, or transformations that appear repeatedly?
  • Do these patterns suggest the endpoint's design could be improved?

Example:

Before all calls of isPublicApi, there is a is KtSymbolWithVisibility type check that looks verbose.

Are usages correct?

Analyze whether the endpoint is being used as intended and whether there are better alternatives available.

Questions to evaluate:

  • Do the actual usages align with the endpoint's intended semantics and purpose?
  • Are there more appropriate or efficient alternatives that could solve the same problems?
  • If better alternatives exist, why aren't they being used?
    • Are the alternatives not well-documented or discoverable?
    • Do corner cases exist that the alternatives don't handle?
    • Is there missing functionality that forces developers to use suboptimal approaches?

Example:

resolveToCall() is often used to get the symbol for a successful call. Argument mapping and other facilities that resolveToCall()
provides are ignored. Possible reasons:

  1. It is not straightforward that resolveToSymbol() can only be called on a reference (mainReference). People use what is available in

    completion.

  1. resolveToCall() and resolveToSymbol() return different symbols in certain cases.

Design

Is it easy to abuse the endpoint?

Assess the potential for misuse and the robustness of the endpoint's design.

Questions to consider:

  • How easy is it to misuse this endpoint? Are there obvious ways it could be called incorrectly?
  • What are the consequences of misuse? (e.g., performance issues, incorrect results, crashes)
  • Are there enough safeguards in place? (e.g., parameter validation, clear error messages)
  • Are there any dangerous edge cases that aren't well-documented?

Is the endpoint generic enough?

Evaluate whether the endpoint strikes the right balance between generality and specificity.

Core vs. Convenience:

  • Is this part of core Analysis API functionality, or is it a convenience method?
  • If it's a convenience method:
    • Can it be implemented outside the Analysis API with equivalent performance?
    • Does it provide significant value beyond what users could easily implement themselves?
  • If it's specific to a particular functionality (e.g., auto-completion, refactoring):
    • Is this specialization clear from the endpoint's name and location?
    • Should it be moved to a different class or module?

Generality and Extensibility:

  • Are there opportunities to extract more generic functionality that could benefit other use cases?
    • Can the endpoint be extended to provide additional related functionality without breaking existing usage?
  • Is the abstraction level appropriate for its intended use cases?

Example:

containingJvmClassName returns the facade name for declarations in multi-file facades. While it might be helpful in understanding
how the method can be called from Java, it does not answer the question of where the actual bytecode will be.

Template

Use this template when creating documents or issues for endpoint analysis:

markdown
# Analysis API Endpoint Card: `EndpointName`

## Description

πŸ“ Provide a clear, concise description of what the endpoint does in 1-2 sentences.

## Understandability

πŸ“ Evaluate how easily developers can understand the endpoint's purpose and behavior.

**Related Issues found:**

πŸ“ Provide a list of related issues.

## Usages

### Where is the endpoint used?

πŸ“ Give approximate usage count and distribution across the library clients:

- [ ] Kotlin IntelliJ IDEA plugin
- [ ] External plugins (use the *Find External Usages* action)
- [ ] KSP
- [ ] Dokka
- [ ] Swift Export
- [ ] Other clients

### Are there common patterns?

πŸ“ Describe any repetitive code patterns around endpoint usage.

### Are usages correct?

πŸ“ Analyze whether the endpoint is being used as intended.

## Design

### Is it easy to abuse the endpoint?

πŸ“ Assess potential for misuse and robustness.

### Is the endpoint generic enough?

πŸ“ Evaluate the balance between generality and specificity.

## Recommendations

πŸ“ Summarize key findings and recommended actions:

1.
2.
3.

## Severity

πŸ“ Assess the endpoint severity based on its usage and impact:

- [ ] High (frequent usage, significant impact)
- [ ] Medium (moderate usage or impact)
- [ ] Low (infrequent usage, minimal impact)

Analysis Docs Design Documents Restricted Analysis (`analysis/docs/design-documents/restricted-analysis.md`)

Dumb Mode Support (Restricted Analysis)

Last update: February 12, 2025

Issue: KT-74090 Analysis API: Support dumb mode

Contributors: Marco Pennekamp, Yan Zhulanow

This is a design document describing how to support IntelliJ’s dumb mode in the Analysis API.

Historical context & motivation

At its inception, the Analysis API was designed without explicit dumb mode support. While it was possible to access the Analysis API during dumb mode, it was implicitly assumed that such accesses were illegal. The underlying reason was that the Analysis API heavily relies on indices and a sophisticated caching infrastructure.

Still, some components of the IntelliJ Kotlin plugin accessed the Analysis API during dumb mode, especially via light classes. As the Analysis API wasn’t designed to be used during dumb mode, dumb mode access to the Analysis API was explicitly forbidden with an exception (KT-63490).

In recent times, IntelliJ has pushed for the support of more and more functionality during dumb mode, in a general effort to improve the user experience during indexing. Dumb mode support for the Analysis API is highly requested to follow this trend.

As such, we decided to add dumb mode support to the Analysis API.

Challenges & goals

Dumb mode support has the following main challenges:

  • Index access: The Analysis API relies heavily on indices to perform even basic resolution, as symbol providers access indices to provide symbols from sources and libraries. Traditionally, index access was prohibited during dumb mode, resulting in IndexNotReadyExceptions, but nowadays it is possible to access only β€œreliable data.” More on that later.
  • Lack of consistency: The Analysis API operates in an immutable project context. Analysis can only be performed in read actions, where code or project structure modification is not possible. Caches are only invalidated when no analysis is performed. In contrast, indices change their state during dumb mode as indexing progresses, even during read actions. As indices are often the source of truth for LL FIR, their changing state introduces mutability into a usually immutable context.
  • Lack of tests in dumb mode: IntelliJ has very few tests which specifically test features during dumb mode. Our performance tests, which are also used as correctness tests, are equally not designed to be executed in dumb mode. Hence, it is difficult to gauge whether the Analysis API works correctly in dumb mode without manual testing or adding additional tests.

There are two dimensions to β€œcorrect” dumb mode support:

  • Correctness of results: This concerns whether the results that the Analysis API produces are correct. 100% correctness cannot be achieved in a project which is not fully indexed, and IntelliJ features are aware of this. Nonetheless, we need to consider correctness as there is certainly a bar where the usefulness of the Analysis API degrades markedly due to too many wrong results.
  • Exceptions: Inconsistencies in the cache state may cause Analysis API and FIR frontend assertions to be triggered. Such assertions typically detect compiler/lazy resolution bugs, but they assume a complete, unchanging project state.

The main risk of naive dumb mode support (without any mitigations) is the risk of exceptions which are difficult to diagnose and (almost) impossible to reproduce. Especially, exceptions from dumb mode should not mix with exceptions from smart mode in exception aggregation tools, as we might otherwise misdiagnose dumb mode exceptions as happening generally in IntelliJ.

However, the lack of tests makes it difficult to ensure that we have a comprehensive picture of the dumb mode support’s quality.

As such, the first implementation of dumb mode support has the following goals:

  • All Analysis API functionality should be allowed in dumb mode. We might disable or simplify certain features if we get too many exceptions for them, but that requires the data first.
  • We should monitor exceptions from dumb mode to gauge their impact on the user experience. Dumb mode exceptions should be separately classifiable in exception aggregation tools.
  • We should focus on exceptions over the correctness of results. The latter is flexible and issues with result correctness can be discovered on a feature-by-feature basis, but exceptions are disruptive to the user experience and cause features to stop working unexpectedly.
  • Due to the lack of tests, certain quality gates must be passed before the feature can be enabled for the general population.

Abstracting over β€œdumb mode” – Restricted Analysis

While the Analysis API is currently tied to the IntelliJ platform and has access to DumbService, it’s generally a good idea to decouple Analysis API concepts from the IntelliJ platform as much as possible. This gives a fair chance to other Analysis API platforms to implement their own conception of a β€œdumb mode” without having to hack around the IntelliJ platform.

Hence, the Analysis API’s version of β€œdumb mode” is called restricted analysis mode. The exact meaning of β€œrestricted” is up to the platform.

We define the following terms:

  • Restricted analysis mode: Analysis is currently restricted, e.g. dumb mode is enabled. The β€œmode” refers to the period of time during which restricted analysis might occur, not the restricted analysis itself.
  • Restricted analysis: This refers to analysis performed during restricted analysis mode. In contrast to the mode, β€œrestricted analysis” is a term for the analysis activity itself.

The optional KotlinRestrictedAnalysisService platform component will be added to allow the Analysis API platform to communicate whether analysis is currently restricted and whether restricted analysis is allowed.

This document describes both the behavior of the Analysis API under restricted analysis mode, as well as the changes that have to be made to the IntelliJ implementation. The Standalone Analysis API is not implementing any restricted analysis functionality at this time.

Analysis API behavior

Exception wrapping

To allow proper classification of exceptions that occurred during restricted analysis in exception aggregation tools, the Analysis API wraps all exceptions passing through analyze in a KaRestrictedAnalysisException during restricted analysis mode.

Even though analyze is an inline function, we can extend the existing try-finally in KaSessionProvider to wrap exceptions. Clients will have to recompile their analyze calls to benefit from the wrapping, but this is fine since the absence of wrapping doesn’t break anything. In particular, binary compatibility is not broken because the previous version of the analyze function will still work.

To avoid additional edits to analyze, the catch should simply call a function handleThrowable which does the wrapping. It should not be performed directly in analyze.

A test should be added for the exception wrapping, for example by mocking KotlinRestrictedAnalysisService and throwing an exception from the analyze block.

Platform-specific implementation

The IntelliJ implementation for KotlinRestrictedAnalysisService reports whether dumb mode is currently active.

Index access with reliable data

In IntelliJ, by default, indices cannot be accessed during dumb mode. It’s required to use a special DumbModeAccessType to access indices. The only access type which fits our needs is RELIABLE_DATA_ONLY, which at any given moment only exposes index data from files which have been fully indexed and are not marked as dirty.

It’s important to note that even with RELIABLE_DATA_ONLY, the index state will change as files are reindexed, even during a read action. This is the consistency issue mentioned in the introductory section. For now, we will ignore potential consistency issues. However, strategies for improving cache consistency are outlined at the bottom of the document.

We have two options for implementing β€œreliable data” access:

  1. The declaration provider, package provider, and other components of the Analysis API platform are responsible for accessing indexes correctly during restricted analysis mode. In IntelliJ, this means that components like declaration providers need to wrap their index access in RELIABLE_DATA_ONLY.ignoreDumbMode. Any index access reachable from the Analysis API should then be wrapped like this.
  2. KotlinRestrictedAnalysisService could provide a function withReliableDataAccess which essentially exposes DumbModeAccessType.ignoreDumbMode to the Analysis API. This would allow the Analysis API to wrap call sites where we expect index access and lift the burden off the Analysis API platform implementation. However, it’s questionable whether other platform implementations would follow the same strategy. Furthermore, it’s much easier to ensure proper coverage of β€œreliable data” access when we do it as close to the index access as possible.
    1. Alternatively, analyze could wrap the whole block in withReliableDataAccess, but this would further complicate the implementation of analyze. The change would also not be binary-compatible, as proper restricted analysis support would require clients to recompile usages of analyze. And finally, the wrapping might be unnecessary if no index access is performed (for example when all symbols and resolution results are already cached).

Given the complications with the second option, the first option is currently the best one.

Cache invalidation

To isolate the effects of restricted analysis from regular analysis, Analysis API caches must be fully invalidated upon exit from restricted analysis mode. We cannot rely on the state of any session whose information might have been built on incomplete indices, so even if there is no further modification upon exiting restricted analysis mode, we should invalidate such sessions.

This is the responsibility of the Analysis API platform. The platform should publish a global module state modification event to invalidate all caches. In IntelliJ, InvalidateCachesAfterDumbMode already handles this, but it needs to be lifted out of its workaround status. (If the additional write action causes freezes, we might be able to replace the global modification event with stop-the-world session invalidation, although this approach disregards other listeners of modification events. But any write action-less solution is likely premature and should only be considered if there are actual freeze reports.)

While it’s theoretically possible to invalidate only sessions which were affected by unindexed files, this is technically too complicated, as dirty files might even be added to the set of files to index during dumb mode (e.g. after another modification). Hence, it is easier to invalidate all caches.

Similarly, any entry to restricted analysis mode should start with fresh caches for all sessions that may be affected by unindexed files. For the time being, in IntelliJ, we will rely on the fact that dumb mode starts in a write action with the modification itself. Every modification which might affect Analysis API caches should result in a modification event, so active session invalidation should already ensure that we enter restricted analysis mode with fresh caches for affected sessions.

The exception might be application start-up: IntelliJ starts in smart mode and scans files at the same time. Here, indices can access stale data. When a lot of file changes are discovered, dumb mode is started. However, at this point we don’t have a modification event which we can rely on, so we might need to invalidate caches explicitly when entering dumb mode on start-up.

Feature flag

To mitigate the risks outlined above, the Kotlin plugin introduces a feature flag kotlin.analysis.allowRestrictedAnalysis. When the flag is enabled, restricted analysis as outlined in this document is allowed. When the flag is disabled, the previous Analysis API behavior is restored where an IndexNotReadyException is thrown when analysis is restricted.

Dumb mode tests

On the Kotlin plugin side, we can add dumb mode tests which access an index with partially reliable data. In other words, some of the files of the index are consistently not part of the reliable data set. This would allow us to write a dumb mode test which doesn’t suffer from timing issues around active reindexing.

While test infrastructure for β€œpartial index tests” doesn’t exist yet, it would be possible to implement such test infrastructure. As a (perhaps simpler) alternative, a test can mark specific files as dirty without triggering reindexing. This might achieve the same state where a few predefined files aren’t part of the reliable data set.

Dumb mode tests could also possibly be adapted from the Java language support.

While it would be great to achieve full test coverage of all features which can work in dumb mode, even having just a few dumb mode tests would be greatly beneficial as a sort of smoke test. A little is much, much better than nothing. That said, it is a non-goal of this proposal to scope out the exact tests that would need to be created.

Exception suppression

KaRestrictedAnalysisExceptions occur frequently, we can consider suppressing them in IntelliJ. If the quality gates aren’t met in reasonable time, suppressing exceptions might be a valid alternative to keeping the feature disabled for external users.

Quality criteria

The following quality criteria need to be assessed before the feature can be enabled by default:

  • Exceptions: The exceptions from restricted analysis must be at an acceptable level. The exact threshold hasn’t been defined, but it depends on both the number of exceptions and the nature of the exceptions.
    • To gauge the impact of the restricted analysis mode, we’ll monitor KaRestrictedAnalysisExceptions in exception aggregation tools. For every (popular) exception, we’ll try to investigate whether the exception needs to be fixed. The total number of exceptions will give a picture whether we need to take additional measures to ensure cache consistency (outlined further below).
  • Tests: At least a few dumb mode tests should be added on the Kotlin plugin side.
  • Manual QA: We should manually test IntelliJ with perpetual dumb mode enabled. While indices are unlikely to be in an inconsistent state during such testing, it might uncover other problems with the dumb mode support.

Cache consistency during restricted analysis

As mentioned above, we will forego measures to improve cache consistency at first. This is due to the complexity of implementing such measures. However, as various possibilities have already been considered and discussed, we want to outline them in this section.

That said, we can decide later to implement consistency improvements if the need arises.

Problem

As explained in the β€œChallenges” section, we have the following consistency challenge:

β€œThe Analysis API operates in an immutable project context. Analysis can only be performed in read actions, where code or project structure modification is not possible. Caches are only invalidated when no analysis is performed. In contrast, indices change their state during dumb mode as indexing progresses, even during read actions. As indices are often the source of truth for LL FIR, their changing state introduces mutability into a usually immutable context.”

Consider the following example:

kotlin
// File: A.kt (needs to be reindexed)
class A

// File: foo.kt (fully indexed)
fun foo1(): A = A() // Resolved *before* `A.kt` has been reindexed.
//          ^ error

fun foo2(): A = A() // Resolved *after* `A.kt` has been reindexed.
//          ^ valid

We could run into a cache state where the usage of A in foo1 is an error, but the usage in foo2 is valid. It’s unclear whether this will only result in incorrect answers from the Analysis API or even exceptions when consistency assertions are triggered in the frontend/LL FIR.

To summarize the problem:

  • Cached information may contradict other cached information.
  • We are uncertain whether this will lead to additional exceptions from consistency assertions.

Invalidation-based approach

If we can hook into events from indexing as files are reindexed, we might be able to invalidate caches at the right moment to ensure cache consistency or at least mitigate the risk of inconsistent caches somewhat.

IntelliJ offers a modification tracker for stub indices: StubIndex#getStubIndexModificationTracker, which is the associated tracker for an implementation of an index update event.

However, even if we can get such an event during dumb mode, indices are updated constantly. This means that such an event would be fired constantly, and probably outside a write action. If it’s fired outside a write action, we can only perform session invalidation with stop-the-world session invalidation. In the worst case, which might actually be quite common if indexing events are fired fast enough, analysis will stall completely as we’re queueing stop-the-world session invalidations after one another.

Furthermore, given that index updates can happen inside read actions as well, an invalidation-based solution would actually not solve the issue, because the index update event will be fired after the index has already been updated. If analysis is running at the time, it’ll see the updated index midway through. Even worse, stop-the-world session invalidation will wait for these analysis calls to finish (with the already updated index), inviting the same consistency exceptions that we want to avoid.

Hence, an invalidation-based approach is not a viable solution.

Scope-based approach

To avoid an inconsistent cache state, we could pin the files that can be accessed by declaration providers to the start of the dumb mode. We’d create a scope which covers all the files that were invalidated and need to be reindexed. Even if a file has been reindexed and is available again, it will be excluded by the scope from declaration providers until dumb mode ends. Hence, the Analysis API would have a stable view on the project.

While it’s a great idea, the solution has a few problems when it comes to the details:

  • While we can treat declarations from a file as non-existing, the file can still be opened in the IDE. As the Analysis API generally strives to be able to provide a KaSymbol for a given (analyzable) PSI declaration, we might run into additional consistency errors when it’s not possible to provide a KaSymbol for a declaration from an excluded file, or to ultimately resolve a FIR symbol for a PSI-based KaSymbol.
    • Since we exclude the files only from the declaration provider’s scope, but not the content scope of the module, such PSI declarations would definitely still be seen as analyzable.
    • To mitigate this problem, we can introduce file-based declaration providers for all excluded files. However, this moves the issue from the project level (one project-wide exclusion scope) to the module level (file-based declaration providers per module). While we can easily get a set of dirty files, associating them with KaModules might be costly, or even difficult/error-prone (since the files are currently dirty).
    • Furthermore, scaling file-based declaration providers with a large number of excluded files might hit memory or performance limits at some point.
  • While it’s currently possible to retrieve a list of dirty files, it might change in the future.
  • During dumb mode, modification events still occur. While cache invalidation will invalidate affected caches, we'd also need to update the project-wide set of excluded files. It is not a huge issue to add modification event-based invalidation here, but it makes the solution more complex.
  • Additional files may be marked as dirty during dumb mode, so the set of excluded files would need to grow. If the change caused a modification event, we may be able to rely on our invalidation mechanisms. But in any case, this adds complexity.

In summary, the solution has significant hurdles which might even cause more exceptions or performance issues than cache inconsistencies would.

Timestamp-based approach

If we can get a timestamp of when an index entry was last reindexed, we can discard any results that were updated after dumb mode started. This would allow us to pin the cache state to a temporal point before dumb mode started (as opposed to a spatial point with the scope-based approach).

This idea is nice, but doesn’t match with the contracts guaranteed by indices. Conceptually, each virtual file has its own modification counter which acts somewhat like a timestamp. But we’d have to remember the modification count of every single virtual file before dumb mode starts for comparison, which is obviously impossible.

Furthermore, the solution also suffers from the same issue as the scope-based solution: the Analysis API wants to allow building a KaSymbol for every PSI declaration in the scope of a context module. If the PSI declaration is not contained in indices, we might not be able to provide a FIR symbol for it, or run into other consistency exceptions.


Analysis Docs Design Documents Project Structure Content And Resolution Scope Architecture (`analysis/docs/design-documents/project-structure/content-and-resolution-scope-architecture.md`)

Architecture of Content Scopes & Resolution Scopes

Last update: January 28, 2025

Issue: KT-73290 Analysis API: Improve the architecture of content scopes and resolution scopes

This document is an implementation design document for KT-73290. Its aim is to outline the technical architecture that should be implemented to improve the consistency of content scopes and resolution scopes.

Overview

Summary of general ideas (elaborated on in the sections below):

  • We distinguish between KaModule content scopes and base content scopes. The former is the module’s full content scope with all enlargements and restrictions, while the latter is the scope defined by the platform which derives from the project structure.
  • Analysis API platforms and the Analysis API implementation have the ability to enlarge and restrict content scopes with custom KotlinContentScopeRefiners.
  • Content scopes are computed from base content scopes using KaContentScopeProvider. This service becomes the default implementation of KaModule.contentScope.
  • Resolution scopes are computed from a module’s content scope and its dependencies’ content scopes. The responsible service KaResolutionScopeProvider uses a fixed algorithm and is implemented on the Analysis API side.

Base content scopes & content scopes

A KaModule has two kinds of scopes:

  • Base content scope: The scope which covers the module’s content according to the project structure. This scope needs to be provided by platform implementations.
  • Content scope: The scope which covers the module’s content taking run-time extensions into account. For example, resolve extensions (one example of a run-time extension) may adjust the content scope to shadow some files and include other files. This scope is computed by the Analysis API implementation from the base content scope and additional refinements.

The reason for this split derives from architectural problems associated with only having content scopes:

  • Content scope refinement depends on the KaModule as we need an anchor point for the calculation. This means that content scope calculation would need to be lazy (as we need to construct the KaModule before we can get the content scope), which is an additional implementation burden placed on the Analysis API platform. It also makes the KaModule API more messy by leaking implementation considerations into the documentation.
    • In particular, resolve extensions lead to a chicken and egg problem. To construct the full content scope of a KaModule, we need to calculate the shadowed scopes and whether a module has resolve extensions, but we also need to have the KaModule in hand to call KaResolveExtensionProvider.provideExtensionsFor. This requires laziness.
  • As a practical consideration, KaModule implementations would have to repeat calls to the engine service which refines content scopes. In essence, each KaModule.contentScope implementation would look like this: lazy { refineContentScope(baseContentScope) }. At that point, it’s easier to simply formalize the concept of a β€œbase content scope.”

As for the implementation, we make the following changes to KaModule:

  • Introduce a property baseContentScope.
  • Provide a default implementation for contentScope which delegates to KaContentScopeProvider (introduced further below).

Content scope refiners

Content scopes can be enlarged and restricted by KotlinContentScopeRefiners. They allow specifying additional scopes which are either applied as a union or an intersection to the original content scope. They are platform components registered via an extension point.

The enlargement (union) scopes are applied first, and in a second step the restriction (intersection) scopes, regardless of the order of registered KotlinContentScopeRefiners. This ensures that the restriction scopes can also cover any files which are added via enlargement scopes.

Content scopes are immutable for the lifetime of the KaModule and scope refiners must be consistent with that. So scope refiners must create the same enlargement and restriction scopes for the same KaModule. (This restriction should be documented in KotlinContentScopeRefiner.)

The recently added KotlinResolutionScopeEnlarger should be removed in favor of and covered by KotlinContentScopeRefiner.

Interface sketch (analysis-api-platform-interface)

kotlin
package org.jetbrains.kotlin.analysis.api.platform.projectStructure

public interface KotlinContentScopeRefiner : KotlinPlatformComponent {
    public fun getEnlargementScopes(module: KaModule): List<GlobalSearchScope> = emptyList()
    public fun getRestrictionScopes(module: KaModule): List<GlobalSearchScope> = emptyList()
}

Resolve extensions

Resolve extensions hook into the refining mechanism:

  • Negated shadowed scopes are applied as restriction scopes, i.e. the content scope is restricted to all files which are not in the shadowed scope.
  • A resolve extension also enlarges the content scope with its own generated files.
    • This will be handled in a follow-up issue: KT-74541 Analysis API: Include files generated by resolve extensions in `KaModule` content scopes

To support this, we would implement a KotlinContentScopeRefiner for resolve extensions in the Analysis API, likely in analysis-api-impl-base.

Content scope computation

We implement a KaContentScopeProvider which calculates a module’s content scope from the base scope and associated KotlinContentScopeRefiners.

KaContentScopeProvider should be an analysis-api service so that we can have a default implementation for KaModule.contentScope. It needs to be marked with @KaPlatformInterface.

Resolution scope computation

We implement a KaResolutionScopeProvider which calculates a resolution scope from the module’s content scope and the content scopes of all dependencies using a fixed algorithm.

KaResolutionScopeProvider should be an analysis-api service so that users can get a resolution scope for the KaModule. Alternatively, we can introduce a property KaModule.resolutionScope which uses KaResolutionScopeProvider as a default implementation. Then the provider mirrors KaContentScopeProvider and needs to be marked with @KaPlatformInterface.

The current KotlinResolutionScopeProvider platform component should be removed. Its usages need to be replaced with KaResolutionScopeProvider. Its implementations should become the basis for the implementation of KaResolutionScopeProvider.

The scope created by KaResolutionScopeProvider should be a modified implementation of the current KaGlobalSearchScope, as it needs to additionally check for generated files.

Source scope optimization in IdeKotlinByModulesResolutionScopeProvider

IdeKotlinByModulesResolutionScopeProvider contains special handling for source modules in excludeIgnoredModulesByKotlinProjectModel. This special handling, especially the usage of moduleWithDependenciesAndLibrariesScope, should be removed completely.

It was a performance optimization for inefficient union resolution scopes, which shouldn’t be necessary anymore now that we have scope merging. And in any case, this implementation would be incorrect in the new model, since it would disregard the adjusted content scopes of individual modules.

The new KaResolutionScopeProvider does not need to handle ignored source module dependencies (like in excludeIgnoredModulesByKotlinProjectModel) since dependencies are already ignored when they’re calculated in KtSourceModuleByModuleInfo.


--- METRICS ---