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.ScrollType
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.idea.caches.resolve.resolveToDescriptor
import org.jetbrains.kotlin.idea.intentions.SelfTargetingRangeIntention
import org.jetbrains.kotlin.idea.intentions.setReceiverType
import org.jetbrains.kotlin.idea.quickfix.moveCaret
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.psi.*
import org.jetbrains.kotlin.utils.addIfNotNull
class ConvertMemberToExtensionIntention : SelfTargetingRangeIntention<KtCallableDeclaration>(KtCallableDeclaration::class.java, "Convert member to extension"), LowPriorityAction {
override fun applicabilityRange(element: KtCallableDeclaration): TextRange? {
@@ -49,11 +58,25 @@ class ConvertMemberToExtensionIntention : SelfTargetingRangeIntention<KtCallable
val containingClass = descriptor.containingDeclaration as ClassDescriptor
val file = element.getContainingKtFile()
val project = file.project
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 project = element.project
val psiFactory = KtPsiFactory(element)
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.ABSTRACT_KEYWORD)?.delete()
extension.modifierList?.getModifier(KtTokens.OPEN_KEYWORD)?.delete()
extension.modifierList?.getModifier(KtTokens.FINAL_KEYWORD)?.delete()
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 {
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);
}
@TestMetadata("final.kt")
public void testFinal() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/declarations/convertMemberToExtension/final.kt");
doTest(fileName);
}
@TestMetadata("funcitonNoName.kt")
public void testFuncitonNoName() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/declarations/convertMemberToExtension/funcitonNoName.kt");
@@ -4825,6 +4831,12 @@ public class IntentionTestGenerated extends AbstractIntentionTest {
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")
public void testOutsideFunction() throws Exception {
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$"));
}
@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")
public void testImplementAbstractMember_implementFunctionInJava_ImplementAllInJava() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/multiFileIntentions/implementAbstractMember/implementFunctionInJava/implementAllInJava.test");