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");