diff --git a/idea/idea-analysis/src/org/jetbrains/kotlin/idea/search/searchUtil.kt b/idea/idea-analysis/src/org/jetbrains/kotlin/idea/search/searchUtil.kt index 7a326b5ea05..9b9d56fd5fc 100644 --- a/idea/idea-analysis/src/org/jetbrains/kotlin/idea/search/searchUtil.kt +++ b/idea/idea-analysis/src/org/jetbrains/kotlin/idea/search/searchUtil.kt @@ -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) diff --git a/idea/src/org/jetbrains/kotlin/idea/intentions/ConvertSealedClassToEnumIntention.kt b/idea/src/org/jetbrains/kotlin/idea/intentions/ConvertSealedClassToEnumIntention.kt index c86843d95c5..201c29b497c 100644 --- a/idea/src/org/jetbrains/kotlin/idea/intentions/ConvertSealedClassToEnumIntention.kt +++ b/idea/src/org/jetbrains/kotlin/idea/intentions/ConvertSealedClassToEnumIntention.kt @@ -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(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).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, 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, 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(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) } diff --git a/idea/testData/multiModuleQuickFix/convertActualSealedClassToEnum/header/header.kt b/idea/testData/multiModuleQuickFix/convertActualSealedClassToEnum/header/header.kt new file mode 100644 index 00000000000..cfba9acdb13 --- /dev/null +++ b/idea/testData/multiModuleQuickFix/convertActualSealedClassToEnum/header/header.kt @@ -0,0 +1,5 @@ +expect sealed class E { + object A : E + object B : E + object C : E +} \ No newline at end of file diff --git a/idea/testData/multiModuleQuickFix/convertActualSealedClassToEnum/header/header.kt.after b/idea/testData/multiModuleQuickFix/convertActualSealedClassToEnum/header/header.kt.after new file mode 100644 index 00000000000..b4dad8dfa19 --- /dev/null +++ b/idea/testData/multiModuleQuickFix/convertActualSealedClassToEnum/header/header.kt.after @@ -0,0 +1,3 @@ +expect enum class E { + A, B, C +} \ No newline at end of file diff --git a/idea/testData/multiModuleQuickFix/convertActualSealedClassToEnum/js/impl.kt b/idea/testData/multiModuleQuickFix/convertActualSealedClassToEnum/js/impl.kt new file mode 100644 index 00000000000..19e27f1b405 --- /dev/null +++ b/idea/testData/multiModuleQuickFix/convertActualSealedClassToEnum/js/impl.kt @@ -0,0 +1,7 @@ +// "Convert to enum class" "true" + +actual sealed class E { + actual object A : E() + actual object B : E() + actual object C : E() +} \ No newline at end of file diff --git a/idea/testData/multiModuleQuickFix/convertActualSealedClassToEnum/js/impl.kt.after b/idea/testData/multiModuleQuickFix/convertActualSealedClassToEnum/js/impl.kt.after new file mode 100644 index 00000000000..3eedbd6bf38 --- /dev/null +++ b/idea/testData/multiModuleQuickFix/convertActualSealedClassToEnum/js/impl.kt.after @@ -0,0 +1,5 @@ +// "Convert to enum class" "true" + +actual enum class E { + A, B, C +} \ No newline at end of file diff --git a/idea/testData/multiModuleQuickFix/convertActualSealedClassToEnum/jvm/impl.kt b/idea/testData/multiModuleQuickFix/convertActualSealedClassToEnum/jvm/impl.kt new file mode 100644 index 00000000000..e48b1294659 --- /dev/null +++ b/idea/testData/multiModuleQuickFix/convertActualSealedClassToEnum/jvm/impl.kt @@ -0,0 +1,5 @@ +actual sealed class E { + actual object A : E() + actual object B : E() + actual object C : E() +} \ No newline at end of file diff --git a/idea/testData/multiModuleQuickFix/convertActualSealedClassToEnum/jvm/impl.kt.after b/idea/testData/multiModuleQuickFix/convertActualSealedClassToEnum/jvm/impl.kt.after new file mode 100644 index 00000000000..1d96930e795 --- /dev/null +++ b/idea/testData/multiModuleQuickFix/convertActualSealedClassToEnum/jvm/impl.kt.after @@ -0,0 +1,3 @@ +actual enum class E { + A, B, C +} \ No newline at end of file diff --git a/idea/testData/multiModuleQuickFix/convertExpectSealedClassToEnum/header/header.kt b/idea/testData/multiModuleQuickFix/convertExpectSealedClassToEnum/header/header.kt new file mode 100644 index 00000000000..da923e1f439 --- /dev/null +++ b/idea/testData/multiModuleQuickFix/convertExpectSealedClassToEnum/header/header.kt @@ -0,0 +1,7 @@ +// "Convert to enum class" "true" + +expect sealed class E { + object A : E + object B : E + object C : E +} \ No newline at end of file diff --git a/idea/testData/multiModuleQuickFix/convertExpectSealedClassToEnum/header/header.kt.after b/idea/testData/multiModuleQuickFix/convertExpectSealedClassToEnum/header/header.kt.after new file mode 100644 index 00000000000..455ae4ffb02 --- /dev/null +++ b/idea/testData/multiModuleQuickFix/convertExpectSealedClassToEnum/header/header.kt.after @@ -0,0 +1,5 @@ +// "Convert to enum class" "true" + +expect enum class E { + A, B, C +} \ No newline at end of file diff --git a/idea/testData/multiModuleQuickFix/convertExpectSealedClassToEnum/js/impl.kt b/idea/testData/multiModuleQuickFix/convertExpectSealedClassToEnum/js/impl.kt new file mode 100644 index 00000000000..e48b1294659 --- /dev/null +++ b/idea/testData/multiModuleQuickFix/convertExpectSealedClassToEnum/js/impl.kt @@ -0,0 +1,5 @@ +actual sealed class E { + actual object A : E() + actual object B : E() + actual object C : E() +} \ No newline at end of file diff --git a/idea/testData/multiModuleQuickFix/convertExpectSealedClassToEnum/js/impl.kt.after b/idea/testData/multiModuleQuickFix/convertExpectSealedClassToEnum/js/impl.kt.after new file mode 100644 index 00000000000..1d96930e795 --- /dev/null +++ b/idea/testData/multiModuleQuickFix/convertExpectSealedClassToEnum/js/impl.kt.after @@ -0,0 +1,3 @@ +actual enum class E { + A, B, C +} \ No newline at end of file diff --git a/idea/testData/multiModuleQuickFix/convertExpectSealedClassToEnum/jvm/impl.kt b/idea/testData/multiModuleQuickFix/convertExpectSealedClassToEnum/jvm/impl.kt new file mode 100644 index 00000000000..e48b1294659 --- /dev/null +++ b/idea/testData/multiModuleQuickFix/convertExpectSealedClassToEnum/jvm/impl.kt @@ -0,0 +1,5 @@ +actual sealed class E { + actual object A : E() + actual object B : E() + actual object C : E() +} \ No newline at end of file diff --git a/idea/testData/multiModuleQuickFix/convertExpectSealedClassToEnum/jvm/impl.kt.after b/idea/testData/multiModuleQuickFix/convertExpectSealedClassToEnum/jvm/impl.kt.after new file mode 100644 index 00000000000..1d96930e795 --- /dev/null +++ b/idea/testData/multiModuleQuickFix/convertExpectSealedClassToEnum/jvm/impl.kt.after @@ -0,0 +1,3 @@ +actual enum class E { + A, B, C +} \ No newline at end of file diff --git a/idea/tests/org/jetbrains/kotlin/idea/quickfix/QuickFixMultiModuleTest.kt b/idea/tests/org/jetbrains/kotlin/idea/quickfix/QuickFixMultiModuleTest.kt index ae1fd006303..02df3dc5d0b 100644 --- a/idea/tests/org/jetbrains/kotlin/idea/quickfix/QuickFixMultiModuleTest.kt +++ b/idea/tests/org/jetbrains/kotlin/idea/quickfix/QuickFixMultiModuleTest.kt @@ -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") }