From 2187a77646713a71153f8222f0bc048e83405eaf Mon Sep 17 00:00:00 2001 From: Alexey Sedunov Date: Mon, 10 Oct 2016 19:33:56 +0300 Subject: [PATCH] Intentions: Implement "Convert enum to sealed class" intention #KT-14245 In Progress --- ChangeLog.md | 1 + .../after.kt.template | 9 +++ .../before.kt.template | 7 ++ .../description.html | 5 ++ idea/src/META-INF/plugin.xml | 5 ++ .../ConvertEnumToSealedClassIntention.kt | 76 +++++++++++++++++++ .../convertEnumToSealedClass/.intention | 1 + .../entriesAndMembers.kt | 21 +++++ .../entriesAndMembers.kt.after | 24 ++++++ .../convertEnumToSealedClass/entriesOnly.kt | 17 +++++ .../entriesOnly.kt.after | 19 +++++ .../convertEnumToSealedClass/membersOnly.kt | 9 +++ .../membersOnly.kt.after | 7 ++ .../convertEnumToSealedClass/notEnum.kt | 5 ++ .../convertEnumToSealedClass/outOfRange.kt | 5 ++ idea/testData/quickfix/implement/enum.kt | 1 + .../quickfix/removeUnused/importEnumValues.kt | 1 + .../intentions/IntentionTestGenerated.java | 39 ++++++++++ 18 files changed, 252 insertions(+) create mode 100644 idea/resources/intentionDescriptions/ConvertEnumToSealedClassIntention/after.kt.template create mode 100644 idea/resources/intentionDescriptions/ConvertEnumToSealedClassIntention/before.kt.template create mode 100644 idea/resources/intentionDescriptions/ConvertEnumToSealedClassIntention/description.html create mode 100644 idea/src/org/jetbrains/kotlin/idea/intentions/ConvertEnumToSealedClassIntention.kt create mode 100644 idea/testData/intentions/convertEnumToSealedClass/.intention create mode 100644 idea/testData/intentions/convertEnumToSealedClass/entriesAndMembers.kt create mode 100644 idea/testData/intentions/convertEnumToSealedClass/entriesAndMembers.kt.after create mode 100644 idea/testData/intentions/convertEnumToSealedClass/entriesOnly.kt create mode 100644 idea/testData/intentions/convertEnumToSealedClass/entriesOnly.kt.after create mode 100644 idea/testData/intentions/convertEnumToSealedClass/membersOnly.kt create mode 100644 idea/testData/intentions/convertEnumToSealedClass/membersOnly.kt.after create mode 100644 idea/testData/intentions/convertEnumToSealedClass/notEnum.kt create mode 100644 idea/testData/intentions/convertEnumToSealedClass/outOfRange.kt 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)