From dc538d420e7a7a72ea12c9481714451c11fa5859 Mon Sep 17 00:00:00 2001 From: Pavel Kirpichenkov Date: Fri, 21 Aug 2020 19:02:46 +0300 Subject: [PATCH] [FIR-IDE] Changes after review Move usage of SingleCandidateResolver into dedicated component from KtFirAnalysisSession. Add validity assertion, use concurrent map for context cache, provide original PSI when building context. Update names, make other minor changes. --- .../KotlinFirCompletionContributor.kt | 50 +++++--- .../idea/frontend/api/KtAnalysisSession.kt | 19 ++- .../KtCompletionCandidateChecker.kt | 22 ++++ .../api/resolver/SingleCandidateResolver.kt | 14 +-- .../api/resolver/candidateInfoProviders.kt | 4 +- .../frontend/api/fir/KtFirAnalysisSession.kt | 61 +--------- .../KtFirCompletionCandidateChecker.kt | 113 ++++++++++++++++++ 7 files changed, 193 insertions(+), 90 deletions(-) create mode 100644 idea/idea-frontend-api/src/org/jetbrains/kotlin/idea/frontend/api/components/KtCompletionCandidateChecker.kt create mode 100644 idea/idea-frontend-fir/src/org/jetbrains/kotlin/idea/frontend/api/fir/components/KtFirCompletionCandidateChecker.kt diff --git a/idea/idea-fir/src/org/jetbrains/kotlin/idea/completion/KotlinFirCompletionContributor.kt b/idea/idea-fir/src/org/jetbrains/kotlin/idea/completion/KotlinFirCompletionContributor.kt index 0fde691dc0b..1ad04e9dd76 100644 --- a/idea/idea-fir/src/org/jetbrains/kotlin/idea/completion/KotlinFirCompletionContributor.kt +++ b/idea/idea-fir/src/org/jetbrains/kotlin/idea/completion/KotlinFirCompletionContributor.kt @@ -8,6 +8,7 @@ package org.jetbrains.kotlin.idea.completion import com.intellij.codeInsight.completion.* import com.intellij.patterns.PlatformPatterns import com.intellij.patterns.PsiJavaPatterns +import com.intellij.psi.PsiElement import com.intellij.util.ProcessingContext import org.jetbrains.kotlin.idea.frontend.api.InvalidWayOfUsingAnalysisSession import org.jetbrains.kotlin.idea.frontend.api.KtAnalysisSession @@ -79,12 +80,19 @@ private object KotlinAvailableScopesCompletionProvider { val explicitReceiver = nameExpression.getQualifiedExpressionForSelector()?.receiverExpression with(getAnalysisSessionFor(originalFile).createContextDependentCopy()) { - val (implicitScopes, implicitReceivers) = originalFile.getScopeContextForPosition(nameExpression) + val (implicitScopes, _) = originalFile.getScopeContextForPosition(nameExpression) when { nameExpression.parent is KtUserType -> collectTypesCompletion(result, implicitScopes) - explicitReceiver != null -> collectDotCompletion(result, implicitScopes, explicitReceiver) - else -> collectDefaultCompletion(result, implicitScopes, implicitReceivers) + explicitReceiver != null -> collectDotCompletion( + result, + implicitScopes, + explicitReceiver, + nameExpression, + originalFile, + parameters.originalPosition + ) + else -> collectDefaultCompletion(result, implicitScopes, nameExpression, originalFile, parameters.originalPosition) } } } @@ -97,7 +105,10 @@ private object KotlinAvailableScopesCompletionProvider { private fun KtAnalysisSession.collectDotCompletion( result: CompletionResultSet, implicitScopes: KtCompositeScope, - explicitReceiver: KtExpression + explicitReceiver: KtExpression, + nameExpression: KtSimpleNameExpression, + originalFile: KtFile, + originalPosition: PsiElement?, ) { val typeOfPossibleReceiver = explicitReceiver.getKtType() val possibleReceiverScope = typeOfPossibleReceiver.getTypeScope() ?: return @@ -108,16 +119,25 @@ private object KotlinAvailableScopesCompletionProvider { val extensionNonMembers = implicitScopes .getCallableSymbols() - .filter { it.isExtension && it.canBeCalledWith(listOf(typeOfPossibleReceiver)) } + .filter { + it.isExtension && it.checkExtensionIsSuitable( + originalFile, + originalPosition, + nameExpression, + explicitReceiver + ) + } nonExtensionMembers.forEach { result.addSymbolToCompletion(it) } extensionNonMembers.forEach { result.addSymbolToCompletion(it) } } - private fun collectDefaultCompletion( + private fun KtAnalysisSession.collectDefaultCompletion( result: CompletionResultSet, implicitScopes: KtCompositeScope, - implicitReceivers: List + nameExpression: KtSimpleNameExpression, + originalFile: KtFile, + originalPosition: PsiElement?, ) { val availableNonExtensions = implicitScopes .getCallableSymbols() @@ -125,7 +145,14 @@ private object KotlinAvailableScopesCompletionProvider { val extensionsWhichCanBeCalled = implicitScopes .getCallableSymbols() - .filter { it.isExtension && it.canBeCalledWith(implicitReceivers) } + .filter { + it.isExtension && it.checkExtensionIsSuitable( + originalFile, + originalPosition, + nameExpression, + null + ) + } availableNonExtensions.forEach { result.addSymbolToCompletion(it) } extensionsWhichCanBeCalled.forEach { result.addSymbolToCompletion(it) } @@ -133,10 +160,3 @@ private object KotlinAvailableScopesCompletionProvider { collectTypesCompletion(result, implicitScopes) } } - -private fun KtCallableSymbol.canBeCalledWith(implicitReceivers: List): Boolean { - val requiredReceiverType = (this as? KtPossibleExtensionSymbol)?.receiverType - ?: error("Extension receiver type should be present on $this") - - return implicitReceivers.any { it.isSubTypeOf(requiredReceiverType) } -} \ No newline at end of file diff --git a/idea/idea-frontend-api/src/org/jetbrains/kotlin/idea/frontend/api/KtAnalysisSession.kt b/idea/idea-frontend-api/src/org/jetbrains/kotlin/idea/frontend/api/KtAnalysisSession.kt index fdd24a71902..3afce0998a1 100644 --- a/idea/idea-frontend-api/src/org/jetbrains/kotlin/idea/frontend/api/KtAnalysisSession.kt +++ b/idea/idea-frontend-api/src/org/jetbrains/kotlin/idea/frontend/api/KtAnalysisSession.kt @@ -40,6 +40,7 @@ abstract class KtAnalysisSession(override val token: ValidityToken) : ValidityTo protected abstract val containingDeclarationProvider: KtSymbolContainingDeclarationProvider protected abstract val symbolProvider: KtSymbolProvider protected abstract val callResolver: KtCallResolver + protected abstract val completionCandidateChecker: KtCompletionCandidateChecker /// TODO: get rid of @@ -119,10 +120,16 @@ abstract class KtAnalysisSession(override val token: ValidityToken) : ValidityTo return resolveToSymbols().singleOrNull() } - abstract fun resolveAndCheckReceivers( - firSymbolForCandidate: KtCallableSymbol, - originalFile: KtFile, - nameExpression: KtSimpleNameExpression, - possibleReceiver: KtExpression?, - ): Boolean + fun KtCallableSymbol.checkExtensionIsSuitable( + originalPsiFile: KtFile, + originalPosition: PsiElement?, + psiFakeCompletionExpression: KtSimpleNameExpression, + psiReceiverExpression: KtExpression?, + ): Boolean = completionCandidateChecker.checkExtensionFitsCandidate( + this, + originalPsiFile, + originalPosition, + psiFakeCompletionExpression, + psiReceiverExpression + ) } diff --git a/idea/idea-frontend-api/src/org/jetbrains/kotlin/idea/frontend/api/components/KtCompletionCandidateChecker.kt b/idea/idea-frontend-api/src/org/jetbrains/kotlin/idea/frontend/api/components/KtCompletionCandidateChecker.kt new file mode 100644 index 00000000000..87768100a1b --- /dev/null +++ b/idea/idea-frontend-api/src/org/jetbrains/kotlin/idea/frontend/api/components/KtCompletionCandidateChecker.kt @@ -0,0 +1,22 @@ +/* + * Copyright 2010-2020 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.idea.frontend.api.components + +import com.intellij.psi.PsiElement +import org.jetbrains.kotlin.idea.frontend.api.symbols.KtCallableSymbol +import org.jetbrains.kotlin.psi.KtExpression +import org.jetbrains.kotlin.psi.KtFile +import org.jetbrains.kotlin.psi.KtSimpleNameExpression + +abstract class KtCompletionCandidateChecker : KtAnalysisSessionComponent() { + abstract fun checkExtensionFitsCandidate( + firSymbolForCandidate: KtCallableSymbol, + originalFile: KtFile, + originalPosition: PsiElement?, + nameExpression: KtSimpleNameExpression, + possibleExplicitReceiver: KtExpression?, + ): Boolean +} \ No newline at end of file diff --git a/idea/idea-frontend-fir/idea-fir-low-level-api/src/org/jetbrains/kotlin/idea/fir/low/level/api/resolver/SingleCandidateResolver.kt b/idea/idea-frontend-fir/idea-fir-low-level-api/src/org/jetbrains/kotlin/idea/fir/low/level/api/resolver/SingleCandidateResolver.kt index 028a99e2e61..3da86b3e175 100644 --- a/idea/idea-frontend-fir/idea-fir-low-level-api/src/org/jetbrains/kotlin/idea/fir/low/level/api/resolver/SingleCandidateResolver.kt +++ b/idea/idea-frontend-fir/idea-fir-low-level-api/src/org/jetbrains/kotlin/idea/fir/low/level/api/resolver/SingleCandidateResolver.kt @@ -49,14 +49,14 @@ class SingleCandidateResolver( resolutionParameters: ResolutionParameters ): FirFunctionCall? { - val partProvider = createCandidatePartsProvider(resolutionParameters) - if (partProvider.shouldFailBeforeResolve()) + val infoProvider = createCandidateInfoProvider(resolutionParameters) + if (infoProvider.shouldFailBeforeResolve()) return null - val callInfo = partProvider.callInfo() - val explicitReceiverKind = partProvider.explicitReceiverKind() - val dispatchReceiverValue = partProvider.dispatchReceiverValue() - val implicitExtensionReceiverValue = partProvider.implicitExtensionReceiverValue() + val callInfo = infoProvider.callInfo() + val explicitReceiverKind = infoProvider.explicitReceiverKind() + val dispatchReceiverValue = infoProvider.dispatchReceiverValue() + val implicitExtensionReceiverValue = infoProvider.implicitExtensionReceiverValue() val candidate = CandidateFactory(bodyResolveComponents, callInfo).createCandidate( resolutionParameters.callableSymbol, @@ -72,7 +72,7 @@ class SingleCandidateResolver( return null } - private fun createCandidatePartsProvider(resolutionParameters: ResolutionParameters): CandidateInfoProvider { + private fun createCandidateInfoProvider(resolutionParameters: ResolutionParameters): CandidateInfoProvider { return when (resolutionParameters.singleCandidateResolutionMode) { SingleCandidateResolutionMode.CHECK_EXTENSION_FOR_COMPLETION -> CheckExtensionForCompletionCandidateInfoProvider( resolutionParameters, diff --git a/idea/idea-frontend-fir/idea-fir-low-level-api/src/org/jetbrains/kotlin/idea/fir/low/level/api/resolver/candidateInfoProviders.kt b/idea/idea-frontend-fir/idea-fir-low-level-api/src/org/jetbrains/kotlin/idea/fir/low/level/api/resolver/candidateInfoProviders.kt index 3bd706c408a..44767ae3ba7 100644 --- a/idea/idea-frontend-fir/idea-fir-low-level-api/src/org/jetbrains/kotlin/idea/fir/low/level/api/resolver/candidateInfoProviders.kt +++ b/idea/idea-frontend-fir/idea-fir-low-level-api/src/org/jetbrains/kotlin/idea/fir/low/level/api/resolver/candidateInfoProviders.kt @@ -35,7 +35,7 @@ abstract class AbstractCandidateInfoProvider( protected val firSession: FirSession, ) : CandidateInfoProvider { override fun callInfo(): CallInfo = with(resolutionParameters) { - return CallInfo( + CallInfo( callKind = callKind(), name = callableSymbol.callableId.callableName, explicitReceiver = explicitReceiver, @@ -81,7 +81,7 @@ class CheckExtensionForCompletionCandidateInfoProvider( // Passing them through can lead to false positives. override fun shouldFailBeforeResolve(): Boolean = with(resolutionParameters) { val callHasExtensionReceiver = explicitReceiverKind() == ExplicitReceiverKind.EXTENSION_RECEIVER - || implicitExtensionReceiverValue() != null + || implicitExtensionReceiverValue() != null val candidateHasExtensionReceiver = callableSymbol.fir.receiverTypeRef != null callHasExtensionReceiver != candidateHasExtensionReceiver } diff --git a/idea/idea-frontend-fir/src/org/jetbrains/kotlin/idea/frontend/api/fir/KtFirAnalysisSession.kt b/idea/idea-frontend-fir/src/org/jetbrains/kotlin/idea/frontend/api/fir/KtFirAnalysisSession.kt index a4c7f6bf6b0..2c6d6e35a95 100644 --- a/idea/idea-frontend-fir/src/org/jetbrains/kotlin/idea/frontend/api/fir/KtFirAnalysisSession.kt +++ b/idea/idea-frontend-fir/src/org/jetbrains/kotlin/idea/frontend/api/fir/KtFirAnalysisSession.kt @@ -5,9 +5,6 @@ package org.jetbrains.kotlin.idea.frontend.api.fir -import com.jetbrains.rd.util.getOrCreate -import org.jetbrains.kotlin.fir.declarations.* -import org.jetbrains.kotlin.fir.expressions.* import org.jetbrains.kotlin.fir.resolve.firSymbolProvider import org.jetbrains.kotlin.idea.fir.low.level.api.FirModuleResolveState import org.jetbrains.kotlin.idea.fir.low.level.api.LowLevelFirApiFacade @@ -20,20 +17,10 @@ import org.jetbrains.kotlin.idea.frontend.api.components.KtDiagnosticProvider import org.jetbrains.kotlin.idea.frontend.api.components.KtSmartCastProvider import org.jetbrains.kotlin.idea.frontend.api.components.KtTypeProvider import org.jetbrains.kotlin.idea.frontend.api.fir.components.* -import org.jetbrains.kotlin.idea.fir.* -import org.jetbrains.kotlin.idea.fir.low.level.api.resolver.SingleCandidateResolver -import org.jetbrains.kotlin.idea.fir.low.level.api.resolver.SingleCandidateResolutionMode -import org.jetbrains.kotlin.idea.fir.low.level.api.resolver.ResolutionParameters -import org.jetbrains.kotlin.idea.frontend.api.fir.symbols.KtFirFunctionSymbol import org.jetbrains.kotlin.idea.frontend.api.fir.symbols.KtFirSymbolProvider import org.jetbrains.kotlin.idea.frontend.api.fir.utils.threadLocal import org.jetbrains.kotlin.idea.frontend.api.symbols.KtSymbolProvider import org.jetbrains.kotlin.psi.KtElement -import org.jetbrains.kotlin.idea.frontend.api.symbols.* -import org.jetbrains.kotlin.idea.util.getElementTextInContext -import org.jetbrains.kotlin.psi.* -import org.jetbrains.kotlin.psi.psiUtil.getNonStrictParentOfType -import org.jetbrains.kotlin.utils.addToStdlib.safeAs internal class KtFirAnalysisSession private constructor( @@ -57,6 +44,7 @@ private constructor( override val scopeProvider by threadLocal { KtFirScopeProvider(this, firSymbolBuilder, project, firResolveState, token) } override val symbolProvider: KtSymbolProvider = KtFirSymbolProvider(this, firResolveState.firIdeLibrariesSession.firSymbolProvider, firResolveState, firSymbolBuilder, token) + override val completionCandidateChecker: KtCompletionCandidateChecker = KtFirCompletionCandidateChecker(this, token) override fun createContextDependentCopy(): KtAnalysisSession { @@ -71,53 +59,6 @@ private constructor( ) } - private val completionContextCache = HashMap, LowLevelFirApiFacade.FirCompletionContext>() - - // Temporary kludge to check single candidate resolver in tests - override fun resolveAndCheckReceivers( - firSymbolForCandidate: KtCallableSymbol, - originalFile: KtFile, - nameExpression: KtSimpleNameExpression, - possibleReceiver: KtExpression?, - ): Boolean { - firSymbolForCandidate.safeAs()?.firRef?.withFir { firFunction -> - val file = originalFile.getOrBuildFirOfType(firResolveState) - val explicitReceiverExpression = possibleReceiver?.getOrBuildFirOfType(firResolveState) - val resolver = SingleCandidateResolver(firResolveState.firIdeSourcesSession, file) - val enclosingFunction = nameExpression.getNonStrictParentOfType() - ?: error("Cannot find enclosing function for ${nameExpression.getElementTextInContext()}") - - val completionContext = completionContextCache.getOrCreate(file to enclosingFunction) { - LowLevelFirApiFacade.buildCompletionContextForFunction( - file, - enclosingFunction, - enclosingFunction, - state = firResolveState - ) - } - - val towerDataContext = completionContext.getTowerDataContext(nameExpression) - - val implicitReceivers = sequence { - yield(null) - yieldAll(towerDataContext.implicitReceiverStack) - } - for (implicitReceiverValue in implicitReceivers) { - val resolutionParameters = ResolutionParameters( - singleCandidateResolutionMode = SingleCandidateResolutionMode.CHECK_EXTENSION_FOR_COMPLETION, - callableSymbol = firFunction.symbol, - implicitReceiver = implicitReceiverValue, - explicitReceiver = explicitReceiverExpression - ) - resolver.resolveSingleCandidate(resolutionParameters)?.let { - // not null if resolved and completed successfully - return true - } - } - } - return false - } - companion object { @Deprecated("Please use org.jetbrains.kotlin.idea.frontend.api.KtAnalysisSessionProviderKt.analyze") internal fun createForElement(element: KtElement): KtFirAnalysisSession { diff --git a/idea/idea-frontend-fir/src/org/jetbrains/kotlin/idea/frontend/api/fir/components/KtFirCompletionCandidateChecker.kt b/idea/idea-frontend-fir/src/org/jetbrains/kotlin/idea/frontend/api/fir/components/KtFirCompletionCandidateChecker.kt new file mode 100644 index 00000000000..c7fa40256c2 --- /dev/null +++ b/idea/idea-frontend-fir/src/org/jetbrains/kotlin/idea/frontend/api/fir/components/KtFirCompletionCandidateChecker.kt @@ -0,0 +1,113 @@ +/* + * Copyright 2010-2020 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.idea.frontend.api.fir.components + +import com.intellij.psi.PsiElement +import com.jetbrains.rd.util.getOrCreate +import org.jetbrains.kotlin.fir.declarations.* +import org.jetbrains.kotlin.fir.expressions.FirExpression +import org.jetbrains.kotlin.fir.resolve.calls.ImplicitReceiverValue +import org.jetbrains.kotlin.idea.fir.getOrBuildFirOfType +import org.jetbrains.kotlin.idea.fir.low.level.api.LowLevelFirApiFacade +import org.jetbrains.kotlin.idea.fir.low.level.api.resolver.ResolutionParameters +import org.jetbrains.kotlin.idea.fir.low.level.api.resolver.SingleCandidateResolutionMode +import org.jetbrains.kotlin.idea.fir.low.level.api.resolver.SingleCandidateResolver +import org.jetbrains.kotlin.idea.frontend.api.ValidityToken +import org.jetbrains.kotlin.idea.frontend.api.components.KtCompletionCandidateChecker +import org.jetbrains.kotlin.idea.frontend.api.fir.KtFirAnalysisSession +import org.jetbrains.kotlin.idea.frontend.api.fir.symbols.KtFirFunctionSymbol +import org.jetbrains.kotlin.idea.frontend.api.fir.symbols.KtFirPropertySymbol +import org.jetbrains.kotlin.idea.frontend.api.fir.symbols.KtFirSymbol +import org.jetbrains.kotlin.idea.frontend.api.symbols.KtCallableSymbol +import org.jetbrains.kotlin.idea.frontend.api.withValidityAssertion +import org.jetbrains.kotlin.idea.util.getElementTextInContext +import org.jetbrains.kotlin.psi.* +import org.jetbrains.kotlin.psi.psiUtil.getNonStrictParentOfType +import org.jetbrains.kotlin.utils.addToStdlib.safeAs +import java.util.concurrent.ConcurrentHashMap + +internal class KtFirCompletionCandidateChecker( + override val analysisSession: KtFirAnalysisSession, + override val token: ValidityToken, +) : KtCompletionCandidateChecker(), KtFirAnalysisSessionComponent { + private val completionContextCache = ConcurrentHashMap, LowLevelFirApiFacade.FirCompletionContext>() + + override fun checkExtensionFitsCandidate( + firSymbolForCandidate: KtCallableSymbol, + originalFile: KtFile, + originalPosition: PsiElement?, + nameExpression: KtSimpleNameExpression, + possibleExplicitReceiver: KtExpression?, + ): Boolean = withValidityAssertion { + val originalEnclosingFunction = originalPosition?.getNonStrictParentOfType() + ?: error("Cannot find enclosing function for completion in provided position (or position is absent)") + + val functionFits = firSymbolForCandidate.withResolvedFirOfType { firFunction -> + checkExtension(firFunction, originalFile, originalEnclosingFunction, nameExpression, possibleExplicitReceiver) + } + val propertyFits = firSymbolForCandidate.withResolvedFirOfType { firProperty -> + checkExtension(firProperty, originalFile, originalEnclosingFunction, nameExpression, possibleExplicitReceiver) + } + + functionFits ?: propertyFits ?: false + } + + private inline fun , F : FirDeclaration, R> KtCallableSymbol.withResolvedFirOfType( + noinline action: (F) -> R, + ): R? = this.safeAs()?.firRef?.withFir(FirResolvePhase.BODY_RESOLVE, action) + + private fun checkExtension( + candidateSymbol: FirCallableDeclaration<*>, + originalFile: KtFile, + originalEnclosingFunction: KtNamedFunction, + nameExpression: KtSimpleNameExpression, + possibleExplicitReceiver: KtExpression?, + ): Boolean { + val file = originalFile.getOrBuildFirOfType(firResolveState) + val explicitReceiverExpression = possibleExplicitReceiver?.getOrBuildFirOfType(firResolveState) + val resolver = SingleCandidateResolver(firResolveState.firIdeSourcesSession, file) + val implicitReceivers = getImplicitReceivers(file, nameExpression, originalEnclosingFunction) + + for (implicitReceiverValue in implicitReceivers) { + val resolutionParameters = ResolutionParameters( + singleCandidateResolutionMode = SingleCandidateResolutionMode.CHECK_EXTENSION_FOR_COMPLETION, + callableSymbol = candidateSymbol.symbol, + implicitReceiver = implicitReceiverValue, + explicitReceiver = explicitReceiverExpression + ) + resolver.resolveSingleCandidate(resolutionParameters)?.let { + // not null if resolved and completed successfully + return true + } + } + return false + } + + private fun getImplicitReceivers( + file: FirFile, + fakeNameExpression: KtElement, + originalEnclosingFunction: KtNamedFunction, + ): Sequence?> { + val fakeEnclosingFunction = fakeNameExpression.getNonStrictParentOfType() + ?: error("Cannot find enclosing function for ${fakeNameExpression.getElementTextInContext()}") + + val completionContext = completionContextCache.getOrCreate(file to fakeEnclosingFunction) { + LowLevelFirApiFacade.buildCompletionContextForFunction( + file, + fakeEnclosingFunction, + originalEnclosingFunction, + state = firResolveState + ) + } + + val towerDataContext = completionContext.getTowerDataContext(fakeNameExpression) + + return sequence { + yield(null) // otherwise explicit receiver won't be checked when there are no implicit receivers in completion position + yieldAll(towerDataContext.implicitReceiverStack) + } + } +}