From ca320f2f8d1ac4b875150f5655c37799060a050b Mon Sep 17 00:00:00 2001 From: Ilya Kirillov Date: Tue, 11 May 2021 19:36:51 +0200 Subject: [PATCH] FIR IDE: add `this` labels to keyword completion --- idea/ide-common/build.gradle.kts | 1 + .../org/jetbrains/kotlin/idea/util/Utils.kt | 25 ------ .../testData/keywords/LabeledLambdaThis.kt | 1 + .../idea-completion/testData/keywords/This.kt | 1 + .../testData/keywords/ThisInCompanion.kt | 11 +++ .../test/KeywordCompletionTestGenerated.java | 5 ++ .../KotlinFirCompletionContributor.kt | 3 +- .../keywords/ThisKeywordHandler.kt | 83 +++++++++++++++++++ .../FirKeywordCompletionTestGenerated.java | 5 ++ .../api/components/KtScopeProvider.kt | 11 ++- .../api/fir/components/KtFirScopeProvider.kt | 12 ++- .../kotlin/idea/util/findLabelAndCall.kt | 35 ++++++++ .../build.gradle.kts | 1 + 13 files changed, 165 insertions(+), 29 deletions(-) create mode 100644 idea/idea-completion/testData/keywords/ThisInCompanion.kt create mode 100644 idea/idea-fir/src/org/jetbrains/kotlin/idea/completion/contributors/keywords/ThisKeywordHandler.kt create mode 100644 idea/idea-frontend-independent/src/org/jetbrains/kotlin/idea/util/findLabelAndCall.kt diff --git a/idea/ide-common/build.gradle.kts b/idea/ide-common/build.gradle.kts index 0e39788721e..f0b837cdb78 100644 --- a/idea/ide-common/build.gradle.kts +++ b/idea/ide-common/build.gradle.kts @@ -8,6 +8,7 @@ dependencies { compileOnly(project(":compiler:util")) compileOnly(project(":compiler:frontend")) compileOnly(project(":compiler:frontend.java")) + compileOnly(project(":idea:idea-frontend-independent")) compileOnly(project(":js:js.frontend")) compileOnly(project(":js:js.serializer")) compileOnly(intellijCoreDep()) { includeJars("intellij-core", "guava", rootProject = rootProject) } diff --git a/idea/ide-common/src/org/jetbrains/kotlin/idea/util/Utils.kt b/idea/ide-common/src/org/jetbrains/kotlin/idea/util/Utils.kt index 62f334d6752..39210db908a 100644 --- a/idea/ide-common/src/org/jetbrains/kotlin/idea/util/Utils.kt +++ b/idea/ide-common/src/org/jetbrains/kotlin/idea/util/Utils.kt @@ -18,31 +18,6 @@ import org.jetbrains.kotlin.types.FlexibleType import org.jetbrains.kotlin.types.KotlinType import org.jetbrains.kotlin.types.checker.KotlinTypeChecker -fun KtFunctionLiteral.findLabelAndCall(): Pair { - val literalParent = (this.parent as KtLambdaExpression).parent - - fun KtValueArgument.callExpression(): KtCallExpression? { - val parent = parent - return (if (parent is KtValueArgumentList) parent else this).parent as? KtCallExpression - } - - when (literalParent) { - is KtLabeledExpression -> { - val callExpression = (literalParent.parent as? KtValueArgument)?.callExpression() - return Pair(literalParent.getLabelNameAsName(), callExpression) - } - - is KtValueArgument -> { - val callExpression = literalParent.callExpression() - val label = (callExpression?.calleeExpression as? KtSimpleNameExpression)?.getReferencedNameAsName() - return Pair(label, callExpression) - } - - else -> { - return Pair(null, null) - } - } -} fun SmartCastManager.getSmartCastVariantsWithLessSpecificExcluded( receiverToCast: ReceiverValue, diff --git a/idea/idea-completion/testData/keywords/LabeledLambdaThis.kt b/idea/idea-completion/testData/keywords/LabeledLambdaThis.kt index 0995a29fd16..d82a5cc2d17 100644 --- a/idea/idea-completion/testData/keywords/LabeledLambdaThis.kt +++ b/idea/idea-completion/testData/keywords/LabeledLambdaThis.kt @@ -1,3 +1,4 @@ +// FIR_COMPARISON fun foo(): String.() -> Unit { return (label@ { f { diff --git a/idea/idea-completion/testData/keywords/This.kt b/idea/idea-completion/testData/keywords/This.kt index 5de20038dac..704206a74b0 100644 --- a/idea/idea-completion/testData/keywords/This.kt +++ b/idea/idea-completion/testData/keywords/This.kt @@ -1,3 +1,4 @@ +// FIR_COMPARISON class Outer { class Nested { inner class Inner { diff --git a/idea/idea-completion/testData/keywords/ThisInCompanion.kt b/idea/idea-completion/testData/keywords/ThisInCompanion.kt new file mode 100644 index 00000000000..d24f9444adf --- /dev/null +++ b/idea/idea-completion/testData/keywords/ThisInCompanion.kt @@ -0,0 +1,11 @@ +// FIR_COMPARISON +class C { + companion object { + fun String.foo() { + + } + } +} + +// INVOCATION_COUNT: 1 +// EXIST: "this@Companion" diff --git a/idea/idea-completion/tests/org/jetbrains/kotlin/idea/completion/test/KeywordCompletionTestGenerated.java b/idea/idea-completion/tests/org/jetbrains/kotlin/idea/completion/test/KeywordCompletionTestGenerated.java index 8ea86e8e415..0f48c3d4e6c 100644 --- a/idea/idea-completion/tests/org/jetbrains/kotlin/idea/completion/test/KeywordCompletionTestGenerated.java +++ b/idea/idea-completion/tests/org/jetbrains/kotlin/idea/completion/test/KeywordCompletionTestGenerated.java @@ -579,6 +579,11 @@ public class KeywordCompletionTestGenerated extends AbstractKeywordCompletionTes runTest("idea/idea-completion/testData/keywords/This.kt"); } + @TestMetadata("ThisInCompanion.kt") + public void testThisInCompanion() throws Exception { + runTest("idea/idea-completion/testData/keywords/ThisInCompanion.kt"); + } + @TestMetadata("ThisPrefixMatching.kt") public void testThisPrefixMatching() throws Exception { runTest("idea/idea-completion/testData/keywords/ThisPrefixMatching.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 413bba77a66..ab012833515 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 @@ -226,7 +226,8 @@ private class KotlinWithNameReferenceCompletionProvider( extensionChecker: ExtensionApplicabilityChecker, visibilityChecker: CompletionVisibilityChecker, ) { - val (implicitScopes, implicitReceiversTypes) = implicitScopesContext + val (implicitScopes, implicitReceivers) = implicitScopesContext + val implicitReceiversTypes = implicitReceivers.map { it.type } val availableNonExtensions = implicitScopes.collectNonExtensions(visibilityChecker) val extensionsWhichCanBeCalled = implicitScopes.collectSuitableExtensions(extensionChecker, visibilityChecker) diff --git a/idea/idea-fir/src/org/jetbrains/kotlin/idea/completion/contributors/keywords/ThisKeywordHandler.kt b/idea/idea-fir/src/org/jetbrains/kotlin/idea/completion/contributors/keywords/ThisKeywordHandler.kt new file mode 100644 index 00000000000..9cd796ca345 --- /dev/null +++ b/idea/idea-fir/src/org/jetbrains/kotlin/idea/completion/contributors/keywords/ThisKeywordHandler.kt @@ -0,0 +1,83 @@ +/* + * Copyright 2010-2021 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.completion.contributors.keywords + +import com.intellij.codeInsight.completion.CompletionParameters +import com.intellij.codeInsight.lookup.LookupElement +import com.intellij.openapi.project.Project +import com.intellij.psi.PsiElement +import com.intellij.psi.util.isAncestor +import org.jetbrains.kotlin.idea.completion.KeywordLookupObject +import org.jetbrains.kotlin.idea.completion.context.FirBasicCompletionContext +import org.jetbrains.kotlin.idea.completion.createKeywordElement +import org.jetbrains.kotlin.idea.completion.keywords.CompletionKeywordHandler +import org.jetbrains.kotlin.idea.completion.labelNameToTail +import org.jetbrains.kotlin.idea.frontend.api.KtAnalysisSession +import org.jetbrains.kotlin.idea.frontend.api.components.KtImplicitReceiver +import org.jetbrains.kotlin.idea.frontend.api.components.KtTypeRendererOptions +import org.jetbrains.kotlin.idea.frontend.api.symbols.KtAnonymousFunctionSymbol +import org.jetbrains.kotlin.idea.frontend.api.symbols.KtClassKind +import org.jetbrains.kotlin.idea.frontend.api.symbols.KtClassOrObjectSymbol +import org.jetbrains.kotlin.idea.frontend.api.symbols.KtSymbol +import org.jetbrains.kotlin.idea.frontend.api.symbols.markers.KtNamedSymbol +import org.jetbrains.kotlin.idea.util.findLabelAndCall +import org.jetbrains.kotlin.lexer.KtTokens +import org.jetbrains.kotlin.name.Name +import org.jetbrains.kotlin.psi.KtClassOrObject +import org.jetbrains.kotlin.psi.KtExpression +import org.jetbrains.kotlin.psi.KtFunctionLiteral +import org.jetbrains.kotlin.psi.KtPsiUtil + +internal class ThisKeywordHandler( + private val basicContext: FirBasicCompletionContext +) : CompletionKeywordHandler(KtTokens.THIS_KEYWORD) { + @OptIn(ExperimentalStdlibApi::class) + override fun KtAnalysisSession.createLookups( + parameters: CompletionParameters, + expression: KtExpression?, + lookup: LookupElement, + project: Project + ): Collection { + if (expression == null) { + // for completion in secondary constructor delegation call + return listOf(lookup) + } + + val result = mutableListOf() + val receivers = basicContext.originalKtFile.getScopeContextForPosition(expression).implicitReceivers + + receivers.forEachIndexed { index, receiver -> + if (!canReferenceSymbolByThis(parameters, receiver.ownerSymbol)) { + return@forEachIndexed + } + val labelName = if (index != 0) getThisLabelBySymbol(receiver.ownerSymbol) else null + result += createThisLookupElement(receiver, labelName) + } + + return result + } + + private fun KtAnalysisSession.canReferenceSymbolByThis(parameters: CompletionParameters, symbol: KtSymbol): Boolean { + if (symbol !is KtClassOrObjectSymbol) return true + if (symbol.classKind != KtClassKind.COMPANION_OBJECT) return true + val companionPsi = symbol.psi as KtClassOrObject + return parameters.offset in companionPsi.textRange + } + + private fun KtAnalysisSession.createThisLookupElement(receiver: KtImplicitReceiver, labelName: Name?): LookupElement { + return createKeywordElement("this", labelName.labelNameToTail(), lookupObject = KeywordLookupObject()) + .withTypeText(receiver.type.render(KtTypeRendererOptions.SHORT_NAMES)) + } + + private fun KtAnalysisSession.getThisLabelBySymbol(symbol: KtSymbol): Name? = when { + symbol is KtNamedSymbol && !symbol.name.isSpecial -> symbol.name + symbol is KtAnonymousFunctionSymbol -> { + val psi = symbol.psi as KtFunctionLiteral + psi.findLabelAndCall().first + } + else -> null + } +} \ No newline at end of file diff --git a/idea/idea-fir/tests/org/jetbrains/kotlin/idea/completion/FirKeywordCompletionTestGenerated.java b/idea/idea-fir/tests/org/jetbrains/kotlin/idea/completion/FirKeywordCompletionTestGenerated.java index f5200a0cbd7..fa02a2503c3 100644 --- a/idea/idea-fir/tests/org/jetbrains/kotlin/idea/completion/FirKeywordCompletionTestGenerated.java +++ b/idea/idea-fir/tests/org/jetbrains/kotlin/idea/completion/FirKeywordCompletionTestGenerated.java @@ -579,6 +579,11 @@ public class FirKeywordCompletionTestGenerated extends AbstractFirKeywordComplet runTest("idea/idea-completion/testData/keywords/This.kt"); } + @TestMetadata("ThisInCompanion.kt") + public void testThisInCompanion() throws Exception { + runTest("idea/idea-completion/testData/keywords/ThisInCompanion.kt"); + } + @TestMetadata("ThisPrefixMatching.kt") public void testThisPrefixMatching() throws Exception { runTest("idea/idea-completion/testData/keywords/ThisPrefixMatching.kt"); diff --git a/idea/idea-frontend-api/src/org/jetbrains/kotlin/idea/frontend/api/components/KtScopeProvider.kt b/idea/idea-frontend-api/src/org/jetbrains/kotlin/idea/frontend/api/components/KtScopeProvider.kt index ce3f3e48a53..862cc61e3d5 100644 --- a/idea/idea-frontend-api/src/org/jetbrains/kotlin/idea/frontend/api/components/KtScopeProvider.kt +++ b/idea/idea-frontend-api/src/org/jetbrains/kotlin/idea/frontend/api/components/KtScopeProvider.kt @@ -5,11 +5,14 @@ package org.jetbrains.kotlin.idea.frontend.api.components +import org.jetbrains.kotlin.idea.frontend.api.ValidityTokenOwner import org.jetbrains.kotlin.idea.frontend.api.scopes.* import org.jetbrains.kotlin.idea.frontend.api.symbols.KtFileSymbol import org.jetbrains.kotlin.idea.frontend.api.symbols.KtPackageSymbol +import org.jetbrains.kotlin.idea.frontend.api.symbols.KtSymbol import org.jetbrains.kotlin.idea.frontend.api.symbols.markers.KtSymbolWithDeclarations import org.jetbrains.kotlin.idea.frontend.api.symbols.markers.KtSymbolWithMembers +import org.jetbrains.kotlin.idea.frontend.api.tokens.ValidityToken import org.jetbrains.kotlin.idea.frontend.api.types.KtType import org.jetbrains.kotlin.psi.KtElement import org.jetbrains.kotlin.psi.KtFile @@ -55,4 +58,10 @@ interface KtScopeProviderMixIn : KtAnalysisSessionMixIn { analysisSession.scopeProvider.getScopeContextForPosition(this, this) } -data class KtScopeContext(val scopes: KtCompositeScope, val implicitReceiversTypes: List) +data class KtScopeContext(val scopes: KtCompositeScope, val implicitReceivers: List) + +class KtImplicitReceiver( + override val token: ValidityToken, + val type: KtType, + val ownerSymbol: KtSymbol +): ValidityTokenOwner diff --git a/idea/idea-frontend-fir/src/org/jetbrains/kotlin/idea/frontend/api/fir/components/KtFirScopeProvider.kt b/idea/idea-frontend-fir/src/org/jetbrains/kotlin/idea/frontend/api/fir/components/KtFirScopeProvider.kt index f79afc12b8e..3cede5c4fa8 100644 --- a/idea/idea-frontend-fir/src/org/jetbrains/kotlin/idea/frontend/api/fir/components/KtFirScopeProvider.kt +++ b/idea/idea-frontend-fir/src/org/jetbrains/kotlin/idea/frontend/api/fir/components/KtFirScopeProvider.kt @@ -9,6 +9,7 @@ import com.intellij.openapi.project.Project import org.jetbrains.kotlin.fir.FirSession import org.jetbrains.kotlin.fir.declarations.FirAnonymousObject import org.jetbrains.kotlin.fir.declarations.FirClass +import org.jetbrains.kotlin.fir.declarations.FirDeclaration import org.jetbrains.kotlin.fir.declarations.FirResolvePhase import org.jetbrains.kotlin.fir.resolve.ScopeSession import org.jetbrains.kotlin.fir.resolve.calls.FirSyntheticPropertiesScope @@ -18,6 +19,7 @@ import org.jetbrains.kotlin.fir.scopes.impl.* import org.jetbrains.kotlin.idea.fir.low.level.api.api.FirModuleResolveState import org.jetbrains.kotlin.idea.fir.low.level.api.api.getTowerDataContextUnsafe import org.jetbrains.kotlin.idea.frontend.api.ValidityTokenOwner +import org.jetbrains.kotlin.idea.frontend.api.components.KtImplicitReceiver import org.jetbrains.kotlin.idea.frontend.api.components.KtScopeContext import org.jetbrains.kotlin.idea.frontend.api.components.KtScopeProvider import org.jetbrains.kotlin.idea.frontend.api.fir.KtFirAnalysisSession @@ -143,7 +145,13 @@ internal class KtFirScopeProvider( val towerDataContext = analysisSession.firResolveState.getTowerDataContextUnsafe(positionInFakeFile) val implicitReceivers = towerDataContext.nonLocalTowerDataElements.mapNotNull { it.implicitReceiver }.distinct() - val implicitReceiversTypes = implicitReceivers.map { builder.typeBuilder.buildKtType(it.type) } + val implicitKtReceivers = implicitReceivers.map { receiver -> + KtImplicitReceiver( + token, + builder.typeBuilder.buildKtType(receiver.type), + builder.buildSymbol(receiver.boundSymbol.fir as FirDeclaration), + ) + } val implicitReceiverScopes = implicitReceivers.mapNotNull { it.implicitScope } val nonLocalScopes = towerDataContext.nonLocalTowerDataElements.mapNotNull { it.scope }.distinct() @@ -158,7 +166,7 @@ internal class KtFirScopeProvider( KtScopeContext( getCompositeScope(allKtScopes.asReversed()), - implicitReceiversTypes.asReversed() + implicitKtReceivers.asReversed() ) } diff --git a/idea/idea-frontend-independent/src/org/jetbrains/kotlin/idea/util/findLabelAndCall.kt b/idea/idea-frontend-independent/src/org/jetbrains/kotlin/idea/util/findLabelAndCall.kt new file mode 100644 index 00000000000..baf71d4a9fa --- /dev/null +++ b/idea/idea-frontend-independent/src/org/jetbrains/kotlin/idea/util/findLabelAndCall.kt @@ -0,0 +1,35 @@ +/* + * Copyright 2010-2021 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.util + +import org.jetbrains.kotlin.name.Name +import org.jetbrains.kotlin.psi.* + +fun KtFunctionLiteral.findLabelAndCall(): Pair { + val literalParent = (this.parent as KtLambdaExpression).parent + + fun KtValueArgument.callExpression(): KtCallExpression? { + val parent = parent + return (if (parent is KtValueArgumentList) parent else this).parent as? KtCallExpression + } + + when (literalParent) { + is KtLabeledExpression -> { + val callExpression = (literalParent.parent as? KtValueArgument)?.callExpression() + return Pair(literalParent.getLabelNameAsName(), callExpression) + } + + is KtValueArgument -> { + val callExpression = literalParent.callExpression() + val label = (callExpression?.calleeExpression as? KtSimpleNameExpression)?.getReferencedNameAsName() + return Pair(label, callExpression) + } + + else -> { + return Pair(null, null) + } + } +} \ No newline at end of file diff --git a/plugins/scripting/scripting-ide-services-test/build.gradle.kts b/plugins/scripting/scripting-ide-services-test/build.gradle.kts index b238295d180..7a5e19fb50f 100644 --- a/plugins/scripting/scripting-ide-services-test/build.gradle.kts +++ b/plugins/scripting/scripting-ide-services-test/build.gradle.kts @@ -28,6 +28,7 @@ dependencies { testRuntimeOnly(project(":kotlin-compiler")) testRuntimeOnly(commonDep("org.jetbrains.intellij.deps", "trove4j")) testRuntimeOnly(project(":idea:ide-common")) { isTransitive = false } + testRuntimeOnly(project(":idea:idea-frontend-independent")) { isTransitive = false } embeddableTestRuntime(project(":kotlin-scripting-ide-services", configuration="runtimeElements")) embeddableTestRuntime(project(":kotlin-scripting-compiler-impl-embeddable", configuration="runtimeElements"))