diff --git a/analysis/low-level-api-fir/src/org/jetbrains/kotlin/analysis/low/level/api/fir/providers/LLFirCombinedJavaSymbolProvider.kt b/analysis/low-level-api-fir/src/org/jetbrains/kotlin/analysis/low/level/api/fir/providers/LLFirCombinedJavaSymbolProvider.kt new file mode 100644 index 00000000000..a43d33e2840 --- /dev/null +++ b/analysis/low-level-api-fir/src/org/jetbrains/kotlin/analysis/low/level/api/fir/providers/LLFirCombinedJavaSymbolProvider.kt @@ -0,0 +1,91 @@ +/* + * Copyright 2010-2023 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +package org.jetbrains.kotlin.analysis.low.level.api.fir.providers + +import com.intellij.openapi.project.Project +import com.intellij.psi.search.GlobalSearchScope +import org.jetbrains.kotlin.analysis.low.level.api.fir.caches.NullableCaffeineCache +import org.jetbrains.kotlin.analysis.low.level.api.fir.project.structure.llFirModuleData +import org.jetbrains.kotlin.analysis.project.structure.KtModule +import org.jetbrains.kotlin.fir.FirSession +import org.jetbrains.kotlin.fir.java.JavaSymbolProvider +import org.jetbrains.kotlin.fir.resolve.providers.FirSymbolProvider +import org.jetbrains.kotlin.fir.resolve.providers.FirSymbolProviderInternals +import org.jetbrains.kotlin.fir.symbols.impl.* +import org.jetbrains.kotlin.load.java.JavaClassFinder +import org.jetbrains.kotlin.load.java.createJavaClassFinder +import org.jetbrains.kotlin.load.java.structure.impl.JavaClassImpl +import org.jetbrains.kotlin.name.ClassId +import org.jetbrains.kotlin.name.FqName +import org.jetbrains.kotlin.name.Name + +/** + * [LLFirCombinedJavaSymbolProvider] combines multiple [JavaSymbolProvider]s with the following advantages: + * + * - For a given class ID, indices can be accessed once to get relevant PSI classes. Then the correct symbol provider(s) to call can be + * found out via the PSI element's [KtModule]s. This avoids the need to call every single subordinate symbol provider. + * - A small Caffeine cache can avoid most index accesses, because many names are requested multiple times, with a minor memory footprint. + * + * [javaClassFinder] must have a scope which combines the scopes of the individual [providers]. + */ +internal class LLFirCombinedJavaSymbolProvider private constructor( + session: FirSession, + project: Project, + providers: List, + private val javaClassFinder: JavaClassFinder, +) : LLFirSelectingCombinedSymbolProvider(session, project, providers) { + /** + * The purpose of this cache is to avoid index access for frequently accessed `ClassId`s, including failures. Because Java symbol + * providers currently cannot benefit from a "name in package" check (see KTIJ-24642), the cache should also store negative results. + */ + private val classCache: NullableCaffeineCache = NullableCaffeineCache { it.maximumSize(2500) } + + override fun getClassLikeSymbolByClassId(classId: ClassId): FirClassLikeSymbol<*>? = + classCache.get(classId) { computeClassLikeSymbolByClassId(it) } + + private fun computeClassLikeSymbolByClassId(classId: ClassId): FirRegularClassSymbol? { + val javaClasses = javaClassFinder.findClasses(classId) + if (javaClasses.isEmpty()) return null + + val (javaClass, provider) = selectFirstElementInClasspathOrder(javaClasses) { javaClass -> + // `JavaClass` doesn't know anything about PSI, but we can be sure that `findClasses` returns a `JavaClassImpl` because it's + // using `KotlinJavaPsiFacade`. The alternative to this hack would be to change the interface of either `JavaClass` (yet the + // module should hardly depend on PSI), or to have `KotlinJavaPsiFacade` and `JavaClassFinderImpl` return `JavaClassImpl` and to + // return `JavaClassFinderImpl` from `createJavaClassFinder`. + check(javaClass is JavaClassImpl) { "`findClasses` as used here should return `JavaClassImpl` results." } + javaClass.psi + } ?: return null + + return provider.getClassLikeSymbolByClassId(classId, javaClass) + } + + @FirSymbolProviderInternals + override fun getTopLevelCallableSymbolsTo(destination: MutableList>, packageFqName: FqName, name: Name) { + } + + @FirSymbolProviderInternals + override fun getTopLevelFunctionSymbolsTo(destination: MutableList, packageFqName: FqName, name: Name) { + } + + @FirSymbolProviderInternals + override fun getTopLevelPropertySymbolsTo(destination: MutableList, packageFqName: FqName, name: Name) { + } + + override fun getPackage(fqName: FqName): FqName? = providers.firstNotNullOfOrNull { it.getPackage(fqName) } + + override fun computePackageSetWithTopLevelCallables(): Set? = null + override fun knownTopLevelClassifiersInPackage(packageFqName: FqName): Set? = null + override fun computeCallableNamesInPackage(packageFqName: FqName): Set? = null + + companion object { + fun merge(session: FirSession, project: Project, providers: List): FirSymbolProvider? = + if (providers.size > 1) { + val combinedScope = GlobalSearchScope.union(providers.map { it.session.llFirModuleData.ktModule.contentScope }) + val javaClassFinder = project.createJavaClassFinder(combinedScope) + LLFirCombinedJavaSymbolProvider(session, project, providers, javaClassFinder) + } else providers.singleOrNull() + } +} diff --git a/analysis/low-level-api-fir/src/org/jetbrains/kotlin/analysis/low/level/api/fir/providers/LLFirCombinedKotlinSymbolProvider.kt b/analysis/low-level-api-fir/src/org/jetbrains/kotlin/analysis/low/level/api/fir/providers/LLFirCombinedKotlinSymbolProvider.kt index d907939c7b3..9bb9938b70e 100644 --- a/analysis/low-level-api-fir/src/org/jetbrains/kotlin/analysis/low/level/api/fir/providers/LLFirCombinedKotlinSymbolProvider.kt +++ b/analysis/low-level-api-fir/src/org/jetbrains/kotlin/analysis/low/level/api/fir/providers/LLFirCombinedKotlinSymbolProvider.kt @@ -67,7 +67,7 @@ internal class LLFirCombinedKotlinSymbolProvider private constructor( @OptIn(FirSymbolProviderInternals::class) private fun computeClassLikeSymbolByClassId(classId: ClassId): FirClassLikeSymbol<*>? { val candidates = declarationProvider.getAllClassesByClassId(classId) + declarationProvider.getAllTypeAliasesByClassId(classId) - val (ktClass, provider) = selectFirstElementInClasspathOrder(candidates) ?: return null + val (ktClass, provider) = selectFirstElementInClasspathOrder(candidates) { it } ?: return null return provider.getClassLikeSymbolByClassId(classId, ktClass) } diff --git a/analysis/low-level-api-fir/src/org/jetbrains/kotlin/analysis/low/level/api/fir/providers/LLFirSelectingCombinedSymbolProvider.kt b/analysis/low-level-api-fir/src/org/jetbrains/kotlin/analysis/low/level/api/fir/providers/LLFirSelectingCombinedSymbolProvider.kt index 4d688697f94..b19807fc1ae 100644 --- a/analysis/low-level-api-fir/src/org/jetbrains/kotlin/analysis/low/level/api/fir/providers/LLFirSelectingCombinedSymbolProvider.kt +++ b/analysis/low-level-api-fir/src/org/jetbrains/kotlin/analysis/low/level/api/fir/providers/LLFirSelectingCombinedSymbolProvider.kt @@ -47,35 +47,39 @@ abstract class LLFirSelectingCombinedSymbolProvider selectFirstElementInClasspathOrder(candidates: Collection): Pair? { + protected fun selectFirstElementInClasspathOrder( + candidates: Collection, + getElement: (CANDIDATE) -> PsiElement?, + ): Pair? { if (candidates.isEmpty()) return null // We're using a custom implementation instead of `minBy` so that `ktModule` doesn't need to be fetched twice. - var currentElement: ELEMENT? = null + var currentCandidate: CANDIDATE? = null var currentPrecedence: Int = Int.MAX_VALUE var currentKtModule: KtModule? = null for (candidate in candidates) { - val ktModule = projectStructureProvider.getKtModuleForKtElement(candidate) + val element = getElement(candidate) ?: continue + val ktModule = projectStructureProvider.getKtModuleForKtElement(element) // If `ktModule` cannot be found in the map, `candidate` cannot be processed by any of the available providers, because none of // them belong to the correct module. We can skip in that case, because iterating through all providers wouldn't lead to any // results for `candidate`. val precedence = modulePrecedenceMap[ktModule] ?: continue if (precedence < currentPrecedence) { - currentElement = candidate + currentCandidate = candidate currentPrecedence = precedence currentKtModule = ktModule } } - val element = currentElement ?: return null - val ktModule = currentKtModule ?: error("`currentKtModule` must not be `null` when `currentElement` has been found.") + val candidate = currentCandidate ?: return null + val ktModule = currentKtModule ?: error("`currentKtModule` must not be `null` when `currentCandidate` has been found.") // The provider will always be found at this point, because `modulePrecedenceMap` contains the same keys as `providersByKtModule` // and a precedence for `currentKtModule` must have been found in the previous step. val provider = providersByKtModule.getValue(ktModule) - return Pair(element, provider) + return Pair(candidate, provider) } } diff --git a/analysis/low-level-api-fir/src/org/jetbrains/kotlin/analysis/low/level/api/fir/sessions/LLFirSessionCache.kt b/analysis/low-level-api-fir/src/org/jetbrains/kotlin/analysis/low/level/api/fir/sessions/LLFirSessionCache.kt index cd913feeeab..b7e19b5bf03 100644 --- a/analysis/low-level-api-fir/src/org/jetbrains/kotlin/analysis/low/level/api/fir/sessions/LLFirSessionCache.kt +++ b/analysis/low-level-api-fir/src/org/jetbrains/kotlin/analysis/low/level/api/fir/sessions/LLFirSessionCache.kt @@ -539,6 +539,7 @@ internal class LLFirSessionCache(private val project: Project) { ) { SymbolProviderMerger(this, destination).apply { merge { LLFirCombinedKotlinSymbolProvider.merge(session, project, it) } + merge { LLFirCombinedJavaSymbolProvider.merge(session, project, it) } merge { LLFirCombinedSyntheticFunctionSymbolProvider.merge(session, it) } finish() } diff --git a/compiler/fir/java/src/org/jetbrains/kotlin/fir/java/JavaUtils.kt b/compiler/fir/java/src/org/jetbrains/kotlin/fir/java/JavaUtils.kt index 6c9036786fc..5596f0d5a39 100644 --- a/compiler/fir/java/src/org/jetbrains/kotlin/fir/java/JavaUtils.kt +++ b/compiler/fir/java/src/org/jetbrains/kotlin/fir/java/JavaUtils.kt @@ -52,7 +52,7 @@ val JavaClass.classKind: ClassKind else -> ClassKind.CLASS } -internal fun JavaClass.hasMetadataAnnotation(): Boolean = +fun JavaClass.hasMetadataAnnotation(): Boolean = annotations.any { it.classId?.asSingleFqName() == JvmAnnotationNames.METADATA_FQ_NAME } internal fun Any?.createConstantOrError(session: FirSession): FirExpression {