Convert member to extension: Support members of header/impl classes
#KT-18883 Fixed
This commit is contained in:
+113
-48
@@ -17,9 +17,12 @@
|
||||
package org.jetbrains.kotlin.idea.intentions.declarations
|
||||
|
||||
import com.intellij.codeInsight.intention.LowPriorityAction
|
||||
import com.intellij.openapi.application.ApplicationManager
|
||||
import com.intellij.openapi.editor.Editor
|
||||
import com.intellij.openapi.editor.ScrollType
|
||||
import com.intellij.openapi.ui.Messages
|
||||
import com.intellij.openapi.util.TextRange
|
||||
import com.intellij.psi.PsiComment
|
||||
import com.intellij.psi.PsiMethodCallExpression
|
||||
import com.intellij.psi.PsiReferenceExpression
|
||||
import com.intellij.psi.PsiWhiteSpace
|
||||
@@ -30,61 +33,89 @@ 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.core.*
|
||||
import org.jetbrains.kotlin.idea.highlighter.markers.headerImplementations
|
||||
import org.jetbrains.kotlin.idea.highlighter.markers.isHeaderOrHeaderClassMember
|
||||
import org.jetbrains.kotlin.idea.highlighter.markers.liftToHeader
|
||||
import org.jetbrains.kotlin.idea.intentions.SelfTargetingRangeIntention
|
||||
import org.jetbrains.kotlin.idea.quickfix.createFromUsage.callableBuilder.getReturnTypeReference
|
||||
import org.jetbrains.kotlin.idea.refactoring.withHeaderImplementations
|
||||
import org.jetbrains.kotlin.idea.references.KtReference
|
||||
import org.jetbrains.kotlin.idea.util.ImportInsertHelper
|
||||
import org.jetbrains.kotlin.idea.util.application.runWriteAction
|
||||
import org.jetbrains.kotlin.lexer.KtTokens
|
||||
import org.jetbrains.kotlin.psi.*
|
||||
import org.jetbrains.kotlin.psi.psiUtil.allChildren
|
||||
import org.jetbrains.kotlin.psi.psiUtil.containingClassOrObject
|
||||
import org.jetbrains.kotlin.psi.psiUtil.endOffset
|
||||
import org.jetbrains.kotlin.psi.psiUtil.siblings
|
||||
import org.jetbrains.kotlin.utils.addIfNotNull
|
||||
|
||||
class ConvertMemberToExtensionIntention : SelfTargetingRangeIntention<KtCallableDeclaration>(KtCallableDeclaration::class.java, "Convert member to extension"), LowPriorityAction {
|
||||
override fun applicabilityRange(element: KtCallableDeclaration): TextRange? {
|
||||
val classBody = element.parent as? KtClassBody ?: return null
|
||||
if (classBody.parent !is KtClass) return null
|
||||
if (element.receiverTypeReference != null) return null
|
||||
if (element.hasModifier(KtTokens.OVERRIDE_KEYWORD)) return null
|
||||
private fun isApplicable(element: KtCallableDeclaration): Boolean {
|
||||
val classBody = element.parent as? KtClassBody ?: return false
|
||||
if (classBody.parent !is KtClass) return false
|
||||
if (element.receiverTypeReference != null) return false
|
||||
if (element.hasModifier(KtTokens.OVERRIDE_KEYWORD)) return false
|
||||
when (element) {
|
||||
is KtProperty -> if (element.hasInitializer()) return null
|
||||
is KtSecondaryConstructor -> return null
|
||||
is KtProperty -> if (element.hasInitializer()) return false
|
||||
is KtSecondaryConstructor -> return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
override fun applicabilityRange(element: KtCallableDeclaration): TextRange? {
|
||||
if (!element.withHeaderImplementations().all { it is KtCallableDeclaration && isApplicable(it) }) return null
|
||||
return (element.nameIdentifier ?: return null).textRange
|
||||
}
|
||||
|
||||
override fun startInWriteAction() = false
|
||||
|
||||
//TODO: local class
|
||||
|
||||
override fun applyTo(element: KtCallableDeclaration, editor: Editor?) {
|
||||
val (extension, bodyToSelect) = createExtensionCallableAndPrepareBodyToSelect(element)
|
||||
var allowHeader = true
|
||||
|
||||
editor?.apply {
|
||||
unblockDocument()
|
||||
|
||||
if (bodyToSelect != null) {
|
||||
val range = bodyToSelect.textRange
|
||||
moveCaret(range.startOffset, ScrollType.CENTER)
|
||||
|
||||
val parent = bodyToSelect.parent
|
||||
val lastSibling =
|
||||
if (parent is KtBlockExpression)
|
||||
parent.rBrace?.siblings(forward = false, withItself = false)?.first { it !is PsiWhiteSpace }
|
||||
else
|
||||
bodyToSelect.siblings(forward = true, withItself = false).lastOrNull()
|
||||
val endOffset = lastSibling?.endOffset ?: range.endOffset
|
||||
selectionModel.setSelection(range.startOffset, endOffset)
|
||||
element.liftToHeader()?.headerImplementations()?.let {
|
||||
if (it.isEmpty()) {
|
||||
allowHeader = askIfHeaderIsAllowed(element.containingKtFile)
|
||||
}
|
||||
else {
|
||||
moveCaret(extension.textOffset, ScrollType.CENTER)
|
||||
}
|
||||
|
||||
runWriteAction {
|
||||
val (extension, bodyToSelect) = createExtensionCallableAndPrepareBodyToSelect(element, allowHeader)
|
||||
|
||||
editor?.apply {
|
||||
unblockDocument()
|
||||
|
||||
if (bodyToSelect != null) {
|
||||
val range = bodyToSelect.textRange
|
||||
moveCaret(range.startOffset, ScrollType.CENTER)
|
||||
|
||||
val parent = bodyToSelect.parent
|
||||
val lastSibling =
|
||||
if (parent is KtBlockExpression)
|
||||
parent.rBrace?.siblings(forward = false, withItself = false)?.first { it !is PsiWhiteSpace }
|
||||
else
|
||||
bodyToSelect.siblings(forward = true, withItself = false).lastOrNull()
|
||||
val endOffset = lastSibling?.endOffset ?: range.endOffset
|
||||
selectionModel.setSelection(range.startOffset, endOffset)
|
||||
}
|
||||
else {
|
||||
moveCaret(extension.textOffset, ScrollType.CENTER)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun createExtensionCallableAndPrepareBodyToSelect(element: KtCallableDeclaration): Pair<KtCallableDeclaration, KtExpression?> {
|
||||
private fun processSingleDeclaration(
|
||||
element: KtCallableDeclaration,
|
||||
allowHeader: Boolean
|
||||
): Pair<KtCallableDeclaration, KtExpression?> {
|
||||
val descriptor = element.resolveToDescriptor()
|
||||
val containingClass = descriptor.containingDeclaration as ClassDescriptor
|
||||
|
||||
val isEffectiveHeader = allowHeader && element.isHeaderOrHeaderClassMember()
|
||||
|
||||
val file = element.containingKtFile
|
||||
val project = file.project
|
||||
val outermostParent = KtPsiUtil.getOutermostParent(element, file, false)
|
||||
@@ -129,6 +160,9 @@ class ConvertMemberToExtensionIntention : SelfTargetingRangeIntention<KtCallable
|
||||
extension.modifierList?.getModifier(KtTokens.OPEN_KEYWORD)?.delete()
|
||||
extension.modifierList?.getModifier(KtTokens.FINAL_KEYWORD)?.delete()
|
||||
|
||||
if (isEffectiveHeader && !extension.hasModifier(KtTokens.HEADER_KEYWORD))
|
||||
extension.addModifier(KtTokens.HEADER_KEYWORD)
|
||||
|
||||
var bodyToSelect: KtExpression? = null
|
||||
|
||||
fun selectBody(declaration: KtDeclarationWithBody) {
|
||||
@@ -148,7 +182,7 @@ class ConvertMemberToExtensionIntention : SelfTargetingRangeIntention<KtCallable
|
||||
|
||||
when (extension) {
|
||||
is KtFunction -> {
|
||||
if (!extension.hasBody()) {
|
||||
if (!extension.hasBody() && !isEffectiveHeader) {
|
||||
//TODO: methods in PSI for setBody
|
||||
extension.add(psiFactory.createBlock(bodyText))
|
||||
selectBody(extension)
|
||||
@@ -157,30 +191,33 @@ class ConvertMemberToExtensionIntention : SelfTargetingRangeIntention<KtCallable
|
||||
|
||||
is KtProperty -> {
|
||||
val templateProperty = psiFactory.createDeclaration<KtProperty>("var v: Any\nget()=$bodyText\nset(value){\n$bodyText\n}")
|
||||
val templateGetter = templateProperty.getter!!
|
||||
val templateSetter = templateProperty.setter!!
|
||||
|
||||
var getter = extension.getter
|
||||
if (getter == null) {
|
||||
getter = extension.addAfter(templateGetter, extension.typeReference) as KtPropertyAccessor
|
||||
extension.addBefore(psiFactory.createNewLine(), getter)
|
||||
selectBody(getter)
|
||||
}
|
||||
else if (!getter.hasBody()) {
|
||||
getter = getter.replace(templateGetter) as KtPropertyAccessor
|
||||
selectBody(getter)
|
||||
}
|
||||
if (!isEffectiveHeader) {
|
||||
val templateGetter = templateProperty.getter!!
|
||||
val templateSetter = templateProperty.setter!!
|
||||
|
||||
if (extension.isVar) {
|
||||
var setter = extension.setter
|
||||
if (setter == null) {
|
||||
setter = extension.addAfter(templateSetter, getter) as KtPropertyAccessor
|
||||
extension.addBefore(psiFactory.createNewLine(), setter)
|
||||
selectBody(setter)
|
||||
var getter = extension.getter
|
||||
if (getter == null) {
|
||||
getter = extension.addAfter(templateGetter, extension.typeReference) as KtPropertyAccessor
|
||||
extension.addBefore(psiFactory.createNewLine(), getter)
|
||||
selectBody(getter)
|
||||
}
|
||||
else if (!setter.hasBody()) {
|
||||
setter = setter.replace(templateSetter) as KtPropertyAccessor
|
||||
selectBody(setter)
|
||||
else if (!getter.hasBody()) {
|
||||
getter = getter.replace(templateGetter) as KtPropertyAccessor
|
||||
selectBody(getter)
|
||||
}
|
||||
|
||||
if (extension.isVar) {
|
||||
var setter = extension.setter
|
||||
if (setter == null) {
|
||||
setter = extension.addAfter(templateSetter, getter) as KtPropertyAccessor
|
||||
extension.addBefore(psiFactory.createNewLine(), setter)
|
||||
selectBody(setter)
|
||||
}
|
||||
else if (!setter.hasBody()) {
|
||||
setter = setter.replace(templateSetter) as KtPropertyAccessor
|
||||
selectBody(setter)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -210,6 +247,34 @@ class ConvertMemberToExtensionIntention : SelfTargetingRangeIntention<KtCallable
|
||||
return extension to bodyToSelect
|
||||
}
|
||||
|
||||
private fun askIfHeaderIsAllowed(file: KtFile): Boolean {
|
||||
if (ApplicationManager.getApplication().isUnitTestMode) {
|
||||
return file.allChildren.any { it is PsiComment && it.text.trim() == "// ALLOW_HEADER_WITHOUT_IMPLS" }
|
||||
}
|
||||
|
||||
return Messages.showYesNoDialog(
|
||||
"Do you want to make new extension a header declaration?",
|
||||
text,
|
||||
Messages.getQuestionIcon()
|
||||
) == Messages.YES
|
||||
}
|
||||
|
||||
private fun createExtensionCallableAndPrepareBodyToSelect(
|
||||
element: KtCallableDeclaration,
|
||||
allowHeader: Boolean = true
|
||||
): Pair<KtCallableDeclaration, KtExpression?> {
|
||||
val headerDeclaration = element.liftToHeader() as? KtCallableDeclaration
|
||||
if (headerDeclaration != null) {
|
||||
element.withHeaderImplementations().filterIsInstance<KtCallableDeclaration>().forEach {
|
||||
if (it != element) {
|
||||
processSingleDeclaration(it, allowHeader)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return processSingleDeclaration(element, allowHeader)
|
||||
}
|
||||
|
||||
private fun newTypeParameterList(member: KtCallableDeclaration): KtTypeParameterList? {
|
||||
val classElement = member.parent.parent as KtClass
|
||||
val classParams = classElement.typeParameters
|
||||
|
||||
@@ -76,6 +76,8 @@ import org.jetbrains.kotlin.idea.caches.resolve.resolveToDescriptor
|
||||
import org.jetbrains.kotlin.idea.codeInsight.DescriptorToSourceUtilsIde
|
||||
import org.jetbrains.kotlin.idea.core.*
|
||||
import org.jetbrains.kotlin.idea.core.util.showYesNoCancelDialog
|
||||
import org.jetbrains.kotlin.idea.highlighter.markers.headerImplementations
|
||||
import org.jetbrains.kotlin.idea.highlighter.markers.liftToHeader
|
||||
import org.jetbrains.kotlin.idea.intentions.RemoveCurlyBracesFromTemplateIntention
|
||||
import org.jetbrains.kotlin.idea.j2k.IdeaJavaToKotlinServices
|
||||
import org.jetbrains.kotlin.idea.refactoring.changeSignature.KotlinValVar
|
||||
@@ -962,4 +964,10 @@ fun checkSuperMethodsWithPopup(
|
||||
fun KtNamedDeclaration.isCompanionMemberOf(klass: KtClassOrObject): Boolean {
|
||||
val containingObject = containingClassOrObject as? KtObjectDeclaration ?: return false
|
||||
return containingObject.isCompanion() && containingObject.containingClassOrObject == klass
|
||||
}
|
||||
|
||||
internal fun KtDeclaration.withHeaderImplementations(): List<KtDeclaration> {
|
||||
val header = liftToHeader() ?: return listOf(this)
|
||||
val implementations = header.headerImplementations() ?: emptySet()
|
||||
return listOf(header) + implementations
|
||||
}
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
// "Convert member to extension" "true"
|
||||
|
||||
header class Foo {
|
||||
fun <caret>foo(n: Int)
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
// "Convert member to extension" "true"
|
||||
|
||||
header class Foo {
|
||||
}
|
||||
|
||||
header fun Foo.foo(n: Int)
|
||||
@@ -0,0 +1,5 @@
|
||||
impl class Foo {
|
||||
impl fun foo(n: Int) {
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
impl class Foo {
|
||||
}
|
||||
|
||||
impl fun Foo.foo(n: Int) {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
impl class Foo {
|
||||
impl fun foo(n: Int) {
|
||||
|
||||
}
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
impl class Foo {
|
||||
}
|
||||
|
||||
impl fun Foo.foo(n: Int) {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
header class Foo {
|
||||
fun foo(n: Int)
|
||||
}
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
header class Foo {
|
||||
}
|
||||
|
||||
header fun Foo.foo(n: Int)
|
||||
@@ -0,0 +1,7 @@
|
||||
// "Convert member to extension" "true"
|
||||
|
||||
impl class Foo {
|
||||
impl fun <caret>foo(n: Int) {
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
// "Convert member to extension" "true"
|
||||
|
||||
impl class Foo {
|
||||
}
|
||||
|
||||
impl fun Foo.foo(n: Int) {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
impl class Foo {
|
||||
impl fun foo(n: Int) {
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
impl class Foo {
|
||||
}
|
||||
|
||||
impl fun Foo.foo(n: Int) {
|
||||
|
||||
}
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
// "Convert member to extension" "true"
|
||||
|
||||
header class Foo {
|
||||
val <caret>foo: Int
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
// "Convert member to extension" "true"
|
||||
|
||||
header class Foo {
|
||||
}
|
||||
|
||||
header val Foo.foo: Int
|
||||
@@ -0,0 +1,3 @@
|
||||
impl class Foo {
|
||||
impl val foo get() = 1
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
impl class Foo {
|
||||
}
|
||||
|
||||
impl val Foo.foo get() = 1
|
||||
@@ -0,0 +1,3 @@
|
||||
impl class Foo {
|
||||
impl val foo get() = 2
|
||||
}
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
impl class Foo {
|
||||
}
|
||||
|
||||
impl val Foo.foo get() = 2
|
||||
Vendored
+9
@@ -0,0 +1,9 @@
|
||||
// "Convert member to extension" "false"
|
||||
// ACTION: Convert property to function
|
||||
// ACTION: Introduce backing property
|
||||
// ACTION: Move to companion object
|
||||
// ACTION: Move to constructor
|
||||
|
||||
header class Foo {
|
||||
val <caret>foo: Int
|
||||
}
|
||||
Vendored
+3
@@ -0,0 +1,3 @@
|
||||
impl class Foo {
|
||||
impl val foo = 1
|
||||
}
|
||||
Vendored
+3
@@ -0,0 +1,3 @@
|
||||
impl class Foo {
|
||||
impl val foo get() = 2
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
header class Foo {
|
||||
val foo: Int
|
||||
}
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
header class Foo {
|
||||
}
|
||||
|
||||
header val Foo.foo: Int
|
||||
@@ -0,0 +1,5 @@
|
||||
// "Convert member to extension" "true"
|
||||
|
||||
impl class Foo {
|
||||
impl val <caret>foo get() = 1
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
// "Convert member to extension" "true"
|
||||
|
||||
impl class Foo {
|
||||
}
|
||||
|
||||
impl val Foo.foo get() = 1
|
||||
@@ -0,0 +1,3 @@
|
||||
impl class Foo {
|
||||
impl val foo get() = 2
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
impl class Foo {
|
||||
}
|
||||
|
||||
impl val Foo.foo get() = 2
|
||||
@@ -22,25 +22,31 @@ import org.jetbrains.kotlin.idea.stubs.createFacet
|
||||
import org.junit.Test
|
||||
|
||||
class QuickFixMultiModuleTest : AbstractQuickFixMultiModuleTest() {
|
||||
|
||||
private fun doMultiPlatformTest(headerName: String = "header",
|
||||
implName: String = "jvm",
|
||||
implKind: TargetPlatformKind<*> = TargetPlatformKind.Jvm[JvmTarget.JVM_1_6],
|
||||
withTests: Boolean = false) {
|
||||
private fun doMultiPlatformTest(
|
||||
headerName: String = "header",
|
||||
vararg impls: Pair<String, TargetPlatformKind<*>> = arrayOf("jvm" to TargetPlatformKind.Jvm[JvmTarget.JVM_1_6]),
|
||||
withTests: Boolean = false
|
||||
) {
|
||||
val header = module(headerName, hasTestRoot = withTests)
|
||||
header.createFacet(TargetPlatformKind.Common)
|
||||
|
||||
val jvm = module(implName, hasTestRoot = withTests)
|
||||
jvm.createFacet(implKind)
|
||||
jvm.enableMultiPlatform()
|
||||
jvm.addDependency(header)
|
||||
impls.forEach { (implName, implKind) ->
|
||||
val implModule = module(implName, hasTestRoot = withTests)
|
||||
implModule.createFacet(implKind)
|
||||
implModule.enableMultiPlatform()
|
||||
implModule.addDependency(header)
|
||||
}
|
||||
|
||||
doQuickFixTest()
|
||||
}
|
||||
|
||||
private fun doTestHeaderWithJvmAndJs() {
|
||||
doMultiPlatformTest(impls = *arrayOf("jvm" to TargetPlatformKind.Jvm[JvmTarget.JVM_1_6], "js" to TargetPlatformKind.JavaScript))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testAbstract() {
|
||||
doMultiPlatformTest(implName = "js", implKind = TargetPlatformKind.JavaScript)
|
||||
doMultiPlatformTest(impls = "js" to TargetPlatformKind.JavaScript)
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -50,7 +56,7 @@ class QuickFixMultiModuleTest : AbstractQuickFixMultiModuleTest() {
|
||||
|
||||
@Test
|
||||
fun testEnum() {
|
||||
doMultiPlatformTest(implName = "js", implKind = TargetPlatformKind.JavaScript)
|
||||
doMultiPlatformTest(impls = "js" to TargetPlatformKind.JavaScript)
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -95,11 +101,26 @@ class QuickFixMultiModuleTest : AbstractQuickFixMultiModuleTest() {
|
||||
|
||||
@Test
|
||||
fun testSealed() {
|
||||
doMultiPlatformTest(implName = "js", implKind = TargetPlatformKind.JavaScript)
|
||||
doMultiPlatformTest(impls = "js" to TargetPlatformKind.JavaScript)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testWithTest() {
|
||||
doMultiPlatformTest(headerName = "common", withTests = true)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testMemberFunToExtensionByHeader() = doTestHeaderWithJvmAndJs()
|
||||
|
||||
@Test
|
||||
fun testMemberFunToExtensionByImpl() = doTestHeaderWithJvmAndJs()
|
||||
|
||||
@Test
|
||||
fun testMemberValToExtensionByHeader() = doTestHeaderWithJvmAndJs()
|
||||
|
||||
@Test
|
||||
fun testMemberValToExtensionByHeaderWithInapplicableImpl() = doTestHeaderWithJvmAndJs()
|
||||
|
||||
@Test
|
||||
fun testMemberValToExtensionByImpl() = doTestHeaderWithJvmAndJs()
|
||||
}
|
||||
Reference in New Issue
Block a user