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)