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.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)
@@ -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)
} }
@@ -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() 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")
} }