The compiler reuses the IntelliJ IDEA Java PSI (Program Structure Interface) to analyze Java code (e.g., to resolve a Java method's return type). To perform such analysis, the Java resolver under the hood needs to understand Kotlin code if it is referenced from Java. However, Kotlin PSI is not compatible with Java PSI, so some bridging is required to allow the Java resolver to work with Kotlin PSI.
Thus, Light Classes are read-only synthetic Java PSI representations of Kotlin declarations.
Simple examples:
PsiMethod
s)PsiAnnotation
s)In most cases, light classes mirror the Kotlin JVM bytecode of the corresponding Kotlin declarations, so the bytecode should be treated as the source of truth.
There is no strict one-to-one relationship between Kotlin PSI and their counterparts in Java PSI. For instance, even if a Kotlin class doesn‘t declare any constructors, a default constructor will still be generated at the JVM bytecode level, and it will be callable from Java. Similarly, in the PsiClass
light class, there will be a PsiMethod
for it, but that method will lack a link to the source KtPrimaryConstructor
(as it isn’t present in the source code).
The name “Light Classes” comes from the initial implementation, inherited from AbstractLightClass.
Light classes in IntelliJ IDEA represent PSI not backed by any real files.
Also, they are “light” in the sense that they don't have to implement all APIs of Java PSI.
Light classes are essential for Java resolver interoperability. They allow Java code to analyze references to Kotlin code.
In particular, they provide support for:
Java Highlighting: Java code can seamlessly call and reference Kotlin code in the editor
Nested Kotlin resolution: There might be cross-references between Kotlin and Java code, using different code analyzers for each language.
Example:
// FILE: KotlinUsage.kt class KotlinUsage: JavaClass() { fun foo() { bar() } }
// FILE: JavaClass.java public class JavaClass extends KotlinBaseClass { }
// FILE: KotlinBaseClass.kt open class KotlinBaseClass { fun bar() {} }
To resolve bar()
in foo()
, the Kotlin resolver would ask JavaClass
about its hierarchy. To answer that, the Java resolver would need to resolve KotlinBaseClass
, which cannot be done without light classes.
PsiElement
s created for source declarations, light classes provide a read-only view of Kotlin declarations. Calling mutating methods on them will result in an exceptionKotlinAsJavaSupport
to find light classes by FQNPsiElement?
utilitiesSome common sense guidelines:
The latest implementation of light classes is powered by the Analysis API. Currently, it is used only for sources (KT-77787).
The main benefit of using the Analysis API is that compiler plugins are supported out of the box by SLC.
A limitation is that SLC must adhere to the resolution contracts of the Kotlin compiler. This means that some scenarios are sensitive to the amount of resolution work performed. In the worst case, this can cause contract violations.
Example: 8afeffee.
The most sensitive place is class members creation (e.g., SymbolLightClassForClassOrObject#getOwnMethods) as it is heavily used in the Java resolver.
The implementation is registered in SymbolKotlinAsJavaSupport.
The implementation of light classes which uses .class
Kotlin output to build Java stubs and provide Java PSI mapping “out of the box”. The implementation is straightforward but has some limitations due to its simplicity. Mainly, it doesn't support constructs that are stored in the bytecode differently than they would be in Java. For instance, type annotations are not supported.
The entry point is DecompiledLightClassesFactory
The next evolution step: KT-77787 Replace DLC with SLC
The K1 implementation of light classes which is built on top of Kotlin PSI.
Location: compiler/light-classes