Intentions: Implement "Convert sealed class to enum" intention

#KT-14245 Fixed
This commit is contained in:
Alexey Sedunov
2016-10-10 20:16:18 +03:00
parent 2187a77646
commit ec00b9f3ea
23 changed files with 358 additions and 1 deletions
+1
View File
@@ -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
@@ -0,0 +1,7 @@
enum class MyClass(val s: String = "") {
FOO("FOO"), BAR("BAR"), DEFAULT();
fun foo() {
}
}
@@ -0,0 +1,9 @@
sealed class MyClass(val s: String = "") {
fun foo() {
}
object FOO : MyEnum("FOO")
object BAR : MyEnum("BAR")
object DEFAULT : MyEnum()
}
@@ -0,0 +1,5 @@
<html>
<body>
This intention converts a sealed class to an enum class and replaces inheriting objects with enum entries
</body>
</html>
+5
View File
@@ -1401,6 +1401,11 @@
<category>Kotlin</category>
</intentionAction>
<intentionAction>
<className>org.jetbrains.kotlin.idea.intentions.ConvertSealedClassToEnumIntention</className>
<category>Kotlin</category>
</intentionAction>
<localInspection implementationClass="org.jetbrains.kotlin.idea.intentions.ObjectLiteralToLambdaInspection"
displayName="Object literal can be converted to lambda"
groupName="Kotlin"
@@ -0,0 +1,122 @@
/*
* Copyright 2010-2016 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.kotlin.idea.intentions
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.util.TextRange
import com.intellij.psi.ElementDescriptionUtil
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.resolveToDescriptor
import org.jetbrains.kotlin.idea.refactoring.runSynchronouslyWithProgress
import org.jetbrains.kotlin.idea.search.declarationsSearch.HierarchySearchRequest
import org.jetbrains.kotlin.idea.search.declarationsSearch.searchInheritors
import org.jetbrains.kotlin.lexer.KtTokens
import org.jetbrains.kotlin.psi.KtClass
import org.jetbrains.kotlin.psi.KtObjectDeclaration
import org.jetbrains.kotlin.psi.KtPsiFactory
import org.jetbrains.kotlin.psi.KtSuperTypeCallEntry
import org.jetbrains.kotlin.psi.psiUtil.containingClassOrObject
import org.jetbrains.kotlin.psi.psiUtil.endOffset
import org.jetbrains.kotlin.psi.psiUtil.startOffset
import org.jetbrains.kotlin.resolve.descriptorUtil.getSuperClassNotAny
class ConvertSealedClassToEnumIntention : SelfTargetingRangeIntention<KtClass>(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)
}
}
}
}
@@ -0,0 +1 @@
org.jetbrains.kotlin.idea.intentions.ConvertSealedClassToEnumIntention
@@ -0,0 +1,3 @@
sealed class <caret>MyClass {
object FOO : MyClass()
}
@@ -0,0 +1,3 @@
enum class <caret>MyClass {
FOO
}
@@ -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 <b><code>A</code></b>
interface I
sealed class <caret>X {
object A : X(), I
object B : X()
}
@@ -0,0 +1,23 @@
// WITH_RUNTIME
sealed class <caret>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
}
}
@@ -0,0 +1,22 @@
// WITH_RUNTIME
enum class <caret>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
}
}
@@ -0,0 +1,19 @@
// WITH_RUNTIME
sealed class <caret>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
}
}
@@ -0,0 +1,17 @@
// WITH_RUNTIME
enum class <caret>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
}
}
@@ -0,0 +1,5 @@
sealed class <caret>MyEnum(val s: String = "") {
fun foo() {
}
}
@@ -0,0 +1,7 @@
enum class MyEnum(val s: String = "") {
;
fun foo() {
}
}
@@ -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><code>B</code></b>
sealed class <caret>X {
object A : X()
}
object B : X()
@@ -0,0 +1,5 @@
// IS_APPLICABLE: false
class <caret>X {
}
@@ -0,0 +1,5 @@
// IS_APPLICABLE: false
<caret>private sealed class X {
}
@@ -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 <b><code>A</code></b>, class <b><code>B</code></b>
sealed class <caret>X {
class A : X()
}
class B : X()
@@ -0,0 +1,7 @@
// IS_APPLICABLE: false
open class C
sealed class <caret>X : C() {
}
@@ -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', ' '));
}
}
@@ -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)