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.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 GlobalSearchScope.not(): GlobalSearchScope = GlobalSearchScope.notScope(this)
|
||||
|
||||
|
||||
+51
-16
@@ -17,14 +17,17 @@
|
||||
package org.jetbrains.kotlin.idea.intentions
|
||||
|
||||
import com.intellij.openapi.editor.Editor
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.util.TextRange
|
||||
import com.intellij.psi.ElementDescriptionUtil
|
||||
import com.intellij.psi.PsiElement
|
||||
import com.intellij.psi.codeStyle.CodeStyleManager
|
||||
import com.intellij.refactoring.util.CommonRefactoringUtil
|
||||
import com.intellij.refactoring.util.RefactoringDescriptionLocation
|
||||
import org.jetbrains.kotlin.asJava.unwrapped
|
||||
import org.jetbrains.kotlin.descriptors.ClassDescriptor
|
||||
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.search.declarationsSearch.HierarchySearchRequest
|
||||
import org.jetbrains.kotlin.idea.search.declarationsSearch.searchInheritors
|
||||
@@ -52,30 +55,62 @@ class ConvertSealedClassToEnumIntention : SelfTargetingRangeIntention<KtClass>(K
|
||||
override fun applyTo(element: KtClass, editor: Editor?) {
|
||||
val project = element.project
|
||||
|
||||
val klass = element.liftToExpected() as? KtClass ?: element
|
||||
|
||||
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
|
||||
|
||||
val inconvertibleSubclasses = subclasses.filter {
|
||||
it !is KtObjectDeclaration || it.containingClassOrObject != element || it.superTypeListEntries.size != 1
|
||||
val subclassesByContainer = subclasses.groupBy {
|
||||
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()) {
|
||||
val message = buildString {
|
||||
append("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.joinTo(this) { ElementDescriptionUtil.getElementDescription(it, RefactoringDescriptionLocation.WITHOUT_PARENT) }
|
||||
}
|
||||
return CommonRefactoringUtil.showErrorHint(project, editor, message, text, null)
|
||||
return showError(
|
||||
"All inheritors must be nested objects of the class itself and may not inherit from other classes or interfaces.\n",
|
||||
inconvertibleSubclasses,
|
||||
project,
|
||||
editor
|
||||
)
|
||||
}
|
||||
|
||||
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 semicolon = psiFactory.createSemicolon()
|
||||
|
||||
val constructorCallNeeded = element.hasExplicitPrimaryConstructor() || element.secondaryConstructors.isNotEmpty()
|
||||
val constructorCallNeeded = klass.hasExplicitPrimaryConstructor() || klass.secondaryConstructors.isNotEmpty()
|
||||
val entriesToAdd = subclasses.mapIndexed { i, subclass ->
|
||||
subclass as KtObjectDeclaration
|
||||
|
||||
@@ -101,19 +136,19 @@ class ConvertSealedClassToEnumIntention : SelfTargetingRangeIntention<KtClass>(K
|
||||
|
||||
subclasses.forEach { it.delete() }
|
||||
|
||||
element.removeModifier(KtTokens.SEALED_KEYWORD)
|
||||
element.addModifier(KtTokens.ENUM_KEYWORD)
|
||||
klass.removeModifier(KtTokens.SEALED_KEYWORD)
|
||||
klass.addModifier(KtTokens.ENUM_KEYWORD)
|
||||
|
||||
if (entriesToAdd.isNotEmpty()) {
|
||||
val firstEntry = entriesToAdd
|
||||
.reversed()
|
||||
.map { element.addDeclarationBefore(it, null) }
|
||||
.map { klass.addDeclarationBefore(it, null) }
|
||||
.last()
|
||||
// TODO: Add formatter rule
|
||||
firstEntry.parent.addBefore(psiFactory.createNewLine(), firstEntry)
|
||||
}
|
||||
else if (needSemicolon) {
|
||||
element.declarations.firstOrNull()?.let { anchor ->
|
||||
klass.declarations.firstOrNull()?.let { anchor ->
|
||||
val delimiter = anchor.parent.addBefore(semicolon, anchor)
|
||||
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()
|
||||
}
|
||||
|
||||
private fun doTestHeaderWithJvmAndJs() {
|
||||
private fun doTestHeaderWithJvmAndJs(expectName: String = "header") {
|
||||
doMultiPlatformTest(impls = *arrayOf("jvm" to TargetPlatformKind.Jvm[JvmTarget.JVM_1_6], "js" to TargetPlatformKind.JavaScript))
|
||||
}
|
||||
|
||||
@@ -258,4 +258,10 @@ class QuickFixMultiModuleTest : AbstractQuickFixMultiModuleTest() {
|
||||
|
||||
@Test
|
||||
fun testCreateVarInExpectClass() = doMultiPlatformTest()
|
||||
|
||||
@Test
|
||||
fun testConvertExpectSealedClassToEnum() = doTestHeaderWithJvmAndJs("header")
|
||||
|
||||
@Test
|
||||
fun testConvertActualSealedClassToEnum() = doTestHeaderWithJvmAndJs("js")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user