Convert member to extension: Support members of header/impl classes

#KT-18883 Fixed
This commit is contained in:
Alexey Sedunov
2017-08-07 13:46:29 +03:00
parent 4dbde03c3b
commit cd5c382179
30 changed files with 285 additions and 60 deletions
@@ -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
}
@@ -0,0 +1,5 @@
// "Convert member to extension" "true"
header class Foo {
fun <caret>foo(n: Int)
}
@@ -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) {
}
}
@@ -0,0 +1,6 @@
impl class Foo {
}
impl fun Foo.foo(n: Int) {
}
@@ -0,0 +1,3 @@
header class Foo {
fun foo(n: Int)
}
@@ -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) {
}
@@ -0,0 +1,5 @@
// "Convert member to extension" "true"
header class Foo {
val <caret>foo: Int
}
@@ -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
}
@@ -0,0 +1,4 @@
impl class Foo {
}
impl val Foo.foo get() = 2
@@ -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
}
@@ -0,0 +1,3 @@
impl class Foo {
impl val foo = 1
}
@@ -0,0 +1,3 @@
impl class Foo {
impl val foo get() = 2
}
@@ -0,0 +1,3 @@
header class Foo {
val foo: Int
}
@@ -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()
}