Do not suggest "convert to object" for sealed sub-classes with state
So #KT-24816 Fixed
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
<html>
|
||||
<body>
|
||||
This inspection reports direct inheritors of <b>sealed class</b> that have default constructors only and thus can be objects.
|
||||
This inspection reports direct inheritors of <b>sealed class</b> that have no state and thus can be objects.
|
||||
</body>
|
||||
</html>
|
||||
+40
-2
@@ -20,13 +20,18 @@ import com.intellij.codeInspection.ProblemHighlightType
|
||||
import com.intellij.codeInspection.ProblemsHolder
|
||||
import com.intellij.psi.PsiElementVisitor
|
||||
import org.jetbrains.kotlin.asJava.classes.KtLightClassImpl
|
||||
import org.jetbrains.kotlin.idea.caches.resolve.resolveToDescriptorIfAny
|
||||
import org.jetbrains.kotlin.idea.core.getModalityFromDescriptor
|
||||
import org.jetbrains.kotlin.idea.quickfix.sealedSubClassToObject.ConvertSealedSubClassToObjectFix
|
||||
import org.jetbrains.kotlin.idea.refactoring.isAbstract
|
||||
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.KtProperty
|
||||
import org.jetbrains.kotlin.psi.KtVisitorVoid
|
||||
import org.jetbrains.kotlin.resolve.DescriptorToSourceUtils
|
||||
import org.jetbrains.kotlin.resolve.descriptorUtil.getSuperClassNotAny
|
||||
|
||||
class CanSealedSubClassBeObjectInspection : AbstractKotlinInspection() {
|
||||
override fun buildVisitor(holder: ProblemsHolder, isOnTheFly: Boolean, session: LocalInspectionToolSession): PsiElementVisitor {
|
||||
@@ -35,13 +40,16 @@ class CanSealedSubClassBeObjectInspection : AbstractKotlinInspection() {
|
||||
if (!klass.hasModifier(KtTokens.SEALED_KEYWORD)) return
|
||||
if (klass.getModalityFromDescriptor() != KtTokens.SEALED_KEYWORD) return
|
||||
|
||||
klass.getSubclasses()
|
||||
val candidates = klass.getSubclasses()
|
||||
.withEmptyConstructors()
|
||||
.thatAreFinal()
|
||||
.thatHasNoTypeParameters()
|
||||
.thatHasNoInnerClasses()
|
||||
.thatHasNoCompanionObjects()
|
||||
.forEach { reportPossibleObject(it) }
|
||||
.thatHasNoState()
|
||||
if (candidates.isEmpty() || !klass.hasNoState() || !klass.baseClassHasNoState()) return
|
||||
|
||||
candidates.forEach { reportPossibleObject(it) }
|
||||
}
|
||||
|
||||
private fun reportPossibleObject(klass: KtClass) {
|
||||
@@ -56,6 +64,14 @@ class CanSealedSubClassBeObjectInspection : AbstractKotlinInspection() {
|
||||
}
|
||||
}
|
||||
|
||||
private tailrec fun KtClass.baseClassHasNoState(): Boolean {
|
||||
val descriptor = resolveToDescriptorIfAny() ?: return false
|
||||
val superDescriptor = descriptor.getSuperClassNotAny() ?: return true // No super class -- no state
|
||||
val superClass = DescriptorToSourceUtils.descriptorToDeclaration(superDescriptor) as? KtClass ?: return false
|
||||
if (!superClass.hasNoState()) return false
|
||||
return superClass.baseClassHasNoState()
|
||||
}
|
||||
|
||||
private fun KtClass.getSubclasses(): List<KtLightClassImpl> {
|
||||
return HierarchySearchRequest(this, this.useScope, false)
|
||||
.searchInheritors().filterIsInstance<KtLightClassImpl>()
|
||||
@@ -83,6 +99,28 @@ class CanSealedSubClassBeObjectInspection : AbstractKotlinInspection() {
|
||||
return filter { klass -> klass.hasNoInnerClass() }
|
||||
}
|
||||
|
||||
private fun List<KtClass>.thatHasNoState(): List<KtClass> {
|
||||
return filter { it.hasNoState() }
|
||||
}
|
||||
|
||||
private fun KtClass.hasNoState(): Boolean {
|
||||
if (primaryConstructor?.valueParameters?.isNotEmpty() == true) return false
|
||||
val body = getBody()
|
||||
return body == null || run {
|
||||
val properties = body.declarations.filterIsInstance<KtProperty>()
|
||||
properties.none { property ->
|
||||
// Simplified "backing field required"
|
||||
when {
|
||||
property.isAbstract() -> false
|
||||
property.initializer != null -> true
|
||||
property.delegate != null -> false
|
||||
!property.isVar -> property.getter == null
|
||||
else -> property.getter == null || property.setter == null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun KtClass.hasNoInnerClass(): Boolean {
|
||||
val internalClasses = getBody()
|
||||
?.declarations
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
// PROBLEM: none
|
||||
|
||||
abstract class Base(var x: String)
|
||||
|
||||
sealed class Sealed(s: String) : Base(s)
|
||||
|
||||
<caret>class Derived : Sealed("123")
|
||||
@@ -0,0 +1,18 @@
|
||||
// WITH_RUNTIME
|
||||
|
||||
abstract class Base {
|
||||
var s: String
|
||||
get() = "Hello"
|
||||
set(value) {}
|
||||
}
|
||||
|
||||
sealed class Sealed : Base() {
|
||||
open val x: List<Int>
|
||||
get() = emptyList()
|
||||
}
|
||||
|
||||
<caret>class Derived : Sealed() {
|
||||
var length: Int
|
||||
get() = s.length
|
||||
set(value) {}
|
||||
}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
// WITH_RUNTIME
|
||||
|
||||
abstract class Base {
|
||||
var s: String
|
||||
get() = "Hello"
|
||||
set(value) {}
|
||||
}
|
||||
|
||||
sealed class Sealed : Base() {
|
||||
open val x: List<Int>
|
||||
get() = emptyList()
|
||||
}
|
||||
|
||||
object Derived : Sealed() {
|
||||
var length: Int
|
||||
get() = s.length
|
||||
set(value) {}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
// PROBLEM: none
|
||||
// WITH_RUNTIME
|
||||
|
||||
sealed class SC {
|
||||
<caret>class U : SC() {
|
||||
val a = mutableListOf<String>()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
// PROBLEM: none
|
||||
|
||||
sealed class SC {
|
||||
var u = 0
|
||||
<caret>class C : SC() {}
|
||||
}
|
||||
+20
@@ -1225,6 +1225,11 @@ public class LocalInspectionTestGenerated extends AbstractLocalInspectionTest {
|
||||
KotlinTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("idea/testData/inspectionsLocal/convertSealedSubClassToObject"), Pattern.compile("^([\\w\\-_]+)\\.(kt|kts)$"), TargetBackend.ANY, true);
|
||||
}
|
||||
|
||||
@TestMetadata("baseState.kt")
|
||||
public void testBaseState() throws Exception {
|
||||
runTest("idea/testData/inspectionsLocal/convertSealedSubClassToObject/baseState.kt");
|
||||
}
|
||||
|
||||
@TestMetadata("convertSubClassWithModifiers.kt")
|
||||
public void testConvertSubClassWithModifiers() throws Exception {
|
||||
runTest("idea/testData/inspectionsLocal/convertSealedSubClassToObject/convertSubClassWithModifiers.kt");
|
||||
@@ -1240,6 +1245,11 @@ public class LocalInspectionTestGenerated extends AbstractLocalInspectionTest {
|
||||
runTest("idea/testData/inspectionsLocal/convertSealedSubClassToObject/convertSubClassWithoutParentheses.kt");
|
||||
}
|
||||
|
||||
@TestMetadata("fakeState.kt")
|
||||
public void testFakeState() throws Exception {
|
||||
runTest("idea/testData/inspectionsLocal/convertSealedSubClassToObject/fakeState.kt");
|
||||
}
|
||||
|
||||
@TestMetadata("generic.kt")
|
||||
public void testGeneric() throws Exception {
|
||||
runTest("idea/testData/inspectionsLocal/convertSealedSubClassToObject/generic.kt");
|
||||
@@ -1255,6 +1265,16 @@ public class LocalInspectionTestGenerated extends AbstractLocalInspectionTest {
|
||||
runTest("idea/testData/inspectionsLocal/convertSealedSubClassToObject/openSubclass.kt");
|
||||
}
|
||||
|
||||
@TestMetadata("ownState.kt")
|
||||
public void testOwnState() throws Exception {
|
||||
runTest("idea/testData/inspectionsLocal/convertSealedSubClassToObject/ownState.kt");
|
||||
}
|
||||
|
||||
@TestMetadata("sealedState.kt")
|
||||
public void testSealedState() throws Exception {
|
||||
runTest("idea/testData/inspectionsLocal/convertSealedSubClassToObject/sealedState.kt");
|
||||
}
|
||||
|
||||
@TestMetadata("withCompanion.kt")
|
||||
public void testWithCompanion() throws Exception {
|
||||
runTest("idea/testData/inspectionsLocal/convertSealedSubClassToObject/withCompanion.kt");
|
||||
|
||||
Reference in New Issue
Block a user