diff --git a/idea/resources/intentionDescriptions/ImportMemberIntention/after.kt.template b/idea/resources/intentionDescriptions/ImportMemberIntention/after.kt.template new file mode 100644 index 00000000000..d5989c7e7e3 --- /dev/null +++ b/idea/resources/intentionDescriptions/ImportMemberIntention/after.kt.template @@ -0,0 +1,7 @@ +import javax.swing.SwingUtilities +import javax.swing.SwingUtilities.invokeLater + +fun foo() { + invokeLater { } + invokeLater() +} diff --git a/idea/resources/intentionDescriptions/ImportMemberIntention/before.kt.template b/idea/resources/intentionDescriptions/ImportMemberIntention/before.kt.template new file mode 100644 index 00000000000..b08e56a217f --- /dev/null +++ b/idea/resources/intentionDescriptions/ImportMemberIntention/before.kt.template @@ -0,0 +1,6 @@ +import javax.swing.SwingUtilities + +fun foo() { + SwingUtilities.invokeLater { } + SwingUtilities.invokeLater() +} diff --git a/idea/resources/intentionDescriptions/ImportMemberIntention/description.html b/idea/resources/intentionDescriptions/ImportMemberIntention/description.html new file mode 100644 index 00000000000..bad3b7d5601 --- /dev/null +++ b/idea/resources/intentionDescriptions/ImportMemberIntention/description.html @@ -0,0 +1,6 @@ + + +This intention converts qualified references to a Java static member, object member or enum class member +into simple references and adds import directive for that symbol. + + \ No newline at end of file diff --git a/idea/src/META-INF/plugin.xml b/idea/src/META-INF/plugin.xml index eaa3985c911..826c7e74452 100644 --- a/idea/src/META-INF/plugin.xml +++ b/idea/src/META-INF/plugin.xml @@ -627,6 +627,11 @@ Kotlin + + org.jetbrains.kotlin.idea.intentions.ImportMemberIntention + Kotlin + + org.jetbrains.kotlin.idea.intentions.SpecifyTypeExplicitlyIntention Kotlin diff --git a/idea/src/org/jetbrains/kotlin/idea/intentions/ImportMemberIntention.kt b/idea/src/org/jetbrains/kotlin/idea/intentions/ImportMemberIntention.kt new file mode 100644 index 00000000000..465ea86d5ec --- /dev/null +++ b/idea/src/org/jetbrains/kotlin/idea/intentions/ImportMemberIntention.kt @@ -0,0 +1,86 @@ +/* + * Copyright 2010-2015 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jetbrains.kotlin.idea.intentions + +import com.intellij.openapi.editor.Editor +import org.jetbrains.kotlin.idea.caches.resolve.analyze +import org.jetbrains.kotlin.idea.imports.canBeReferencedViaImport +import org.jetbrains.kotlin.idea.imports.importableFqName +import org.jetbrains.kotlin.idea.references.mainReference +import org.jetbrains.kotlin.idea.util.ImportInsertHelper +import org.jetbrains.kotlin.idea.util.ShortenReferences +import org.jetbrains.kotlin.name.FqName +import org.jetbrains.kotlin.psi.KtCallExpression +import org.jetbrains.kotlin.psi.KtDotQualifiedExpression +import org.jetbrains.kotlin.psi.KtNameReferenceExpression +import org.jetbrains.kotlin.psi.psiUtil.collectDescendantsOfType +import org.jetbrains.kotlin.psi.psiUtil.getQualifiedElementSelector +import org.jetbrains.kotlin.resolve.BindingContext +import org.jetbrains.kotlin.resolve.lazy.BodyResolveMode + +class ImportMemberIntention : SelfTargetingOffsetIndependentIntention( + KtNameReferenceExpression::class.java, + "Add import for member" +){ + override fun isApplicableTo(element: KtNameReferenceExpression): Boolean { + val qualifiedExpression = qualifiedExpression(element) ?: return false + + val fqName = targetFqName(qualifiedExpression) ?: return false + + text = "Add import for '${fqName.asString()}'" + return true + } + + override fun applyTo(element: KtNameReferenceExpression, editor: Editor) { + val bindingContext = element.analyze(BodyResolveMode.PARTIAL) + val targets = element.mainReference.resolveToDescriptors(bindingContext) + val fqName = targets.map { it.importableFqName!! }.single() + + val file = element.getContainingKtFile() + val helper = ImportInsertHelper.getInstance(element.project) + if (helper.importDescriptor(file, targets.first()) == ImportInsertHelper.ImportDescriptorResult.FAIL) return + + val qualifiedExpressions = file.collectDescendantsOfType { qualifiedExpression -> + val selector = qualifiedExpression.getQualifiedElementSelector() as? KtNameReferenceExpression + ?: return@collectDescendantsOfType false + val qualifierName = qualifiedExpression.receiverExpression.getQualifiedElementSelector() as? KtNameReferenceExpression + ?: return@collectDescendantsOfType false + selector.getReferencedNameAsName() == fqName.shortName() + && qualifierName.getReferencedNameAsName() == fqName.parent().shortName() + && targetFqName(qualifiedExpression) == fqName + } + + //TODO: not deep + ShortenReferences.DEFAULT.process(qualifiedExpressions) + } + + private fun qualifiedExpression(element: KtNameReferenceExpression): KtDotQualifiedExpression? { + val parent = element.parent + return (if (parent is KtCallExpression) parent.parent else parent) as? KtDotQualifiedExpression + } + + private fun targetFqName(qualifiedExpression: KtDotQualifiedExpression): FqName? { + val nameExpression = qualifiedExpression.getQualifiedElementSelector() as? KtNameReferenceExpression ?: return null + val bindingContext = qualifiedExpression.analyze(BodyResolveMode.PARTIAL) + if (bindingContext[BindingContext.QUALIFIER, qualifiedExpression.receiverExpression] == null) return null + + val targets = nameExpression.mainReference.resolveToDescriptors(bindingContext) + if (targets.isEmpty()) return null + if (!targets.all { it.canBeReferencedViaImport() }) return null + return targets.map { it.importableFqName }.singleOrNull() + } +} \ No newline at end of file diff --git a/idea/testData/intentions/importMember/.intention b/idea/testData/intentions/importMember/.intention new file mode 100644 index 00000000000..da84047aa11 --- /dev/null +++ b/idea/testData/intentions/importMember/.intention @@ -0,0 +1 @@ +org.jetbrains.kotlin.idea.intentions.ImportMemberIntention diff --git a/idea/testData/intentions/importMember/EnumMember.kt b/idea/testData/intentions/importMember/EnumMember.kt new file mode 100644 index 00000000000..82a086dd37d --- /dev/null +++ b/idea/testData/intentions/importMember/EnumMember.kt @@ -0,0 +1,7 @@ +// INTENTION_TEXT: "Add import for 'kotlin.LazyThreadSafetyMode.NONE'" +// WITH_RUNTIME + +class A { + val v1: Int by lazy(LazyThreadSafetyMode.NONE) { 1 } + val v2: Int by lazy(LazyThreadSafetyMode.NONE) { 1 } +} \ No newline at end of file diff --git a/idea/testData/intentions/importMember/EnumMember.kt.after b/idea/testData/intentions/importMember/EnumMember.kt.after new file mode 100644 index 00000000000..ba89f2f360d --- /dev/null +++ b/idea/testData/intentions/importMember/EnumMember.kt.after @@ -0,0 +1,9 @@ +import kotlin.LazyThreadSafetyMode.NONE + +// INTENTION_TEXT: "Add import for 'kotlin.LazyThreadSafetyMode.NONE'" +// WITH_RUNTIME + +class A { + val v1: Int by lazy(NONE) { 1 } + val v2: Int by lazy(NONE) { 1 } +} \ No newline at end of file diff --git a/idea/testData/intentions/importMember/NoTarget.kt b/idea/testData/intentions/importMember/NoTarget.kt new file mode 100644 index 00000000000..9b746166f17 --- /dev/null +++ b/idea/testData/intentions/importMember/NoTarget.kt @@ -0,0 +1,10 @@ +// IS_APPLICABLE: false +// WITH_RUNTIME +// ERROR: Unresolved reference: xxx + +import javax.swing.SwingUtilities + +fun foo() { + SwingUtilities.xxx { + } +} diff --git a/idea/testData/intentions/importMember/ObjectMethod.kt b/idea/testData/intentions/importMember/ObjectMethod.kt new file mode 100644 index 00000000000..ebba24edeb0 --- /dev/null +++ b/idea/testData/intentions/importMember/ObjectMethod.kt @@ -0,0 +1,9 @@ +// INTENTION_TEXT: "Add import for 'kotlin.properties.Delegates.notNull'" +// WITH_RUNTIME + +import kotlin.properties.Delegates + +class A { + val v1: Int by Delegates.notNull() + val v2: Char by Delegates.notNull() +} \ No newline at end of file diff --git a/idea/testData/intentions/importMember/ObjectMethod.kt.after b/idea/testData/intentions/importMember/ObjectMethod.kt.after new file mode 100644 index 00000000000..f99423d9879 --- /dev/null +++ b/idea/testData/intentions/importMember/ObjectMethod.kt.after @@ -0,0 +1,10 @@ +// INTENTION_TEXT: "Add import for 'kotlin.properties.Delegates.notNull'" +// WITH_RUNTIME + +import kotlin.properties.Delegates +import kotlin.properties.Delegates.notNull + +class A { + val v1: Int by notNull() + val v2: Char by notNull() +} \ No newline at end of file diff --git a/idea/testData/intentions/importMember/StaticJavaField.kt b/idea/testData/intentions/importMember/StaticJavaField.kt new file mode 100644 index 00000000000..685fb48f0d7 --- /dev/null +++ b/idea/testData/intentions/importMember/StaticJavaField.kt @@ -0,0 +1,10 @@ +// INTENTION_TEXT: "Add import for 'javax.swing.SwingConstants.CENTER'" +// WITH_RUNTIME + +import javax.swing.SwingConstants + +fun foo() { + val v = SwingConstants.CENTER + + SwingConstants.CENTER +} diff --git a/idea/testData/intentions/importMember/StaticJavaField.kt.after b/idea/testData/intentions/importMember/StaticJavaField.kt.after new file mode 100644 index 00000000000..3dafbdc51b4 --- /dev/null +++ b/idea/testData/intentions/importMember/StaticJavaField.kt.after @@ -0,0 +1,11 @@ +// INTENTION_TEXT: "Add import for 'javax.swing.SwingConstants.CENTER'" +// WITH_RUNTIME + +import javax.swing.SwingConstants +import javax.swing.SwingConstants.CENTER + +fun foo() { + val v = CENTER + + CENTER +} diff --git a/idea/testData/intentions/importMember/StaticJavaMethod.kt b/idea/testData/intentions/importMember/StaticJavaMethod.kt new file mode 100644 index 00000000000..96e5332a568 --- /dev/null +++ b/idea/testData/intentions/importMember/StaticJavaMethod.kt @@ -0,0 +1,26 @@ +// INTENTION_TEXT: "Add import for 'javax.swing.SwingUtilities.invokeLater'" +// WITH_RUNTIME +// ERROR: No value passed for parameter p0 +// ERROR: Unresolved reference: SomethingElse +// ERROR: Unresolved reference: somethingElse + +import javax.swing.SwingUtilities + +fun foo() { + SwingUtilities.invokeLater() +} + +fun bar() { + javax.swing.SwingUtilities.invokeLater { + } + + javax.swing.SwingUtilities.invokeLater(Runnable { + SwingUtilities.invokeLater { } + }) + + SwingUtilities.invokeAndWait { } + + SomethingElse.invokeLater() + + somethingElse.SwingUtilities.invokeLater() +} \ No newline at end of file diff --git a/idea/testData/intentions/importMember/StaticJavaMethod.kt.after b/idea/testData/intentions/importMember/StaticJavaMethod.kt.after new file mode 100644 index 00000000000..7a0320768d0 --- /dev/null +++ b/idea/testData/intentions/importMember/StaticJavaMethod.kt.after @@ -0,0 +1,27 @@ +// INTENTION_TEXT: "Add import for 'javax.swing.SwingUtilities.invokeLater'" +// WITH_RUNTIME +// ERROR: No value passed for parameter p0 +// ERROR: Unresolved reference: SomethingElse +// ERROR: Unresolved reference: somethingElse + +import javax.swing.SwingUtilities +import javax.swing.SwingUtilities.invokeLater + +fun foo() { + invokeLater() +} + +fun bar() { + invokeLater { + } + + invokeLater(Runnable { + invokeLater { } + }) + + SwingUtilities.invokeAndWait { } + + SomethingElse.invokeLater() + + somethingElse.SwingUtilities.invokeLater() +} \ No newline at end of file diff --git a/idea/tests/org/jetbrains/kotlin/idea/intentions/IntentionTestGenerated.java b/idea/tests/org/jetbrains/kotlin/idea/intentions/IntentionTestGenerated.java index 4acf911137f..e00f3d069e3 100644 --- a/idea/tests/org/jetbrains/kotlin/idea/intentions/IntentionTestGenerated.java +++ b/idea/tests/org/jetbrains/kotlin/idea/intentions/IntentionTestGenerated.java @@ -5098,6 +5098,45 @@ public class IntentionTestGenerated extends AbstractIntentionTest { } } + @TestMetadata("idea/testData/intentions/importMember") + @TestDataPath("$PROJECT_ROOT") + @RunWith(JUnit3RunnerWithInners.class) + public static class ImportMember extends AbstractIntentionTest { + public void testAllFilesPresentInImportMember() throws Exception { + KotlinTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("idea/testData/intentions/importMember"), Pattern.compile("^([\\w\\-_]+)\\.kt$"), true); + } + + @TestMetadata("EnumMember.kt") + public void testEnumMember() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/importMember/EnumMember.kt"); + doTest(fileName); + } + + @TestMetadata("NoTarget.kt") + public void testNoTarget() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/importMember/NoTarget.kt"); + doTest(fileName); + } + + @TestMetadata("ObjectMethod.kt") + public void testObjectMethod() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/importMember/ObjectMethod.kt"); + doTest(fileName); + } + + @TestMetadata("StaticJavaField.kt") + public void testStaticJavaField() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/importMember/StaticJavaField.kt"); + doTest(fileName); + } + + @TestMetadata("StaticJavaMethod.kt") + public void testStaticJavaMethod() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/importMember/StaticJavaMethod.kt"); + doTest(fileName); + } + } + @TestMetadata("idea/testData/intentions/infixCallToOrdinary") @TestDataPath("$PROJECT_ROOT") @RunWith(JUnit3RunnerWithInners.class)