diff --git a/generators/idea-generator/tests/org/jetbrains/kotlin/generators/tests/idea/GenerateTests.kt b/generators/idea-generator/tests/org/jetbrains/kotlin/generators/tests/idea/GenerateTests.kt index e84e4613475..ea4b2a4ffd7 100644 --- a/generators/idea-generator/tests/org/jetbrains/kotlin/generators/tests/idea/GenerateTests.kt +++ b/generators/idea-generator/tests/org/jetbrains/kotlin/generators/tests/idea/GenerateTests.kt @@ -1028,7 +1028,6 @@ fun main(args: Array) { model("checker/diagnosticsMessage", excludedPattern = excludedFirTestdataPattern) } - testClass { val pattern = "^([\\w\\-_]+)\\.kt$" model("quickfix/abstract", pattern = pattern, filenameStartsLowerCase = true) @@ -1067,6 +1066,7 @@ fun main(args: Array) { val pattern = "^([\\w\\-_]+)\\.(kt|kts)$" model("intentions/addPropertyAccessors", pattern = pattern) model("intentions/specifyTypeExplicitly", pattern = pattern) + model("intentions/importAllMembers", pattern = pattern) } testClass { diff --git a/idea/idea-fir/src/org/jetbrains/kotlin/idea/fir/intentions/HLImportAllMembersIntention.kt b/idea/idea-fir/src/org/jetbrains/kotlin/idea/fir/intentions/HLImportAllMembersIntention.kt new file mode 100644 index 00000000000..5c22915d249 --- /dev/null +++ b/idea/idea-fir/src/org/jetbrains/kotlin/idea/fir/intentions/HLImportAllMembersIntention.kt @@ -0,0 +1,116 @@ +/* + * 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.fir.intentions + +import com.intellij.codeInsight.intention.HighPriorityAction +import org.jetbrains.kotlin.idea.KotlinBundle +import org.jetbrains.kotlin.idea.api.applicator.HLApplicatorInput +import org.jetbrains.kotlin.idea.api.applicator.applicator +import org.jetbrains.kotlin.idea.fir.api.AbstractHLIntention +import org.jetbrains.kotlin.idea.fir.api.applicator.HLApplicabilityRange +import org.jetbrains.kotlin.idea.fir.api.applicator.HLApplicatorInputProvider +import org.jetbrains.kotlin.idea.fir.api.applicator.inputProvider +import org.jetbrains.kotlin.idea.fir.applicators.ApplicabilityRanges +import org.jetbrains.kotlin.idea.frontend.api.KtAnalysisSession +import org.jetbrains.kotlin.idea.frontend.api.calls.getSuccessCallSymbolOrNull +import org.jetbrains.kotlin.idea.frontend.api.components.ShortenCommand +import org.jetbrains.kotlin.idea.frontend.api.components.ShortenOption +import org.jetbrains.kotlin.idea.frontend.api.symbols.* +import org.jetbrains.kotlin.idea.frontend.api.symbols.markers.KtSymbolWithKind +import org.jetbrains.kotlin.idea.references.KtReference +import org.jetbrains.kotlin.idea.references.mainReference +import org.jetbrains.kotlin.name.ClassId +import org.jetbrains.kotlin.name.FqName +import org.jetbrains.kotlin.psi.* +import org.jetbrains.kotlin.psi.psiUtil.getQualifiedExpressionForReceiver +import org.jetbrains.kotlin.psi.psiUtil.isInImportDirective + +class HLImportAllMembersIntention : + AbstractHLIntention(KtExpression::class, Companion.applicator), HighPriorityAction { + override val applicabilityRange: HLApplicabilityRange get() = ApplicabilityRanges.SELF + + override val inputProvider: HLApplicatorInputProvider = inputProvider { psi -> + val target = psi.actualReference?.resolveToSymbol() as? KtNamedClassOrObjectSymbol ?: return@inputProvider null + val classId = target.classIdIfNonLocal ?: return@inputProvider null + if (target.origin != KtSymbolOrigin.JAVA && + (target.classKind == KtClassKind.OBJECT || + // One cannot use on-demand import for properties or functions declared inside objects + isReferenceToObjectMemberOrUnresolved(psi)) + ) { + // Import all members of an object is not supported by Kotlin. + return@inputProvider null + } + val shortenCommand = collectPossibleReferenceShortenings( + psi.containingKtFile, + classShortenOption = { + if (it.classIdIfNonLocal?.isNestedClassIn(classId) == true) { + ShortenOption.SHORTEN_AND_STAR_IMPORT + } else { + ShortenOption.DO_NOT_SHORTEN + } + }, + callableShortenOption = { + val containingClassId = if (it is KtConstructorSymbol) { + it.containingClassIdIfNonLocal?.outerClassId + } else { + it.callableIdIfNonLocal?.classId + } + if (containingClassId == classId) { + ShortenOption.SHORTEN_AND_STAR_IMPORT + } else { + ShortenOption.DO_NOT_SHORTEN + } + } + ) + if (shortenCommand.isEmpty) return@inputProvider null + Input(classId.asSingleFqName(), shortenCommand) + } + + private fun ClassId.isNestedClassIn(classId: ClassId) = + packageFqName == classId.packageFqName && relativeClassName.parent() == classId.relativeClassName + + class Input(val fqName: FqName, val shortenCommand: ShortenCommand) : HLApplicatorInput + + companion object { + val applicator = applicator { + familyName(KotlinBundle.lazyMessage("import.members.with")) + actionName { _, input -> KotlinBundle.message("import.members.from.0", input.fqName.asString()) } + isApplicableByPsi { it.isOnTheLeftOfQualificationDot && !it.isInImportDirective() } + applyTo { _, input -> + input.shortenCommand.invokeShortening() + } + } + + private val KtExpression.isOnTheLeftOfQualificationDot: Boolean + get() { + return when (val parent = parent) { + is KtDotQualifiedExpression -> this == parent.receiverExpression + is KtUserType -> { + val grandParent = parent.parent as? KtUserType ?: return false + grandParent.qualifier == parent && parent.referenceExpression == this + } + else -> false + } + } + + private val KtExpression.actualReference: KtReference? + get() = when (this) { + is KtDotQualifiedExpression -> selectorExpression?.mainReference ?: mainReference + else -> mainReference + } + + private fun KtAnalysisSession.isReferenceToObjectMemberOrUnresolved(qualifiedAccess: KtExpression): Boolean { + val selectorExpression: KtExpression? = qualifiedAccess.getQualifiedExpressionForReceiver()?.selectorExpression + val referencedSymbol = when (selectorExpression) { + is KtCallExpression -> selectorExpression.resolveCall()?.targetFunction?.getSuccessCallSymbolOrNull() + is KtNameReferenceExpression -> selectorExpression.mainReference.resolveToSymbol() + else -> return false + } as? KtSymbolWithKind ?: return true + if (referencedSymbol is KtConstructorSymbol) return false + return (referencedSymbol.getContainingSymbol() as? KtClassOrObjectSymbol)?.classKind?.isObject ?: true + } + } +} \ No newline at end of file diff --git a/idea/idea-fir/tests/org/jetbrains/kotlin/idea/fir/intentions/HLIntentionTestGenerated.java b/idea/idea-fir/tests/org/jetbrains/kotlin/idea/fir/intentions/HLIntentionTestGenerated.java index 5bb6ad55ee2..cc31035e34d 100644 --- a/idea/idea-fir/tests/org/jetbrains/kotlin/idea/fir/intentions/HLIntentionTestGenerated.java +++ b/idea/idea-fir/tests/org/jetbrains/kotlin/idea/fir/intentions/HLIntentionTestGenerated.java @@ -473,4 +473,87 @@ public class HLIntentionTestGenerated extends AbstractHLIntentionTest { runTest("idea/testData/intentions/specifyTypeExplicitly/unitType.kt"); } } + + @TestMetadata("idea/testData/intentions/importAllMembers") + @TestDataPath("$PROJECT_ROOT") + @RunWith(JUnit3RunnerWithInners.class) + public static class ImportAllMembers extends AbstractHLIntentionTest { + private void runTest(String testDataFilePath) throws Exception { + KotlinTestUtils.runTest(this::doTest, this, testDataFilePath); + } + + public void testAllFilesPresentInImportAllMembers() throws Exception { + KtTestUtil.assertAllTestsPresentByMetadataWithExcluded(this.getClass(), new File("idea/testData/intentions/importAllMembers"), Pattern.compile("^([\\w\\-_]+)\\.(kt|kts)$"), null, true); + } + + @TestMetadata("AlreadyImported.kt") + public void testAlreadyImported() throws Exception { + runTest("idea/testData/intentions/importAllMembers/AlreadyImported.kt"); + } + + @TestMetadata("AlreadyImportedWithStar.kt") + public void testAlreadyImportedWithStar() throws Exception { + runTest("idea/testData/intentions/importAllMembers/AlreadyImportedWithStar.kt"); + } + + @TestMetadata("AmbiguousCalls.kt") + public void testAmbiguousCalls() throws Exception { + runTest("idea/testData/intentions/importAllMembers/AmbiguousCalls.kt"); + } + + @TestMetadata("EnumMembers.kt") + public void testEnumMembers() throws Exception { + runTest("idea/testData/intentions/importAllMembers/EnumMembers.kt"); + } + + @TestMetadata("ImportAllMembersInImport.kt") + public void testImportAllMembersInImport() throws Exception { + runTest("idea/testData/intentions/importAllMembers/ImportAllMembersInImport.kt"); + } + + @TestMetadata("NotFromCompanionObject.kt") + public void testNotFromCompanionObject() throws Exception { + runTest("idea/testData/intentions/importAllMembers/NotFromCompanionObject.kt"); + } + + @TestMetadata("NotFromObject.kt") + public void testNotFromObject() throws Exception { + runTest("idea/testData/intentions/importAllMembers/NotFromObject.kt"); + } + + @TestMetadata("QualifiedName.kt") + public void testQualifiedName() throws Exception { + runTest("idea/testData/intentions/importAllMembers/QualifiedName.kt"); + } + + @TestMetadata("QualifiedName2.kt") + public void testQualifiedName2() throws Exception { + runTest("idea/testData/intentions/importAllMembers/QualifiedName2.kt"); + } + + @TestMetadata("RemoveSingleImports.kt") + public void testRemoveSingleImports() throws Exception { + runTest("idea/testData/intentions/importAllMembers/RemoveSingleImports.kt"); + } + + @TestMetadata("StaticJavaMembers.kt") + public void testStaticJavaMembers() throws Exception { + runTest("idea/testData/intentions/importAllMembers/StaticJavaMembers.kt"); + } + + @TestMetadata("TypeReference.kt") + public void testTypeReference() throws Exception { + runTest("idea/testData/intentions/importAllMembers/TypeReference.kt"); + } + + @TestMetadata("TypeReference2.kt") + public void testTypeReference2() throws Exception { + runTest("idea/testData/intentions/importAllMembers/TypeReference2.kt"); + } + + @TestMetadata("UnresolvedMember.kt") + public void testUnresolvedMember() throws Exception { + runTest("idea/testData/intentions/importAllMembers/UnresolvedMember.kt"); + } + } } diff --git a/idea/resources-en/intentionDescriptions/HLImportAllMembersIntention/after.kt.template b/idea/resources-en/intentionDescriptions/HLImportAllMembersIntention/after.kt.template new file mode 100644 index 00000000000..8c1b2d2555f --- /dev/null +++ b/idea/resources-en/intentionDescriptions/HLImportAllMembersIntention/after.kt.template @@ -0,0 +1,7 @@ +import javax.swing.SwingUtilities +import javax.swing.SwingUtilities.* + +fun foo() { + SwingUtilities.invokeLater { } + SwingUtilities.invokeAndWait() +} diff --git a/idea/resources-en/intentionDescriptions/HLImportAllMembersIntention/before.kt.template b/idea/resources-en/intentionDescriptions/HLImportAllMembersIntention/before.kt.template new file mode 100644 index 00000000000..6fa7ea870c7 --- /dev/null +++ b/idea/resources-en/intentionDescriptions/HLImportAllMembersIntention/before.kt.template @@ -0,0 +1,6 @@ +import javax.swing.SwingUtilities + +fun foo() { + SwingUtilities.invokeLater { } + SwingUtilities.invokeAndWait() +} diff --git a/idea/resources-en/intentionDescriptions/HLImportAllMembersIntention/description.html b/idea/resources-en/intentionDescriptions/HLImportAllMembersIntention/description.html new file mode 100644 index 00000000000..4c854dd0c9a --- /dev/null +++ b/idea/resources-en/intentionDescriptions/HLImportAllMembersIntention/description.html @@ -0,0 +1,5 @@ + + +This intention converts the selected qualified reference into a simple reference and adds an import for the corresponding class. + + \ No newline at end of file diff --git a/idea/resources-fir/META-INF/firIntentions.xml b/idea/resources-fir/META-INF/firIntentions.xml index 1d0c861bed2..f7eee7e1939 100644 --- a/idea/resources-fir/META-INF/firIntentions.xml +++ b/idea/resources-fir/META-INF/firIntentions.xml @@ -19,5 +19,10 @@ org.jetbrains.kotlin.idea.fir.intentions.HLAddSetterIntention Kotlin + + + org.jetbrains.kotlin.idea.fir.intentions.HLImportAllMembersIntention + Kotlin + \ No newline at end of file diff --git a/idea/testData/intentions/importAllMembers/.firIntention b/idea/testData/intentions/importAllMembers/.firIntention new file mode 100644 index 00000000000..d2a8dcc082c --- /dev/null +++ b/idea/testData/intentions/importAllMembers/.firIntention @@ -0,0 +1 @@ +org.jetbrains.kotlin.idea.fir.intentions.HLImportAllMembersIntention \ No newline at end of file