Migrating from K1
For many years, the only practical way to analyze Kotlin code in IntelliJ IDEA and other tools was to use the Kotlin compiler's internals, an unsafe API not designed for external usage.
The Analysis API, on the other hand, offers a much cleaner and robust set of utilities. It exposes almost the same set of concepts. Thus, if you already have code that depends on the Kotlin compiler, migrating it to the new API should not be time-consuming. This migration guide outlines the differences between the APIs and explains how to port the descriptor-based resolution logic to the Analysis API.
Once an IntelliJ plugin has been migrated to the Analysis API, it will need to declare its compatibility with the K2 Kotlin mode in the plugin.xml
. Otherwise, the plugin will not be loaded when the K2 mode is active.
The Conceptual Difference
The cornerstone of the old compiler API is a BindingContext
, a universal dictionary:
BindingContext
maps syntactic declarations (KtDeclaration
) to their semantic parts,DeclarationDescriptor
s. For instance, for aKtFunction
that represents a function in code, there is aFunctionDescriptor
with semantic information, which includes resolved parameter and return types.In a similar fashion, a type reference (
KtTypeReference
) is mapped to aKotlinType
with resolved type attributes.For calls, including
KtCallExpression
, there is aResolvedCall
object containing data collected by call resolution and inference machinery.
A BindingContext
merely acts as a storage that is filled up during declaration analysis. In the compiler, an instance of BindingContext
goes through the whole compilation pipeline, collecting more and more semantic information, and used later for code generation. The IDE does not, however, analyze the entire project at once, as it would take a lot of time. Instead, a declaration is analyzed only when some IDE feature needs to understand it better.
Therefore, in the IDE, a BindingContext
only contains mappings for declarations that have already been analyzed. The correct filling and use of the dictionary is the caller's responsibility.
Moreover, classes such as DeclarationDescriptor
and KotlinType
do not have any specific lifecycle, so there's nothing stopping them from being passed around or cached. This is unfortunate because these classes usually retain the entire compiler resolution session, a very heavy tree of objects. As sporadic errors on outdated descriptor usage are rare, it becomes easy to inadvertently create a substantial memory leak.
The Analysis API significantly simplifies this by offering a single analyze {}
entry point. Inside the analysis block, all declarations are analyzed on-demand, eliminating occasional "descriptor was not found for declaration" errors. Further, entities representing declarations and types can only be accessed from within the owning analysis block. This way, The Analysis API not only guards against memory leaks but also ensures that all analysis results are consistent.
Analysis Entry Point
The Kotlin 1.0 compiler itself does not offer any stable code analysis entry points. However, the Kotlin IntelliJ IDEA plugin, which is built on top of the compiler, does provide several:
These are not the only ones available – there are numerous more sophisticated ones, including analyzeWithAllCompilerChecks()
, analyzeWithContent()
, analyzeInContext()
, and others.
In contrast, the Analysis API offers a single analyze {}
entry point. Most of the API surface is accessible inside the lambda, including the symbol
extension property that maps a KtDeclaration
to its symbol:
This is not just a syntax difference. The Analysis API requires all analysis-related code to be housed in a single location. You can, of course, extract parts of the logic to separate functions, and even create your set of utilities. Nevertheless, you cannot freely mix symbols from unrelated analysis sessions.
While this change might seem like a significant new restriction, it actually has always been in place. Careless handling of BindingContext
and its contents was often a source of exceptions, incorrect behavior, and leaks. Therefore, the new API naturally guides you on how to analyze the code correctly.
You can read more about the API entity lifetime in the KaLifetimeOwner documentation section.
Declarations
Both the old compiler API and the Analysis API are built on top of PsiElement
, the API in IntelliJ IDEA responsible for creating syntax trees. However, unlike some other language implementations, Kotlin distinctly separates PSI and semantic declaration representation. Refer to the Symbols vs. PSI section for additional information.
In the old compiler, this semantic representation is called DeclarationDescriptor
. There are specific interfaces for each declaration type, including ClassDescriptor
, FunctionDescriptor
, and PropertyDescriptor
. Descriptors are obtained from a BindingContext
:
In the Analysis API, a concept similar to descriptors is named KaSymbol
. Just like descriptors, there are KaClassSymbol
, KaFunctionSymbol
, and KaPropertySymbol
.
To get a symbol for a KtDeclaration
, simply use the symbol
extension property. symbol
is overloaded for subtypes of KaSymbol
, meaning that if you call it on some specific declaration type, you will get a more precise symbol type. For instance:
For a KtClassOrObject
, however, symbol
will return just a KaDeclarationSymbol
. The reason for this is Kotlin PSI's legacy: A KtEnumEntry
is a subtype of KtClassOrObject
, whereas in K2, an enum entry is a variable. So, you may wish to use ktClassOrObject.classSymbol
instead, as it will return a KaClassSymbol?
.
Approach the symbol documentation for more detailed information about symbols.
Declaration Names
In the old compiler, FqName
was often used to store fully-qualified declaration names. Although it is a straightforward abstraction, FqName
cannot differentiate between package and classifier components. For instance, in foo.Bar.Baz
, Bar
could either be a package or an outer class name. While Kotlin's coding conventions discourage capitalized package names, technically it remains possible. Consequently, the Analysis API employs a different abstraction for storing qualified names, namely ClassId
for classes and CallableId
for functions and properties.
To construct a ClassId
, merely pass its text representation to ClassId.fromString()
. The slashes /
separate package components, whereas dots .
distinguish nested class names.
The StandardClassIds
class provides ClassId
s for many common class names from the Kotlin standard library.
You can construct a CallableId
by supplying either an outer ClassId
or a package FqName
and a callable name.
Classes
Class-related hierarchies are quite similar in both K1 and K2 APIs.
The difference is that in the Analysis API, named and anonymous classes have distinct subclasses.
- Getting simple and qualified class names
Old API:
classDescriptor.name
,classDescriptor.classId
,classDescriptor.fqNameSafe
Analysis API:
classSymbol.name
,classSymbol.classId
(FqName
represents nested classes ambiguously, always useClassId
)- Checking class kind
Old API:
classDescriptor.kind == ClassKind.INTERFACE
Analysis API:
classSymbol.classKind == KaClassKind.INTERFACE
- Checking ordinary class traits
Old API:
classDescriptor.isData
Analysis API:
(classSymbol as? KaNamedClassSymbol)?.isData
(anonymous classes cannot bedata
)- Getting class supertypes
Old API:
classDescriptor.typeConstructor.supertypes
Analysis API:
classSymbol.superTypes
There is no
TypeConstructor
abstraction in the Analysis API. Use symbols directly.- Getting class declarations
Old API:
classDescriptor.unsubstitutedMemberScope.getContributedDescriptors()
Analysis API:
classSymbol.memberScope.declarations
(orclassifiers
,callables
for specific kinds)
Functions
Both APIs have almost the same set of classes representing functions, constructors and property accessors.
In the Analysis API, the hierarchy is rather flat. The most significant change is that anonymous function is not an ordinary "named" function anymore.
- Getting simple and qualified callable names
Old API:
functionDescriptor.fqNameSafe
Analysis API:
functionDescriptor.callableId
- Getting parameter and return types
Old API:
functionDescriptor.valueParameters.map { it.type }
,functionDescriptor.returnType
Analysis API:
functionSymbol.valueParameters.map { it.returnType }
,functionSymbol.returnType
- Checking function visibility
Old API:
functionDescriptor.visibility == DescriptorVisibilities.PUBLIC
Analysis API:
functionSymbol.visibility == KaSymbolVisibility.PUBLIC
- Checking function traits
Old API:
functionDescriptor.isInline
Analysis API:
functionSymbol.isInline
Variables
In the old API, the variable hierarchy was quite basic.
In the Analysis API, backing fields, receiver parameters enum entries became a part of the variable hierarchy. The changes reflect evolution of these concepts in the language and the K2 compiler.
- Getting getter and setter
Old API:
propertyDescriptor.getter
,propertyDescriptor.setter
Analysis API:
propertySymbol.getter
,propertySymbol.setter
- Getting a return type
Old API:
propertyDescriptor.type
Analysis API:
propertySymbol.returnType
Calls and references
Check out the difference between reference and call resolution in the References and calls article.
The old API offered a single ResolveCall
class that represented all kinds of calls (successful and error calls, simple and compound calls). For compound calls, ResolveCall
itself represents only one of calls, while additional data was available in quite obscure places, like CallTransformer.CallForImplicitInvoke
.
The Analysis API makes the distinction between calls explicit, making it harder to forget about more sophisticated call kinds.
In addition, the Analysis API separates successful and error calls. In the IDE, the user edits and refactors code all the time, and source files often contain unresolved or ambiguous references. Handling them properly (or skipping them) during static checks is important.
- Resolving a reference
Old API:
bindingContext[BindingContext.REFERENCE_TARGET, referenceExpression]
Analysis API:
referenceExpression.mainReference.resolveToSymbol()
- Resolving a successful call
Old API:
expression.getResolvedCall(bindingContext)?.takeIf { it.isReallySuccess() }
Analysis API:
expression.resolveToCall().successfulCallOrNull()
- Getting parameter-argument mapping
Old API:
val call = unaryExpression.getResolvedCall(bindingContext) ?: return call.valueArgumentsAnalysis API:
val call = expression.resolveToCall()?.successfulFunctionCallOrNull() ?: return call.argumentMapping- Getting dispatch and extension receivers
Old API:
val call: ResolvedCall<*> = ... operatorCall.dispatchReceiver operatorCall.extensionReceiverAnalysis API:
val call: KaCallableMemberCall<*, *> = ... call.partiallyAppliedSymbol.dispatchReceiver call.partiallyAppliedSymbol.extensionReceiver
Types
The old compiler had a sophisticated hierarchy of KotlinType
s, including wrapped, deferred and delegating types. The Analysis API provides much simpler API that actually represents all language types.
The KaType
is a base interface for all Kotlin types. The most common type kind is a KaClassType
, which represents not-null and nullable class types, such as kotlin.Int
or List<String>?
.
Check the type documentation for more details.
- Resolving a type reference
Old API:
context[BindingContext.TYPE, typeReference]
Analysis API:
typeReference.type
- Getting an expression type
Old API:
expression.getType(bindingContext)
Analysis API:
expression.expressionType
- Checking for a built-in type
Old API:
KotlinBuiltIns.isUnit(type)
Analysis API:
type.isUnitType
- Checking for a primitive type
Old API:
KotlinBuiltIns.isPrimitiveType(type)
Analysis API:
type.isPrimitive
Misc
- Getting a containing declaration
Old API:
descriptor.containingDeclaration
Analysis API:
symbol.containingDeclaration
orsymbol.containingSymbol
- Checking for annotation presence
Old API:
descriptor.annotations.hasAnnotation(FqName("kotlin.jvm.JvmName"))
Analysis API:
ClassId.fromString("kotlin/jvm/JvmName") in symbol.annotations
Check the Annotations documentation for more details.
Example
Below, the same annotation check is implemented with the old compiler API, and with the Analysis API.