[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