diff --git a/idea/src/org/jetbrains/kotlin/idea/intentions/MoveMemberOutOfCompanionObjectIntention.kt b/idea/src/org/jetbrains/kotlin/idea/intentions/MoveMemberOutOfCompanionObjectIntention.kt index f1c1901a700..14258761072 100644 --- a/idea/src/org/jetbrains/kotlin/idea/intentions/MoveMemberOutOfCompanionObjectIntention.kt +++ b/idea/src/org/jetbrains/kotlin/idea/intentions/MoveMemberOutOfCompanionObjectIntention.kt @@ -24,19 +24,28 @@ import com.intellij.refactoring.util.RefactoringUIUtil import com.intellij.util.containers.MultiMap import org.jetbrains.kotlin.descriptors.CallableMemberDescriptor import org.jetbrains.kotlin.descriptors.ClassDescriptor +import org.jetbrains.kotlin.idea.caches.resolve.analyzeFully import org.jetbrains.kotlin.idea.caches.resolve.resolveToDescriptor +import org.jetbrains.kotlin.idea.caches.resolve.resolveToDescriptorIfAny import org.jetbrains.kotlin.idea.codeInsight.DescriptorToSourceUtilsIde import org.jetbrains.kotlin.idea.refactoring.checkConflictsInteractively +import org.jetbrains.kotlin.idea.refactoring.getUsageContext import org.jetbrains.kotlin.idea.refactoring.move.moveDeclarations.* import org.jetbrains.kotlin.idea.runSynchronouslyWithProgress +import org.jetbrains.kotlin.idea.util.application.runReadAction import org.jetbrains.kotlin.idea.util.application.runWriteAction import org.jetbrains.kotlin.psi.* import org.jetbrains.kotlin.psi.psiUtil.containingClassOrObject -import org.jetbrains.kotlin.psi.psiUtil.isAncestor +import org.jetbrains.kotlin.psi.psiUtil.parents +import org.jetbrains.kotlin.resolve.calls.callUtil.getResolvedCall +import org.jetbrains.kotlin.resolve.descriptorUtil.isSubclassOf +import org.jetbrains.kotlin.resolve.scopes.receivers.ImplicitClassReceiver import org.jetbrains.kotlin.util.findCallableMemberBySignature class MoveMemberOutOfCompanionObjectIntention : SelfTargetingRangeIntention(KtNamedDeclaration::class.java, "Move out of companion object") { + override fun startInWriteAction() = false + override fun applicabilityRange(element: KtNamedDeclaration): TextRange? { if (element !is KtNamedFunction && element !is KtProperty && element !is KtClassOrObject) return null val container = element.containingClassOrObject @@ -62,30 +71,52 @@ class MoveMemberOutOfCompanionObjectIntention : SelfTargetingRangeIntention() + + val refsRequiringClassInstance = project.runSynchronouslyWithProgress("Searching for ${element.name}", true) { + runReadAction { + ReferencesSearch + .search(element) + .mapNotNull { it.element } + .filter { + if (it !is KtElement) return@filter true + val resolvedCall = it.getResolvedCall(it.analyzeFully()) ?: return@filter false + val dispatchReceiver = resolvedCall.dispatchReceiver ?: return@filter false + if (dispatchReceiver !is ImplicitClassReceiver) return@filter true + it.parents + .filterIsInstance() + .none { + val classDescriptor = it.resolveToDescriptorIfAny() as? ClassDescriptor + if (classDescriptor != null && classDescriptor.isSubclassOf(targetClassDescriptor)) return@none true + if (it.isTopLevel() || it is KtObjectDeclaration || (it is KtClass && !it.isInner())) return@filter true + false + } + } } } ?: return - val conflicts = MultiMap() - for (ref in externalRefs) { - val refElement = ref.element ?: continue - conflicts.putValue(refElement, "Class instance required: ${refElement.text}") + for (refElement in refsRequiringClassInstance) { + val context = refElement.getUsageContext() + val message = "'${refElement.text}' in ${RefactoringUIUtil.getDescription(context, false)} will require class instance" + conflicts.putValue(refElement, message) } - val targetClassDescriptor = targetClass.resolveToDescriptor() as ClassDescriptor - val callableDescriptor = element.resolveToDescriptor() as CallableMemberDescriptor - targetClassDescriptor.findCallableMemberBySignature(callableDescriptor)?.let { - DescriptorToSourceUtilsIde.getAnyDeclaration(project, it) - }?.let { - conflicts.putValue(it, "Class '${targetClass.name}' already contains ${RefactoringUIUtil.getDescription(it, false)}") + runReadAction { + val callableDescriptor = element.resolveToDescriptor() as CallableMemberDescriptor + targetClassDescriptor.findCallableMemberBySignature(callableDescriptor)?.let { + DescriptorToSourceUtilsIde.getAnyDeclaration(project, it) + }?.let { + conflicts.putValue(it, "Class '${targetClass.name}' already contains ${RefactoringUIUtil.getDescription(it, false)}") + } } project.checkConflictsInteractively(conflicts) { diff --git a/idea/testData/intentions/moveOutOfCompanion/companionAsImplicitReceiver.kt b/idea/testData/intentions/moveOutOfCompanion/companionAsImplicitReceiver.kt new file mode 100644 index 00000000000..94153e3b46e --- /dev/null +++ b/idea/testData/intentions/moveOutOfCompanion/companionAsImplicitReceiver.kt @@ -0,0 +1,46 @@ +// WITH_RUNTIME +// SHOULD_FAIL_WITH: 'FOO' in property bar2 will require class instance, 'FOO' in property bar4 will require class instance, 'FOO' in property bar6 will require class instance, 'FOO' in variable bar7 will require class instance, 'FOO' in variable bar8 will require class instance +package test + +open class A { + companion object { + val FOO = 1 + } +} + +class B : A() { + val bar1 = FOO + + class X { + val bar2 = FOO + } + + class W : A() { + val bar3 = FOO + } + + object Y { + val bar4 = FOO + } + + inner class Z { + val bar5 = FOO + } + + companion object { + val bar6 = FOO + } +} + +class C { + inner class D : A() { + val bar9 = FOO + } +} + +fun foo() { + val bar7 = A.FOO + with(A) { + val bar8 = FOO + } +} \ No newline at end of file diff --git a/idea/testData/intentions/moveOutOfCompanion/moveFunctionWithExternalRefs.kt b/idea/testData/intentions/moveOutOfCompanion/moveFunctionWithExternalRefs.kt index 0f3552b3843..ae1cae5983a 100644 --- a/idea/testData/intentions/moveOutOfCompanion/moveFunctionWithExternalRefs.kt +++ b/idea/testData/intentions/moveOutOfCompanion/moveFunctionWithExternalRefs.kt @@ -1,5 +1,5 @@ // WITH_RUNTIME -// SHOULD_FAIL_WITH: Class instance required: foo, Class instance required: foo, Class instance required: foo, Class instance required: foo +// SHOULD_FAIL_WITH: 'foo' in class B will require class instance, 'foo' in function test() will require class instance, 'foo' in function test() will require class instance, 'foo' in lambda <anonymous>() on A.Companion will require class instance class A { companion object { class B { diff --git a/idea/testData/intentions/moveOutOfCompanion/movePropertyWithExternalRefs.kt b/idea/testData/intentions/moveOutOfCompanion/movePropertyWithExternalRefs.kt index aa80a9a3a2f..3c2d5648b06 100644 --- a/idea/testData/intentions/moveOutOfCompanion/movePropertyWithExternalRefs.kt +++ b/idea/testData/intentions/moveOutOfCompanion/movePropertyWithExternalRefs.kt @@ -1,5 +1,5 @@ // WITH_RUNTIME -// SHOULD_FAIL_WITH: Class instance required: foo, Class instance required: foo, Class instance required: foo, Class instance required: foo +// SHOULD_FAIL_WITH: 'foo' in class B will require class instance, 'foo' in function test() will require class instance, 'foo' in function test() will require class instance, 'foo' in lambda <anonymous>() on A.Companion will require class instance class A { companion object { class B { diff --git a/idea/tests/org/jetbrains/kotlin/idea/intentions/AbstractIntentionTest.kt b/idea/tests/org/jetbrains/kotlin/idea/intentions/AbstractIntentionTest.kt index f430f2f6b47..71062f89a25 100644 --- a/idea/tests/org/jetbrains/kotlin/idea/intentions/AbstractIntentionTest.kt +++ b/idea/tests/org/jetbrains/kotlin/idea/intentions/AbstractIntentionTest.kt @@ -169,7 +169,7 @@ abstract class AbstractIntentionTest : KotlinLightCodeInsightFixtureTestCase() { TestCase.assertEquals("Expected test to fail.", "", shouldFailString) } catch (e: BaseRefactoringProcessor.ConflictsInTestsException) { - TestCase.assertEquals("Failure message mismatch.", shouldFailString, StringUtil.join(e.messages, ", ")) + TestCase.assertEquals("Failure message mismatch.", shouldFailString, StringUtil.join(e.messages.sorted(), ", ")) } catch (e: CommonRefactoringUtil.RefactoringErrorHintException) { TestCase.assertEquals("Failure message mismatch.", shouldFailString, e.message?.replace('\n', ' ')) diff --git a/idea/tests/org/jetbrains/kotlin/idea/intentions/IntentionTestGenerated.java b/idea/tests/org/jetbrains/kotlin/idea/intentions/IntentionTestGenerated.java index 4b24103ab71..be897dd03d1 100644 --- a/idea/tests/org/jetbrains/kotlin/idea/intentions/IntentionTestGenerated.java +++ b/idea/tests/org/jetbrains/kotlin/idea/intentions/IntentionTestGenerated.java @@ -11178,6 +11178,12 @@ public class IntentionTestGenerated extends AbstractIntentionTest { KotlinTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("idea/testData/intentions/moveOutOfCompanion"), Pattern.compile("^([\\w\\-_]+)\\.kt$"), TargetBackend.ANY, true); } + @TestMetadata("companionAsImplicitReceiver.kt") + public void testCompanionAsImplicitReceiver() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/moveOutOfCompanion/companionAsImplicitReceiver.kt"); + doTest(fileName); + } + @TestMetadata("moveAndDropCompanion.kt") public void testMoveAndDropCompanion() throws Exception { String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/moveOutOfCompanion/moveAndDropCompanion.kt");