blob: c2df0f75d772aa99a266d7def68342fc7278d847 [file] [view] [edit]
# Java Facade for Kotlin compiler
The module is intended to replace the PSI-based facade that pulls a lot of IntelliJ platform code into compiler
with a lightweight "direct" implementation.
## Status
The module is functional and integrated into the compiler via the compiler option (`-Xjava-direct`).
The old PSI-based java class finder is still used for binary classes (via `CombinedJavaClassFinder`) due to some
quirks of the FIR providers architecture. On the next iteration it should be replaced with FIR-based symbol providers.
## Purpose
Kotlin has bidirectional Java interop, meaning that in a module there could be both Java files referencing Kotlin declarations
and vice versa. Therefore, Kotlin compiler cannot rely on Java files being available in a binary form, when the Kotlin sources
are compiled and need to process Java sources directly, extract the declarations and make them accessible for the FIR resolution.
This was implemented initially via the infrastructure from the IntelliJ platform, often referred to as PSI-based Java facade.
(There was also an attempt to implement it via unofficial `javac` APIs, but it wasn't properly supported.)
## Architecture
### Output
The module provides a so-called "Java model" as the output, that is the implementation of the interfaces defined
in the `org.jetbrains.kotlin.load.java.structure` package in the `core.jvm` module.
The access is provided via the implementation of the `JavaClassFinder` interface.
### Laziness
Since we only need to consider Java declarations, which are accessed from FIR resolution, and there could be modules with many Java files
and very little interop in this direction, the implementation is made as lazy as possible. It starts with source roots analysis:
for the directory-based roots we consider that the directory structure should correspond to the package structure and only access
files when the corresponding package is requested. For the file-based roots the files are scanned without parsing to extract the
package name and top-level classes and then parsed only if later requested from FIR.
Parsing is done eagerly into a light-tree structure (see below), but further extraction of the Java Model is also done lazily with some
caching on top.
### Parser
The module uses lightweight KMP parsers infrastructure being developed in the IntelliJ platform, but without pulling the heavy
parts of the platform with it. The libraries (`org.jetbrains:syntax-api` and `org.jetbrains:java-syntax`) are extracted and published
independently by the Fleet team.
The parsing produces a light-tree structure similar to the Kotlin light-tree parser, but without any use of the IntelliJ platform-specific
infrastructure.
### Java model entry point
`JavaClassFinderOverAstImpl` - main entry point: finds sources-based classes and packages by FqName. It implements the `JavaClassFinder`
interface, which is used by Fir for accessing all Java declarations, including source-based ones.
It retuns Java model entities: classes in the `org.jetbrains.kotlin.java.direct.model` package.
In most cases they use parsed data to lazily construct sub-elements, but in cases where some external (to this element) entities are
required, the resolution mechanism (see below) is used to find these elements and expose it accordingly.
### Resolution
The Java model requires symbol resolution, for many cases, such as references to Java source declarations in the same module, references
to library declarations, and references to Kotlin declarations. These "external" declarations are expected by the model to be "resolved",
i.e., expressed in the terms of the Java model and exposed accordingly. E.g. `JavaClass.supertypes` is expected to return a collection of
`JavaClassifierType`s regardless of whether those are Java declarations in the same set of java sources, binary java classes or Kotlin
declarations for Kotlin sources in the same module.
In contrast to the PSI-based facade, the `java-direct` module uses FIR-based resolution for all non-Java-sources references, via the
FirSession stored in the class finder. Then the resulting Fir is wrapped to the Java model representation (see `FirBackedJava*` classes).
#### Main entities participating in the resolution
- classes in the `org.jetbrains.kotlin.java.direct.model` are main entry points to the resolution: they call other helpers at the point they need to resolve the "external" declarations.
- `JavaResolutionContext` is the current declaration context information required for resolving names referenced by the declaration; it contains information collected from an enclosing file as well as a semantical "scope" (see below)
- `JavaTypeResolver.kt` is a set of helpers for resolving names (strings) to `ClassId`s, `FqName`s and values
- `JavaScopeResolver.kt` is an additional set of helper for resolving names to Java model entities in the current scope (type parameters (including inherited), containing class, top level classes from the same file, nested classes)
- `JavaImportResolver.kt` is a holder + extractors for resolved imports
- `JavaInheritedMemberResolver` - supertype-hierarchy traversal for inherited member types
- `JavaExternalConstResolver.kt` - set of helper for accessing const values via `FirExpressionEvaluator`
- `JavaModelSessionAccess.kt` - cycle breakers for the resolution logic + `TYPE_USE` annotations cache
- `FirBackedJavaClassAdapter` - an adapter from ClassId to `JavaClass`, lazily resolved via Fir
#### Main resolution scenarios
### Scenario A — Classifier for a type reference (model entry dispatcher)
Entry: `JavaTypeOverAst.computeClassifier`
1. Split the reference into `rawTypeNameParts` (identifiers only; annotations / `<...>` dropped).
2. If single-part, try in priority order and return the first hit:
1. own type parameter — `JavaScopeResolver.findTypeParameter` (high priority).
2. in-scope class — `JavaScopeResolver.findClassInCurrentScope` (Scenario C).
3. inherited (outer) type parameter — `findInheritedTypeParameter` (low priority, shadowed by 2).
3. Resolve `parts[0]` via `findClassInCurrentScope`. If it is an AST `JavaClass`, navigate each
remaining part with `declaredOrSameFileInherited` and return the final inner class (same-file
AST path).
4. Otherwise (cross-file) resolve the whole name to a `ClassId` via `JavaTypeResolver.resolve`
(Scenarios B/D) and wrap it in a `FirBackedJavaClassAdapter` (`classifierAdapterFor`).
5. If nothing matched, return `null` (FIR's `findClassId` fallback then runs).
Corner cases: type-parameter-vs-inner-class shadowing (2 before 3); same-file multi-segment
navigation handled purely on AST without touching the symbol provider.
### Scenario B — Simple name to `ClassId` (JLS 6.4.1 shadowing ladder)
Entry: `JavaTypeResolver.resolve` → `resolveSimpleNameToClassIdImpl`. A flat ordered ladder; each
step probes candidate `ClassId`s through `tryResolve` and returns the first hit.
1. **Local scope** (`resolveFromLocalScope`) — member types declared *and* inherited by the
containing-class chain, walked innermost→outermost, interleaving declared and inherited per
level (Scenario D for the inherited part). *(skipped in the reentrance-safe flavor)*
2. **Same-file top-level** (`resolveFromSameFile`) — via `sameFileTopLevelClassProvider`.
3. **Single-type import** (`resolveFromExplicitImport`) — `import a.b.C;`, rank 4.
4. **Single-static import, type arm** (`resolveFromStaticSingleImport`) — `import static a.b.C.X;`,
rank 4, probed after step 3.
5. **Same-package, other file** (`resolveFromSamePackage`) — `ClassId(package, name)`.
6. **`java.lang.*`** (`resolveFromJavaLang`) — implicit import; also accepts a
`JavaToKotlinClassMap` hit.
7. **Type-import-on-demand** (`resolveFromTypeStarImports`) — `import a.b.*;`, rank 6; falls back to
member types of an imported *class* (`import a.D.*`).
8. **Static-import-on-demand** (`resolveFromStaticStarImports`) — `import static a.b.C.*;`, rank 7.
Corner cases: rank-4 type import probed before rank-4 static import; star-import ambiguity →
`null`; the class-as-`PackageOrTypeName` fallback in steps 7–8.
### Scenario C — In-scope (AST) classifier lookup
Entry: `JavaScopeResolver.findClassInCurrentScope`. AST-only; produces a structural `JavaClass`
with its full outer chain (needed for navigation and outer-arg substitution).
1. Inner class **declared or same-file-inherited** by the containing class
(`declaredOrSameFileInherited` → `findInnerClassInSameFileSupertypes`).
2. Inner class **inherited from supertypes** of the containing class
(`JavaInheritedMemberResolver.findInnerClassFromSupertypes`) — runs before step 3 because an
inherited member type shadows a merely lexically-enclosing one (JLS 6.4.1).
3. Sibling inner class of the immediate outer class.
4. Inner class of each further outer class up the containing chain.
5. Same-file top-level class (`sameFileTopLevelClassProvider`).
Corner case: the same-file supertype walk works on **raw AST text**
(`directSupertypeRefNames`), deliberately distinct from the resolved-classifier walk in
`JavaInheritedMemberResolver`, to avoid re-entering type construction; package-qualified
supertypes are declined here and handed to the `ClassId` path.
### Scenario D — Qualified / nested name to `ClassId` (JLS 6.5.5)
Entry: `JavaTypeResolver.resolve` (dotted name) → `resolveQualifiedNameToClassIdFromParts`.
A single left-to-right pass mirroring javac's PackageOrTypeName classification (JLS 6.5.4):
1. **Leftmost type** (JLS 6.5.4): the first segment as a simple type name in scope (Scenario B);
failing that, the package prefix grows one segment at a time until a segment names a
top-level type in it (`java.util.List` → packages `java`, `java.util`, type `List`).
2. **Member-type descent** (JLS 6.5.5.2): each remaining segment must be a member type of the
previous one — declared, or inherited from its supertypes (`findInheritedNestedClass`,
supertype walk + finder).
We resolve qualified names like javac: once a segment resolves to a type, we commit to that reading
and never backtrack. If a later segment turns out not to be a real nested class, we don't retry the
name as a package — we report the missing nested name, and everything after it stays unresolved.
The consequence: when a package and a type share a name (JLS 6.1), the type shadows the package.
That matches javac and differs from the PSI Java model, which falls back to the package reading.
Tests live in the java-direct-owned `testData/diagnostics` root: `qualifiedNamePackageClassClash.kt`
and `PackageVsClass2.kt` (KT-87813).
Corner cases: `Map.Entry`-style inherited nested classes.
### Scenario E — Inherited member type via supertypes
Entry: `JavaInheritedMemberResolver`. Two complementary outputs:
- `findInnerClassFromSupertypes` → a `JavaClass` with AST outer chain (for the AST pipeline /
outer-arg substitution); uses same-file supertypes plus the `LeanJavaClassFinder` for cross-file
Java source.
- `resolveInheritedInnerClassToClassId` → a bare `ClassId` via a two-pass BFS:
1. **`walkJavaSourceSupertypes`** — Java-source supertypes through the finder's source index,
resolving each level against *that file's* imports; independent of FIR lazy phases.
2. **`walkBinarySupertypes`** — Kotlin / binary supertypes through the per-origin
`directSupertypeClassIds` dispatcher (Scenario F).
Both passes share a `visited` set, detect cross-pass ambiguity (→ `null`), and are bounded by
`MAX_SUPERTYPE_DEPTH = 5`.
### Scenario F — Direct-supertype `ClassId` graph
Entry: `JavaTypeResolver.directSupertypeClassIds`, guarded by `cycleGuardedSupertypeWalk`.
Per-origin dispatch:
1. **Source Java** — finder has the class in its index: walk `JavaClass.supertypes` and read each
`classifier.classId` (no FIR phase).
2. **Binary Java** — symbol is a `FirJavaClass`: read the pre-resolved
`directSupertypeClassIds()` cache (never triggers enhancement).
3. **Kotlin / built-in / deserialized** — `lazyResolveToPhase(SUPER_TYPES)` then read
`superTypeRefs` cone class ids.
Corner case: `Java.Source` (lazy `superTypeRefs`) must be distinguished from `Java.Library`
(pre-populated) to avoid premature-resolution cycles — handled by routing source Java through
the finder arm, not the FIR arm.
### Scenario G — Implicit outer-class type-argument recovery
Entry: `JavaTypeResolver.recoverInheritedOuterTypeArguments`, used by
`JavaTypeOverAst.computeTypeArguments` for a bare inherited inner-class reference whose outer args
are neither written nor lexically in scope.
1. From the lexical containing class, walk **outward** (its outer classes already have supertypes
resolved).
2. Stop the walk at the first `static` class along the chain (a static nested class severs the
enclosing-instance chain — JLS).
3. For each outer, descend its `FirBackedJavaClassAdapter.supertypes` looking for the inner
class's outer `ClassId`, substituting type args down each intermediate class
(`findTypeArgsForClassInHierarchy` / `substituteTypeArgs`).
4. Return the recovered args as FIR-backed `JavaType`s, or `null` (top-level inner, no containing
class, static break, or not found).
### Scenario H — Annotation reference resolution
Entry: `JavaAnnotationOverAst`. Reuses the type pipeline: `JavaTypeResolver.resolve` on the
annotation's written name (same import/scope rules as Scenario B/D), yielding the annotation's
`ClassId`. The no-symbol-provider fixtures fall back to a package+name heuristic. TYPE_USE-ness for
filtering is answered by `JavaModelSessionAccess.isTypeUseAnnotationClass` (cached per session,
inspects the annotation class's own `@Target`).
### Scenario I — Cross-language constant value resolution
Entry: `JavaExternalConstResolver`, used by `JavaFieldOverAst.initializerValue` and the
enum-entry-vs-`const val` disambiguation in annotation arguments.
- `resolveExternalFieldValue(qualifier, field)` — tries, in order: top-level property via JVM
facade (`MainKt.FOO`), class member, companion-object member; returns the evaluated literal.
- `resolveConstFieldValue(classId, field)` — enum class → companion only; otherwise class member
then companion then top-level facade fallback.
Const values are read via `FirExpressionEvaluator` / already-evaluated initializers; unqualified
cross-language references are unsupported (return `null`).
### Integration
The new java facade builder is introduced to allow substituting the implementation depending on the compiler option.
### Tests
The module contains unit tests and also "steals" all phased diagnostics and box tests that contain Java files from the main compiler
testdata.