[Analysis API] Introduce a `KaUseSiteVisibilityChecker`
as a replacement for `KaVisibilityChecker.isVisible`
`KaUseSiteVisibilityChecker` is designed to make multiple requests
for the same use site without recomputing use-site related information
KT-74246
diff --git a/analysis/analysis-api-fe10/src/org/jetbrains/kotlin/analysis/api/descriptors/components/KaFe10VisibilityChecker.kt b/analysis/analysis-api-fe10/src/org/jetbrains/kotlin/analysis/api/descriptors/components/KaFe10VisibilityChecker.kt
index 4a1d9e7..8dd92e6 100644
--- a/analysis/analysis-api-fe10/src/org/jetbrains/kotlin/analysis/api/descriptors/components/KaFe10VisibilityChecker.kt
+++ b/analysis/analysis-api-fe10/src/org/jetbrains/kotlin/analysis/api/descriptors/components/KaFe10VisibilityChecker.kt
@@ -6,13 +6,16 @@
package org.jetbrains.kotlin.analysis.api.descriptors.components
import com.intellij.psi.PsiElement
+import org.jetbrains.kotlin.analysis.api.components.KaUseSiteVisibilityChecker
import org.jetbrains.kotlin.analysis.api.components.KaVisibilityChecker
+import org.jetbrains.kotlin.analysis.api.descriptors.Fe10AnalysisContext
import org.jetbrains.kotlin.analysis.api.descriptors.Fe10AnalysisFacade.AnalysisMode
import org.jetbrains.kotlin.analysis.api.descriptors.KaFe10Session
import org.jetbrains.kotlin.analysis.api.descriptors.components.base.KaFe10SessionComponent
import org.jetbrains.kotlin.analysis.api.descriptors.symbols.descriptorBased.base.getSymbolDescriptor
import org.jetbrains.kotlin.analysis.api.descriptors.symbols.psiBased.base.getResolutionScope
import org.jetbrains.kotlin.analysis.api.impl.base.components.KaBaseSessionComponent
+import org.jetbrains.kotlin.analysis.api.lifetime.KaLifetimeToken
import org.jetbrains.kotlin.analysis.api.lifetime.withValidityAssertion
import org.jetbrains.kotlin.analysis.api.symbols.KaCallableSymbol
import org.jetbrains.kotlin.analysis.api.symbols.KaClassSymbol
@@ -34,14 +37,47 @@
import org.jetbrains.kotlin.resolve.scopes.utils.getImplicitReceiversHierarchy
internal class KaFe10VisibilityChecker(
- override val analysisSessionProvider: () -> KaFe10Session
+ override val analysisSessionProvider: () -> KaFe10Session,
) : KaBaseSessionComponent<KaFe10Session>(), KaVisibilityChecker, KaFe10SessionComponent {
- override fun isVisible(
- candidateSymbol: KaDeclarationSymbol,
+ override fun createUseSiteVisibilityChecker(
useSiteFile: KaFileSymbol,
receiverExpression: KtExpression?,
- position: PsiElement
- ): Boolean = withValidityAssertion {
+ position: PsiElement,
+ ): KaUseSiteVisibilityChecker = withValidityAssertion {
+ KaFe10UseSiteVisibilityChecker(receiverExpression, position, analysisContext, token)
+ }
+
+ override fun KaCallableSymbol.isVisibleInClass(classSymbol: KaClassSymbol): Boolean = withValidityAssertion {
+ val memberDescriptor = getSymbolDescriptor(this) as? DeclarationDescriptorWithVisibility ?: return false
+ val classDescriptor = getSymbolDescriptor(classSymbol) ?: return false
+ return isVisibleWithAnyReceiver(memberDescriptor, classDescriptor, analysisSession.analysisContext.languageVersionSettings)
+ }
+
+ override fun isPublicApi(symbol: KaDeclarationSymbol): Boolean = withValidityAssertion {
+ val descriptor = getSymbolDescriptor(symbol) as? DeclarationDescriptorWithVisibility ?: return false
+ return descriptor.isEffectivelyPublicApi || descriptor.isPublishedApi()
+ }
+}
+
+private fun findContainingNonLocalDeclaration(element: PsiElement): KtCallableDeclaration? {
+ for (parent in element.parentsWithSelf) {
+ if (parent is KtCallableDeclaration && !KtPsiUtil.isLocal(parent)) {
+ return parent
+ }
+ }
+
+ return null
+}
+
+
+// This implementation is not optimized for multiple invocations at the same use-site.
+private class KaFe10UseSiteVisibilityChecker(
+ private val receiverExpression: KtExpression?,
+ private val position: PsiElement,
+ private val analysisContext: Fe10AnalysisContext,
+ override val token: KaLifetimeToken,
+) : KaUseSiteVisibilityChecker {
+ override fun isVisible(candidateSymbol: KaDeclarationSymbol): Boolean = withValidityAssertion {
if (candidateSymbol.visibility == KaSymbolVisibility.PUBLIC) {
return true
}
@@ -75,25 +111,4 @@
return false
}
-
- override fun KaCallableSymbol.isVisibleInClass(classSymbol: KaClassSymbol): Boolean = withValidityAssertion {
- val memberDescriptor = getSymbolDescriptor(this) as? DeclarationDescriptorWithVisibility ?: return false
- val classDescriptor = getSymbolDescriptor(classSymbol) ?: return false
- return isVisibleWithAnyReceiver(memberDescriptor, classDescriptor, analysisSession.analysisContext.languageVersionSettings)
- }
-
- override fun isPublicApi(symbol: KaDeclarationSymbol): Boolean = withValidityAssertion {
- val descriptor = getSymbolDescriptor(symbol) as? DeclarationDescriptorWithVisibility ?: return false
- return descriptor.isEffectivelyPublicApi || descriptor.isPublishedApi()
- }
-}
-
-private fun findContainingNonLocalDeclaration(element: PsiElement): KtCallableDeclaration? {
- for (parent in element.parentsWithSelf) {
- if (parent is KtCallableDeclaration && !KtPsiUtil.isLocal(parent)) {
- return parent
- }
- }
-
- return null
-}
+}
\ No newline at end of file
diff --git a/analysis/analysis-api-fir/src/org/jetbrains/kotlin/analysis/api/fir/components/KaFirVisibilityChecker.kt b/analysis/analysis-api-fir/src/org/jetbrains/kotlin/analysis/api/fir/components/KaFirVisibilityChecker.kt
index d213350..18a4e1b 100644
--- a/analysis/analysis-api-fir/src/org/jetbrains/kotlin/analysis/api/fir/components/KaFirVisibilityChecker.kt
+++ b/analysis/analysis-api-fir/src/org/jetbrains/kotlin/analysis/api/fir/components/KaFirVisibilityChecker.kt
@@ -6,26 +6,31 @@
package org.jetbrains.kotlin.analysis.api.fir.components
import com.intellij.psi.PsiElement
+import org.jetbrains.kotlin.analysis.api.components.KaUseSiteVisibilityChecker
import org.jetbrains.kotlin.analysis.api.components.KaVisibilityChecker
import org.jetbrains.kotlin.analysis.api.fir.KaFirSession
import org.jetbrains.kotlin.analysis.api.fir.symbols.KaFirFileSymbol
import org.jetbrains.kotlin.analysis.api.fir.symbols.KaFirPsiJavaClassSymbol
import org.jetbrains.kotlin.analysis.api.fir.symbols.KaFirSymbol
import org.jetbrains.kotlin.analysis.api.impl.base.components.KaBaseSessionComponent
+import org.jetbrains.kotlin.analysis.api.lifetime.KaLifetimeToken
import org.jetbrains.kotlin.analysis.api.lifetime.withValidityAssertion
import org.jetbrains.kotlin.analysis.api.projectStructure.KaDanglingFileModule
+import org.jetbrains.kotlin.analysis.api.projectStructure.KaModule
import org.jetbrains.kotlin.analysis.api.symbols.KaCallableSymbol
import org.jetbrains.kotlin.analysis.api.symbols.KaClassSymbol
import org.jetbrains.kotlin.analysis.api.symbols.KaDeclarationSymbol
import org.jetbrains.kotlin.analysis.api.symbols.KaFileSymbol
import org.jetbrains.kotlin.analysis.api.symbols.KaReceiverParameterSymbol
import org.jetbrains.kotlin.analysis.api.symbols.KaSymbolVisibility
+import org.jetbrains.kotlin.analysis.low.level.api.fir.api.LLFirResolveSession
import org.jetbrains.kotlin.analysis.low.level.api.fir.api.getOrBuildFirSafe
import org.jetbrains.kotlin.analysis.low.level.api.fir.projectStructure.llFirModuleData
import org.jetbrains.kotlin.analysis.low.level.api.fir.util.collectUseSiteContainers
import org.jetbrains.kotlin.fir.analysis.checkers.isVisibleInClass
import org.jetbrains.kotlin.fir.declarations.FirCallableDeclaration
import org.jetbrains.kotlin.fir.declarations.FirClass
+import org.jetbrains.kotlin.fir.declarations.FirDeclaration
import org.jetbrains.kotlin.fir.declarations.FirMemberDeclaration
import org.jetbrains.kotlin.fir.declarations.FirResolvePhase
import org.jetbrains.kotlin.fir.declarations.utils.effectiveVisibility
@@ -38,17 +43,74 @@
import org.jetbrains.kotlin.utils.addToStdlib.runIf
internal class KaFirVisibilityChecker(
- override val analysisSessionProvider: () -> KaFirSession
+ override val analysisSessionProvider: () -> KaFirSession,
) : KaBaseSessionComponent<KaFirSession>(), KaVisibilityChecker, KaFirSessionComponent {
- override fun isVisible(
- candidateSymbol: KaDeclarationSymbol,
+ override fun createUseSiteVisibilityChecker(
useSiteFile: KaFileSymbol,
receiverExpression: KtExpression?,
- position: PsiElement
- ): Boolean = withValidityAssertion {
- require(candidateSymbol is KaFirSymbol<*>)
+ position: PsiElement,
+ ): KaUseSiteVisibilityChecker = withValidityAssertion {
require(useSiteFile is KaFirFileSymbol)
+ val dispatchReceiver = receiverExpression?.getOrBuildFirSafe<FirExpression>(analysisSession.firResolveSession)
+
+ val positionModule = firResolveSession.moduleProvider.getModule(position)
+ val effectiveContainers = collectUseSiteContainers(position, firResolveSession).orEmpty()
+
+ KaFirUseSiteVisibilityChecker(
+ positionModule,
+ effectiveContainers,
+ dispatchReceiver,
+ useSiteFile,
+ firResolveSession,
+ token,
+ )
+ }
+
+ override fun KaCallableSymbol.isVisibleInClass(classSymbol: KaClassSymbol): Boolean = withValidityAssertion {
+ if (this is KaReceiverParameterSymbol) {
+ // Receiver parameters are local
+ return false
+ }
+
+ require(this is KaFirSymbol<*>)
+ require(classSymbol is KaFirSymbol<*>)
+
+ val memberFir = firSymbol.fir as? FirCallableDeclaration ?: return false
+ val parentClassFir = classSymbol.firSymbol.fir as? FirClass ?: return false
+
+ // Inspecting visibility requires resolving to status
+ classSymbol.firSymbol.lazyResolveToPhase(FirResolvePhase.STATUS)
+
+ return memberFir.symbol.isVisibleInClass(parentClassFir.symbol, memberFir.symbol.resolvedStatus)
+ }
+
+ override fun isPublicApi(symbol: KaDeclarationSymbol): Boolean = withValidityAssertion {
+ if (symbol is KaReceiverParameterSymbol) {
+ return isPublicApi(symbol.owningCallableSymbol)
+ }
+
+ require(symbol is KaFirSymbol<*>)
+ val declaration = symbol.firSymbol.fir as? FirMemberDeclaration ?: return false
+
+ // Inspecting visibility requires resolving to status
+ declaration.lazyResolveToPhase(FirResolvePhase.STATUS)
+ return declaration.effectiveVisibility.publicApi || declaration.publishedApiEffectiveVisibility?.publicApi == true
+ }
+}
+
+
+private class KaFirUseSiteVisibilityChecker(
+ private val positionModule: KaModule,
+ private val effectiveContainers: List<FirDeclaration>,
+ private val dispatchReceiver: FirExpression?,
+ private val useSiteFile: KaFirFileSymbol,
+ private val firResolveSession: LLFirResolveSession,
+ override val token: KaLifetimeToken,
+) : KaUseSiteVisibilityChecker {
+ override fun isVisible(candidateSymbol: KaDeclarationSymbol): Boolean = withValidityAssertion {
+ require(candidateSymbol is KaFirSymbol<*>)
+
if (candidateSymbol is KaFirPsiJavaClassSymbol) {
candidateSymbol.isVisibleByPsi(useSiteFile)?.let { return it }
}
@@ -56,11 +118,8 @@
val candidateDeclaration = candidateSymbol.firSymbol.fir as? FirMemberDeclaration ?: return true
val dispatchReceiverCanBeExplicit = candidateSymbol is KaCallableSymbol && !candidateSymbol.isExtension
- val explicitDispatchReceiver = runIf(dispatchReceiverCanBeExplicit) {
- receiverExpression?.getOrBuildFirSafe<FirExpression>(analysisSession.firResolveSession)
- }
+ val explicitDispatchReceiver = runIf(dispatchReceiverCanBeExplicit) { dispatchReceiver }
- val positionModule = firResolveSession.moduleProvider.getModule(position)
val candidateModule = candidateDeclaration.llFirModuleData.ktModule
val effectiveSession = if (positionModule is KaDanglingFileModule && candidateModule != positionModule) {
@@ -71,8 +130,6 @@
firResolveSession.getSessionFor(positionModule)
}
- val effectiveContainers = collectUseSiteContainers(position, firResolveSession).orEmpty()
-
return effectiveSession.visibilityChecker.isVisible(
candidateDeclaration,
effectiveSession,
@@ -111,35 +168,4 @@
else -> null
}
-
- override fun KaCallableSymbol.isVisibleInClass(classSymbol: KaClassSymbol): Boolean = withValidityAssertion {
- if (this is KaReceiverParameterSymbol) {
- // Receiver parameters are local
- return false
- }
-
- require(this is KaFirSymbol<*>)
- require(classSymbol is KaFirSymbol<*>)
-
- val memberFir = firSymbol.fir as? FirCallableDeclaration ?: return false
- val parentClassFir = classSymbol.firSymbol.fir as? FirClass ?: return false
-
- // Inspecting visibility requires resolving to status
- classSymbol.firSymbol.lazyResolveToPhase(FirResolvePhase.STATUS)
-
- return memberFir.symbol.isVisibleInClass(parentClassFir.symbol, memberFir.symbol.resolvedStatus)
- }
-
- override fun isPublicApi(symbol: KaDeclarationSymbol): Boolean = withValidityAssertion {
- if (symbol is KaReceiverParameterSymbol) {
- return isPublicApi(symbol.owningCallableSymbol)
- }
-
- require(symbol is KaFirSymbol<*>)
- val declaration = symbol.firSymbol.fir as? FirMemberDeclaration ?: return false
-
- // Inspecting visibility requires resolving to status
- declaration.lazyResolveToPhase(FirResolvePhase.STATUS)
- return declaration.effectiveVisibility.publicApi || declaration.publishedApiEffectiveVisibility?.publicApi == true
- }
-}
+}
\ No newline at end of file
diff --git a/analysis/analysis-api-impl-base/tests/org/jetbrains/kotlin/analysis/api/impl/base/test/cases/components/visibilityChecker/AbstractVisibilityCheckerTest.kt b/analysis/analysis-api-impl-base/tests/org/jetbrains/kotlin/analysis/api/impl/base/test/cases/components/visibilityChecker/AbstractVisibilityCheckerTest.kt
index d7d08d0..37bbc89 100644
--- a/analysis/analysis-api-impl-base/tests/org/jetbrains/kotlin/analysis/api/impl/base/test/cases/components/visibilityChecker/AbstractVisibilityCheckerTest.kt
+++ b/analysis/analysis-api-impl-base/tests/org/jetbrains/kotlin/analysis/api/impl/base/test/cases/components/visibilityChecker/AbstractVisibilityCheckerTest.kt
@@ -42,11 +42,20 @@
val useSiteFileSymbol = mainFile.symbol
- val visible = isVisible(declarationSymbol, useSiteFileSymbol, null, useSiteElement)
+ val visibleByUseSiteVisibilityChecker =
+ createUseSiteVisibilityChecker(useSiteFileSymbol, null, useSiteElement).isVisible(declarationSymbol)
+
+ @Suppress("DEPRECATION")
+ val isVisibleByDeprecatedVisibilityFunction =
+ isVisible(declarationSymbol, useSiteFileSymbol, null, useSiteElement)
+
+ testServices.assertions.assertEquals(isVisibleByDeprecatedVisibilityFunction, visibleByUseSiteVisibilityChecker) {
+ "createUseSiteVisibilityChecker(..).isVisible(..) returning $visibleByUseSiteVisibilityChecker is inconsistent with isVisible(...) returning $isVisibleByDeprecatedVisibilityFunction"
+ }
"""
Declaration: ${declarationSymbol.render(KaDeclarationRendererForDebug.WITH_QUALIFIED_NAMES)}
At usage site: ${useSiteElement.text}
- Is visible: $visible
+ Is visible: $visibleByUseSiteVisibilityChecker
""".trimIndent()
}
diff --git a/analysis/analysis-api/src/org/jetbrains/kotlin/analysis/api/components/KaVisibilityChecker.kt b/analysis/analysis-api/src/org/jetbrains/kotlin/analysis/api/components/KaVisibilityChecker.kt
index e441d41..1a3d44c 100644
--- a/analysis/analysis-api/src/org/jetbrains/kotlin/analysis/api/components/KaVisibilityChecker.kt
+++ b/analysis/analysis-api/src/org/jetbrains/kotlin/analysis/api/components/KaVisibilityChecker.kt
@@ -7,6 +7,8 @@
import com.intellij.psi.PsiElement
import org.jetbrains.kotlin.analysis.api.KaExperimentalApi
+import org.jetbrains.kotlin.analysis.api.lifetime.KaLifetimeOwner
+import org.jetbrains.kotlin.analysis.api.lifetime.withValidityAssertion
import org.jetbrains.kotlin.analysis.api.symbols.KaCallableSymbol
import org.jetbrains.kotlin.analysis.api.symbols.KaClassSymbol
import org.jetbrains.kotlin.analysis.api.symbols.KaDeclarationSymbol
@@ -20,13 +22,34 @@
* @param receiverExpression The [dispatch receiver](https://kotlin.github.io/analysis-api/receivers.html#types-of-receivers) expression
* which the [candidateSymbol] is called on, if applicable.
*/
+ @Deprecated(
+ "Use `createUseSiteVisibilityChecker` instead. It's much more performant for multiple visibility checks on the same use-site",
+ replaceWith = ReplaceWith("createUseSiteVisibilityChecker(useSiteFile, receiverExpression, position).isVisible(candidateSymbol)")
+ )
@KaExperimentalApi
public fun isVisible(
candidateSymbol: KaDeclarationSymbol,
useSiteFile: KaFileSymbol,
receiverExpression: KtExpression? = null,
- position: PsiElement
- ): Boolean
+ position: PsiElement,
+ ): Boolean = withValidityAssertion {
+ createUseSiteVisibilityChecker(useSiteFile, receiverExpression, position).isVisible(candidateSymbol)
+ }
+
+ /**
+ * Creates a visibility checker for the given use-site position.
+ *
+ * @param receiverExpression The [dispatch receiver](https://kotlin.github.io/analysis-api/receivers.html#types-of-receivers) expression
+ * which the candidate symbol is called on, if applicable.
+ *
+ * @see KaUseSiteVisibilityChecker
+ */
+ @KaExperimentalApi
+ public fun createUseSiteVisibilityChecker(
+ useSiteFile: KaFileSymbol,
+ receiverExpression: KtExpression? = null,
+ position: PsiElement,
+ ): KaUseSiteVisibilityChecker
/**
* Checks whether the given [KaCallableSymbol] (possibly inherited from a superclass) is visible in the given [classSymbol].
@@ -42,3 +65,23 @@
*/
public fun isPublicApi(symbol: KaDeclarationSymbol): Boolean
}
+
+/**
+ * Allows checking if [KaDeclarationSymbol] is visible from the current use-site.
+ *
+ * [KaUseSiteVisibilityChecker] is created by [KaVisibilityChecker.createUseSiteVisibilityChecker].
+ *
+ * [KaUseSiteVisibilityChecker] is designed to be reused. Therefore, if you have multiple candidates to check from the same use-site position,
+ * it will be more performant to reuse the same [KaUseSiteVisibilityChecker].
+ */
+@KaExperimentalApi
+public interface KaUseSiteVisibilityChecker : KaLifetimeOwner {
+ /**
+ * Checks whether the [candidateSymbol] is visible at the current use-site.
+ *
+ * @param candidateSymbol The symbol whose visibility is to be checked.
+ * @return `true` if the [candidateSymbol] is visible from the current use-site, `false` otherwise.
+ */
+ @KaExperimentalApi
+ public fun isVisible(candidateSymbol: KaDeclarationSymbol): Boolean
+}
\ No newline at end of file