Convert Sealed Class to Enum: Support expect/actual classes
#KT-18912 Fixed
This commit is contained in:
@@ -34,6 +34,7 @@ import org.jetbrains.kotlin.types.expressions.OperatorConventions
|
|||||||
|
|
||||||
infix fun SearchScope.and(otherScope: SearchScope): SearchScope = intersectWith(otherScope)
|
infix fun SearchScope.and(otherScope: SearchScope): SearchScope = intersectWith(otherScope)
|
||||||
infix fun SearchScope.or(otherScope: SearchScope): SearchScope = union(otherScope)
|
infix fun SearchScope.or(otherScope: SearchScope): SearchScope = union(otherScope)
|
||||||
|
infix fun GlobalSearchScope.or(otherScope: SearchScope): GlobalSearchScope = union(otherScope)
|
||||||
operator fun SearchScope.minus(otherScope: GlobalSearchScope): SearchScope = this and !otherScope
|
operator fun SearchScope.minus(otherScope: GlobalSearchScope): SearchScope = this and !otherScope
|
||||||
operator fun GlobalSearchScope.not(): GlobalSearchScope = GlobalSearchScope.notScope(this)
|
operator fun GlobalSearchScope.not(): GlobalSearchScope = GlobalSearchScope.notScope(this)
|
||||||
|
|
||||||
|
|||||||
+51
-16
@@ -17,14 +17,17 @@
|
|||||||
package org.jetbrains.kotlin.idea.intentions
|
package org.jetbrains.kotlin.idea.intentions
|
||||||
|
|
||||||
import com.intellij.openapi.editor.Editor
|
import com.intellij.openapi.editor.Editor
|
||||||
|
import com.intellij.openapi.project.Project
|
||||||
import com.intellij.openapi.util.TextRange
|
import com.intellij.openapi.util.TextRange
|
||||||
import com.intellij.psi.ElementDescriptionUtil
|
import com.intellij.psi.ElementDescriptionUtil
|
||||||
|
import com.intellij.psi.PsiElement
|
||||||
import com.intellij.psi.codeStyle.CodeStyleManager
|
import com.intellij.psi.codeStyle.CodeStyleManager
|
||||||
import com.intellij.refactoring.util.CommonRefactoringUtil
|
import com.intellij.refactoring.util.CommonRefactoringUtil
|
||||||
import com.intellij.refactoring.util.RefactoringDescriptionLocation
|
import com.intellij.refactoring.util.RefactoringDescriptionLocation
|
||||||
import org.jetbrains.kotlin.asJava.unwrapped
|
import org.jetbrains.kotlin.asJava.unwrapped
|
||||||
import org.jetbrains.kotlin.descriptors.ClassDescriptor
|
import org.jetbrains.kotlin.descriptors.ClassDescriptor
|
||||||
import org.jetbrains.kotlin.idea.caches.resolve.resolveToDescriptorIfAny
|
import org.jetbrains.kotlin.idea.caches.resolve.resolveToDescriptorIfAny
|
||||||
|
import org.jetbrains.kotlin.idea.highlighter.markers.liftToExpected
|
||||||
import org.jetbrains.kotlin.idea.runSynchronouslyWithProgress
|
import org.jetbrains.kotlin.idea.runSynchronouslyWithProgress
|
||||||
import org.jetbrains.kotlin.idea.search.declarationsSearch.HierarchySearchRequest
|
import org.jetbrains.kotlin.idea.search.declarationsSearch.HierarchySearchRequest
|
||||||
import org.jetbrains.kotlin.idea.search.declarationsSearch.searchInheritors
|
import org.jetbrains.kotlin.idea.search.declarationsSearch.searchInheritors
|
||||||
@@ -52,30 +55,62 @@ class ConvertSealedClassToEnumIntention : SelfTargetingRangeIntention<KtClass>(K
|
|||||||
override fun applyTo(element: KtClass, editor: Editor?) {
|
override fun applyTo(element: KtClass, editor: Editor?) {
|
||||||
val project = element.project
|
val project = element.project
|
||||||
|
|
||||||
|
val klass = element.liftToExpected() as? KtClass ?: element
|
||||||
|
|
||||||
val subclasses = project.runSynchronouslyWithProgress("Searching inheritors...", true) {
|
val subclasses = project.runSynchronouslyWithProgress("Searching inheritors...", true) {
|
||||||
HierarchySearchRequest(element, element.useScope, false).searchInheritors().mapNotNull { it.unwrapped }
|
HierarchySearchRequest(klass, klass.useScope, false).searchInheritors().mapNotNull { it.unwrapped }
|
||||||
} ?: return
|
} ?: return
|
||||||
|
|
||||||
val inconvertibleSubclasses = subclasses.filter {
|
val subclassesByContainer = subclasses.groupBy {
|
||||||
it !is KtObjectDeclaration || it.containingClassOrObject != element || it.superTypeListEntries.size != 1
|
if (it !is KtObjectDeclaration) return@groupBy null
|
||||||
|
if (it.superTypeListEntries.size != 1) return@groupBy null
|
||||||
|
val containingClass = it.containingClassOrObject as? KtClass ?: return@groupBy null
|
||||||
|
if (containingClass != klass && containingClass.liftToExpected() != klass) return@groupBy null
|
||||||
|
containingClass
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val inconvertibleSubclasses = subclassesByContainer[null] ?: emptyList()
|
||||||
if (inconvertibleSubclasses.isNotEmpty()) {
|
if (inconvertibleSubclasses.isNotEmpty()) {
|
||||||
val message = buildString {
|
return showError(
|
||||||
append("All inheritors must be nested objects of the class itself and may not inherit from other classes or interfaces.\n")
|
"All inheritors must be nested objects of the class itself and may not inherit from other classes or interfaces.\n",
|
||||||
append("Following problems are found:\n")
|
inconvertibleSubclasses,
|
||||||
inconvertibleSubclasses.joinTo(this) { ElementDescriptionUtil.getElementDescription(it, RefactoringDescriptionLocation.WITHOUT_PARENT) }
|
project,
|
||||||
}
|
editor
|
||||||
return CommonRefactoringUtil.showErrorHint(project, editor, message, text, null)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
val needSemicolon = element.declarations.size > subclasses.size
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
val nonSealedClasses = (subclassesByContainer.keys as Set<KtClass>).filter { !it.isSealed() }
|
||||||
|
if (nonSealedClasses.isNotEmpty()) {
|
||||||
|
return showError("All expected and actual classes must be sealed classes.\n", nonSealedClasses, project, editor)
|
||||||
|
}
|
||||||
|
|
||||||
val psiFactory = KtPsiFactory(element)
|
if (subclassesByContainer.isNotEmpty()) {
|
||||||
|
subclassesByContainer.forEach { currentClass, currentSubclasses -> processClass(currentClass!!, currentSubclasses, project) }
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
processClass(klass, emptyList(), project)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showError(message: String, elements: List<PsiElement>, project: Project, editor: Editor?) {
|
||||||
|
val errorText = buildString {
|
||||||
|
append(message)
|
||||||
|
append("Following problems are found:\n")
|
||||||
|
elements.joinTo(this) { ElementDescriptionUtil.getElementDescription(it, RefactoringDescriptionLocation.WITHOUT_PARENT) }
|
||||||
|
}
|
||||||
|
return CommonRefactoringUtil.showErrorHint(project, editor, errorText, text, null)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun processClass(klass: KtClass, subclasses: List<PsiElement>, project: Project) {
|
||||||
|
val needSemicolon = klass.declarations.size > subclasses.size
|
||||||
|
|
||||||
|
val psiFactory = KtPsiFactory(klass)
|
||||||
|
|
||||||
val comma = psiFactory.createComma()
|
val comma = psiFactory.createComma()
|
||||||
val semicolon = psiFactory.createSemicolon()
|
val semicolon = psiFactory.createSemicolon()
|
||||||
|
|
||||||
val constructorCallNeeded = element.hasExplicitPrimaryConstructor() || element.secondaryConstructors.isNotEmpty()
|
val constructorCallNeeded = klass.hasExplicitPrimaryConstructor() || klass.secondaryConstructors.isNotEmpty()
|
||||||
val entriesToAdd = subclasses.mapIndexed { i, subclass ->
|
val entriesToAdd = subclasses.mapIndexed { i, subclass ->
|
||||||
subclass as KtObjectDeclaration
|
subclass as KtObjectDeclaration
|
||||||
|
|
||||||
@@ -101,19 +136,19 @@ class ConvertSealedClassToEnumIntention : SelfTargetingRangeIntention<KtClass>(K
|
|||||||
|
|
||||||
subclasses.forEach { it.delete() }
|
subclasses.forEach { it.delete() }
|
||||||
|
|
||||||
element.removeModifier(KtTokens.SEALED_KEYWORD)
|
klass.removeModifier(KtTokens.SEALED_KEYWORD)
|
||||||
element.addModifier(KtTokens.ENUM_KEYWORD)
|
klass.addModifier(KtTokens.ENUM_KEYWORD)
|
||||||
|
|
||||||
if (entriesToAdd.isNotEmpty()) {
|
if (entriesToAdd.isNotEmpty()) {
|
||||||
val firstEntry = entriesToAdd
|
val firstEntry = entriesToAdd
|
||||||
.reversed()
|
.reversed()
|
||||||
.map { element.addDeclarationBefore(it, null) }
|
.map { klass.addDeclarationBefore(it, null) }
|
||||||
.last()
|
.last()
|
||||||
// TODO: Add formatter rule
|
// TODO: Add formatter rule
|
||||||
firstEntry.parent.addBefore(psiFactory.createNewLine(), firstEntry)
|
firstEntry.parent.addBefore(psiFactory.createNewLine(), firstEntry)
|
||||||
}
|
}
|
||||||
else if (needSemicolon) {
|
else if (needSemicolon) {
|
||||||
element.declarations.firstOrNull()?.let { anchor ->
|
klass.declarations.firstOrNull()?.let { anchor ->
|
||||||
val delimiter = anchor.parent.addBefore(semicolon, anchor)
|
val delimiter = anchor.parent.addBefore(semicolon, anchor)
|
||||||
CodeStyleManager.getInstance(project).reformat(delimiter)
|
CodeStyleManager.getInstance(project).reformat(delimiter)
|
||||||
}
|
}
|
||||||
|
|||||||
+5
@@ -0,0 +1,5 @@
|
|||||||
|
expect sealed class E {
|
||||||
|
object A : E
|
||||||
|
object B : E
|
||||||
|
object C : E
|
||||||
|
}
|
||||||
+3
@@ -0,0 +1,3 @@
|
|||||||
|
expect enum class E {
|
||||||
|
A, B, C
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
// "Convert to enum class" "true"
|
||||||
|
|
||||||
|
actual sealed class <caret>E {
|
||||||
|
actual object A : E()
|
||||||
|
actual object B : E()
|
||||||
|
actual object C : E()
|
||||||
|
}
|
||||||
+5
@@ -0,0 +1,5 @@
|
|||||||
|
// "Convert to enum class" "true"
|
||||||
|
|
||||||
|
actual enum class <caret>E {
|
||||||
|
A, B, C
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
actual sealed class E {
|
||||||
|
actual object A : E()
|
||||||
|
actual object B : E()
|
||||||
|
actual object C : E()
|
||||||
|
}
|
||||||
+3
@@ -0,0 +1,3 @@
|
|||||||
|
actual enum class E {
|
||||||
|
A, B, C
|
||||||
|
}
|
||||||
+7
@@ -0,0 +1,7 @@
|
|||||||
|
// "Convert to enum class" "true"
|
||||||
|
|
||||||
|
expect sealed class <caret>E {
|
||||||
|
object A : E
|
||||||
|
object B : E
|
||||||
|
object C : E
|
||||||
|
}
|
||||||
+5
@@ -0,0 +1,5 @@
|
|||||||
|
// "Convert to enum class" "true"
|
||||||
|
|
||||||
|
expect enum class <caret>E {
|
||||||
|
A, B, C
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
actual sealed class E {
|
||||||
|
actual object A : E()
|
||||||
|
actual object B : E()
|
||||||
|
actual object C : E()
|
||||||
|
}
|
||||||
+3
@@ -0,0 +1,3 @@
|
|||||||
|
actual enum class E {
|
||||||
|
A, B, C
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
actual sealed class E {
|
||||||
|
actual object A : E()
|
||||||
|
actual object B : E()
|
||||||
|
actual object C : E()
|
||||||
|
}
|
||||||
+3
@@ -0,0 +1,3 @@
|
|||||||
|
actual enum class E {
|
||||||
|
A, B, C
|
||||||
|
}
|
||||||
@@ -40,7 +40,7 @@ class QuickFixMultiModuleTest : AbstractQuickFixMultiModuleTest() {
|
|||||||
doQuickFixTest()
|
doQuickFixTest()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun doTestHeaderWithJvmAndJs() {
|
private fun doTestHeaderWithJvmAndJs(expectName: String = "header") {
|
||||||
doMultiPlatformTest(impls = *arrayOf("jvm" to TargetPlatformKind.Jvm[JvmTarget.JVM_1_6], "js" to TargetPlatformKind.JavaScript))
|
doMultiPlatformTest(impls = *arrayOf("jvm" to TargetPlatformKind.Jvm[JvmTarget.JVM_1_6], "js" to TargetPlatformKind.JavaScript))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -258,4 +258,10 @@ class QuickFixMultiModuleTest : AbstractQuickFixMultiModuleTest() {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun testCreateVarInExpectClass() = doMultiPlatformTest()
|
fun testCreateVarInExpectClass() = doMultiPlatformTest()
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testConvertExpectSealedClassToEnum() = doTestHeaderWithJvmAndJs("header")
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testConvertActualSealedClassToEnum() = doTestHeaderWithJvmAndJs("js")
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user