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 | Managed by a |
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.
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.
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:
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:
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:
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.