diff --git a/compiler/frontend/src/org/jetbrains/kotlin/cfg/WhenChecker.java b/compiler/frontend/src/org/jetbrains/kotlin/cfg/WhenChecker.java index 4c0d3df6e31..ccb942af9fa 100644 --- a/compiler/frontend/src/org/jetbrains/kotlin/cfg/WhenChecker.java +++ b/compiler/frontend/src/org/jetbrains/kotlin/cfg/WhenChecker.java @@ -22,11 +22,13 @@ import org.jetbrains.kotlin.builtins.KotlinBuiltIns; import org.jetbrains.kotlin.descriptors.*; import org.jetbrains.kotlin.psi.*; import org.jetbrains.kotlin.resolve.BindingContext; +import org.jetbrains.kotlin.resolve.BindingContextUtils; import org.jetbrains.kotlin.resolve.BindingTrace; import org.jetbrains.kotlin.resolve.CompileTimeConstantUtils; import org.jetbrains.kotlin.resolve.bindingContextUtil.BindingContextUtilPackage; import org.jetbrains.kotlin.types.JetType; import org.jetbrains.kotlin.types.TypeUtils; +import org.jetbrains.kotlin.types.expressions.JetTypeInfo; import static org.jetbrains.kotlin.resolve.DescriptorUtils.isEnumEntry; import static org.jetbrains.kotlin.resolve.DescriptorUtils.isEnumClass; @@ -96,6 +98,27 @@ public final class WhenChecker { return notEmpty; } + /** + * 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 static boolean isNullableTypeWithoutPossibleSmartCast( + @Nullable JetExpression expression, + @NotNull JetType type, + @NotNull BindingContext context + ) { + 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; + } + public static boolean isWhenExhaustive(@NotNull JetWhenExpression expression, @NotNull BindingTrace trace) { JetType type = whenSubjectType(expression, trace.getBindingContext()); if (type == null) return false; @@ -115,10 +138,11 @@ public final class WhenChecker { exhaustive = isWhenOnEnumExhaustive(expression, trace, enumClassDescriptor); } if (exhaustive) { - if (!TypeUtils.isNullableType(type) + if (// Flexible (nullable) enum types are also counted as exhaustive + (enumClassDescriptor != null && isFlexible(type)) || containsNullCase(expression, trace) - // Flexible (nullable) enum types are also counted as exhaustive - || (enumClassDescriptor != null && isFlexible(type))) { + || !isNullableTypeWithoutPossibleSmartCast(expression.getSubjectExpression(), type, trace.getBindingContext())) { + trace.record(BindingContext.EXHAUSTIVE_WHEN, expression); return true; } diff --git a/compiler/frontend/src/org/jetbrains/kotlin/types/expressions/DataFlowUtils.java b/compiler/frontend/src/org/jetbrains/kotlin/types/expressions/DataFlowUtils.java index e0e87051ae4..f297b7e2d6d 100644 --- a/compiler/frontend/src/org/jetbrains/kotlin/types/expressions/DataFlowUtils.java +++ b/compiler/frontend/src/org/jetbrains/kotlin/types/expressions/DataFlowUtils.java @@ -191,6 +191,20 @@ public class DataFlowUtils { return expressionType; } + JetType possibleType = checkPossibleCast(expressionType, expression, c); + if (possibleType != null) return possibleType; + + c.trace.report(TYPE_MISMATCH.on(expression, c.expectedType, expressionType)); + if (hasError != null) hasError.set(true); + return expressionType; + } + + @Nullable + public static JetType checkPossibleCast( + @NotNull JetType expressionType, + @NotNull JetExpression expression, + @NotNull ResolutionContext c + ) { DataFlowValue dataFlowValue = DataFlowValueFactory.createDataFlowValue(expression, expressionType, c); for (JetType possibleType : c.dataFlowInfo.getPossibleTypes(dataFlowValue)) { @@ -199,9 +213,8 @@ public class DataFlowUtils { return possibleType; } } - c.trace.report(TYPE_MISMATCH.on(expression, c.expectedType, expressionType)); - if (hasError != null) hasError.set(true); - return expressionType; + + return null; } public static void recordExpectedType(@NotNull BindingTrace trace, @NotNull JetExpression expression, @NotNull JetType expectedType) { diff --git a/compiler/frontend/src/org/jetbrains/kotlin/types/expressions/PatternMatchingTypingVisitor.java b/compiler/frontend/src/org/jetbrains/kotlin/types/expressions/PatternMatchingTypingVisitor.java index 130003a923e..fe83a05a997 100644 --- a/compiler/frontend/src/org/jetbrains/kotlin/types/expressions/PatternMatchingTypingVisitor.java +++ b/compiler/frontend/src/org/jetbrains/kotlin/types/expressions/PatternMatchingTypingVisitor.java @@ -89,6 +89,11 @@ public class PatternMatchingTypingVisitor extends ExpressionTypingVisitor { JetTypeInfo typeInfo = facade.safeGetTypeInfo(subjectExpression, context); loopBreakContinuePossible = typeInfo.getJumpOutPossible(); subjectType = typeInfo.getType(); + assert subjectType != null; + if (TypeUtils.isNullableType(subjectType) && !WhenChecker.containsNullCase(expression, context.trace)) { + ExpressionTypingContext subjectContext = context.replaceExpectedType(TypeUtils.makeNotNullable(subjectType)); + DataFlowUtils.checkPossibleCast(subjectType, JetPsiUtil.safeDeparenthesize(subjectExpression, false), subjectContext); + } context = context.replaceDataFlowInfo(typeInfo.getDataFlowInfo()); } DataFlowValue subjectDataFlowValue = subjectExpression != null @@ -138,8 +143,8 @@ public class PatternMatchingTypingVisitor extends ExpressionTypingVisitor { return TypeInfoFactoryPackage.createTypeInfo(expressionTypes.isEmpty() ? null : DataFlowUtils.checkType( DataFlowUtils.checkImplicitCast( - CommonSupertypes.commonSupertype(expressionTypes), expression, - contextWithExpectedType, isStatement), + CommonSupertypes.commonSupertype(expressionTypes), expression, + contextWithExpectedType, isStatement), expression, contextWithExpectedType), commonDataFlowInfo, loopBreakContinuePossible, diff --git a/compiler/testData/diagnostics/tests/dataFlowInfoTraversal/When.kt b/compiler/testData/diagnostics/tests/dataFlowInfoTraversal/When.kt index 5d42fafa81e..f453112916d 100644 --- a/compiler/testData/diagnostics/tests/dataFlowInfoTraversal/When.kt +++ b/compiler/testData/diagnostics/tests/dataFlowInfoTraversal/When.kt @@ -4,7 +4,7 @@ fun foo() { val x: Int? = null if (x != null) { - when (x) { + when (x) { 0 -> bar(x) else -> {} } diff --git a/compiler/testData/diagnostics/tests/when/ExhaustiveWithNullabilityCheck.kt b/compiler/testData/diagnostics/tests/when/ExhaustiveWithNullabilityCheck.kt new file mode 100644 index 00000000000..d140b62eb6f --- /dev/null +++ b/compiler/testData/diagnostics/tests/when/ExhaustiveWithNullabilityCheck.kt @@ -0,0 +1,14 @@ +// KT-7857: when exhaustiveness does not take previous nullability checks into account +enum class X { A, B } +fun foo(arg: X?): Int { + if (arg != null) { + return when (arg) { + X.A -> 1 + X.B -> 2 + // else or null branch should not be required here! + } + } + else { + return 0 + } +} \ No newline at end of file diff --git a/compiler/testData/diagnostics/tests/when/ExhaustiveWithNullabilityCheck.txt b/compiler/testData/diagnostics/tests/when/ExhaustiveWithNullabilityCheck.txt new file mode 100644 index 00000000000..27255e8808f --- /dev/null +++ b/compiler/testData/diagnostics/tests/when/ExhaustiveWithNullabilityCheck.txt @@ -0,0 +1,37 @@ +package + +internal fun foo(/*0*/ arg: X?): kotlin.Int + +internal final enum class X : kotlin.Enum { + public enum entry A : X { + private constructor A() + public final override /*1*/ /*fake_override*/ fun compareTo(/*0*/ other: X): kotlin.Int + public final override /*1*/ /*fake_override*/ fun equals(/*0*/ other: kotlin.Any?): kotlin.Boolean + public final override /*1*/ /*fake_override*/ fun hashCode(): kotlin.Int + public final override /*1*/ /*fake_override*/ fun name(): kotlin.String + public final override /*1*/ /*fake_override*/ fun ordinal(): kotlin.Int + public open override /*1*/ /*fake_override*/ fun toString(): kotlin.String + } + + public enum entry B : X { + private constructor B() + public final override /*1*/ /*fake_override*/ fun compareTo(/*0*/ other: X): kotlin.Int + public final override /*1*/ /*fake_override*/ fun equals(/*0*/ other: kotlin.Any?): kotlin.Boolean + public final override /*1*/ /*fake_override*/ fun hashCode(): kotlin.Int + public final override /*1*/ /*fake_override*/ fun name(): kotlin.String + public final override /*1*/ /*fake_override*/ fun ordinal(): kotlin.Int + public open override /*1*/ /*fake_override*/ fun toString(): kotlin.String + } + + private constructor X() + public final override /*1*/ /*fake_override*/ fun compareTo(/*0*/ other: X): kotlin.Int + public final override /*1*/ /*fake_override*/ fun equals(/*0*/ other: kotlin.Any?): kotlin.Boolean + public final override /*1*/ /*fake_override*/ fun hashCode(): kotlin.Int + public final override /*1*/ /*fake_override*/ fun name(): kotlin.String + public final override /*1*/ /*fake_override*/ fun ordinal(): kotlin.Int + public open override /*1*/ /*fake_override*/ fun toString(): kotlin.String + + // Static members + public final /*synthesized*/ fun valueOf(/*0*/ value: kotlin.String): X + public final /*synthesized*/ fun values(): kotlin.Array +} diff --git a/compiler/testData/diagnostics/tests/when/ExhaustiveWithNullabilityCheckBefore.kt b/compiler/testData/diagnostics/tests/when/ExhaustiveWithNullabilityCheckBefore.kt new file mode 100644 index 00000000000..194c0dafe0a --- /dev/null +++ b/compiler/testData/diagnostics/tests/when/ExhaustiveWithNullabilityCheckBefore.kt @@ -0,0 +1,12 @@ +// KT-7857: when exhaustiveness does not take previous nullability checks into account +enum class X { A, B } +fun foo(arg: X?): Int { + if (arg == null) { + return 0 + } + return when (arg) { + X.A -> 1 + X.B -> 2 + // else or null branch should not be required here! + } +} \ No newline at end of file diff --git a/compiler/testData/diagnostics/tests/when/ExhaustiveWithNullabilityCheckBefore.txt b/compiler/testData/diagnostics/tests/when/ExhaustiveWithNullabilityCheckBefore.txt new file mode 100644 index 00000000000..27255e8808f --- /dev/null +++ b/compiler/testData/diagnostics/tests/when/ExhaustiveWithNullabilityCheckBefore.txt @@ -0,0 +1,37 @@ +package + +internal fun foo(/*0*/ arg: X?): kotlin.Int + +internal final enum class X : kotlin.Enum { + public enum entry A : X { + private constructor A() + public final override /*1*/ /*fake_override*/ fun compareTo(/*0*/ other: X): kotlin.Int + public final override /*1*/ /*fake_override*/ fun equals(/*0*/ other: kotlin.Any?): kotlin.Boolean + public final override /*1*/ /*fake_override*/ fun hashCode(): kotlin.Int + public final override /*1*/ /*fake_override*/ fun name(): kotlin.String + public final override /*1*/ /*fake_override*/ fun ordinal(): kotlin.Int + public open override /*1*/ /*fake_override*/ fun toString(): kotlin.String + } + + public enum entry B : X { + private constructor B() + public final override /*1*/ /*fake_override*/ fun compareTo(/*0*/ other: X): kotlin.Int + public final override /*1*/ /*fake_override*/ fun equals(/*0*/ other: kotlin.Any?): kotlin.Boolean + public final override /*1*/ /*fake_override*/ fun hashCode(): kotlin.Int + public final override /*1*/ /*fake_override*/ fun name(): kotlin.String + public final override /*1*/ /*fake_override*/ fun ordinal(): kotlin.Int + public open override /*1*/ /*fake_override*/ fun toString(): kotlin.String + } + + private constructor X() + public final override /*1*/ /*fake_override*/ fun compareTo(/*0*/ other: X): kotlin.Int + public final override /*1*/ /*fake_override*/ fun equals(/*0*/ other: kotlin.Any?): kotlin.Boolean + public final override /*1*/ /*fake_override*/ fun hashCode(): kotlin.Int + public final override /*1*/ /*fake_override*/ fun name(): kotlin.String + public final override /*1*/ /*fake_override*/ fun ordinal(): kotlin.Int + public open override /*1*/ /*fake_override*/ fun toString(): kotlin.String + + // Static members + public final /*synthesized*/ fun valueOf(/*0*/ value: kotlin.String): X + public final /*synthesized*/ fun values(): kotlin.Array +} diff --git a/compiler/testData/diagnostics/tests/when/ExhaustiveWithNullabilityCheckBoolean.kt b/compiler/testData/diagnostics/tests/when/ExhaustiveWithNullabilityCheckBoolean.kt new file mode 100644 index 00000000000..351bd904902 --- /dev/null +++ b/compiler/testData/diagnostics/tests/when/ExhaustiveWithNullabilityCheckBoolean.kt @@ -0,0 +1,13 @@ +// KT-7857: when exhaustiveness does not take previous nullability checks into account +fun foo(arg: Boolean?): Int { + if (arg != null) { + return when (arg) { + true -> 1 + false -> 0 + // else or null branch should not be required here! + } + } + else { + return -1 + } +} \ No newline at end of file diff --git a/compiler/testData/diagnostics/tests/when/ExhaustiveWithNullabilityCheckBoolean.txt b/compiler/testData/diagnostics/tests/when/ExhaustiveWithNullabilityCheckBoolean.txt new file mode 100644 index 00000000000..36ea855bbb4 --- /dev/null +++ b/compiler/testData/diagnostics/tests/when/ExhaustiveWithNullabilityCheckBoolean.txt @@ -0,0 +1,3 @@ +package + +internal fun foo(/*0*/ arg: kotlin.Boolean?): kotlin.Int diff --git a/compiler/testData/diagnostics/tests/when/ExhaustiveWithNullabilityCheckElse.kt b/compiler/testData/diagnostics/tests/when/ExhaustiveWithNullabilityCheckElse.kt new file mode 100644 index 00000000000..a301f843b80 --- /dev/null +++ b/compiler/testData/diagnostics/tests/when/ExhaustiveWithNullabilityCheckElse.kt @@ -0,0 +1,14 @@ +// KT-7857: when exhaustiveness does not take previous nullability checks into account +enum class X { A, B } +fun foo(arg: X?): Int { + if (arg == null) { + return 0 + } + else { + return when (arg) { + X.A -> 1 + X.B -> 2 + // else or null branch should not be required here! + } + } +} \ No newline at end of file diff --git a/compiler/testData/diagnostics/tests/when/ExhaustiveWithNullabilityCheckElse.txt b/compiler/testData/diagnostics/tests/when/ExhaustiveWithNullabilityCheckElse.txt new file mode 100644 index 00000000000..27255e8808f --- /dev/null +++ b/compiler/testData/diagnostics/tests/when/ExhaustiveWithNullabilityCheckElse.txt @@ -0,0 +1,37 @@ +package + +internal fun foo(/*0*/ arg: X?): kotlin.Int + +internal final enum class X : kotlin.Enum { + public enum entry A : X { + private constructor A() + public final override /*1*/ /*fake_override*/ fun compareTo(/*0*/ other: X): kotlin.Int + public final override /*1*/ /*fake_override*/ fun equals(/*0*/ other: kotlin.Any?): kotlin.Boolean + public final override /*1*/ /*fake_override*/ fun hashCode(): kotlin.Int + public final override /*1*/ /*fake_override*/ fun name(): kotlin.String + public final override /*1*/ /*fake_override*/ fun ordinal(): kotlin.Int + public open override /*1*/ /*fake_override*/ fun toString(): kotlin.String + } + + public enum entry B : X { + private constructor B() + public final override /*1*/ /*fake_override*/ fun compareTo(/*0*/ other: X): kotlin.Int + public final override /*1*/ /*fake_override*/ fun equals(/*0*/ other: kotlin.Any?): kotlin.Boolean + public final override /*1*/ /*fake_override*/ fun hashCode(): kotlin.Int + public final override /*1*/ /*fake_override*/ fun name(): kotlin.String + public final override /*1*/ /*fake_override*/ fun ordinal(): kotlin.Int + public open override /*1*/ /*fake_override*/ fun toString(): kotlin.String + } + + private constructor X() + public final override /*1*/ /*fake_override*/ fun compareTo(/*0*/ other: X): kotlin.Int + public final override /*1*/ /*fake_override*/ fun equals(/*0*/ other: kotlin.Any?): kotlin.Boolean + public final override /*1*/ /*fake_override*/ fun hashCode(): kotlin.Int + public final override /*1*/ /*fake_override*/ fun name(): kotlin.String + public final override /*1*/ /*fake_override*/ fun ordinal(): kotlin.Int + public open override /*1*/ /*fake_override*/ fun toString(): kotlin.String + + // Static members + public final /*synthesized*/ fun valueOf(/*0*/ value: kotlin.String): X + public final /*synthesized*/ fun values(): kotlin.Array +} diff --git a/compiler/testData/diagnostics/tests/when/NonExhaustiveWithNullabilityCheck.kt b/compiler/testData/diagnostics/tests/when/NonExhaustiveWithNullabilityCheck.kt new file mode 100644 index 00000000000..6af95a177ff --- /dev/null +++ b/compiler/testData/diagnostics/tests/when/NonExhaustiveWithNullabilityCheck.kt @@ -0,0 +1,12 @@ +// KT-7857: when exhaustiveness does not take previous nullability checks into account +enum class X { A, B } +fun foo(arg: X?): Int { + if (arg != null) { + return when (arg) { + X.B -> 2 + } + } + else { + return 0 + } +} \ No newline at end of file diff --git a/compiler/testData/diagnostics/tests/when/NonExhaustiveWithNullabilityCheck.txt b/compiler/testData/diagnostics/tests/when/NonExhaustiveWithNullabilityCheck.txt new file mode 100644 index 00000000000..27255e8808f --- /dev/null +++ b/compiler/testData/diagnostics/tests/when/NonExhaustiveWithNullabilityCheck.txt @@ -0,0 +1,37 @@ +package + +internal fun foo(/*0*/ arg: X?): kotlin.Int + +internal final enum class X : kotlin.Enum { + public enum entry A : X { + private constructor A() + public final override /*1*/ /*fake_override*/ fun compareTo(/*0*/ other: X): kotlin.Int + public final override /*1*/ /*fake_override*/ fun equals(/*0*/ other: kotlin.Any?): kotlin.Boolean + public final override /*1*/ /*fake_override*/ fun hashCode(): kotlin.Int + public final override /*1*/ /*fake_override*/ fun name(): kotlin.String + public final override /*1*/ /*fake_override*/ fun ordinal(): kotlin.Int + public open override /*1*/ /*fake_override*/ fun toString(): kotlin.String + } + + public enum entry B : X { + private constructor B() + public final override /*1*/ /*fake_override*/ fun compareTo(/*0*/ other: X): kotlin.Int + public final override /*1*/ /*fake_override*/ fun equals(/*0*/ other: kotlin.Any?): kotlin.Boolean + public final override /*1*/ /*fake_override*/ fun hashCode(): kotlin.Int + public final override /*1*/ /*fake_override*/ fun name(): kotlin.String + public final override /*1*/ /*fake_override*/ fun ordinal(): kotlin.Int + public open override /*1*/ /*fake_override*/ fun toString(): kotlin.String + } + + private constructor X() + public final override /*1*/ /*fake_override*/ fun compareTo(/*0*/ other: X): kotlin.Int + public final override /*1*/ /*fake_override*/ fun equals(/*0*/ other: kotlin.Any?): kotlin.Boolean + public final override /*1*/ /*fake_override*/ fun hashCode(): kotlin.Int + public final override /*1*/ /*fake_override*/ fun name(): kotlin.String + public final override /*1*/ /*fake_override*/ fun ordinal(): kotlin.Int + public open override /*1*/ /*fake_override*/ fun toString(): kotlin.String + + // Static members + public final /*synthesized*/ fun valueOf(/*0*/ value: kotlin.String): X + public final /*synthesized*/ fun values(): kotlin.Array +} diff --git a/compiler/tests/org/jetbrains/kotlin/checkers/JetDiagnosticsTestGenerated.java b/compiler/tests/org/jetbrains/kotlin/checkers/JetDiagnosticsTestGenerated.java index 7831087ef06..be00a022092 100644 --- a/compiler/tests/org/jetbrains/kotlin/checkers/JetDiagnosticsTestGenerated.java +++ b/compiler/tests/org/jetbrains/kotlin/checkers/JetDiagnosticsTestGenerated.java @@ -13668,6 +13668,30 @@ public class JetDiagnosticsTestGenerated extends AbstractJetDiagnosticsTest { doTest(fileName); } + @TestMetadata("ExhaustiveWithNullabilityCheck.kt") + public void testExhaustiveWithNullabilityCheck() throws Exception { + String fileName = JetTestUtils.navigationMetadata("compiler/testData/diagnostics/tests/when/ExhaustiveWithNullabilityCheck.kt"); + doTest(fileName); + } + + @TestMetadata("ExhaustiveWithNullabilityCheckBefore.kt") + public void testExhaustiveWithNullabilityCheckBefore() throws Exception { + String fileName = JetTestUtils.navigationMetadata("compiler/testData/diagnostics/tests/when/ExhaustiveWithNullabilityCheckBefore.kt"); + doTest(fileName); + } + + @TestMetadata("ExhaustiveWithNullabilityCheckBoolean.kt") + public void testExhaustiveWithNullabilityCheckBoolean() throws Exception { + String fileName = JetTestUtils.navigationMetadata("compiler/testData/diagnostics/tests/when/ExhaustiveWithNullabilityCheckBoolean.kt"); + doTest(fileName); + } + + @TestMetadata("ExhaustiveWithNullabilityCheckElse.kt") + public void testExhaustiveWithNullabilityCheckElse() throws Exception { + String fileName = JetTestUtils.navigationMetadata("compiler/testData/diagnostics/tests/when/ExhaustiveWithNullabilityCheckElse.kt"); + doTest(fileName); + } + @TestMetadata("kt4434.kt") public void testKt4434() throws Exception { String fileName = JetTestUtils.navigationMetadata("compiler/testData/diagnostics/tests/when/kt4434.kt"); @@ -13752,6 +13776,12 @@ public class JetDiagnosticsTestGenerated extends AbstractJetDiagnosticsTest { doTest(fileName); } + @TestMetadata("NonExhaustiveWithNullabilityCheck.kt") + public void testNonExhaustiveWithNullabilityCheck() throws Exception { + String fileName = JetTestUtils.navigationMetadata("compiler/testData/diagnostics/tests/when/NonExhaustiveWithNullabilityCheck.kt"); + doTest(fileName); + } + @TestMetadata("PropertyNotInitialized.kt") public void testPropertyNotInitialized() throws Exception { String fileName = JetTestUtils.navigationMetadata("compiler/testData/diagnostics/tests/when/PropertyNotInitialized.kt");