KTIJ-36616 Add KotlinSubclassesIndex to graph for Kotlin CRI support

New index was introduced because doing the same filtration on consumer side requires a lot of requests to the graph, which could add significant IO overhead.

Space-RevId: 4b3db4beadbbe5063090363f74135541ee451c63

GitOrigin-RevId: e9707eb8ddcf4d18df534c73e9a750f95bab7185
This commit is contained in:
Aleksei.Cherepanov
2026-01-22 12:40:18 +00:00
committed by intellij-monorepo-bot
parent c0a1e7da7c
commit af74b3c7a6
4 changed files with 94 additions and 17 deletions
@@ -165,6 +165,11 @@ public final class KotlinMeta implements JvmMetadata<KotlinMeta, KotlinMeta.Diff
return container instanceof KmClass? ((KmClass)container).getSealedSubclasses() : Collections.emptyList();
}
public Iterable<KmType> getSupertypes() {
KmDeclarationContainer container = getDeclarationContainer();
return container instanceof KmClass? ((KmClass)container).getSupertypes() : Collections.emptyList();
}
public Visibility getContainerVisibility() {
KmDeclarationContainer container = getDeclarationContainer();
return container instanceof KmClass? Attributes.getVisibility((KmClass)container) : Visibility.PUBLIC;
@@ -0,0 +1,65 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package org.jetbrains.jps.dependency.kotlin;
import kotlin.metadata.KmClassifier;
import kotlin.metadata.KmType;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.jps.dependency.MapletFactory;
import org.jetbrains.jps.dependency.Node;
import org.jetbrains.jps.dependency.ReferenceID;
import org.jetbrains.jps.dependency.impl.BackDependencyIndexImpl;
import org.jetbrains.jps.dependency.java.JvmClass;
import org.jetbrains.jps.dependency.java.JvmNodeReferenceID;
import org.jetbrains.jps.dependency.java.KotlinMeta;
import org.jetbrains.jps.util.Iterators;
import java.util.Collections;
import java.util.Objects;
/**
* <code>KotlinSubclassesIndex</code> back-dependency index for Kotlin classes that tracks direct subclass relationships.
* This index is used by the <code>KotlinCompilerReferenceIndex</code>.
*
* <p>The index is built based on <b>Kotlin metadata</b> and applies several filters:
* <ul>
* <li>Java classes are excluded from values, so only JvmClass nodes with Kotlin metadata are accepted.
* In fact, this means that this index stores relations like: [Kotlin/Java]Nodes to [Kotlin]Nodes</li>
* <li>Only Kotlin classes with classKind = CLASS are indexed (filters out annotations, Kotlin file facades, etc.)</li>
* <li>Local classes are excluded from indexing</li>
* <li>The kotlin.Any supertype is filtered out as it's implicit for all Kotlin classes</li>
* </ul>
*/
public class KotlinSubclassesIndex extends BackDependencyIndexImpl {
public static final String NAME = "kotlin-direct-subclasses";
public KotlinSubclassesIndex(@NotNull MapletFactory cFactory) {
super(NAME, cFactory);
}
@Override
public Iterable<ReferenceID> getIndexedDependencies(@NotNull Node<?, ?> node) {
if (!(node instanceof JvmClass)) {
return Collections.emptyList();
}
JvmClass classNode = (JvmClass)node;
KotlinMeta kotlinMeta = KJvmUtils.getKotlinMeta(classNode);
if (kotlinMeta == null || kotlinMeta.getKind() != 1 // CLASS
|| classNode.isLocal()) {
return Collections.emptyList();
}
Iterable<String> fqNames =
Iterators.filter(
Iterators.map(
Iterators.filter(
Iterators.map(kotlinMeta.getSupertypes(), KmType::getClassifier),
kmClassifier -> kmClassifier instanceof KmClassifier.Class
),
kmClassifier -> ((KmClassifier.Class)kmClassifier).getName()
), fqName -> !Objects.equals(fqName, "kotlin/Any")
);
return Iterators.map(fqNames, name -> new JvmNodeReferenceID(name));
}
}
@@ -13,6 +13,7 @@ import org.jetbrains.annotations.Nullable;
import org.jetbrains.jps.dependency.DependencyGraph;
import org.jetbrains.jps.dependency.GraphConfiguration;
import org.jetbrains.jps.dependency.impl.DependencyGraphImpl;
import org.jetbrains.jps.dependency.kotlin.KotlinSubclassesIndex;
import org.jetbrains.jps.dependency.kotlin.LookupsIndex;
import java.io.*;
@@ -119,7 +120,7 @@ public class StorageManager implements CloseableExt {
if (isKotlinCriDataGenerationEnabled) {
return new DependencyGraphImpl(
containerFactory,
DependencyGraphImpl.IndexFactory.create(LookupsIndex::new)
DependencyGraphImpl.IndexFactory.create(LookupsIndex::new, KotlinSubclassesIndex::new)
);
}
else {
@@ -8,10 +8,10 @@ import org.jetbrains.jps.dependency.BackDependencyIndex
import org.jetbrains.jps.dependency.DependencyGraph
import org.jetbrains.jps.dependency.ReferenceID
import org.jetbrains.jps.dependency.java.JvmNodeReferenceID
import org.jetbrains.jps.dependency.java.SubclassesIndex
import org.jetbrains.jps.dependency.kotlin.KotlinSubclassesIndex
import org.jetbrains.jps.dependency.kotlin.LookupsIndex
const val VERSION = 3
const val VERSION = 4
internal fun prepareSerializedData(graph: DependencyGraph): ByteArray {
val serializedDataResult = ByteArrayOutputStream().use { byteStream ->
@@ -19,10 +19,12 @@ internal fun prepareSerializedData(graph: DependencyGraph): ByteArray {
dataOut.writeVersion()
// process subtypes
val subclassesIndex: BackDependencyIndex? = graph.getIndex(SubclassesIndex.NAME)
val subclassesCount = subclassesIndex?.keys?.count() ?: 0
val subclassesIndex: BackDependencyIndex? = graph.getIndex(KotlinSubclassesIndex.NAME)
val subclassesFilteredKeys = subclassesIndex?.keys
?.filter { key -> subclassesIndex.getDependencies(key).iterator().hasNext() }
val subclassesCount = subclassesFilteredKeys?.count() ?: 0
dataOut.writeInt(subclassesCount)
subclassesIndex?.keys
subclassesFilteredKeys
?.sortedBy { it.toFqName() }
?.forEach { key ->
val dependencies = subclassesIndex.getDependencies(key).map { it.toFqName() }.sorted()
@@ -63,23 +65,27 @@ internal fun prepareSerializedData(graph: DependencyGraph): ByteArray {
}
/**
* Converts the current `ReferenceID` to its fully qualified name (FqName) representation.
* Converts a JVM reference ID to a dot-separated fully qualified name.
*
* Example transformation:
* Input: `com/intellij/AppKt$foo$3$1`
* Output: `com.intellij.AppKt.foo`
*
* @return The fully qualified name as a `String`.
* Examples:
* - `com/example/Foo` → `com.example.Foo`
* - `com/example/Foo$Bar` → `com.example.Foo.Bar`
* - `com/example/Foo$1` → `com.example.Foo`
* - `com/example/Foo$$Lambda$3` → `com.example.Foo.$Lambda`
*/
private fun ReferenceID.toFqName(): String {
return (this as? JvmNodeReferenceID)?.nodeName
?.replace('/', '.')
?.replace(DOLLAR_DIGITS_SUFFIX_REGEX, "")
?.replace('$', '.')
?: this.toString()
val jvmName = (this as? JvmNodeReferenceID)?.nodeName ?: return toString()
return jvmName
.replace('/', '.')
.replace("$$", DOUBLE_DOLLAR_PLACEHOLDER)
.replace(DOLLAR_DIGITS_SUFFIX_REGEX, "")
.replace('$', '.')
.replace(DOUBLE_DOLLAR_PLACEHOLDER, ".$")
}
private val DOLLAR_DIGITS_SUFFIX_REGEX = Regex("(?:\\$\\d+)+$")
private const val DOUBLE_DOLLAR_PLACEHOLDER = "\u0000DOUBLE_DOLLAR\u0000"
private fun String.addFilePathIfNeeded(fileIdToPathEntryAccumulator: MutableMap<String, Int>): Int {
return fileIdToPathEntryAccumulator.computeIfAbsent(this) { fileIdToPathEntryAccumulator.size + 1 }