From 22db894076e6ea2442eaacd60bea77422229565b Mon Sep 17 00:00:00 2001 From: Roman Golyshev Date: Fri, 16 Apr 2021 13:26:52 +0500 Subject: [PATCH] FIR IDE: Add `Add Import` quickfix This quickfix can import unresolved types and callables It currently does not support Java types and is not as advanced as in the old plugin Also, enable tests that now pass --- .../idea/quickfix/MainKtQuickFixRegistrar.kt | 7 + .../idea/quickfix/fixes/ImportQuickFix.kt | 212 ++++++++++++++++++ ...ghLevelQuickFixMultiFileTestGenerated.java | 10 + .../idea/fir/low/level/api/IndexHelper.kt | 21 ++ idea/resources-fir/META-INF/plugin.xml | 3 +- .../importNullableTraitWithGenerics.after.kt | 9 + ...ableTraitWithGenerics.before.Dependency.kt | 3 + ...rtNullableTraitWithGenerics.before.Main.kt | 7 + .../quickfix/autoImports/importTrait.after.kt | 1 + .../autoImports/importTrait.before.Main.kt | 1 + ...ortForPrivateFunction.before.Dependency.kt | 3 + .../noImportForPrivateFunction.before.Main.kt | 8 + .../QuickFixMultiFileTestGenerated.java | 10 + 13 files changed, 293 insertions(+), 2 deletions(-) create mode 100644 idea/idea-fir/src/org/jetbrains/kotlin/idea/quickfix/fixes/ImportQuickFix.kt create mode 100644 idea/testData/quickfix/autoImports/importNullableTraitWithGenerics.after.kt create mode 100644 idea/testData/quickfix/autoImports/importNullableTraitWithGenerics.before.Dependency.kt create mode 100644 idea/testData/quickfix/autoImports/importNullableTraitWithGenerics.before.Main.kt create mode 100644 idea/testData/quickfix/autoImports/noImportForPrivateFunction.before.Dependency.kt create mode 100644 idea/testData/quickfix/autoImports/noImportForPrivateFunction.before.Main.kt diff --git a/idea/idea-fir/src/org/jetbrains/kotlin/idea/quickfix/MainKtQuickFixRegistrar.kt b/idea/idea-fir/src/org/jetbrains/kotlin/idea/quickfix/MainKtQuickFixRegistrar.kt index 2c082fbdbf7..339123db67b 100644 --- a/idea/idea-fir/src/org/jetbrains/kotlin/idea/quickfix/MainKtQuickFixRegistrar.kt +++ b/idea/idea-fir/src/org/jetbrains/kotlin/idea/quickfix/MainKtQuickFixRegistrar.kt @@ -12,6 +12,8 @@ import org.jetbrains.kotlin.idea.fir.api.fixes.KtQuickFixesListBuilder import org.jetbrains.kotlin.idea.frontend.api.fir.diagnostics.KtFirDiagnostic import org.jetbrains.kotlin.idea.quickfix.fixes.* import org.jetbrains.kotlin.idea.quickfix.fixes.InitializePropertyQuickFixFactory +import org.jetbrains.kotlin.idea.quickfix.fixes.ChangeTypeQuickFix +import org.jetbrains.kotlin.idea.quickfix.fixes.ReplaceCallFixFactories class MainKtQuickFixRegistrar : KtQuickFixRegistrar() { private val modifiers = KtQuickFixesListBuilder.registerPsiQuickFix { @@ -85,6 +87,10 @@ class MainKtQuickFixRegistrar : KtQuickFixRegistrar() { registerApplicator(MemberNotImplementedQuickfixFactories.manyImplMemberNotImplemented) } + private val imports = KtQuickFixesListBuilder.registerPsiQuickFix { + registerApplicator(ImportQuickFix.FACTORY) + } + private val mutability = KtQuickFixesListBuilder.registerPsiQuickFix { registerPsiQuickFixes(KtFirDiagnostic.VarOverriddenByVal::class, ChangeVariableMutabilityFix.VAR_OVERRIDDEN_BY_VAL_FACTORY) registerPsiQuickFixes(KtFirDiagnostic.VarAnnotationParameter::class, ChangeVariableMutabilityFix.VAR_ANNOTATION_PARAMETER_FACTORY) @@ -106,6 +112,7 @@ class MainKtQuickFixRegistrar : KtQuickFixRegistrar() { modifiers, propertyInitialization, overrides, + imports, mutability, expressions, ) diff --git a/idea/idea-fir/src/org/jetbrains/kotlin/idea/quickfix/fixes/ImportQuickFix.kt b/idea/idea-fir/src/org/jetbrains/kotlin/idea/quickfix/fixes/ImportQuickFix.kt new file mode 100644 index 00000000000..0ebe5e2b1a3 --- /dev/null +++ b/idea/idea-fir/src/org/jetbrains/kotlin/idea/quickfix/fixes/ImportQuickFix.kt @@ -0,0 +1,212 @@ +/* + * 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.quickfix.fixes + +import com.intellij.codeInsight.daemon.QuickFixBundle +import com.intellij.codeInsight.hint.HintManager +import com.intellij.codeInsight.hint.QuestionAction +import com.intellij.codeInspection.HintAction +import com.intellij.openapi.editor.Editor +import com.intellij.openapi.project.Project +import com.intellij.openapi.ui.popup.JBPopup +import com.intellij.openapi.ui.popup.JBPopupFactory +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiFile +import com.intellij.psi.search.GlobalSearchScope +import org.jetbrains.kotlin.idea.KotlinBundle +import org.jetbrains.kotlin.idea.caches.project.getModuleInfo +import org.jetbrains.kotlin.idea.fir.api.fixes.diagnosticFixFactory +import org.jetbrains.kotlin.idea.fir.low.level.api.IndexHelper +import org.jetbrains.kotlin.idea.fir.low.level.api.util.createScopeForModuleLibraries +import org.jetbrains.kotlin.idea.frontend.api.KtAnalysisSession +import org.jetbrains.kotlin.idea.frontend.api.fir.diagnostics.KtFirDiagnostic +import org.jetbrains.kotlin.idea.frontend.api.fir.utils.addImportToFile +import org.jetbrains.kotlin.idea.frontend.api.symbols.KtCallableSymbol +import org.jetbrains.kotlin.idea.frontend.api.symbols.KtNamedClassOrObjectSymbol +import org.jetbrains.kotlin.idea.frontend.api.symbols.KtSymbol +import org.jetbrains.kotlin.idea.frontend.api.symbols.markers.KtSymbolWithVisibility +import org.jetbrains.kotlin.idea.quickfix.QuickFixActionBase +import org.jetbrains.kotlin.idea.util.application.executeWriteCommand +import org.jetbrains.kotlin.idea.util.module +import org.jetbrains.kotlin.name.FqName +import org.jetbrains.kotlin.name.Name +import org.jetbrains.kotlin.psi.* +import org.jetbrains.kotlin.psi.KtPsiUtil.isSelectorInQualified +import org.jetbrains.kotlin.psi.psiUtil.unwrapNullability + +internal class ImportQuickFix( + element: KtElement, + private val importCandidates: List +) : QuickFixActionBase(element), HintAction { + + init { + require(importCandidates.isNotEmpty()) + } + + override fun getText(): String = KotlinBundle.message("fix.import") + + override fun getFamilyName(): String = KotlinBundle.message("fix.import") + + override fun invoke(project: Project, editor: Editor, file: PsiFile) { + if (file !is KtFile) return + + createAddImportAction(project, editor, file).execute() + } + + private fun createAddImportAction(project: Project, editor: Editor, file: KtFile): QuestionAction { + return ImportQuestionAction(project, editor, file, importCandidates) + } + + override fun showHint(editor: Editor): Boolean { + val element = element ?: return false + val file = element.containingKtFile + val project = file.project + + val elementRange = element.textRange + val autoImportHintText = "Import ${importCandidates.first().asString()}?" + + HintManager.getInstance().showQuestionHint( + editor, + autoImportHintText, + elementRange.startOffset, + elementRange.endOffset, + createAddImportAction(project, editor, file) + ) + + return true + } + + private class ImportQuestionAction( + private val project: Project, + private val editor: Editor, + private val file: KtFile, + private val importCandidates: List + ) : QuestionAction { + + init { + require(importCandidates.isNotEmpty()) + } + + override fun execute(): Boolean { + val unambiguousImport = importCandidates.singleOrNull() + if (unambiguousImport != null) { + addImport(unambiguousImport) + return true + } + + createImportSelectorPopup().showInBestPositionFor(editor) + + return true + } + + private fun createImportSelectorPopup(): JBPopup { + return JBPopupFactory.getInstance() + .createPopupChooserBuilder(importCandidates) + .setTitle(KotlinBundle.message("action.add.import.chooser.title")) + .setItemChosenCallback { selectedValue: FqName -> addImport(selectedValue) } + .createPopup() + } + + private fun addImport(nameToImport: FqName) { + project.executeWriteCommand(QuickFixBundle.message("add.import")) { + addImportToFile(project, file, nameToImport) + } + } + } + + internal companion object { + val FACTORY = diagnosticFixFactory { diagnostic -> + val element = diagnostic.psi + + val indexHelper = IndexHelper(element.project, createSearchScope(element)) + + val quickFix = when (element) { + is KtTypeReference -> createImportTypeFix(indexHelper, element) + is KtNameReferenceExpression -> createImportNameFix(indexHelper, element) + else -> null + } + + listOfNotNull(quickFix) + } + + private fun createSearchScope(element: PsiElement): GlobalSearchScope { + val contentScope = element.getModuleInfo().contentScope() + val librariesScope = element.module?.let(::createScopeForModuleLibraries) + ?: GlobalSearchScope.EMPTY_SCOPE + + return contentScope.uniteWith(librariesScope) + } + + private fun KtAnalysisSession.createImportNameFix( + indexHelper: IndexHelper, + element: KtNameReferenceExpression + ): ImportQuickFix? { + if (isSelectorInQualified(element)) return null + + val firFile = element.containingKtFile.getFileSymbol() + val unresolvedName = element.getReferencedNameAsName() + + val isVisible: (KtSymbol) -> Boolean = + { it !is KtSymbolWithVisibility || isVisible(it, firFile, null, element) } + + val callableCandidates = collectCallableCandidates(indexHelper, unresolvedName, isVisible) + val typeCandidates = collectTypesCandidates(indexHelper, unresolvedName, isVisible) + + val importCandidates = (callableCandidates + typeCandidates).distinct() + if (importCandidates.isEmpty()) return null + + return ImportQuickFix(element, importCandidates) + } + + private fun KtAnalysisSession.createImportTypeFix(indexHelper: IndexHelper, element: KtTypeReference): ImportQuickFix? { + val firFile = element.containingKtFile.getFileSymbol() + val unresolvedName = element.typeName ?: return null + + val isVisible: (KtNamedClassOrObjectSymbol) -> Boolean = + { isVisible(it, firFile, null, element) } + + val acceptableClasses = collectTypesCandidates(indexHelper, unresolvedName, isVisible) + if (acceptableClasses.isEmpty()) return null + + return ImportQuickFix(element, acceptableClasses) + } + + private fun KtAnalysisSession.collectCallableCandidates( + indexHelper: IndexHelper, + unresolvedName: Name, + isVisible: (KtCallableSymbol) -> Boolean + ): List { + val callablesCandidates = indexHelper.getKotlinCallablesByName(unresolvedName) + + return callablesCandidates + .asSequence() + .map { it.getSymbol() as KtCallableSymbol } + .filter(isVisible) + .mapNotNull { it.callableIdIfNonLocal?.asSingleFqName() } + .toList() + } + + private fun KtAnalysisSession.collectTypesCandidates( + indexHelper: IndexHelper, + unresolvedName: Name, + isVisible: (KtNamedClassOrObjectSymbol) -> Boolean + ): List { + val classesCandidates = indexHelper.getKotlinClassesByName(unresolvedName) + + return classesCandidates.asSequence() + .mapNotNull { it.getNamedClassOrObjectSymbol() } + .filter(isVisible) + .mapNotNull { it.classIdIfNonLocal?.asSingleFqName() } + .toList() + } + } +} + +private val KtTypeReference.typeName: Name? + get() { + val userType = typeElement?.unwrapNullability() as? KtUserType + return userType?.referencedName?.let(Name::identifier) + } \ No newline at end of file diff --git a/idea/idea-fir/tests/org/jetbrains/kotlin/idea/quickfix/HighLevelQuickFixMultiFileTestGenerated.java b/idea/idea-fir/tests/org/jetbrains/kotlin/idea/quickfix/HighLevelQuickFixMultiFileTestGenerated.java index 231ec4b65f4..befaddc8cdf 100644 --- a/idea/idea-fir/tests/org/jetbrains/kotlin/idea/quickfix/HighLevelQuickFixMultiFileTestGenerated.java +++ b/idea/idea-fir/tests/org/jetbrains/kotlin/idea/quickfix/HighLevelQuickFixMultiFileTestGenerated.java @@ -164,6 +164,11 @@ public class HighLevelQuickFixMultiFileTestGenerated extends AbstractHighLevelQu runTest("idea/testData/quickfix/autoImports/importInFirstPartInQualifiedExpression.before.Main.kt"); } + @TestMetadata("importNullableTraitWithGenerics.before.Main.kt") + public void testImportNullableTraitWithGenerics() throws Exception { + runTest("idea/testData/quickfix/autoImports/importNullableTraitWithGenerics.before.Main.kt"); + } + @TestMetadata("ImportOperatorInvokeWithConvention.before.Main.kt") public void testImportOperatorInvokeWithConvention() throws Exception { runTest("idea/testData/quickfix/autoImports/ImportOperatorInvokeWithConvention.before.Main.kt"); @@ -219,6 +224,11 @@ public class HighLevelQuickFixMultiFileTestGenerated extends AbstractHighLevelQu runTest("idea/testData/quickfix/autoImports/noImportForPrivateClass.before.Main.kt"); } + @TestMetadata("noImportForPrivateFunction.before.Main.kt") + public void testNoImportForPrivateFunction() throws Exception { + runTest("idea/testData/quickfix/autoImports/noImportForPrivateFunction.before.Main.kt"); + } + @TestMetadata("noImportInImports.before.Main.kt") public void testNoImportInImports() throws Exception { runTest("idea/testData/quickfix/autoImports/noImportInImports.before.Main.kt"); diff --git a/idea/idea-frontend-fir/idea-fir-low-level-api/src/org/jetbrains/kotlin/idea/fir/low/level/api/IndexHelper.kt b/idea/idea-frontend-fir/idea-fir-low-level-api/src/org/jetbrains/kotlin/idea/fir/low/level/api/IndexHelper.kt index 82e21c3b46d..3aef27f2748 100644 --- a/idea/idea-frontend-fir/idea-fir-low-level-api/src/org/jetbrains/kotlin/idea/fir/low/level/api/IndexHelper.kt +++ b/idea/idea-frontend-fir/idea-fir-low-level-api/src/org/jetbrains/kotlin/idea/fir/low/level/api/IndexHelper.kt @@ -7,17 +7,22 @@ package org.jetbrains.kotlin.idea.fir.low.level.api import com.intellij.openapi.progress.ProgressManager import com.intellij.openapi.project.Project +import com.intellij.psi.PsiClass import com.intellij.psi.PsiElement import com.intellij.psi.search.GlobalSearchScope +import com.intellij.psi.search.PsiShortNamesCache import com.intellij.psi.stubs.StringStubIndexExtension import com.intellij.psi.stubs.StubIndex import com.intellij.psi.stubs.StubIndexKey +import org.jetbrains.kotlin.descriptors.ClassDescriptor +import org.jetbrains.kotlin.idea.caches.resolve.util.resolveToDescriptor import org.jetbrains.kotlin.name.CallableId import org.jetbrains.kotlin.idea.stubindex.* import org.jetbrains.kotlin.name.ClassId import org.jetbrains.kotlin.name.FqName import org.jetbrains.kotlin.name.Name import org.jetbrains.kotlin.psi.* +import org.jetbrains.kotlin.psi.psiUtil.contains import org.jetbrains.kotlin.utils.addToStdlib.firstNotNullResult /* @@ -103,6 +108,22 @@ public class IndexHelper(val project: Project, private val scope: GlobalSearchSc return (functions + properties).toList() } + fun getKotlinCallablesByName(name: Name): Collection { + val functions: Sequence = + KotlinFunctionShortNameIndex.getInstance().get(name.asString(), project, scope).asSequence() + + val properties: Sequence = + KotlinPropertyShortNameIndex.getInstance().get(name.asString(), project, scope).asSequence() + + return (functions + properties) + .filterIsInstance() + .toList() + } + + fun getKotlinClassesByName(name: Name): Collection { + return KotlinClassShortNameIndex.getInstance().get(name.asString(), project, scope) + } + fun getTopLevelExtensions(nameFilter: (Name) -> Boolean, receiverTypeNames: Set): Collection { val index = KotlinTopLevelExtensionsByReceiverTypeIndex.INSTANCE diff --git a/idea/resources-fir/META-INF/plugin.xml b/idea/resources-fir/META-INF/plugin.xml index 02c4bf50dc5..8d9e22d9d5f 100644 --- a/idea/resources-fir/META-INF/plugin.xml +++ b/idea/resources-fir/META-INF/plugin.xml @@ -303,7 +303,6 @@ The Kotlin FIR plugin provides language support in IntelliJ IDEA and Android Stu fieldName="INSTANCE" extensions="kotlin_module"/> - - + diff --git a/idea/testData/quickfix/autoImports/importNullableTraitWithGenerics.after.kt b/idea/testData/quickfix/autoImports/importNullableTraitWithGenerics.after.kt new file mode 100644 index 00000000000..6f8c4c27f85 --- /dev/null +++ b/idea/testData/quickfix/autoImports/importNullableTraitWithGenerics.after.kt @@ -0,0 +1,9 @@ +import importTrait.data.TestTrait + +// "Import" "true" +// FIR_IGNORE +// ERROR: Unresolved reference: TestTrait + +fun test() { + val a: TestTrait? = null +} diff --git a/idea/testData/quickfix/autoImports/importNullableTraitWithGenerics.before.Dependency.kt b/idea/testData/quickfix/autoImports/importNullableTraitWithGenerics.before.Dependency.kt new file mode 100644 index 00000000000..4ed12093d85 --- /dev/null +++ b/idea/testData/quickfix/autoImports/importNullableTraitWithGenerics.before.Dependency.kt @@ -0,0 +1,3 @@ +package importTrait.data + +interface TestTrait \ No newline at end of file diff --git a/idea/testData/quickfix/autoImports/importNullableTraitWithGenerics.before.Main.kt b/idea/testData/quickfix/autoImports/importNullableTraitWithGenerics.before.Main.kt new file mode 100644 index 00000000000..70070ee84b2 --- /dev/null +++ b/idea/testData/quickfix/autoImports/importNullableTraitWithGenerics.before.Main.kt @@ -0,0 +1,7 @@ +// "Import" "true" +// FIR_IGNORE +// ERROR: Unresolved reference: TestTrait + +fun test() { + val a: TestTrait? = null +} diff --git a/idea/testData/quickfix/autoImports/importTrait.after.kt b/idea/testData/quickfix/autoImports/importTrait.after.kt index fd2ed286f34..7cea02ae01d 100644 --- a/idea/testData/quickfix/autoImports/importTrait.after.kt +++ b/idea/testData/quickfix/autoImports/importTrait.after.kt @@ -1,6 +1,7 @@ import importTrait.data.TestTrait // "Import" "true" +// IGNORE_FIR // ERROR: Unresolved reference: TestTrait fun test() { diff --git a/idea/testData/quickfix/autoImports/importTrait.before.Main.kt b/idea/testData/quickfix/autoImports/importTrait.before.Main.kt index 2ed655fa292..fb3cabd6cc5 100644 --- a/idea/testData/quickfix/autoImports/importTrait.before.Main.kt +++ b/idea/testData/quickfix/autoImports/importTrait.before.Main.kt @@ -1,4 +1,5 @@ // "Import" "true" +// IGNORE_FIR // ERROR: Unresolved reference: TestTrait fun test() { diff --git a/idea/testData/quickfix/autoImports/noImportForPrivateFunction.before.Dependency.kt b/idea/testData/quickfix/autoImports/noImportForPrivateFunction.before.Dependency.kt new file mode 100644 index 00000000000..aa39e5d06c4 --- /dev/null +++ b/idea/testData/quickfix/autoImports/noImportForPrivateFunction.before.Dependency.kt @@ -0,0 +1,3 @@ +package sometest + +private fun privateFun() {} \ No newline at end of file diff --git a/idea/testData/quickfix/autoImports/noImportForPrivateFunction.before.Main.kt b/idea/testData/quickfix/autoImports/noImportForPrivateFunction.before.Main.kt new file mode 100644 index 00000000000..f9a5ba36a81 --- /dev/null +++ b/idea/testData/quickfix/autoImports/noImportForPrivateFunction.before.Main.kt @@ -0,0 +1,8 @@ +// "Import" "false" +// ACTION: Create function 'privateFun' +// ACTION: Rename reference +// ERROR: Unresolved reference: privateFun + +fun test() { + privateFun() +} \ No newline at end of file diff --git a/idea/tests/org/jetbrains/kotlin/idea/quickfix/QuickFixMultiFileTestGenerated.java b/idea/tests/org/jetbrains/kotlin/idea/quickfix/QuickFixMultiFileTestGenerated.java index 269b86c356a..d8c28bacb81 100644 --- a/idea/tests/org/jetbrains/kotlin/idea/quickfix/QuickFixMultiFileTestGenerated.java +++ b/idea/tests/org/jetbrains/kotlin/idea/quickfix/QuickFixMultiFileTestGenerated.java @@ -761,6 +761,11 @@ public class QuickFixMultiFileTestGenerated extends AbstractQuickFixMultiFileTes runTest("idea/testData/quickfix/autoImports/importKotlinStaticPropertyOverloadedSetterFromJava.test"); } + @TestMetadata("importNullableTraitWithGenerics.before.Main.kt") + public void testImportNullableTraitWithGenerics() throws Exception { + runTest("idea/testData/quickfix/autoImports/importNullableTraitWithGenerics.before.Main.kt"); + } + @TestMetadata("ImportOperatorInvokeWithConvention.before.Main.kt") public void testImportOperatorInvokeWithConvention() throws Exception { runTest("idea/testData/quickfix/autoImports/ImportOperatorInvokeWithConvention.before.Main.kt"); @@ -951,6 +956,11 @@ public class QuickFixMultiFileTestGenerated extends AbstractQuickFixMultiFileTes runTest("idea/testData/quickfix/autoImports/noImportForPrivateClass.before.Main.kt"); } + @TestMetadata("noImportForPrivateFunction.before.Main.kt") + public void testNoImportForPrivateFunction() throws Exception { + runTest("idea/testData/quickfix/autoImports/noImportForPrivateFunction.before.Main.kt"); + } + @TestMetadata("noImportInImports.before.Main.kt") public void testNoImportInImports() throws Exception { runTest("idea/testData/quickfix/autoImports/noImportInImports.before.Main.kt");