Convert Sealed Class to Enum: Support expect/actual classes

#KT-18912 Fixed
This commit is contained in:
Alexey Sedunov
2017-10-19 21:36:40 +03:00
parent dbd7ceb5fd
commit 9d5d85a1c5
15 changed files with 115 additions and 17 deletions
@@ -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)
@@ -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)
}
@@ -0,0 +1,5 @@
expect sealed class E {
object A : E
object B : E
object C : E
}
@@ -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()
}
@@ -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()
}
@@ -0,0 +1,3 @@
actual enum class E {
A, B, C
}
@@ -0,0 +1,7 @@
// "Convert to enum class" "true"
expect sealed class <caret>E {
object A : E
object B : E
object C : E
}
@@ -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()
}
@@ -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()
}
@@ -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")
}