[LL FIR] KT-57207 Combine Java symbol providers
- `LLFirCombinedJavaSymbolProvider` combines multiple `JavaSymbolProvider`s. Its advantages are: combined index access, caching, classpath order disambiguation. - Scopes can still be optimized with a combined scope instead of a naive union scope. ^KT-57207 fixed
This commit is contained in:
committed by
Space Team
parent
3da3e14543
commit
e13d4f2328
+91
@@ -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<JavaSymbolProvider>,
|
||||||
|
private val javaClassFinder: JavaClassFinder,
|
||||||
|
) : LLFirSelectingCombinedSymbolProvider<JavaSymbolProvider>(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<ClassId, FirRegularClassSymbol> = 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<FirCallableSymbol<*>>, packageFqName: FqName, name: Name) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@FirSymbolProviderInternals
|
||||||
|
override fun getTopLevelFunctionSymbolsTo(destination: MutableList<FirNamedFunctionSymbol>, packageFqName: FqName, name: Name) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@FirSymbolProviderInternals
|
||||||
|
override fun getTopLevelPropertySymbolsTo(destination: MutableList<FirPropertySymbol>, packageFqName: FqName, name: Name) {
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getPackage(fqName: FqName): FqName? = providers.firstNotNullOfOrNull { it.getPackage(fqName) }
|
||||||
|
|
||||||
|
override fun computePackageSetWithTopLevelCallables(): Set<String>? = null
|
||||||
|
override fun knownTopLevelClassifiersInPackage(packageFqName: FqName): Set<String>? = null
|
||||||
|
override fun computeCallableNamesInPackage(packageFqName: FqName): Set<Name>? = null
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun merge(session: FirSession, project: Project, providers: List<JavaSymbolProvider>): 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
-1
@@ -67,7 +67,7 @@ internal class LLFirCombinedKotlinSymbolProvider private constructor(
|
|||||||
@OptIn(FirSymbolProviderInternals::class)
|
@OptIn(FirSymbolProviderInternals::class)
|
||||||
private fun computeClassLikeSymbolByClassId(classId: ClassId): FirClassLikeSymbol<*>? {
|
private fun computeClassLikeSymbolByClassId(classId: ClassId): FirClassLikeSymbol<*>? {
|
||||||
val candidates = declarationProvider.getAllClassesByClassId(classId) + declarationProvider.getAllTypeAliasesByClassId(classId)
|
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)
|
return provider.getClassLikeSymbolByClassId(classId, ktClass)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+11
-7
@@ -47,35 +47,39 @@ abstract class LLFirSelectingCombinedSymbolProvider<PROVIDER : FirSymbolProvider
|
|||||||
* should be delegated. This is a post-processing step that preserves classpath order when, for example, an index access with a combined
|
* should be delegated. This is a post-processing step that preserves classpath order when, for example, an index access with a combined
|
||||||
* scope isn't guaranteed to return the first element in classpath order.
|
* scope isn't guaranteed to return the first element in classpath order.
|
||||||
*/
|
*/
|
||||||
protected fun <ELEMENT : PsiElement> selectFirstElementInClasspathOrder(candidates: Collection<ELEMENT>): Pair<ELEMENT, PROVIDER>? {
|
protected fun <CANDIDATE> selectFirstElementInClasspathOrder(
|
||||||
|
candidates: Collection<CANDIDATE>,
|
||||||
|
getElement: (CANDIDATE) -> PsiElement?,
|
||||||
|
): Pair<CANDIDATE, PROVIDER>? {
|
||||||
if (candidates.isEmpty()) return null
|
if (candidates.isEmpty()) return null
|
||||||
|
|
||||||
// We're using a custom implementation instead of `minBy` so that `ktModule` doesn't need to be fetched twice.
|
// 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 currentPrecedence: Int = Int.MAX_VALUE
|
||||||
var currentKtModule: KtModule? = null
|
var currentKtModule: KtModule? = null
|
||||||
|
|
||||||
for (candidate in candidates) {
|
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
|
// 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
|
// 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`.
|
// results for `candidate`.
|
||||||
val precedence = modulePrecedenceMap[ktModule] ?: continue
|
val precedence = modulePrecedenceMap[ktModule] ?: continue
|
||||||
if (precedence < currentPrecedence) {
|
if (precedence < currentPrecedence) {
|
||||||
currentElement = candidate
|
currentCandidate = candidate
|
||||||
currentPrecedence = precedence
|
currentPrecedence = precedence
|
||||||
currentKtModule = ktModule
|
currentKtModule = ktModule
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val element = currentElement ?: return null
|
val candidate = currentCandidate ?: return null
|
||||||
val ktModule = currentKtModule ?: error("`currentKtModule` must not be `null` when `currentElement` has been found.")
|
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`
|
// 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.
|
// and a precedence for `currentKtModule` must have been found in the previous step.
|
||||||
val provider = providersByKtModule.getValue(ktModule)
|
val provider = providersByKtModule.getValue(ktModule)
|
||||||
|
|
||||||
return Pair(element, provider)
|
return Pair(candidate, provider)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
@@ -539,6 +539,7 @@ internal class LLFirSessionCache(private val project: Project) {
|
|||||||
) {
|
) {
|
||||||
SymbolProviderMerger(this, destination).apply {
|
SymbolProviderMerger(this, destination).apply {
|
||||||
merge<LLFirProvider.SymbolProvider> { LLFirCombinedKotlinSymbolProvider.merge(session, project, it) }
|
merge<LLFirProvider.SymbolProvider> { LLFirCombinedKotlinSymbolProvider.merge(session, project, it) }
|
||||||
|
merge<JavaSymbolProvider> { LLFirCombinedJavaSymbolProvider.merge(session, project, it) }
|
||||||
merge<FirExtensionSyntheticFunctionInterfaceProvider> { LLFirCombinedSyntheticFunctionSymbolProvider.merge(session, it) }
|
merge<FirExtensionSyntheticFunctionInterfaceProvider> { LLFirCombinedSyntheticFunctionSymbolProvider.merge(session, it) }
|
||||||
finish()
|
finish()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ val JavaClass.classKind: ClassKind
|
|||||||
else -> ClassKind.CLASS
|
else -> ClassKind.CLASS
|
||||||
}
|
}
|
||||||
|
|
||||||
internal fun JavaClass.hasMetadataAnnotation(): Boolean =
|
fun JavaClass.hasMetadataAnnotation(): Boolean =
|
||||||
annotations.any { it.classId?.asSingleFqName() == JvmAnnotationNames.METADATA_FQ_NAME }
|
annotations.any { it.classId?.asSingleFqName() == JvmAnnotationNames.METADATA_FQ_NAME }
|
||||||
|
|
||||||
internal fun Any?.createConstantOrError(session: FirSession): FirExpression {
|
internal fun Any?.createConstantOrError(session: FirSession): FirExpression {
|
||||||
|
|||||||
Reference in New Issue
Block a user