Exhaustive when on sealed trees implemented #KT-13130 Fixed

Also #KT-13227 Fixed
This commit is contained in:
Mikhail Glukhikh
2017-01-18 18:40:42 +03:00
parent 3d7bcbdff9
commit 9625c32527
8 changed files with 292 additions and 36 deletions
@@ -20,6 +20,7 @@ import com.intellij.psi.PsiElement
import org.jetbrains.kotlin.builtins.KotlinBuiltIns
import org.jetbrains.kotlin.descriptors.ClassDescriptor
import org.jetbrains.kotlin.descriptors.ClassKind
import org.jetbrains.kotlin.descriptors.Modality
import org.jetbrains.kotlin.diagnostics.Errors
import org.jetbrains.kotlin.lexer.KtTokens
import org.jetbrains.kotlin.psi.*
@@ -34,7 +35,6 @@ import org.jetbrains.kotlin.resolve.DescriptorUtils.isEnumEntry
import org.jetbrains.kotlin.resolve.bindingContextUtil.isUsedAsExpression
import org.jetbrains.kotlin.resolve.constants.CompileTimeConstant
import org.jetbrains.kotlin.resolve.constants.evaluate.ConstantExpressionEvaluator
import org.jetbrains.kotlin.resolve.descriptorUtil.computeSealedSubclasses
import org.jetbrains.kotlin.types.KotlinType
import org.jetbrains.kotlin.types.TypeUtils
import java.util.*
@@ -144,6 +144,36 @@ internal abstract class WhenOnClassExhaustivenessChecker : WhenExhaustivenessChe
else -> null
}
protected val ClassDescriptor.deepSealedSubclasses: List<ClassDescriptor>
get() = this.sealedSubclasses.flatMap {
if (it.modality == Modality.SEALED) it.deepSealedSubclasses
else setOf(it)
}
private val KtWhenCondition.negated
get() = (this as? KtWhenConditionIsPattern)?.isNegated ?: false
private fun KtWhenCondition.isRelevant(checkedDescriptor: ClassDescriptor) =
this !is KtWhenConditionWithExpression ||
DescriptorUtils.isObject(checkedDescriptor) ||
DescriptorUtils.isEnumEntry(checkedDescriptor)
private fun KtWhenCondition.getCheckedDescriptor(context: BindingContext): ClassDescriptor? {
return when (this) {
is KtWhenConditionIsPattern -> {
val checkedType = context.get(BindingContext.TYPE, typeReference) ?: return null
TypeUtils.getClassDescriptor(checkedType)
}
is KtWhenConditionWithExpression -> {
val reference = expression?.let { getReference(it) } ?: return null
context.get(BindingContext.REFERENCE_TARGET, reference) as? ClassDescriptor
}
else -> {
null
}
}
}
protected fun getMissingClassCases(
whenExpression: KtWhenExpression,
subclasses: Set<ClassDescriptor>,
@@ -152,50 +182,32 @@ internal abstract class WhenOnClassExhaustivenessChecker : WhenExhaustivenessChe
// when on empty enum / sealed is considered non-exhaustive, see test whenOnEmptySealed
if (subclasses.isEmpty()) return listOf(UnknownMissingCase)
val checkedDescriptors = LinkedHashSet<ClassDescriptor>()
val checkedDescriptors = linkedSetOf<ClassDescriptor>()
for (whenEntry in whenExpression.entries) {
for (condition in whenEntry.conditions) {
var negated = false
var checkedDescriptor: ClassDescriptor? = null
if (condition is KtWhenConditionIsPattern) {
val checkedType = context.get(BindingContext.TYPE, condition.typeReference)
if (checkedType != null) {
checkedDescriptor = TypeUtils.getClassDescriptor(checkedType)
}
negated = condition.isNegated
}
else if (condition is KtWhenConditionWithExpression) {
if (condition.expression != null) {
val reference = getReference(condition.expression)
if (reference != null) {
val target = context.get(BindingContext.REFERENCE_TARGET, reference)
if (target is ClassDescriptor) {
checkedDescriptor = target
}
}
}
}
val negated = condition.negated
val checkedDescriptor = condition.getCheckedDescriptor(context) ?: continue
val checkedDescriptorSubclasses =
if (checkedDescriptor.modality == Modality.SEALED) checkedDescriptor.deepSealedSubclasses
else listOf(checkedDescriptor)
// Checks are important only for nested subclasses of the sealed class
// In additional, check without "is" is important only for objects
if (checkedDescriptor == null ||
!subclasses.contains(checkedDescriptor) ||
(condition is KtWhenConditionWithExpression &&
!DescriptorUtils.isObject(checkedDescriptor) &&
!DescriptorUtils.isEnumEntry(checkedDescriptor))) {
if (checkedDescriptorSubclasses.none { subclasses.contains(it) } ||
!condition.isRelevant(checkedDescriptor)) {
continue
}
if (negated) {
if (checkedDescriptors.contains(checkedDescriptor)) return listOf() // all members are already there
if (checkedDescriptors.containsAll(checkedDescriptorSubclasses)) return listOf()
checkedDescriptors.addAll(subclasses)
checkedDescriptors.remove(checkedDescriptor)
checkedDescriptors.removeAll(checkedDescriptorSubclasses)
}
else {
checkedDescriptors.add(checkedDescriptor)
checkedDescriptors.addAll(checkedDescriptorSubclasses)
}
}
}
return (subclasses - checkedDescriptors).toList().map { ClassMissingCase(it) }
return (subclasses - checkedDescriptors).map(::ClassMissingCase)
}
}
@@ -209,7 +221,7 @@ private object WhenOnEnumExhaustivenessChecker : WhenOnClassExhaustivenessChecke
assert(isEnumClass(subjectDescriptor)) { "isWhenOnEnumExhaustive should be called with an enum class descriptor" }
val entryDescriptors =
DescriptorUtils.getAllDescriptors(subjectDescriptor!!.unsubstitutedInnerClassesScope)
.filter { isEnumEntry(it) }
.filter(::isEnumEntry)
.filterIsInstance<ClassDescriptor>()
.toSet()
return getMissingClassCases(expression, entryDescriptors, context) +
@@ -222,6 +234,7 @@ private object WhenOnEnumExhaustivenessChecker : WhenOnClassExhaustivenessChecke
}
internal object WhenOnSealedExhaustivenessChecker : WhenOnClassExhaustivenessChecker() {
override fun getMissingCases(
expression: KtWhenExpression,
context: BindingContext,
@@ -231,9 +244,9 @@ internal object WhenOnSealedExhaustivenessChecker : WhenOnClassExhaustivenessChe
assert(DescriptorUtils.isSealedClass(subjectDescriptor)) {
"isWhenOnSealedClassExhaustive should be called with a sealed class descriptor: $subjectDescriptor"
}
val subclasses = subjectDescriptor!!.sealedSubclasses
// When on a sealed class without derived members is considered non-exhaustive (see test WhenOnEmptySealed)
return getMissingClassCases(expression, subclasses.toSet(), context) +
val allSubclasses = subjectDescriptor!!.deepSealedSubclasses
return getMissingClassCases(expression, allSubclasses.toSet(), context) +
WhenOnNullableExhaustivenessChecker.getMissingCases(expression, context, nullable)
}
@@ -268,7 +281,7 @@ object WhenChecker {
}
private fun whenSubjectType(expression: KtWhenExpression, context: BindingContext) =
expression.subjectExpression?.let { context.get(SMARTCAST, it)?.defaultType ?: context.getType(it) } ?: null
expression.subjectExpression?.let { context.get(SMARTCAST, it)?.defaultType ?: context.getType(it) }
@JvmStatic
fun getEnumMissingCases(
@@ -0,0 +1,23 @@
sealed class Stmt
class ForStmt : Stmt()
sealed class Expr : Stmt() {
object BinExpr : Expr()
}
fun test(x: Stmt): String =
when (x) {
is Expr -> "expr"
is Stmt -> "stmt"
}
fun test2(x: Stmt): String =
<!NO_ELSE_IN_WHEN!>when<!> (x) {
is Expr -> "expr"
}
fun test3(x: Expr): String =
when (x) {
is Stmt -> "stmt"
}
@@ -0,0 +1,33 @@
package
public fun test(/*0*/ x: Stmt): kotlin.String
public fun test2(/*0*/ x: Stmt): kotlin.String
public fun test3(/*0*/ x: Expr): kotlin.String
public sealed class Expr : Stmt {
private constructor Expr()
public open override /*1*/ /*fake_override*/ fun equals(/*0*/ other: kotlin.Any?): kotlin.Boolean
public open override /*1*/ /*fake_override*/ fun hashCode(): kotlin.Int
public open override /*1*/ /*fake_override*/ fun toString(): kotlin.String
public object BinExpr : Expr {
private constructor BinExpr()
public open override /*1*/ /*fake_override*/ fun equals(/*0*/ other: kotlin.Any?): kotlin.Boolean
public open override /*1*/ /*fake_override*/ fun hashCode(): kotlin.Int
public open override /*1*/ /*fake_override*/ fun toString(): kotlin.String
}
}
public final class ForStmt : Stmt {
public constructor ForStmt()
public open override /*1*/ /*fake_override*/ fun equals(/*0*/ other: kotlin.Any?): kotlin.Boolean
public open override /*1*/ /*fake_override*/ fun hashCode(): kotlin.Int
public open override /*1*/ /*fake_override*/ fun toString(): kotlin.String
}
public sealed class Stmt {
private constructor Stmt()
public open override /*1*/ /*fake_override*/ fun equals(/*0*/ other: kotlin.Any?): kotlin.Boolean
public open override /*1*/ /*fake_override*/ fun hashCode(): kotlin.Int
public open override /*1*/ /*fake_override*/ fun toString(): kotlin.String
}
@@ -0,0 +1,34 @@
sealed class Base {
sealed class A : Base() {
object A1 : A()
sealed class A2 : A()
}
sealed class B : Base() {
sealed class B1 : B()
object B2 : B()
}
fun foo() = when (this) {
is A -> 1
is B.B1 -> 2
B.B2 -> 3
// No else required
}
fun bar() = <!NO_ELSE_IN_WHEN!>when<!> (this) {
is A -> 1
is B.B1 -> 2
}
fun baz() = when (this) {
is A -> 1
B.B2 -> 3
// No else required (no possible B1 instances)
}
fun negated() = when (this) {
!is A -> 1
A.A1 -> 2
is A.A2 -> 3
}
}
@@ -0,0 +1,78 @@
package
public sealed class Base {
private constructor Base()
public final fun bar(): kotlin.Int
public final fun baz(): kotlin.Int
public open override /*1*/ /*fake_override*/ fun equals(/*0*/ other: kotlin.Any?): kotlin.Boolean
public final fun foo(): kotlin.Int
public open override /*1*/ /*fake_override*/ fun hashCode(): kotlin.Int
public final fun negated(): kotlin.Int
public open override /*1*/ /*fake_override*/ fun toString(): kotlin.String
public sealed class A : Base {
private constructor A()
public final override /*1*/ /*fake_override*/ fun bar(): kotlin.Int
public final override /*1*/ /*fake_override*/ fun baz(): kotlin.Int
public open override /*1*/ /*fake_override*/ fun equals(/*0*/ other: kotlin.Any?): kotlin.Boolean
public final override /*1*/ /*fake_override*/ fun foo(): kotlin.Int
public open override /*1*/ /*fake_override*/ fun hashCode(): kotlin.Int
public final override /*1*/ /*fake_override*/ fun negated(): kotlin.Int
public open override /*1*/ /*fake_override*/ fun toString(): kotlin.String
public object A1 : Base.A {
private constructor A1()
public final override /*1*/ /*fake_override*/ fun bar(): kotlin.Int
public final override /*1*/ /*fake_override*/ fun baz(): kotlin.Int
public open override /*1*/ /*fake_override*/ fun equals(/*0*/ other: kotlin.Any?): kotlin.Boolean
public final override /*1*/ /*fake_override*/ fun foo(): kotlin.Int
public open override /*1*/ /*fake_override*/ fun hashCode(): kotlin.Int
public final override /*1*/ /*fake_override*/ fun negated(): kotlin.Int
public open override /*1*/ /*fake_override*/ fun toString(): kotlin.String
}
public sealed class A2 : Base.A {
private constructor A2()
public final override /*1*/ /*fake_override*/ fun bar(): kotlin.Int
public final override /*1*/ /*fake_override*/ fun baz(): kotlin.Int
public open override /*1*/ /*fake_override*/ fun equals(/*0*/ other: kotlin.Any?): kotlin.Boolean
public final override /*1*/ /*fake_override*/ fun foo(): kotlin.Int
public open override /*1*/ /*fake_override*/ fun hashCode(): kotlin.Int
public final override /*1*/ /*fake_override*/ fun negated(): kotlin.Int
public open override /*1*/ /*fake_override*/ fun toString(): kotlin.String
}
}
public sealed class B : Base {
private constructor B()
public final override /*1*/ /*fake_override*/ fun bar(): kotlin.Int
public final override /*1*/ /*fake_override*/ fun baz(): kotlin.Int
public open override /*1*/ /*fake_override*/ fun equals(/*0*/ other: kotlin.Any?): kotlin.Boolean
public final override /*1*/ /*fake_override*/ fun foo(): kotlin.Int
public open override /*1*/ /*fake_override*/ fun hashCode(): kotlin.Int
public final override /*1*/ /*fake_override*/ fun negated(): kotlin.Int
public open override /*1*/ /*fake_override*/ fun toString(): kotlin.String
public sealed class B1 : Base.B {
private constructor B1()
public final override /*1*/ /*fake_override*/ fun bar(): kotlin.Int
public final override /*1*/ /*fake_override*/ fun baz(): kotlin.Int
public open override /*1*/ /*fake_override*/ fun equals(/*0*/ other: kotlin.Any?): kotlin.Boolean
public final override /*1*/ /*fake_override*/ fun foo(): kotlin.Int
public open override /*1*/ /*fake_override*/ fun hashCode(): kotlin.Int
public final override /*1*/ /*fake_override*/ fun negated(): kotlin.Int
public open override /*1*/ /*fake_override*/ fun toString(): kotlin.String
}
public object B2 : Base.B {
private constructor B2()
public final override /*1*/ /*fake_override*/ fun bar(): kotlin.Int
public final override /*1*/ /*fake_override*/ fun baz(): kotlin.Int
public open override /*1*/ /*fake_override*/ fun equals(/*0*/ other: kotlin.Any?): kotlin.Boolean
public final override /*1*/ /*fake_override*/ fun foo(): kotlin.Int
public open override /*1*/ /*fake_override*/ fun hashCode(): kotlin.Int
public final override /*1*/ /*fake_override*/ fun negated(): kotlin.Int
public open override /*1*/ /*fake_override*/ fun toString(): kotlin.String
}
}
}
@@ -0,0 +1,24 @@
sealed class A
sealed class B : A()
class C : B()
class D : B()
fun test(a: A): Any {
return when (a) {
is C -> ""
is D -> ""
}
}
fun test2(a: A): Any {
return when (a) {
is B -> ""
}
}
fun test3(a: A): Any {
return <!NO_ELSE_IN_WHEN!>when<!> (a) {
is D -> ""
}
}
@@ -0,0 +1,33 @@
package
public fun test(/*0*/ a: A): kotlin.Any
public fun test2(/*0*/ a: A): kotlin.Any
public fun test3(/*0*/ a: A): kotlin.Any
public sealed class A {
private constructor A()
public open override /*1*/ /*fake_override*/ fun equals(/*0*/ other: kotlin.Any?): kotlin.Boolean
public open override /*1*/ /*fake_override*/ fun hashCode(): kotlin.Int
public open override /*1*/ /*fake_override*/ fun toString(): kotlin.String
}
public sealed class B : A {
private constructor B()
public open override /*1*/ /*fake_override*/ fun equals(/*0*/ other: kotlin.Any?): kotlin.Boolean
public open override /*1*/ /*fake_override*/ fun hashCode(): kotlin.Int
public open override /*1*/ /*fake_override*/ fun toString(): kotlin.String
}
public final class C : B {
public constructor C()
public open override /*1*/ /*fake_override*/ fun equals(/*0*/ other: kotlin.Any?): kotlin.Boolean
public open override /*1*/ /*fake_override*/ fun hashCode(): kotlin.Int
public open override /*1*/ /*fake_override*/ fun toString(): kotlin.String
}
public final class D : B {
public constructor D()
public open override /*1*/ /*fake_override*/ fun equals(/*0*/ other: kotlin.Any?): kotlin.Boolean
public open override /*1*/ /*fake_override*/ fun hashCode(): kotlin.Int
public open override /*1*/ /*fake_override*/ fun toString(): kotlin.String
}
@@ -18334,6 +18334,24 @@ public class DiagnosticsTestGenerated extends AbstractDiagnosticsTest {
doTest(fileName);
}
@TestMetadata("ExhaustiveOnRoot.kt")
public void testExhaustiveOnRoot() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/diagnostics/tests/sealed/ExhaustiveOnRoot.kt");
doTest(fileName);
}
@TestMetadata("ExhaustiveOnTree.kt")
public void testExhaustiveOnTree() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/diagnostics/tests/sealed/ExhaustiveOnTree.kt");
doTest(fileName);
}
@TestMetadata("ExhaustiveOnTriangleStar.kt")
public void testExhaustiveOnTriangleStar() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/diagnostics/tests/sealed/ExhaustiveOnTriangleStar.kt");
doTest(fileName);
}
@TestMetadata("ExhaustiveWhen.kt")
public void testExhaustiveWhen() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/diagnostics/tests/sealed/ExhaustiveWhen.kt");