diff --git a/ChangeLog.md b/ChangeLog.md
index e161c2621aa..4c5b8641da8 100644
--- a/ChangeLog.md
+++ b/ChangeLog.md
@@ -225,6 +225,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
#### Refactorings
diff --git a/idea/resources/intentionDescriptions/ConvertEnumToSealedClassIntention/after.kt.template b/idea/resources/intentionDescriptions/ConvertEnumToSealedClassIntention/after.kt.template
new file mode 100644
index 00000000000..f7e51a04f5c
--- /dev/null
+++ b/idea/resources/intentionDescriptions/ConvertEnumToSealedClassIntention/after.kt.template
@@ -0,0 +1,9 @@
+sealed class MyEnum(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/ConvertEnumToSealedClassIntention/before.kt.template b/idea/resources/intentionDescriptions/ConvertEnumToSealedClassIntention/before.kt.template
new file mode 100644
index 00000000000..5301d65dd8a
--- /dev/null
+++ b/idea/resources/intentionDescriptions/ConvertEnumToSealedClassIntention/before.kt.template
@@ -0,0 +1,7 @@
+enum class MyEnum(val s: String = "") {
+ FOO("FOO"), BAR("BAR"), DEFAULT();
+
+ fun foo() {
+
+ }
+}
diff --git a/idea/resources/intentionDescriptions/ConvertEnumToSealedClassIntention/description.html b/idea/resources/intentionDescriptions/ConvertEnumToSealedClassIntention/description.html
new file mode 100644
index 00000000000..11d3bcf0cac
--- /dev/null
+++ b/idea/resources/intentionDescriptions/ConvertEnumToSealedClassIntention/description.html
@@ -0,0 +1,5 @@
+
+
+This intention converts an enum class to a sealed class hierarchy with enum entries turned into objects
+
+
\ No newline at end of file
diff --git a/idea/src/META-INF/plugin.xml b/idea/src/META-INF/plugin.xml
index 6fc8021210d..33e80c71f94 100644
--- a/idea/src/META-INF/plugin.xml
+++ b/idea/src/META-INF/plugin.xml
@@ -1396,6 +1396,11 @@
Kotlin
+
+ org.jetbrains.kotlin.idea.intentions.ConvertEnumToSealedClassIntention
+ Kotlin
+
+
(KtClass::class.java, "Convert to sealed class") {
+ override fun applicabilityRange(element: KtClass): TextRange? {
+ val nameIdentifier = element.nameIdentifier ?: return null
+ val enumKeyword = element.modifierList?.getModifier(KtTokens.ENUM_KEYWORD) ?: return null
+ return TextRange(enumKeyword.startOffset, nameIdentifier.endOffset)
+ }
+
+ override fun applyTo(element: KtClass, editor: Editor?) {
+ element.removeModifier(KtTokens.ENUM_KEYWORD)
+ element.addModifier(KtTokens.SEALED_KEYWORD)
+
+ val psiFactory = KtPsiFactory(element)
+
+ for (member in element.declarations) {
+ if (member !is KtEnumEntry) continue
+
+ val obj = psiFactory.createDeclaration("object ${member.name}")
+
+ val initializers = member.initializerList?.initializers ?: emptyList()
+ if (initializers.isNotEmpty()) {
+ initializers.forEach { obj.addSuperTypeListEntry(psiFactory.createSuperTypeCallEntry("${element.name}${it.text}")) }
+ }
+ else {
+ obj.addSuperTypeListEntry(psiFactory.createSuperTypeCallEntry("${element.name}()"))
+ }
+
+ member.getBody()?.let { body -> obj.add(body) }
+
+ member.delete()
+ element.addDeclaration(obj)
+ }
+
+ element.getBody()?.let { body ->
+ val semicolon = body
+ .allChildren
+ .takeWhile { it !is KtDeclaration }
+ .firstOrNull { it.node.elementType == KtTokens.SEMICOLON }
+ if (semicolon != null) {
+ val nonWhiteSibling = semicolon.siblings(forward = true, withItself = false).firstOrNull { it !is PsiWhiteSpace }
+ body.deleteChildRange(semicolon, nonWhiteSibling?.prevSibling ?: semicolon)
+ if (nonWhiteSibling != null) {
+ CodeStyleManager.getInstance(element.project).reformat(nonWhiteSibling.firstChild ?: nonWhiteSibling)
+ }
+ }
+ }
+ }
+}
diff --git a/idea/testData/intentions/convertEnumToSealedClass/.intention b/idea/testData/intentions/convertEnumToSealedClass/.intention
new file mode 100644
index 00000000000..a98388ae09e
--- /dev/null
+++ b/idea/testData/intentions/convertEnumToSealedClass/.intention
@@ -0,0 +1 @@
+org.jetbrains.kotlin.idea.intentions.ConvertEnumToSealedClassIntention
diff --git a/idea/testData/intentions/convertEnumToSealedClass/entriesAndMembers.kt b/idea/testData/intentions/convertEnumToSealedClass/entriesAndMembers.kt
new file mode 100644
index 00000000000..5d5267c8617
--- /dev/null
+++ b/idea/testData/intentions/convertEnumToSealedClass/entriesAndMembers.kt
@@ -0,0 +1,21 @@
+// WITH_RUNTIME
+
+enum class MyEnum(val s: String = "") {
+ FOO("FOO"), BAR("BAR"), DEFAULT();
+
+ fun foo() {
+
+ }
+}
+
+fun test(e: MyEnum) {
+ if (e == MyEnum.BAR) {
+ println()
+ }
+
+ val n = when (e) {
+ MyEnum.BAR -> 1
+ MyEnum.FOO -> 2
+ MyEnum.DEFAULT -> 0
+ }
+}
\ No newline at end of file
diff --git a/idea/testData/intentions/convertEnumToSealedClass/entriesAndMembers.kt.after b/idea/testData/intentions/convertEnumToSealedClass/entriesAndMembers.kt.after
new file mode 100644
index 00000000000..b423d08e25d
--- /dev/null
+++ b/idea/testData/intentions/convertEnumToSealedClass/entriesAndMembers.kt.after
@@ -0,0 +1,24 @@
+// WITH_RUNTIME
+
+sealed class MyEnum(val s: String = "") {
+
+ fun foo() {
+
+ }
+
+ object FOO : MyEnum("FOO")
+ object BAR : MyEnum("BAR")
+ object DEFAULT : MyEnum()
+}
+
+fun test(e: MyEnum) {
+ if (e == MyEnum.BAR) {
+ println()
+ }
+
+ val n = when (e) {
+ MyEnum.BAR -> 1
+ MyEnum.FOO -> 2
+ MyEnum.DEFAULT -> 0
+ }
+}
\ No newline at end of file
diff --git a/idea/testData/intentions/convertEnumToSealedClass/entriesOnly.kt b/idea/testData/intentions/convertEnumToSealedClass/entriesOnly.kt
new file mode 100644
index 00000000000..d83cd62ff0b
--- /dev/null
+++ b/idea/testData/intentions/convertEnumToSealedClass/entriesOnly.kt
@@ -0,0 +1,17 @@
+// WITH_RUNTIME
+
+enum class MyEnum(val s: String = "") {
+ FOO("FOO"), BAR("BAR"), DEFAULT()
+}
+
+fun test(e: MyEnum) {
+ if (e == MyEnum.BAR) {
+ println()
+ }
+
+ val n = when (e) {
+ MyEnum.BAR -> 1
+ MyEnum.FOO -> 2
+ MyEnum.DEFAULT -> 0
+ }
+}
\ No newline at end of file
diff --git a/idea/testData/intentions/convertEnumToSealedClass/entriesOnly.kt.after b/idea/testData/intentions/convertEnumToSealedClass/entriesOnly.kt.after
new file mode 100644
index 00000000000..6cb4c7669ab
--- /dev/null
+++ b/idea/testData/intentions/convertEnumToSealedClass/entriesOnly.kt.after
@@ -0,0 +1,19 @@
+// WITH_RUNTIME
+
+sealed class MyEnum(val s: String = "") {
+ object FOO : MyEnum("FOO")
+ object BAR : MyEnum("BAR")
+ object DEFAULT : MyEnum()
+}
+
+fun test(e: MyEnum) {
+ if (e == MyEnum.BAR) {
+ println()
+ }
+
+ val n = when (e) {
+ MyEnum.BAR -> 1
+ MyEnum.FOO -> 2
+ MyEnum.DEFAULT -> 0
+ }
+}
\ No newline at end of file
diff --git a/idea/testData/intentions/convertEnumToSealedClass/membersOnly.kt b/idea/testData/intentions/convertEnumToSealedClass/membersOnly.kt
new file mode 100644
index 00000000000..4ebfa8e9434
--- /dev/null
+++ b/idea/testData/intentions/convertEnumToSealedClass/membersOnly.kt
@@ -0,0 +1,9 @@
+// WITH_RUNTIME
+
+enum class MyEnum(val s: String = "") {
+ ;
+
+ fun foo() {
+
+ }
+}
\ No newline at end of file
diff --git a/idea/testData/intentions/convertEnumToSealedClass/membersOnly.kt.after b/idea/testData/intentions/convertEnumToSealedClass/membersOnly.kt.after
new file mode 100644
index 00000000000..1a35b348d31
--- /dev/null
+++ b/idea/testData/intentions/convertEnumToSealedClass/membersOnly.kt.after
@@ -0,0 +1,7 @@
+// WITH_RUNTIME
+
+sealed class MyEnum(val s: String = "") {
+ fun foo() {
+
+ }
+}
\ No newline at end of file
diff --git a/idea/testData/intentions/convertEnumToSealedClass/notEnum.kt b/idea/testData/intentions/convertEnumToSealedClass/notEnum.kt
new file mode 100644
index 00000000000..b2232b46283
--- /dev/null
+++ b/idea/testData/intentions/convertEnumToSealedClass/notEnum.kt
@@ -0,0 +1,5 @@
+// IS_APPLICABLE: false
+
+class A {
+
+}
\ No newline at end of file
diff --git a/idea/testData/intentions/convertEnumToSealedClass/outOfRange.kt b/idea/testData/intentions/convertEnumToSealedClass/outOfRange.kt
new file mode 100644
index 00000000000..e15ffc58481
--- /dev/null
+++ b/idea/testData/intentions/convertEnumToSealedClass/outOfRange.kt
@@ -0,0 +1,5 @@
+// IS_APPLICABLE: false
+
+internal enum class MyEnum {
+ A, B, C
+}
\ No newline at end of file
diff --git a/idea/testData/quickfix/implement/enum.kt b/idea/testData/quickfix/implement/enum.kt
index e88a58f4fed..63fa37df843 100644
--- a/idea/testData/quickfix/implement/enum.kt
+++ b/idea/testData/quickfix/implement/enum.kt
@@ -1,5 +1,6 @@
// "Create subclass" "false"
// ACTION: Create test
+// ACTION: Convert to sealed class
enum class My {
SINGLE {
diff --git a/idea/testData/quickfix/removeUnused/importEnumValues.kt b/idea/testData/quickfix/removeUnused/importEnumValues.kt
index 04167b7e17d..ce4451dbe73 100644
--- a/idea/testData/quickfix/removeUnused/importEnumValues.kt
+++ b/idea/testData/quickfix/removeUnused/importEnumValues.kt
@@ -1,5 +1,6 @@
// "Safe delete 'MyEnum'" "false"
// ACTION: Create test
+// ACTION: Convert to sealed class
import MyEnum.values
diff --git a/idea/tests/org/jetbrains/kotlin/idea/intentions/IntentionTestGenerated.java b/idea/tests/org/jetbrains/kotlin/idea/intentions/IntentionTestGenerated.java
index 7ee26b22bcf..b97ebf47763 100644
--- a/idea/tests/org/jetbrains/kotlin/idea/intentions/IntentionTestGenerated.java
+++ b/idea/tests/org/jetbrains/kotlin/idea/intentions/IntentionTestGenerated.java
@@ -3569,6 +3569,45 @@ public class IntentionTestGenerated extends AbstractIntentionTest {
}
}
+ @TestMetadata("idea/testData/intentions/convertEnumToSealedClass")
+ @TestDataPath("$PROJECT_ROOT")
+ @RunWith(JUnit3RunnerWithInners.class)
+ public static class ConvertEnumToSealedClass extends AbstractIntentionTest {
+ public void testAllFilesPresentInConvertEnumToSealedClass() throws Exception {
+ KotlinTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("idea/testData/intentions/convertEnumToSealedClass"), Pattern.compile("^([\\w\\-_]+)\\.kt$"), true);
+ }
+
+ @TestMetadata("entriesAndMembers.kt")
+ public void testEntriesAndMembers() throws Exception {
+ String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/convertEnumToSealedClass/entriesAndMembers.kt");
+ doTest(fileName);
+ }
+
+ @TestMetadata("entriesOnly.kt")
+ public void testEntriesOnly() throws Exception {
+ String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/convertEnumToSealedClass/entriesOnly.kt");
+ doTest(fileName);
+ }
+
+ @TestMetadata("membersOnly.kt")
+ public void testMembersOnly() throws Exception {
+ String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/convertEnumToSealedClass/membersOnly.kt");
+ doTest(fileName);
+ }
+
+ @TestMetadata("notEnum.kt")
+ public void testNotEnum() throws Exception {
+ String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/convertEnumToSealedClass/notEnum.kt");
+ doTest(fileName);
+ }
+
+ @TestMetadata("outOfRange.kt")
+ public void testOutOfRange() throws Exception {
+ String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/convertEnumToSealedClass/outOfRange.kt");
+ doTest(fileName);
+ }
+ }
+
@TestMetadata("idea/testData/intentions/convertForEachToForLoop")
@TestDataPath("$PROJECT_ROOT")
@RunWith(JUnit3RunnerWithInners.class)