Symbols¶
Symbols are a core concept in the Analysis API, representing declarations visible to Kotlin. Symbols are created not only for declarations in source files but also for declarations from Kotlin and Java libraries, and those generated by compiler plugins and the compiler itself.
Symbols vs. PSI¶
While the PSI represents the concrete syntax in Kotlin code, KaSymbol provides access to
the semantic information of declarations. The table below summarizes the key differences:
| Feature | KtDeclaration | KaSymbol |
|---|---|---|
| Focus | Syntax structure of source files | Semantic information container (regardless of declaration origin) |
| Information | Lexical and syntactic information (text) | Resolved semantic information (name, type, visibility, etc.) |
| Mutability | Mutable (via PSI modifications) | Immutable (a different symbol is created after PSI modifications) |
| Lifetime | Tied to a containing KtFile |
Managed by a KaSession |
Semantic information¶
In the context of the Analysis API symbols, the word "semantic" refers to the meaning and relationships of declarations within the code from the compiler's standpoint, as opposed to their purely syntactic representation.
Here are a couple of examples of semantic symbol information that is unavailable in KtDeclaration (which primarily
focuses on syntactic structure).
Resolved types¶
While a KtCallableDeclaration can provide the declared type of a function or property, KaSymbol provides the
resolved type, which takes into account imports, aliases, and other parts of the lexical context.
In the following function, the PSI can only say that the text parameter has some type called String, while the
The Analysis API can check that it indeed is kotlin.String, and not something like my.app.String.
Visibility and accessibility¶
KaSymbol provides information about the visibility of a symbol (public, private, etc.), even if it is not explicitly
stated in the PSI.
abstract class Base {
protected abstract fun check(value: String): Boolean
}
class Impl : Base() {
override fun check(value: String) = value.isNotEmpty()
}
Even though there is no protected modifier for Impl.check(), the compiler knows it is there.
By providing access to this semantic information, the Analysis API symbols enable a more powerful and comprehensive analysis of Kotlin code.
Kinds of symbols¶
The graph below shows the relation between high-level parts of the KaSymbol type hierarchy.
graph TD
KaSymbol
KaSymbol --> KaFileSymbol
KaSymbol --> KaPackageSymbol
KaSymbol --> KaDeclarationSymbol
KaDeclarationSymbol --> KaClassifierSymbol
KaDeclarationSymbol --> KaCallableSymbol
KaDeclarationSymbol --> KaScriptSymbol
KaSymbol is a root type for the whole symbol hierarchy. Here are a few subtypes of it:
KaFileSymbol: Represents a Kotlin file.KaPackageSymbol: Represents a package with declarations and subpackages.KaDeclarationSymbol: Represents a declaration in a Kotlin file.KaClassifierSymbol: Represents a class, type alias, or a type parameter.KaCallableSymbol: Represents a function or a variable.KaScriptSymbol: Represents a script declaration (implicitly nested in a script file).
See more information on specific subtypes of KaSymbol in dedicated documentation pages.
Getting a KaSymbol¶
To get a KaSymbol instance, use the symbol extension property:
// Let's say you have a KtNamedFunction
val ktNamedFunction: KtNamedFunction = ...
analyze(ktNamedFunction) {
val functionSymbol = ktNamedFunction.symbol
// Although the PSI element is named 'KtNamedFunction',
// it can be an anonymous one (the name is 'null')
if (functionSymbol is KaNamedFunctionSymbol) {
// Here we have a truly named function
check(functionSymbol.isOperator)
}
}
This code snippet retrieves the symbol for a KtNamedFunction. You can then check the type of the symbol and access its
properties, such as its return type, visibility, and more.
The symbol property is overloaded, and the compiler automatically chooses the most specific variant.
For a KtDeclaration, it will return a base KaDeclarationSymbol.
However, for a KtTypeAlias the result will be a KaTypeAliasSymbol.
The KtClassOrObject hierarchy includes a KtEnumEntry. In the old compiler, an enum entry was a class with a specific
kind. In the Analysis API, though, similarly to the K2 compiler, it is a KaVariableSymbol. Because of this
inconsistency, the Analysis API provides specific nullable versions of symbol properties:
public val KtClassOrObject.classSymbol: KaClassSymbol?
public val KtClassOrObject.namedClassSymbol: KaNamedClassSymbol?
KaSymbolPointer¶
KaSymbolPointer is a mechanism for referencing a KaSymbol across different analysis sessions. Since symbols
are valid only within the specific analysis session in which they were created,
symbol pointers offer a way to persist references and restore the corresponding symbols later.
Creating a pointer¶
To create a pointer, use the createPointer() function available for all KaSymbol instances:
Restoring a symbol¶
To restore a symbol from a KaSymbolPointer, use the restoreSymbol() extension function within an analyze block:
analyze(someKtElement) {
val restoredSymbol = functionPointer.restoreSymbol()
if (restoredSymbol != null) {
// Use the restored symbol
}
}
The restoreSymbol() function attempts to restore the symbol within the current analysis session. It returns the
restored symbol, or null if the declaration does not exist anymore, or it cannot be accessed from the use-site
session.
Use Cases¶
Symbol pointers are essential for scenarios where you need to maintain references to symbols across different analysis sessions. Some common use cases include:
- Caching analysis results: Store symbol pointers for later retrieval and analysis.
- Cross-module analysis: Reference symbols from different modules within the same project.
- Long-running operations: Maintain references to symbols during background tasks or across multiple user interactions.
By using symbol pointers, you can ensure that your code maintains valid references to symbols, avoids potential issues with invalidated state, and does not cause memory drain.