diff --git a/idea/ide-common/src/org/jetbrains/kotlin/util/descriptorUtils.kt b/idea/ide-common/src/org/jetbrains/kotlin/util/descriptorUtils.kt index 140856a968c..cc88f6266ab 100644 --- a/idea/ide-common/src/org/jetbrains/kotlin/util/descriptorUtils.kt +++ b/idea/ide-common/src/org/jetbrains/kotlin/util/descriptorUtils.kt @@ -28,10 +28,14 @@ import org.jetbrains.kotlin.types.checker.KotlinTypeChecker import org.jetbrains.kotlin.types.checker.KotlinTypeCheckerImpl import org.jetbrains.kotlin.types.typeUtil.equalTypesOrNulls -fun descriptorsEqualWithSubstitution(descriptor1: DeclarationDescriptor?, descriptor2: DeclarationDescriptor?): Boolean { +fun descriptorsEqualWithSubstitution( + descriptor1: DeclarationDescriptor?, + descriptor2: DeclarationDescriptor?, + checkOriginals: Boolean = true +): Boolean { if (descriptor1 == descriptor2) return true if (descriptor1 == null || descriptor2 == null) return false - if (descriptor1.original != descriptor2.original) return false + if (checkOriginals && descriptor1.original != descriptor2.original) return false if (descriptor1 !is CallableDescriptor) return true descriptor2 as CallableDescriptor diff --git a/idea/src/META-INF/plugin.xml b/idea/src/META-INF/plugin.xml index eef2b37024f..7b17ccbaf3f 100644 --- a/idea/src/META-INF/plugin.xml +++ b/idea/src/META-INF/plugin.xml @@ -1645,7 +1645,12 @@ Kotlin - + org.jetbrains.kotlin.idea.intentions.MoveMemberToTopLevelIntention + Kotlin + + + (KtNamedDeclaration::class.java, text) { override fun startInWriteAction() = false diff --git a/idea/src/org/jetbrains/kotlin/idea/intentions/MoveMemberToTopLevelIntention.kt b/idea/src/org/jetbrains/kotlin/idea/intentions/MoveMemberToTopLevelIntention.kt new file mode 100644 index 00000000000..1b340a54803 --- /dev/null +++ b/idea/src/org/jetbrains/kotlin/idea/intentions/MoveMemberToTopLevelIntention.kt @@ -0,0 +1,67 @@ +/* + * Copyright 2010-2017 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.util.TextRange +import com.intellij.psi.PsiElement +import com.intellij.refactoring.util.RefactoringUIUtil +import com.intellij.util.containers.MultiMap +import org.jetbrains.kotlin.idea.caches.resolve.findModuleDescriptor +import org.jetbrains.kotlin.idea.search.usagesSearch.descriptor +import org.jetbrains.kotlin.incremental.components.NoLookupLocation +import org.jetbrains.kotlin.name.Name +import org.jetbrains.kotlin.psi.* +import org.jetbrains.kotlin.psi.psiUtil.containingClassOrObject +import org.jetbrains.kotlin.resolve.lazy.descriptors.LazyPackageDescriptor +import org.jetbrains.kotlin.util.descriptorsEqualWithSubstitution +import org.jetbrains.kotlin.utils.addToStdlib.firstIsInstance + +class MoveMemberToTopLevelIntention : MoveMemberOutOfObjectIntention("Move to top level") { + override fun addConflicts(element: KtNamedDeclaration, conflicts: MultiMap) { + val packageViewDescriptor = element.findModuleDescriptor().getPackage(element.containingKtFile.packageFqName) + val packageDescriptor = packageViewDescriptor.fragments.firstIsInstance() + val memberScope = packageDescriptor.getMemberScope() + val packageName = packageViewDescriptor.fqName.asString().takeIf { it.isNotBlank() } ?: "default" + + val name = element.name ?: return + + val isRedeclaration = when (element) { + is KtProperty -> memberScope.getVariableNames().any { name == it.identifier } + + is KtFunction -> { + memberScope.getContributedFunctions(Name.identifier(name), NoLookupLocation.FROM_IDE).filter { + descriptorsEqualWithSubstitution(element.descriptor, it, false) + }.isNotEmpty() + } + + else -> false + } + + if (isRedeclaration) { + conflicts.putValue(element, "Package '$packageName' already contains ${RefactoringUIUtil.getDescription(element, false)}") + } + } + + override fun getDestination(element: KtNamedDeclaration) = element.containingKtFile + + override fun applicabilityRange(element: KtNamedDeclaration): TextRange? { + if (element !is KtNamedFunction && element !is KtProperty) return null + element.containingClassOrObject as? KtObjectDeclaration ?: return null + return element.nameIdentifier?.textRange + } + +} \ No newline at end of file diff --git a/idea/testData/intentions/moveMemberToTopLevel/.intention b/idea/testData/intentions/moveMemberToTopLevel/.intention new file mode 100644 index 00000000000..d17ee135a90 --- /dev/null +++ b/idea/testData/intentions/moveMemberToTopLevel/.intention @@ -0,0 +1 @@ +org.jetbrains.kotlin.idea.intentions.MoveMemberToTopLevelIntention \ No newline at end of file diff --git a/idea/testData/intentions/moveMemberToTopLevel/abstractFunction.kt b/idea/testData/intentions/moveMemberToTopLevel/abstractFunction.kt new file mode 100644 index 00000000000..5f0b37191ed --- /dev/null +++ b/idea/testData/intentions/moveMemberToTopLevel/abstractFunction.kt @@ -0,0 +1,5 @@ +// IS_APPLICABLE: false + +abstract class A { + abstract fun foo() +} \ No newline at end of file diff --git a/idea/testData/intentions/moveMemberToTopLevel/abstractProperty.kt b/idea/testData/intentions/moveMemberToTopLevel/abstractProperty.kt new file mode 100644 index 00000000000..2562dc13eef --- /dev/null +++ b/idea/testData/intentions/moveMemberToTopLevel/abstractProperty.kt @@ -0,0 +1,5 @@ +// IS_APPLICABLE: false + +abstract class A { + abstract val foo: Int +} \ No newline at end of file diff --git a/idea/testData/intentions/moveMemberToTopLevel/function.kt b/idea/testData/intentions/moveMemberToTopLevel/function.kt new file mode 100644 index 00000000000..0a2e6c62271 --- /dev/null +++ b/idea/testData/intentions/moveMemberToTopLevel/function.kt @@ -0,0 +1,5 @@ +// WITH_RUNTIME + +object A { + private fun foo() = 1 +} \ No newline at end of file diff --git a/idea/testData/intentions/moveMemberToTopLevel/function.kt.after b/idea/testData/intentions/moveMemberToTopLevel/function.kt.after new file mode 100644 index 00000000000..d7707d28b1f --- /dev/null +++ b/idea/testData/intentions/moveMemberToTopLevel/function.kt.after @@ -0,0 +1,3 @@ +// WITH_RUNTIME + +private fun foo() = 1 \ No newline at end of file diff --git a/idea/testData/intentions/moveMemberToTopLevel/functionInCompanion.kt b/idea/testData/intentions/moveMemberToTopLevel/functionInCompanion.kt new file mode 100644 index 00000000000..41c2b06c4c4 --- /dev/null +++ b/idea/testData/intentions/moveMemberToTopLevel/functionInCompanion.kt @@ -0,0 +1,7 @@ +// WITH_RUNTIME + +class A { + companion object { + private fun foo() = 1 + } +} \ No newline at end of file diff --git a/idea/testData/intentions/moveMemberToTopLevel/functionInCompanion.kt.after b/idea/testData/intentions/moveMemberToTopLevel/functionInCompanion.kt.after new file mode 100644 index 00000000000..e9b5998eced --- /dev/null +++ b/idea/testData/intentions/moveMemberToTopLevel/functionInCompanion.kt.after @@ -0,0 +1,6 @@ +// WITH_RUNTIME + +class A { +} + +private fun foo() = 1 diff --git a/idea/testData/intentions/moveMemberToTopLevel/property.kt b/idea/testData/intentions/moveMemberToTopLevel/property.kt new file mode 100644 index 00000000000..f04472954b8 --- /dev/null +++ b/idea/testData/intentions/moveMemberToTopLevel/property.kt @@ -0,0 +1,5 @@ +// WITH_RUNTIME + +object A { + val foo: Int = 1 +} \ No newline at end of file diff --git a/idea/testData/intentions/moveMemberToTopLevel/property.kt.after b/idea/testData/intentions/moveMemberToTopLevel/property.kt.after new file mode 100644 index 00000000000..9cb9f19b3a1 --- /dev/null +++ b/idea/testData/intentions/moveMemberToTopLevel/property.kt.after @@ -0,0 +1,3 @@ +// WITH_RUNTIME + +val foo: Int = 1 \ No newline at end of file diff --git a/idea/testData/intentions/moveMemberToTopLevel/propertyInCompanion.kt b/idea/testData/intentions/moveMemberToTopLevel/propertyInCompanion.kt new file mode 100644 index 00000000000..d45e603d9d9 --- /dev/null +++ b/idea/testData/intentions/moveMemberToTopLevel/propertyInCompanion.kt @@ -0,0 +1,7 @@ +// WITH_RUNTIME + +class A { + companion object { + val foo: Int = 1 + } +} \ No newline at end of file diff --git a/idea/testData/intentions/moveMemberToTopLevel/propertyInCompanion.kt.after b/idea/testData/intentions/moveMemberToTopLevel/propertyInCompanion.kt.after new file mode 100644 index 00000000000..bd1139da7e0 --- /dev/null +++ b/idea/testData/intentions/moveMemberToTopLevel/propertyInCompanion.kt.after @@ -0,0 +1,6 @@ +// WITH_RUNTIME + +class A { +} + +val foo: Int = 1 diff --git a/idea/testData/intentions/moveMemberToTopLevel/redeclarationConflict.kt b/idea/testData/intentions/moveMemberToTopLevel/redeclarationConflict.kt new file mode 100644 index 00000000000..432df631c19 --- /dev/null +++ b/idea/testData/intentions/moveMemberToTopLevel/redeclarationConflict.kt @@ -0,0 +1,6 @@ +// SHOULD_FAIL_WITH: Package 'default' already contains function f1(Int) +object Test { + fun f1(n: Int) {} +} + +fun f1(n: Int) {} diff --git a/idea/testData/intentions/moveMemberToTopLevel/redeclarationConflictWithPackage.kt b/idea/testData/intentions/moveMemberToTopLevel/redeclarationConflictWithPackage.kt new file mode 100644 index 00000000000..8d1b1e5b518 --- /dev/null +++ b/idea/testData/intentions/moveMemberToTopLevel/redeclarationConflictWithPackage.kt @@ -0,0 +1,8 @@ +// SHOULD_FAIL_WITH: Package 'foo.bar' already contains function f1(Int) +package foo.bar + +object Test { + fun f1(n: Int) {} +} + +fun f1(n: Int) {} diff --git a/idea/testData/intentions/moveMemberToTopLevel/redeclarationPropertyConflict.kt b/idea/testData/intentions/moveMemberToTopLevel/redeclarationPropertyConflict.kt new file mode 100644 index 00000000000..d081c814598 --- /dev/null +++ b/idea/testData/intentions/moveMemberToTopLevel/redeclarationPropertyConflict.kt @@ -0,0 +1,6 @@ +// SHOULD_FAIL_WITH: Package 'default' already contains property f1 +object Test { + val f1 = 1 +} + +val f1 = 1 diff --git a/idea/tests/org/jetbrains/kotlin/idea/intentions/IntentionTestGenerated.java b/idea/tests/org/jetbrains/kotlin/idea/intentions/IntentionTestGenerated.java index 4b4621a6a37..4334978b8d9 100644 --- a/idea/tests/org/jetbrains/kotlin/idea/intentions/IntentionTestGenerated.java +++ b/idea/tests/org/jetbrains/kotlin/idea/intentions/IntentionTestGenerated.java @@ -11706,6 +11706,72 @@ public class IntentionTestGenerated extends AbstractIntentionTest { } } + @TestMetadata("idea/testData/intentions/moveMemberToTopLevel") + @TestDataPath("$PROJECT_ROOT") + @RunWith(JUnit3RunnerWithInners.class) + public static class MoveMemberToTopLevel extends AbstractIntentionTest { + @TestMetadata("abstractFunction.kt") + public void testAbstractFunction() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/moveMemberToTopLevel/abstractFunction.kt"); + doTest(fileName); + } + + @TestMetadata("abstractProperty.kt") + public void testAbstractProperty() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/moveMemberToTopLevel/abstractProperty.kt"); + doTest(fileName); + } + + public void testAllFilesPresentInMoveMemberToTopLevel() throws Exception { + KotlinTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("idea/testData/intentions/moveMemberToTopLevel"), + Pattern.compile("^([\\w\\-_]+)\\.kt$"), TargetBackend.ANY, true); + } + + @TestMetadata("function.kt") + public void testFunction() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/moveMemberToTopLevel/function.kt"); + doTest(fileName); + } + + @TestMetadata("functionInCompanion.kt") + public void testFunctionInCompanion() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/moveMemberToTopLevel/functionInCompanion.kt"); + doTest(fileName); + } + + @TestMetadata("property.kt") + public void testProperty() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/moveMemberToTopLevel/property.kt"); + doTest(fileName); + } + + @TestMetadata("propertyInCompanion.kt") + public void testPropertyInCompanion() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/moveMemberToTopLevel/propertyInCompanion.kt"); + doTest(fileName); + } + + @TestMetadata("redeclarationConflict.kt") + public void testRedeclarationConflict() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/moveMemberToTopLevel/redeclarationConflict.kt"); + doTest(fileName); + } + + @TestMetadata("redeclarationConflictWithPackage.kt") + public void testRedeclarationConflictWithPackage() throws Exception { + String fileName = + KotlinTestUtils.navigationMetadata("idea/testData/intentions/moveMemberToTopLevel/redeclarationConflictWithPackage.kt"); + doTest(fileName); + } + + @TestMetadata("redeclarationPropertyConflict.kt") + public void testRedeclarationPropertyConflict() throws Exception { + String fileName = + KotlinTestUtils.navigationMetadata("idea/testData/intentions/moveMemberToTopLevel/redeclarationPropertyConflict.kt"); + doTest(fileName); + } + } + @TestMetadata("idea/testData/intentions/moveOutOfCompanion") @TestDataPath("$PROJECT_ROOT") @RunWith(JUnit3RunnerWithInners.class)