Convert Member to Extension Intention: Drop open/final modifiers, add imports when necessary, transform Java-site calls

#KT-8876 Fixed
This commit is contained in:
Alexey Sedunov
2016-01-28 12:41:50 +03:00
parent 99e4c8c06d
commit 3e2036b6ec
14 changed files with 143 additions and 1 deletions
@@ -20,14 +20,23 @@ import com.intellij.codeInsight.intention.LowPriorityAction
import com.intellij.openapi.editor.Editor import com.intellij.openapi.editor.Editor
import com.intellij.openapi.editor.ScrollType import com.intellij.openapi.editor.ScrollType
import com.intellij.openapi.util.TextRange import com.intellij.openapi.util.TextRange
import com.intellij.psi.PsiMethodCallExpression
import com.intellij.psi.PsiReferenceExpression
import com.intellij.psi.codeStyle.JavaCodeStyleManager
import com.intellij.psi.search.searches.ReferencesSearch
import com.intellij.util.SmartList
import org.jetbrains.kotlin.asJava.toLightMethods
import org.jetbrains.kotlin.descriptors.ClassDescriptor import org.jetbrains.kotlin.descriptors.ClassDescriptor
import org.jetbrains.kotlin.idea.caches.resolve.resolveToDescriptor import org.jetbrains.kotlin.idea.caches.resolve.resolveToDescriptor
import org.jetbrains.kotlin.idea.intentions.SelfTargetingRangeIntention import org.jetbrains.kotlin.idea.intentions.SelfTargetingRangeIntention
import org.jetbrains.kotlin.idea.intentions.setReceiverType import org.jetbrains.kotlin.idea.intentions.setReceiverType
import org.jetbrains.kotlin.idea.quickfix.moveCaret import org.jetbrains.kotlin.idea.quickfix.moveCaret
import org.jetbrains.kotlin.idea.quickfix.unblockDocument import org.jetbrains.kotlin.idea.quickfix.unblockDocument
import org.jetbrains.kotlin.idea.references.KtReference
import org.jetbrains.kotlin.idea.util.ImportInsertHelper
import org.jetbrains.kotlin.lexer.KtTokens import org.jetbrains.kotlin.lexer.KtTokens
import org.jetbrains.kotlin.psi.* import org.jetbrains.kotlin.psi.*
import org.jetbrains.kotlin.utils.addIfNotNull
class ConvertMemberToExtensionIntention : SelfTargetingRangeIntention<KtCallableDeclaration>(KtCallableDeclaration::class.java, "Convert member to extension"), LowPriorityAction { class ConvertMemberToExtensionIntention : SelfTargetingRangeIntention<KtCallableDeclaration>(KtCallableDeclaration::class.java, "Convert member to extension"), LowPriorityAction {
override fun applicabilityRange(element: KtCallableDeclaration): TextRange? { override fun applicabilityRange(element: KtCallableDeclaration): TextRange? {
@@ -49,11 +58,25 @@ class ConvertMemberToExtensionIntention : SelfTargetingRangeIntention<KtCallable
val containingClass = descriptor.containingDeclaration as ClassDescriptor val containingClass = descriptor.containingDeclaration as ClassDescriptor
val file = element.getContainingKtFile() val file = element.getContainingKtFile()
val project = file.project
val outermostParent = KtPsiUtil.getOutermostParent(element, file, false) val outermostParent = KtPsiUtil.getOutermostParent(element, file, false)
val ktFilesToAddImports = SmartList<KtFile>()
val javaCallsToFix = SmartList<PsiMethodCallExpression>()
for (ref in ReferencesSearch.search(element)) {
when (ref) {
is KtReference -> {
val refFile = ref.element.getContainingKtFile()
if (refFile != file) {
ktFilesToAddImports.add(refFile)
}
}
is PsiReferenceExpression -> javaCallsToFix.addIfNotNull(ref.parent as? PsiMethodCallExpression)
}
}
val typeParameterList = newTypeParameterList(element) val typeParameterList = newTypeParameterList(element)
val project = element.project
val psiFactory = KtPsiFactory(element) val psiFactory = KtPsiFactory(element)
val extension = file.addAfter(element, outermostParent) as KtCallableDeclaration val extension = file.addAfter(element, outermostParent) as KtCallableDeclaration
@@ -75,6 +98,8 @@ class ConvertMemberToExtensionIntention : SelfTargetingRangeIntention<KtCallable
extension.modifierList?.getModifier(KtTokens.PROTECTED_KEYWORD)?.delete() extension.modifierList?.getModifier(KtTokens.PROTECTED_KEYWORD)?.delete()
extension.modifierList?.getModifier(KtTokens.ABSTRACT_KEYWORD)?.delete() extension.modifierList?.getModifier(KtTokens.ABSTRACT_KEYWORD)?.delete()
extension.modifierList?.getModifier(KtTokens.OPEN_KEYWORD)?.delete()
extension.modifierList?.getModifier(KtTokens.FINAL_KEYWORD)?.delete()
var bodyToSelect: KtExpression? = null var bodyToSelect: KtExpression? = null
@@ -125,6 +150,27 @@ class ConvertMemberToExtensionIntention : SelfTargetingRangeIntention<KtCallable
} }
} }
if (ktFilesToAddImports.isNotEmpty()) {
val newDescriptor = extension.resolveToDescriptor()
val importInsertHelper = ImportInsertHelper.getInstance(project)
for (ktFileToAddImport in ktFilesToAddImports) {
importInsertHelper.importDescriptor(ktFileToAddImport, newDescriptor)
}
}
if (javaCallsToFix.isNotEmpty()) {
val lightMethod = extension.toLightMethods().first()
for (javaCallToFix in javaCallsToFix) {
javaCallToFix.methodExpression.qualifierExpression?.let {
val argumentList = javaCallToFix.argumentList
argumentList.addBefore(it, argumentList.expressions.firstOrNull())
}
val newRef = javaCallToFix.methodExpression.bindToElement(lightMethod)
JavaCodeStyleManager.getInstance(project).shortenClassReferences(newRef)
}
}
editor?.apply { editor?.apply {
unblockDocument() unblockDocument()
@@ -0,0 +1,6 @@
// WITH_RUNTIME
class Owner {
final fun <caret>f() {
}
}
@@ -0,0 +1,7 @@
// WITH_RUNTIME
class Owner {
}
fun Owner.f() {
}
@@ -0,0 +1,6 @@
// WITH_RUNTIME
open class Owner {
open fun <caret>f() {
}
}
@@ -0,0 +1,7 @@
// WITH_RUNTIME
open class Owner {
}
fun Owner.f() {
}
@@ -0,0 +1,5 @@
{
"mainFile": "x/test.kt",
"intentionClass": "org.jetbrains.kotlin.idea.intentions.declarations.ConvertMemberToExtensionIntention",
"withRuntime": "true"
}
@@ -0,0 +1,8 @@
package x
interface A {
}
fun A.foo(n: Int) {
throw UnsupportedOperationException()
}
@@ -0,0 +1,10 @@
package y;
import x.A;
import x.TestKt;
class J {
static void foo(A a) {
TestKt.foo(a, 1);
}
}
@@ -0,0 +1,8 @@
package y
import x.A
import x.foo
fun bar(a: A) {
a.foo(1)
}
@@ -0,0 +1,5 @@
package x
interface A {
fun <caret>foo(n: Int)
}
@@ -0,0 +1,9 @@
package y;
import x.A;
class J {
static void foo(A a) {
a.foo(1);
}
}
@@ -0,0 +1,7 @@
package y
import x.A
fun bar(a: A) {
a.foo(1)
}
@@ -4741,6 +4741,12 @@ public class IntentionTestGenerated extends AbstractIntentionTest {
doTest(fileName); doTest(fileName);
} }
@TestMetadata("final.kt")
public void testFinal() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/declarations/convertMemberToExtension/final.kt");
doTest(fileName);
}
@TestMetadata("funcitonNoName.kt") @TestMetadata("funcitonNoName.kt")
public void testFuncitonNoName() throws Exception { public void testFuncitonNoName() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/declarations/convertMemberToExtension/funcitonNoName.kt"); String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/declarations/convertMemberToExtension/funcitonNoName.kt");
@@ -4825,6 +4831,12 @@ public class IntentionTestGenerated extends AbstractIntentionTest {
doTest(fileName); doTest(fileName);
} }
@TestMetadata("open.kt")
public void testOpen() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/declarations/convertMemberToExtension/open.kt");
doTest(fileName);
}
@TestMetadata("outsideFunction.kt") @TestMetadata("outsideFunction.kt")
public void testOutsideFunction() throws Exception { public void testOutsideFunction() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/declarations/convertMemberToExtension/outsideFunction.kt"); String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/declarations/convertMemberToExtension/outsideFunction.kt");
@@ -35,6 +35,12 @@ public class MultiFileIntentionTestGenerated extends AbstractMultiFileIntentionT
KotlinTestUtils.assertAllTestsPresentInSingleGeneratedClass(this.getClass(), new File("idea/testData/multiFileIntentions"), Pattern.compile("^(.+)\\.test$")); KotlinTestUtils.assertAllTestsPresentInSingleGeneratedClass(this.getClass(), new File("idea/testData/multiFileIntentions"), Pattern.compile("^(.+)\\.test$"));
} }
@TestMetadata("convertMemberToExtension/addImports/addImports.test")
public void testConvertMemberToExtension_addImports_AddImports() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/multiFileIntentions/convertMemberToExtension/addImports/addImports.test");
doTest(fileName);
}
@TestMetadata("implementAbstractMember/implementFunctionInJava/implementAllInJava.test") @TestMetadata("implementAbstractMember/implementFunctionInJava/implementAllInJava.test")
public void testImplementAbstractMember_implementFunctionInJava_ImplementAllInJava() throws Exception { public void testImplementAbstractMember_implementFunctionInJava_ImplementAllInJava() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/multiFileIntentions/implementAbstractMember/implementFunctionInJava/implementAllInJava.test"); String fileName = KotlinTestUtils.navigationMetadata("idea/testData/multiFileIntentions/implementAbstractMember/implementFunctionInJava/implementAllInJava.test");