Move Member out of Companion: Improve conflict checking

Also do not show conflict dialog under write action
This commit is contained in:
Alexey Sedunov
2017-04-11 14:55:51 +03:00
parent 7a25af0f2f
commit fc33d89e28
6 changed files with 103 additions and 20 deletions
@@ -24,19 +24,28 @@ import com.intellij.refactoring.util.RefactoringUIUtil
import com.intellij.util.containers.MultiMap import com.intellij.util.containers.MultiMap
import org.jetbrains.kotlin.descriptors.CallableMemberDescriptor import org.jetbrains.kotlin.descriptors.CallableMemberDescriptor
import org.jetbrains.kotlin.descriptors.ClassDescriptor 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.resolveToDescriptor
import org.jetbrains.kotlin.idea.caches.resolve.resolveToDescriptorIfAny
import org.jetbrains.kotlin.idea.codeInsight.DescriptorToSourceUtilsIde import org.jetbrains.kotlin.idea.codeInsight.DescriptorToSourceUtilsIde
import org.jetbrains.kotlin.idea.refactoring.checkConflictsInteractively 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.refactoring.move.moveDeclarations.*
import org.jetbrains.kotlin.idea.runSynchronouslyWithProgress 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.idea.util.application.runWriteAction
import org.jetbrains.kotlin.psi.* import org.jetbrains.kotlin.psi.*
import org.jetbrains.kotlin.psi.psiUtil.containingClassOrObject 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 import org.jetbrains.kotlin.util.findCallableMemberBySignature
class MoveMemberOutOfCompanionObjectIntention : SelfTargetingRangeIntention<KtNamedDeclaration>(KtNamedDeclaration::class.java, class MoveMemberOutOfCompanionObjectIntention : SelfTargetingRangeIntention<KtNamedDeclaration>(KtNamedDeclaration::class.java,
"Move out of companion object") { "Move out of companion object") {
override fun startInWriteAction() = false
override fun applicabilityRange(element: KtNamedDeclaration): TextRange? { override fun applicabilityRange(element: KtNamedDeclaration): TextRange? {
if (element !is KtNamedFunction && element !is KtProperty && element !is KtClassOrObject) return null if (element !is KtNamedFunction && element !is KtProperty && element !is KtClassOrObject) return null
val container = element.containingClassOrObject val container = element.containingClassOrObject
@@ -62,30 +71,52 @@ class MoveMemberOutOfCompanionObjectIntention : SelfTargetingRangeIntention<KtNa
listOf(element), listOf(element),
KotlinMoveTargetForExistingElement(targetClass), KotlinMoveTargetForExistingElement(targetClass),
MoveDeclarationsDelegate.NestedClass()) MoveDeclarationsDelegate.NestedClass())
MoveKotlinDeclarationsProcessor(moveDescriptor).run() runWriteAction {
deleteCompanionIfEmpty() MoveKotlinDeclarationsProcessor(moveDescriptor).run()
deleteCompanionIfEmpty()
}
return return
} }
val externalRefs = project.runSynchronouslyWithProgress("Searching for ${element.name}", true) { val targetClassDescriptor = runReadAction { targetClass.resolveToDescriptor() as ClassDescriptor }
ReferencesSearch.search(element).filter {
val refElement = it.element ?: return@filter false val conflicts = MultiMap<PsiElement, String>()
!targetClass.isAncestor(refElement) || companionObject.isAncestor(refElement)
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<KtClassOrObject>()
.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 } ?: return
val conflicts = MultiMap<PsiElement, String>() for (refElement in refsRequiringClassInstance) {
for (ref in externalRefs) { val context = refElement.getUsageContext()
val refElement = ref.element ?: continue val message = "'${refElement.text}' in ${RefactoringUIUtil.getDescription(context, false)} will require class instance"
conflicts.putValue(refElement, "Class instance required: ${refElement.text}") conflicts.putValue(refElement, message)
} }
val targetClassDescriptor = targetClass.resolveToDescriptor() as ClassDescriptor runReadAction {
val callableDescriptor = element.resolveToDescriptor() as CallableMemberDescriptor val callableDescriptor = element.resolveToDescriptor() as CallableMemberDescriptor
targetClassDescriptor.findCallableMemberBySignature(callableDescriptor)?.let { targetClassDescriptor.findCallableMemberBySignature(callableDescriptor)?.let {
DescriptorToSourceUtilsIde.getAnyDeclaration(project, it) DescriptorToSourceUtilsIde.getAnyDeclaration(project, it)
}?.let { }?.let {
conflicts.putValue(it, "Class '${targetClass.name}' already contains ${RefactoringUIUtil.getDescription(it, false)}") conflicts.putValue(it, "Class '${targetClass.name}' already contains ${RefactoringUIUtil.getDescription(it, false)}")
}
} }
project.checkConflictsInteractively(conflicts) { project.checkConflictsInteractively(conflicts) {
@@ -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 <caret>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
}
}
@@ -1,5 +1,5 @@
// WITH_RUNTIME // 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 &lt;anonymous&gt;() on A.Companion will require class instance
class A { class A {
companion object { companion object {
class B { class B {
@@ -1,5 +1,5 @@
// WITH_RUNTIME // 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 &lt;anonymous&gt;() on A.Companion will require class instance
class A { class A {
companion object { companion object {
class B { class B {
@@ -169,7 +169,7 @@ abstract class AbstractIntentionTest : KotlinLightCodeInsightFixtureTestCase() {
TestCase.assertEquals("Expected test to fail.", "", shouldFailString) TestCase.assertEquals("Expected test to fail.", "", shouldFailString)
} }
catch (e: BaseRefactoringProcessor.ConflictsInTestsException) { 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) { catch (e: CommonRefactoringUtil.RefactoringErrorHintException) {
TestCase.assertEquals("Failure message mismatch.", shouldFailString, e.message?.replace('\n', ' ')) TestCase.assertEquals("Failure message mismatch.", shouldFailString, e.message?.replace('\n', ' '))
@@ -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); 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") @TestMetadata("moveAndDropCompanion.kt")
public void testMoveAndDropCompanion() throws Exception { public void testMoveAndDropCompanion() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/moveOutOfCompanion/moveAndDropCompanion.kt"); String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/moveOutOfCompanion/moveAndDropCompanion.kt");