tree: 40afd536ffa5d1718a90c27c56ca6d986967eb4d [path history] [tgz]
  1. resources/
  2. src/
  3. testData/
  4. testFixtures/
  5. tests-gen/
  6. build.gradle.kts
  7. README.md
analysis/symbol-light-classes/README.md

Light Classes

What are Light Classes?

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:

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).

Name

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.

What are Light Classes for?

Light classes are essential for Java resolver interoperability. They allow Java code to analyze references to Kotlin code.

In particular, they provide support for:

  1. Java Highlighting: Java code can seamlessly call and reference Kotlin code in the editor

  2. 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.

What are Light Classes not for?

  1. Modifications: Unlike PsiElements created for source declarations, light classes provide a read-only view of Kotlin declarations. Calling mutating methods on them will result in an exception
  2. Kotlin Code Analysis: Light classes are not intended for Kotlin code analysis, as they only provide resolution-required information. For instance, declarations that are not visible from Java code might not be represented. Anti-examples:
    • Code insight for Kotlin code
    • UAST

Entry Points

  1. LightClassUtilsKt – a set of PSI utilities to get light classes
  2. KotlinAsJavaSupport – a service that provides light classes. Usually not used directly, but rather via utilities
  3. JavaElementFinder – the main entry point for Java resolve. It uses KotlinAsJavaSupport to find light classes by FQN
  4. (TBD KT-78862) KaSymbol -> PsiElement? utilities

Implementations

Some common sense guidelines:

  • Light classes should be as lightweight as possible to avoid affecting performance (both CPU and memory)
  • Prefer not to store information directly in light classes unless necessary (e.g., a name computation can be done lazily and stored in a hard reference if it is on a hot path). It is usually a trade-off between performance and memory usage
  • Potentially heavy computations should be performed lazily

Symbol Light Classes (a.k.a. SLC)

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.

Decompiled Light Classes (a.k.a. DLC)

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

Ultra Light Classes (a.k.a. ULC)

The K1 implementation of light classes which is built on top of Kotlin PSI.

Location: compiler/light-classes