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 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>(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<KtNa
listOf(element),
KotlinMoveTargetForExistingElement(targetClass),
MoveDeclarationsDelegate.NestedClass())
MoveKotlinDeclarationsProcessor(moveDescriptor).run()
deleteCompanionIfEmpty()
runWriteAction {
MoveKotlinDeclarationsProcessor(moveDescriptor).run()
deleteCompanionIfEmpty()
}
return
}
val externalRefs = project.runSynchronouslyWithProgress("Searching for ${element.name}", true) {
ReferencesSearch.search(element).filter {
val refElement = it.element ?: return@filter false
!targetClass.isAncestor(refElement) || companionObject.isAncestor(refElement)
val targetClassDescriptor = runReadAction { targetClass.resolveToDescriptor() as ClassDescriptor }
val conflicts = MultiMap<PsiElement, String>()
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
val conflicts = MultiMap<PsiElement, String>()
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) {
@@ -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
// 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 {
companion object {
class B {
@@ -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 &lt;anonymous&gt;() on A.Companion will require class instance
class A {
companion object {
class B {
@@ -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', ' '))
@@ -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");