diff --git a/ChangeLog.md b/ChangeLog.md
index 4c5b8641da8..b0426b7de50 100644
--- a/ChangeLog.md
+++ b/ChangeLog.md
@@ -226,6 +226,7 @@ These artifacts include extensions for the types available in the latter JDKs, s
- [`KT-11525`](https://youtrack.jetbrains.com/issue/KT-11525) Implement "Create type parameter" quickfix
- [`KT-9931`](https://youtrack.jetbrains.com/issue/KT-9931) Implement "Remove unused assignment" quickfix
- [`KT-14245`](https://youtrack.jetbrains.com/issue/KT-14245) Implement "Convert enum to sealed class" intention
+- [`KT-14245`](https://youtrack.jetbrains.com/issue/KT-14245) Implement "Convert sealed class to enum" intention
#### Refactorings
diff --git a/idea/resources/intentionDescriptions/ConvertSealedClassToEnumIntention/after.kt.template b/idea/resources/intentionDescriptions/ConvertSealedClassToEnumIntention/after.kt.template
new file mode 100644
index 00000000000..8a7d03073db
--- /dev/null
+++ b/idea/resources/intentionDescriptions/ConvertSealedClassToEnumIntention/after.kt.template
@@ -0,0 +1,7 @@
+enum class MyClass(val s: String = "") {
+ FOO("FOO"), BAR("BAR"), DEFAULT();
+
+ fun foo() {
+
+ }
+}
\ No newline at end of file
diff --git a/idea/resources/intentionDescriptions/ConvertSealedClassToEnumIntention/before.kt.template b/idea/resources/intentionDescriptions/ConvertSealedClassToEnumIntention/before.kt.template
new file mode 100644
index 00000000000..534bf0391c3
--- /dev/null
+++ b/idea/resources/intentionDescriptions/ConvertSealedClassToEnumIntention/before.kt.template
@@ -0,0 +1,9 @@
+sealed class MyClass(val s: String = "") {
+ fun foo() {
+
+ }
+
+ object FOO : MyEnum("FOO")
+ object BAR : MyEnum("BAR")
+ object DEFAULT : MyEnum()
+}
\ No newline at end of file
diff --git a/idea/resources/intentionDescriptions/ConvertSealedClassToEnumIntention/description.html b/idea/resources/intentionDescriptions/ConvertSealedClassToEnumIntention/description.html
new file mode 100644
index 00000000000..47e91efd694
--- /dev/null
+++ b/idea/resources/intentionDescriptions/ConvertSealedClassToEnumIntention/description.html
@@ -0,0 +1,5 @@
+
+
+This intention converts a sealed class to an enum class and replaces inheriting objects with enum entries
+
+
\ No newline at end of file
diff --git a/idea/src/META-INF/plugin.xml b/idea/src/META-INF/plugin.xml
index 33e80c71f94..fe1be9146f2 100644
--- a/idea/src/META-INF/plugin.xml
+++ b/idea/src/META-INF/plugin.xml
@@ -1401,6 +1401,11 @@
Kotlin
+
+ org.jetbrains.kotlin.idea.intentions.ConvertSealedClassToEnumIntention
+ Kotlin
+
+
(KtClass::class.java, "Convert to enum class") {
+ override fun applicabilityRange(element: KtClass): TextRange? {
+ val nameIdentifier = element.nameIdentifier ?: return null
+ val sealedKeyword = element.modifierList?.getModifier(KtTokens.SEALED_KEYWORD) ?: return null
+
+ val classDescriptor = element.resolveToDescriptor() as ClassDescriptor
+ if (classDescriptor.getSuperClassNotAny() != null) return null
+
+ return TextRange(sealedKeyword.startOffset, nameIdentifier.endOffset)
+ }
+
+ override fun applyTo(element: KtClass, editor: Editor?) {
+ val project = element.project
+
+ val subclasses = project.runSynchronouslyWithProgress("Searching inheritors...", true) {
+ HierarchySearchRequest(element, element.useScope, false).searchInheritors().mapNotNull { it.unwrapped }
+ } ?: return
+
+ val inconvertibleSubclasses = subclasses.filter {
+ it !is KtObjectDeclaration || it.containingClassOrObject != element || it.getSuperTypeListEntries().size != 1
+ }
+ 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)
+ }
+
+ val needSemicolon = element.declarations.size > subclasses.size
+
+ val psiFactory = KtPsiFactory(element)
+
+ val comma = psiFactory.createComma()
+ val semicolon = psiFactory.createSemicolon()
+
+ val constructorCallNeeded = element.hasExplicitPrimaryConstructor() || element.getSecondaryConstructors().isNotEmpty()
+ val entriesToAdd = subclasses.mapIndexed { i, subclass ->
+ subclass as KtObjectDeclaration
+
+ val entryText = buildString {
+ append(subclass.name)
+ if (constructorCallNeeded) {
+ append((subclass.getSuperTypeListEntries().firstOrNull() as? KtSuperTypeCallEntry)?.valueArgumentList?.text ?: "()")
+ }
+ }
+ val entry = psiFactory.createEnumEntry(entryText)
+
+ subclass.getBody()?.let { body -> entry.add(body) }
+
+ if (i < subclasses.lastIndex) {
+ entry.add(comma)
+ }
+ else if (needSemicolon) {
+ entry.add(semicolon)
+ }
+
+ entry
+ }
+
+ subclasses.forEach { it.delete() }
+
+ element.removeModifier(KtTokens.SEALED_KEYWORD)
+ element.addModifier(KtTokens.ENUM_KEYWORD)
+
+ if (entriesToAdd.isNotEmpty()) {
+ val firstEntry = entriesToAdd
+ .reversed()
+ .map { element.addDeclarationBefore(it, null) }
+ .last()
+ // TODO: Add formatter rule
+ firstEntry.parent.addBefore(psiFactory.createNewLine(), firstEntry)
+ }
+ else if (needSemicolon) {
+ element.declarations.firstOrNull()?.let { anchor ->
+ val delimiter = anchor.parent.addBefore(semicolon, anchor)
+ CodeStyleManager.getInstance(project).reformat(delimiter)
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/idea/testData/intentions/convertSealedClassToEnum/.intention b/idea/testData/intentions/convertSealedClassToEnum/.intention
new file mode 100644
index 00000000000..8185480a4dd
--- /dev/null
+++ b/idea/testData/intentions/convertSealedClassToEnum/.intention
@@ -0,0 +1 @@
+org.jetbrains.kotlin.idea.intentions.ConvertSealedClassToEnumIntention
diff --git a/idea/testData/intentions/convertSealedClassToEnum/dropDefaultConstructorCall.kt b/idea/testData/intentions/convertSealedClassToEnum/dropDefaultConstructorCall.kt
new file mode 100644
index 00000000000..84c3f2c12fa
--- /dev/null
+++ b/idea/testData/intentions/convertSealedClassToEnum/dropDefaultConstructorCall.kt
@@ -0,0 +1,3 @@
+sealed class MyClass {
+ object FOO : MyClass()
+}
\ No newline at end of file
diff --git a/idea/testData/intentions/convertSealedClassToEnum/dropDefaultConstructorCall.kt.after b/idea/testData/intentions/convertSealedClassToEnum/dropDefaultConstructorCall.kt.after
new file mode 100644
index 00000000000..c84f9a3b32f
--- /dev/null
+++ b/idea/testData/intentions/convertSealedClassToEnum/dropDefaultConstructorCall.kt.after
@@ -0,0 +1,3 @@
+enum class MyClass {
+ FOO
+}
\ No newline at end of file
diff --git a/idea/testData/intentions/convertSealedClassToEnum/inheritorsWithMultipleSupertypes.kt b/idea/testData/intentions/convertSealedClassToEnum/inheritorsWithMultipleSupertypes.kt
new file mode 100644
index 00000000000..731a7f509d5
--- /dev/null
+++ b/idea/testData/intentions/convertSealedClassToEnum/inheritorsWithMultipleSupertypes.kt
@@ -0,0 +1,8 @@
+// SHOULD_FAIL_WITH: All inheritors must be nested objects of the class itself and may not inherit from other classes or interfaces. Following problems are found: object A
+
+interface I
+
+sealed class X {
+ object A : X(), I
+ object B : X()
+}
\ No newline at end of file
diff --git a/idea/testData/intentions/convertSealedClassToEnum/instancesAndMembers.kt b/idea/testData/intentions/convertSealedClassToEnum/instancesAndMembers.kt
new file mode 100644
index 00000000000..ca98838221f
--- /dev/null
+++ b/idea/testData/intentions/convertSealedClassToEnum/instancesAndMembers.kt
@@ -0,0 +1,23 @@
+// WITH_RUNTIME
+
+sealed class MyClass(val s: String = "") {
+ fun foo() {
+
+ }
+
+ object FOO : MyClass("FOO")
+ object BAR : MyClass("BAR")
+ object DEFAULT : MyClass()
+}
+
+fun test(e: MyClass) {
+ if (e == MyClass.BAR) {
+ println()
+ }
+
+ val n = when (e) {
+ MyClass.BAR -> 1
+ MyClass.FOO -> 2
+ MyClass.DEFAULT -> 0
+ }
+}
\ No newline at end of file
diff --git a/idea/testData/intentions/convertSealedClassToEnum/instancesAndMembers.kt.after b/idea/testData/intentions/convertSealedClassToEnum/instancesAndMembers.kt.after
new file mode 100644
index 00000000000..0fd19c05a3c
--- /dev/null
+++ b/idea/testData/intentions/convertSealedClassToEnum/instancesAndMembers.kt.after
@@ -0,0 +1,22 @@
+// WITH_RUNTIME
+
+enum class MyClass(val s: String = "") {
+ FOO("FOO"), BAR("BAR"), DEFAULT();
+
+ fun foo() {
+
+ }
+
+}
+
+fun test(e: MyClass) {
+ if (e == MyClass.BAR) {
+ println()
+ }
+
+ val n = when (e) {
+ MyClass.BAR -> 1
+ MyClass.FOO -> 2
+ MyClass.DEFAULT -> 0
+ }
+}
\ No newline at end of file
diff --git a/idea/testData/intentions/convertSealedClassToEnum/instancesOnly.kt b/idea/testData/intentions/convertSealedClassToEnum/instancesOnly.kt
new file mode 100644
index 00000000000..13fed070b2d
--- /dev/null
+++ b/idea/testData/intentions/convertSealedClassToEnum/instancesOnly.kt
@@ -0,0 +1,19 @@
+// WITH_RUNTIME
+
+sealed class MyClass(val s: String = "") {
+ object FOO : MyClass("FOO")
+ object BAR : MyClass("BAR")
+ object DEFAULT : MyClass()
+}
+
+fun test(e: MyClass) {
+ if (e == MyClass.BAR) {
+ println()
+ }
+
+ val n = when (e) {
+ MyClass.BAR -> 1
+ MyClass.FOO -> 2
+ MyClass.DEFAULT -> 0
+ }
+}
\ No newline at end of file
diff --git a/idea/testData/intentions/convertSealedClassToEnum/instancesOnly.kt.after b/idea/testData/intentions/convertSealedClassToEnum/instancesOnly.kt.after
new file mode 100644
index 00000000000..55bd8f36969
--- /dev/null
+++ b/idea/testData/intentions/convertSealedClassToEnum/instancesOnly.kt.after
@@ -0,0 +1,17 @@
+// WITH_RUNTIME
+
+enum class MyClass(val s: String = "") {
+ FOO("FOO"), BAR("BAR"), DEFAULT()
+}
+
+fun test(e: MyClass) {
+ if (e == MyClass.BAR) {
+ println()
+ }
+
+ val n = when (e) {
+ MyClass.BAR -> 1
+ MyClass.FOO -> 2
+ MyClass.DEFAULT -> 0
+ }
+}
\ No newline at end of file
diff --git a/idea/testData/intentions/convertSealedClassToEnum/membersOnly.kt b/idea/testData/intentions/convertSealedClassToEnum/membersOnly.kt
new file mode 100644
index 00000000000..658f29e0ecf
--- /dev/null
+++ b/idea/testData/intentions/convertSealedClassToEnum/membersOnly.kt
@@ -0,0 +1,5 @@
+sealed class MyEnum(val s: String = "") {
+ fun foo() {
+
+ }
+}
\ No newline at end of file
diff --git a/idea/testData/intentions/convertSealedClassToEnum/membersOnly.kt.after b/idea/testData/intentions/convertSealedClassToEnum/membersOnly.kt.after
new file mode 100644
index 00000000000..5276fc86669
--- /dev/null
+++ b/idea/testData/intentions/convertSealedClassToEnum/membersOnly.kt.after
@@ -0,0 +1,7 @@
+enum class MyEnum(val s: String = "") {
+ ;
+
+ fun foo() {
+
+ }
+}
\ No newline at end of file
diff --git a/idea/testData/intentions/convertSealedClassToEnum/nonNestedInheritors.kt b/idea/testData/intentions/convertSealedClassToEnum/nonNestedInheritors.kt
new file mode 100644
index 00000000000..c59d54a89aa
--- /dev/null
+++ b/idea/testData/intentions/convertSealedClassToEnum/nonNestedInheritors.kt
@@ -0,0 +1,7 @@
+// SHOULD_FAIL_WITH: All inheritors must be nested objects of the class itself and may not inherit from other classes or interfaces. Following problems are found: object B
+
+sealed class X {
+ object A : X()
+}
+
+object B : X()
\ No newline at end of file
diff --git a/idea/testData/intentions/convertSealedClassToEnum/notSealedClass.kt b/idea/testData/intentions/convertSealedClassToEnum/notSealedClass.kt
new file mode 100644
index 00000000000..ee8d2e762a7
--- /dev/null
+++ b/idea/testData/intentions/convertSealedClassToEnum/notSealedClass.kt
@@ -0,0 +1,5 @@
+// IS_APPLICABLE: false
+
+class X {
+
+}
\ No newline at end of file
diff --git a/idea/testData/intentions/convertSealedClassToEnum/outOfRange.kt b/idea/testData/intentions/convertSealedClassToEnum/outOfRange.kt
new file mode 100644
index 00000000000..80fcfe1c6b3
--- /dev/null
+++ b/idea/testData/intentions/convertSealedClassToEnum/outOfRange.kt
@@ -0,0 +1,5 @@
+// IS_APPLICABLE: false
+
+private sealed class X {
+
+}
\ No newline at end of file
diff --git a/idea/testData/intentions/convertSealedClassToEnum/withNonObjectInheritors.kt b/idea/testData/intentions/convertSealedClassToEnum/withNonObjectInheritors.kt
new file mode 100644
index 00000000000..deeea949a61
--- /dev/null
+++ b/idea/testData/intentions/convertSealedClassToEnum/withNonObjectInheritors.kt
@@ -0,0 +1,7 @@
+// SHOULD_FAIL_WITH: All inheritors must be nested objects of the class itself and may not inherit from other classes or interfaces. Following problems are found: class A, class B
+
+sealed class X {
+ class A : X()
+}
+
+class B : X()
\ No newline at end of file
diff --git a/idea/testData/intentions/convertSealedClassToEnum/withSuperclass.kt b/idea/testData/intentions/convertSealedClassToEnum/withSuperclass.kt
new file mode 100644
index 00000000000..8985e69a41e
--- /dev/null
+++ b/idea/testData/intentions/convertSealedClassToEnum/withSuperclass.kt
@@ -0,0 +1,7 @@
+// IS_APPLICABLE: false
+
+open class C
+
+sealed class X : C() {
+
+}
\ No newline at end of file
diff --git a/idea/tests/org/jetbrains/kotlin/idea/intentions/AbstractIntentionTest.java b/idea/tests/org/jetbrains/kotlin/idea/intentions/AbstractIntentionTest.java
index 878e362762c..4e32f80351b 100644
--- a/idea/tests/org/jetbrains/kotlin/idea/intentions/AbstractIntentionTest.java
+++ b/idea/tests/org/jetbrains/kotlin/idea/intentions/AbstractIntentionTest.java
@@ -240,7 +240,7 @@ public abstract class AbstractIntentionTest extends KotlinCodeInsightTestCase {
assertEquals("Failure message mismatch.", shouldFailString, StringUtil.join(e.getMessages(), ", "));
}
catch (CommonRefactoringUtil.RefactoringErrorHintException e) {
- assertEquals("Failure message mismatch.", shouldFailString, e.getMessage());
+ assertEquals("Failure message mismatch.", shouldFailString, e.getMessage().replace('\n', ' '));
}
}
diff --git a/idea/tests/org/jetbrains/kotlin/idea/intentions/IntentionTestGenerated.java b/idea/tests/org/jetbrains/kotlin/idea/intentions/IntentionTestGenerated.java
index b97ebf47763..3e859633134 100644
--- a/idea/tests/org/jetbrains/kotlin/idea/intentions/IntentionTestGenerated.java
+++ b/idea/tests/org/jetbrains/kotlin/idea/intentions/IntentionTestGenerated.java
@@ -4763,6 +4763,75 @@ public class IntentionTestGenerated extends AbstractIntentionTest {
}
}
+ @TestMetadata("idea/testData/intentions/convertSealedClassToEnum")
+ @TestDataPath("$PROJECT_ROOT")
+ @RunWith(JUnit3RunnerWithInners.class)
+ public static class ConvertSealedClassToEnum extends AbstractIntentionTest {
+ public void testAllFilesPresentInConvertSealedClassToEnum() throws Exception {
+ KotlinTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("idea/testData/intentions/convertSealedClassToEnum"), Pattern.compile("^([\\w\\-_]+)\\.kt$"), true);
+ }
+
+ @TestMetadata("dropDefaultConstructorCall.kt")
+ public void testDropDefaultConstructorCall() throws Exception {
+ String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/convertSealedClassToEnum/dropDefaultConstructorCall.kt");
+ doTest(fileName);
+ }
+
+ @TestMetadata("inheritorsWithMultipleSupertypes.kt")
+ public void testInheritorsWithMultipleSupertypes() throws Exception {
+ String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/convertSealedClassToEnum/inheritorsWithMultipleSupertypes.kt");
+ doTest(fileName);
+ }
+
+ @TestMetadata("instancesAndMembers.kt")
+ public void testInstancesAndMembers() throws Exception {
+ String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/convertSealedClassToEnum/instancesAndMembers.kt");
+ doTest(fileName);
+ }
+
+ @TestMetadata("instancesOnly.kt")
+ public void testInstancesOnly() throws Exception {
+ String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/convertSealedClassToEnum/instancesOnly.kt");
+ doTest(fileName);
+ }
+
+ @TestMetadata("membersOnly.kt")
+ public void testMembersOnly() throws Exception {
+ String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/convertSealedClassToEnum/membersOnly.kt");
+ doTest(fileName);
+ }
+
+ @TestMetadata("nonNestedInheritors.kt")
+ public void testNonNestedInheritors() throws Exception {
+ String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/convertSealedClassToEnum/nonNestedInheritors.kt");
+ doTest(fileName);
+ }
+
+ @TestMetadata("notSealedClass.kt")
+ public void testNotSealedClass() throws Exception {
+ String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/convertSealedClassToEnum/notSealedClass.kt");
+ doTest(fileName);
+ }
+
+ @TestMetadata("outOfRange.kt")
+ public void testOutOfRange() throws Exception {
+ String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/convertSealedClassToEnum/outOfRange.kt");
+ doTest(fileName);
+ }
+
+ @TestMetadata("withNonObjectInheritors.kt")
+ public void testWithNonObjectInheritors() throws Exception {
+ String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/convertSealedClassToEnum/withNonObjectInheritors.kt");
+ doTest(fileName);
+ }
+
+ @TestMetadata("withSuperclass.kt")
+ public void testWithSuperclass() throws Exception {
+ String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/convertSealedClassToEnum/withSuperclass.kt");
+ doTest(fileName);
+ }
+ }
+
@TestMetadata("idea/testData/intentions/convertSecondaryConstructorToPrimary")
@TestDataPath("$PROJECT_ROOT")
@RunWith(JUnit3RunnerWithInners.class)