Sealed class hierarchies are now correctly processed in when (by checking possible smart casts to nested sealed classes) #KT-10648 Fixed

(cherry picked from commit 2eaaf9c)
This commit is contained in:
Mikhail Glukhikh
2016-06-16 14:46:10 +03:00
committed by Mikhail Glukhikh
parent 01430b4b99
commit 49fb9ff424
5 changed files with 154 additions and 39 deletions
@@ -27,6 +27,7 @@ import org.jetbrains.kotlin.lexer.KtTokens
import org.jetbrains.kotlin.psi.*
import org.jetbrains.kotlin.psi.psiUtil.checkReservedPrefixWord
import org.jetbrains.kotlin.resolve.BindingContext
import org.jetbrains.kotlin.resolve.BindingContext.SMARTCAST
import org.jetbrains.kotlin.resolve.BindingTrace
import org.jetbrains.kotlin.resolve.CompileTimeConstantUtils
import org.jetbrains.kotlin.resolve.DescriptorUtils
@@ -139,7 +140,7 @@ private class ClassMissingCase(val descriptor: ClassDescriptor): WhenMissingCase
}
}
private abstract class WhenOnClassExhaustivenessChecker : WhenExhaustivenessChecker {
internal abstract class WhenOnClassExhaustivenessChecker : WhenExhaustivenessChecker {
private fun getReference(expression: KtExpression?): KtSimpleNameExpression? =
when (expression) {
is KtSimpleNameExpression -> expression
@@ -223,7 +224,7 @@ private object WhenOnEnumExhaustivenessChecker : WhenOnClassExhaustivenessChecke
}
}
private object WhenOnSealedExhaustivenessChecker : WhenOnClassExhaustivenessChecker() {
internal object WhenOnSealedExhaustivenessChecker : WhenOnClassExhaustivenessChecker() {
override fun getMissingCases(
expression: KtWhenExpression,
context: BindingContext,
@@ -233,8 +234,7 @@ private object WhenOnSealedExhaustivenessChecker : WhenOnClassExhaustivenessChec
assert(DescriptorUtils.isSealedClass(subjectDescriptor)) {
"isWhenOnSealedClassExhaustive should be called with a sealed class descriptor: $subjectDescriptor"
}
val memberClassDescriptors = LinkedHashSet<ClassDescriptor>()
collectNestedSubclasses(subjectDescriptor!!, subjectDescriptor, memberClassDescriptors)
val memberClassDescriptors = getNestedSubclasses(subjectDescriptor!!)
// When on a sealed class without derived members is considered non-exhaustive (see test WhenOnEmptySealed)
return getMissingClassCases(expression, memberClassDescriptors, context) +
WhenOnNullableExhaustivenessChecker.getMissingCases(expression, context, nullable)
@@ -244,6 +244,12 @@ private object WhenOnSealedExhaustivenessChecker : WhenOnClassExhaustivenessChec
return DescriptorUtils.isSealedClass(TypeUtils.getClassDescriptor(subjectType))
}
internal fun getNestedSubclasses(baseDescriptor: ClassDescriptor): Set<ClassDescriptor> {
val memberClassDescriptors = LinkedHashSet<ClassDescriptor>()
collectNestedSubclasses(baseDescriptor, baseDescriptor, memberClassDescriptors)
return memberClassDescriptors
}
private fun collectNestedSubclasses(
baseDescriptor: ClassDescriptor,
currentDescriptor: ClassDescriptor,
@@ -291,7 +297,7 @@ object WhenChecker {
}
private fun whenSubjectType(expression: KtWhenExpression, context: BindingContext) =
expression.subjectExpression?.let { context.getType(it) } ?: null
expression.subjectExpression?.let { context.get(SMARTCAST, it) ?: context.getType(it) } ?: null
@JvmStatic
fun getEnumMissingCases(
@@ -300,30 +306,9 @@ object WhenChecker {
enumClassDescriptor: ClassDescriptor
) = WhenOnEnumExhaustivenessChecker.getMissingCases(expression, context, enumClassDescriptor, false)
/**
* It's assumed that function is called for a final type. In this case the only possible smart cast is to not nullable type.
* @return true if type is nullable, and cannot be smart casted
*/
private fun isNullableTypeWithoutPossibleSmartCast(
expression: KtExpression?,
type: KotlinType,
context: BindingContext
): Boolean {
if (expression == null) return false // Normally should not happen
if (!TypeUtils.isNullableType(type)) return false
// We cannot read data flow information here due to lack of inputs (module descriptor is necessary)
if (context.get(BindingContext.SMARTCAST, expression) != null) {
// We have smart cast from enum or boolean to something
// Not very nice but we *can* decide it was smart cast to not-null
// because both enum and boolean are final
return false
}
return true
}
fun getMissingCases(expression: KtWhenExpression, context: BindingContext): List<WhenMissingCase> {
val type = whenSubjectType(expression, context) ?: return listOf(UnknownMissingCase)
val nullable = !type.isFlexible() && isNullableTypeWithoutPossibleSmartCast(expression.subjectExpression, type, context)
val nullable = type.isMarkedNullable
val checkers = exhaustivenessCheckers.filter { it.isApplicable(type) }
if (checkers.isEmpty()) return listOf(UnknownMissingCase)
return checkers.map { it.getMissingCases(expression, context, TypeUtils.getClassDescriptor(type), nullable) }.flatten()
@@ -19,6 +19,9 @@ package org.jetbrains.kotlin.types.expressions
import org.jetbrains.kotlin.builtins.KotlinBuiltIns
import org.jetbrains.kotlin.builtins.KotlinBuiltIns.isBoolean
import org.jetbrains.kotlin.cfg.WhenChecker
import org.jetbrains.kotlin.cfg.WhenOnSealedExhaustivenessChecker
import org.jetbrains.kotlin.descriptors.ClassDescriptor
import org.jetbrains.kotlin.descriptors.Modality
import org.jetbrains.kotlin.diagnostics.Errors
import org.jetbrains.kotlin.diagnostics.Errors.*
import org.jetbrains.kotlin.psi.*
@@ -206,22 +209,44 @@ class PatternMatchingTypingVisitor internal constructor(facade: ExpressionTyping
return Pair(currentDataFlowInfo ?: contextAfterSubject.dataFlowInfo, jumpOutPossible)
}
private fun checkSmartCastsInSubjectIfRequired(expression: KtWhenExpression, contextBeforeSubject: ExpressionTypingContext, subjectType: KotlinType) {
val subjectExpression = expression.subjectExpression
if (subjectExpression != null &&
TypeUtils.isNullableType(subjectType) &&
!WhenChecker.containsNullCase(expression, contextBeforeSubject.trace.bindingContext)
) {
val trace = TemporaryBindingTrace.create(contextBeforeSubject.trace, "Temporary trace for when subject nullability")
val subjectContext = contextBeforeSubject.replaceExpectedType(TypeUtils.makeNotNullable(subjectType)).replaceBindingTrace(trace)
val castResult = DataFlowAnalyzer.checkPossibleCast(
subjectType, KtPsiUtil.safeDeparenthesize(subjectExpression), subjectContext)
if (castResult != null && castResult.isCorrect) {
trace.commit()
private fun checkSmartCastsInSubjectIfRequired(
expression: KtWhenExpression,
contextBeforeSubject: ExpressionTypingContext,
subjectType: KotlinType
) {
val subjectExpression = expression.subjectExpression ?: return
val nullableType = TypeUtils.isNullableType(subjectType)
val bindingContext = contextBeforeSubject.trace.bindingContext
if (nullableType && !WhenChecker.containsNullCase(expression, bindingContext)) {
val notNullableType = TypeUtils.makeNotNullable(subjectType)
checkSmartCastToExpectedTypeInSubject(contextBeforeSubject, subjectExpression, subjectType, notNullableType)
}
val subjectClass = subjectType.constructor.declarationDescriptor as? ClassDescriptor ?: return
if (subjectClass.modality == Modality.SEALED &&
WhenOnSealedExhaustivenessChecker.getMissingCases(expression, bindingContext, subjectClass, false).isNotEmpty()) {
for (descriptor in WhenOnSealedExhaustivenessChecker.getNestedSubclasses(subjectClass)) {
if (descriptor.modality == Modality.SEALED && DescriptorUtils.isDirectSubclass(descriptor, subjectClass)) {
checkSmartCastToExpectedTypeInSubject(contextBeforeSubject, subjectExpression, subjectType, descriptor.defaultType)
}
}
}
}
private fun checkSmartCastToExpectedTypeInSubject(
contextBeforeSubject: ExpressionTypingContext,
subjectExpression: KtExpression,
subjectType: KotlinType,
expectedType: KotlinType
) {
val trace = TemporaryBindingTrace.create(contextBeforeSubject.trace, "Temporary trace for when subject nullability")
val subjectContext = contextBeforeSubject.replaceExpectedType(expectedType).replaceBindingTrace(trace)
val castResult = DataFlowAnalyzer.checkPossibleCast(
subjectType, KtPsiUtil.safeDeparenthesize(subjectExpression), subjectContext)
if (castResult != null && castResult.isCorrect) {
trace.commit()
}
}
private fun analyzeWhenEntryConditions(
whenEntry: KtWhenEntry,
context: ExpressionTypingContext,
@@ -0,0 +1,45 @@
// See KT-10648: Exhaustiveness check does not work with nested sealed hierarchy
sealed class Base {
sealed class A : Base() {
class A1 : A()
class A2 : A()
}
sealed class B : Base() {
class B1 : B()
class B2 : B()
}
}
fun foo(b: Base) = when (b) {
is Base.A -> when(<!DEBUG_INFO_SMARTCAST!>b<!>) {
is Base.A.A1 -> 1
is Base.A.A2 -> 2
}
is Base.B -> when(<!DEBUG_INFO_SMARTCAST!>b<!>) {
is Base.B.B1 -> 3
is Base.B.B2 -> 4
}
}
fun bar(b: Base?) = if (b == null) 0 else when (<!DEBUG_INFO_SMARTCAST!>b<!>) {
is Base.A -> when(<!DEBUG_INFO_SMARTCAST!>b<!>) {
is Base.A.A1 -> 1
is Base.A.A2 -> 2
}
is Base.B -> when(<!DEBUG_INFO_SMARTCAST!>b<!>) {
is Base.B.B1 -> 3
is Base.B.B2 -> 4
}
}
fun gav(b: Base?) = when (b) {
null -> 0
is Base.A -> when(<!DEBUG_INFO_SMARTCAST!>b<!>) {
is Base.A.A1 -> 1
is Base.A.A2 -> 2
}
is Base.B -> when(<!DEBUG_INFO_SMARTCAST!>b<!>) {
is Base.B.B1 -> 3
is Base.B.B2 -> 4
}
}
@@ -0,0 +1,54 @@
package
public fun bar(/*0*/ b: Base?): kotlin.Int
public fun foo(/*0*/ b: Base): kotlin.Int
public fun gav(/*0*/ b: Base?): kotlin.Int
public sealed class Base {
private constructor Base()
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 A : Base {
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 final class A1 : Base.A {
public constructor A1()
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 A2 : Base.A {
public constructor A2()
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 : Base {
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 B1 : Base.B {
public constructor B1()
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 B2 : Base.B {
public constructor B2()
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
}
}
}
@@ -16143,6 +16143,12 @@ public class DiagnosticsTestGenerated extends AbstractDiagnosticsTest {
doTest(fileName);
}
@TestMetadata("NestedSealed.kt")
public void testNestedSealed() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/diagnostics/tests/sealed/NestedSealed.kt");
doTest(fileName);
}
@TestMetadata("NeverConstructed.kt")
public void testNeverConstructed() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/diagnostics/tests/sealed/NeverConstructed.kt");