Better diagnostics for non-exhaustive whens, relevant test #KT-10295 Fixed

This commit is contained in:
Mikhail Glukhikh
2015-12-14 19:46:03 +03:00
parent 61c2a88a32
commit c085eb650f
8 changed files with 95 additions and 18 deletions
@@ -105,7 +105,7 @@ class JavaNullabilityWarningsChecker : AdditionalTypeChecker {
if (type.isFlexible() && TypeUtils.isNullableType(type.flexibility().upperBound) && !type.annotations.isMarkedNotNull()) { if (type.isFlexible() && TypeUtils.isNullableType(type.flexibility().upperBound) && !type.annotations.isMarkedNotNull()) {
val enumClassDescriptor = WhenChecker.getClassDescriptorOfTypeIfEnum(type) ?: return val enumClassDescriptor = WhenChecker.getClassDescriptorOfTypeIfEnum(type) ?: return
if (WhenChecker.isWhenOnEnumExhaustive(expression, c.trace, enumClassDescriptor) if (WhenChecker.getEnumMissingCases(expression, c.trace, enumClassDescriptor).isEmpty()
&& !WhenChecker.containsNullCase(expression, c.trace)) { && !WhenChecker.containsNullCase(expression, c.trace)) {
c.trace.report(ErrorsJvm.WHEN_ENUM_CAN_BE_NULL_IN_JAVA.on(expression.getSubjectExpression())) c.trace.report(ErrorsJvm.WHEN_ENUM_CAN_BE_NULL_IN_JAVA.on(expression.getSubjectExpression()))
@@ -743,15 +743,20 @@ public class ControlFlowInformationProvider {
KtWhenExpression whenExpression = (KtWhenExpression) element; KtWhenExpression whenExpression = (KtWhenExpression) element;
if (whenExpression.getElseExpression() != null) continue; if (whenExpression.getElseExpression() != null) continue;
if (WhenChecker.mustHaveElse(whenExpression, trace)) { List<WhenMissingCase> necessaryCases = WhenChecker.getNecessaryCases(whenExpression, trace);
trace.report(NO_ELSE_IN_WHEN.on(whenExpression)); if (!necessaryCases.isEmpty()) {
trace.report(NO_ELSE_IN_WHEN.on(whenExpression, necessaryCases));
} }
else if (whenExpression.getSubjectExpression() != null) { else if (whenExpression.getSubjectExpression() != null) {
ClassDescriptor enumClassDescriptor = WhenChecker.getClassDescriptorOfTypeIfEnum( ClassDescriptor enumClassDescriptor = WhenChecker.getClassDescriptorOfTypeIfEnum(
trace.getType(whenExpression.getSubjectExpression())); trace.getType(whenExpression.getSubjectExpression()));
if (enumClassDescriptor != null if (enumClassDescriptor != null) {
&& !WhenChecker.isWhenOnEnumExhaustive(whenExpression, trace, enumClassDescriptor)) { List<WhenMissingCase> missingCases = WhenChecker.getEnumMissingCases(
trace.report(NON_EXHAUSTIVE_WHEN.on(whenExpression)); whenExpression, trace, enumClassDescriptor
);
if (!missingCases.isEmpty()) {
trace.report(NON_EXHAUSTIVE_WHEN.on(whenExpression, missingCases));
}
} }
} }
} }
@@ -34,18 +34,21 @@ import org.jetbrains.kotlin.types.*
import org.jetbrains.kotlin.types.KotlinType import org.jetbrains.kotlin.types.KotlinType
import org.jetbrains.kotlin.types.TypeUtils import org.jetbrains.kotlin.types.TypeUtils
import java.util.HashSet
import org.jetbrains.kotlin.resolve.DescriptorUtils.isEnumClass import org.jetbrains.kotlin.resolve.DescriptorUtils.isEnumClass
import org.jetbrains.kotlin.resolve.DescriptorUtils.isEnumEntry import org.jetbrains.kotlin.resolve.DescriptorUtils.isEnumEntry
import org.jetbrains.kotlin.resolve.bindingContextUtil.isUsedAsExpression import org.jetbrains.kotlin.resolve.bindingContextUtil.isUsedAsExpression
import java.util.*
interface WhenMissingCase interface WhenMissingCase
// Always must be first in the list
private object UnknownMissingCase : WhenMissingCase { private object UnknownMissingCase : WhenMissingCase {
override fun toString() = "unknown" override fun toString() = "unknown"
} }
val List<WhenMissingCase>.hasUnknown: Boolean
get() = firstOrNull() == UnknownMissingCase
private interface WhenExhaustivenessChecker { private interface WhenExhaustivenessChecker {
fun getMissingCases( fun getMissingCases(
expression: KtWhenExpression, expression: KtWhenExpression,
@@ -135,7 +138,7 @@ private abstract class WhenOnClassExhaustivenessChecker : WhenExhaustivenessChec
): List<WhenMissingCase> { ): List<WhenMissingCase> {
// when on empty enum / sealed is considered non-exhaustive, see test whenOnEmptySealed // when on empty enum / sealed is considered non-exhaustive, see test whenOnEmptySealed
if (memberDescriptors.isEmpty()) return listOf(UnknownMissingCase) if (memberDescriptors.isEmpty()) return listOf(UnknownMissingCase)
val checkedDescriptors = HashSet<ClassDescriptor>() val checkedDescriptors = LinkedHashSet<ClassDescriptor>()
for (whenEntry in whenExpression.entries) { for (whenEntry in whenExpression.entries) {
for (condition in whenEntry.conditions) { for (condition in whenEntry.conditions) {
var negated = false var negated = false
@@ -214,7 +217,7 @@ private object WhenOnSealedExhaustivenessChecker : WhenOnClassExhaustivenessChec
assert(subjectDescriptor!!.modality === Modality.SEALED) { assert(subjectDescriptor!!.modality === Modality.SEALED) {
"isWhenOnSealedClassExhaustive should be called with a sealed class descriptor" "isWhenOnSealedClassExhaustive should be called with a sealed class descriptor"
} }
val memberClassDescriptors = HashSet<ClassDescriptor>() val memberClassDescriptors = LinkedHashSet<ClassDescriptor>()
collectNestedSubclasses(subjectDescriptor!!, subjectDescriptor, memberClassDescriptors) collectNestedSubclasses(subjectDescriptor!!, subjectDescriptor, memberClassDescriptors)
// When on a sealed class without derived members is considered non-exhaustive (see test WhenOnEmptySealed) // When on a sealed class without derived members is considered non-exhaustive (see test WhenOnEmptySealed)
return getMissingClassCases(expression, memberClassDescriptors, trace) return getMissingClassCases(expression, memberClassDescriptors, trace)
@@ -248,8 +251,9 @@ object WhenChecker {
WhenOnNullableExhaustivenessChecker) WhenOnNullableExhaustivenessChecker)
@JvmStatic @JvmStatic
fun mustHaveElse(expression: KtWhenExpression, trace: BindingTrace) = fun getNecessaryCases(expression: KtWhenExpression, trace: BindingTrace) =
expression.isUsedAsExpression(trace.bindingContext) && !isWhenExhaustive(expression, trace) if (expression.isUsedAsExpression(trace.bindingContext)) getMissingCases(expression, trace)
else listOf()
@JvmStatic @JvmStatic
fun isWhenByEnum(expression: KtWhenExpression, context: BindingContext) = fun isWhenByEnum(expression: KtWhenExpression, context: BindingContext) =
@@ -268,11 +272,11 @@ object WhenChecker {
expression.subjectExpression?.let { context.getType(it) } ?: null expression.subjectExpression?.let { context.getType(it) } ?: null
@JvmStatic @JvmStatic
fun isWhenOnEnumExhaustive( fun getEnumMissingCases(
expression: KtWhenExpression, expression: KtWhenExpression,
trace: BindingTrace, trace: BindingTrace,
enumClassDescriptor: ClassDescriptor enumClassDescriptor: ClassDescriptor
) = WhenOnEnumExhaustivenessChecker.getMissingCases(expression, trace, enumClassDescriptor, false).isEmpty() ) = WhenOnEnumExhaustivenessChecker.getMissingCases(expression, trace, 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. * It's assumed that function is called for a final type. In this case the only possible smart cast is to not nullable type.
@@ -21,6 +21,7 @@ import com.intellij.openapi.util.TextRange;
import com.intellij.psi.PsiElement; import com.intellij.psi.PsiElement;
import com.intellij.psi.impl.source.tree.LeafPsiElement; import com.intellij.psi.impl.source.tree.LeafPsiElement;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.jetbrains.kotlin.cfg.WhenMissingCase;
import org.jetbrains.kotlin.descriptors.*; import org.jetbrains.kotlin.descriptors.*;
import org.jetbrains.kotlin.lexer.KtKeywordToken; import org.jetbrains.kotlin.lexer.KtKeywordToken;
import org.jetbrains.kotlin.lexer.KtModifierKeywordToken; import org.jetbrains.kotlin.lexer.KtModifierKeywordToken;
@@ -705,8 +706,8 @@ public interface Errors {
DiagnosticFactory0<KtWhenCondition> EXPECTED_CONDITION = DiagnosticFactory0.create(ERROR); DiagnosticFactory0<KtWhenCondition> EXPECTED_CONDITION = DiagnosticFactory0.create(ERROR);
DiagnosticFactory0<KtWhenEntry> ELSE_MISPLACED_IN_WHEN = DiagnosticFactory0.create(ERROR, ELSE_ENTRY); DiagnosticFactory0<KtWhenEntry> ELSE_MISPLACED_IN_WHEN = DiagnosticFactory0.create(ERROR, ELSE_ENTRY);
DiagnosticFactory0<KtWhenExpression> NO_ELSE_IN_WHEN = DiagnosticFactory0.create(ERROR, WHEN_EXPRESSION); DiagnosticFactory1<KtWhenExpression, List<WhenMissingCase>> NO_ELSE_IN_WHEN = DiagnosticFactory1.create(ERROR, WHEN_EXPRESSION);
DiagnosticFactory0<KtWhenExpression> NON_EXHAUSTIVE_WHEN = DiagnosticFactory0.create(WARNING, WHEN_EXPRESSION); DiagnosticFactory1<KtWhenExpression, List<WhenMissingCase>> NON_EXHAUSTIVE_WHEN = DiagnosticFactory1.create(WARNING, WHEN_EXPRESSION);
DiagnosticFactory0<PsiElement> COMMA_IN_WHEN_CONDITION_WITHOUT_ARGUMENT = DiagnosticFactory0.create(ERROR); DiagnosticFactory0<PsiElement> COMMA_IN_WHEN_CONDITION_WITHOUT_ARGUMENT = DiagnosticFactory0.create(ERROR);
// Type mismatch // Type mismatch
@@ -419,8 +419,8 @@ public class DefaultErrorMessages {
MAP.put(ELSE_MISPLACED_IN_WHEN, "'else' entry must be the last one in a when-expression"); MAP.put(ELSE_MISPLACED_IN_WHEN, "'else' entry must be the last one in a when-expression");
MAP.put(COMMA_IN_WHEN_CONDITION_WITHOUT_ARGUMENT, "Deprecated syntax. Use '||' instead of commas in when-condition for 'when' without argument"); MAP.put(COMMA_IN_WHEN_CONDITION_WITHOUT_ARGUMENT, "Deprecated syntax. Use '||' instead of commas in when-condition for 'when' without argument");
MAP.put(NO_ELSE_IN_WHEN, "'when' expression must be exhaustive or contain an 'else' branch"); MAP.put(NO_ELSE_IN_WHEN, "'when' expression must be exhaustive, add necessary {0}", RENDER_WHEN_MISSING_CASES);
MAP.put(NON_EXHAUSTIVE_WHEN, "'when' expression contains only some variants and no 'else' branch"); MAP.put(NON_EXHAUSTIVE_WHEN, "'when' expression on enum is recommended to be exhaustive, add {0}", RENDER_WHEN_MISSING_CASES);
MAP.put(TYPE_MISMATCH_IN_RANGE, "Type mismatch: incompatible types of range and element checked in it"); MAP.put(TYPE_MISMATCH_IN_RANGE, "Type mismatch: incompatible types of range and element checked in it");
MAP.put(CYCLIC_INHERITANCE_HIERARCHY, "There's a cycle in the inheritance hierarchy for this type"); MAP.put(CYCLIC_INHERITANCE_HIERARCHY, "There's a cycle in the inheritance hierarchy for this type");
@@ -16,12 +16,15 @@
package org.jetbrains.kotlin.diagnostics.rendering package org.jetbrains.kotlin.diagnostics.rendering
import com.google.common.base.Strings
import com.google.common.collect.Lists import com.google.common.collect.Lists
import com.google.common.collect.Sets import com.google.common.collect.Sets
import com.intellij.openapi.diagnostic.Logger import com.intellij.openapi.diagnostic.Logger
import com.intellij.openapi.util.text.StringUtil import com.intellij.openapi.util.text.StringUtil
import com.intellij.psi.PsiElement import com.intellij.psi.PsiElement
import org.jetbrains.kotlin.builtins.KotlinBuiltIns import org.jetbrains.kotlin.builtins.KotlinBuiltIns
import org.jetbrains.kotlin.cfg.WhenMissingCase
import org.jetbrains.kotlin.cfg.hasUnknown
import org.jetbrains.kotlin.descriptors.* import org.jetbrains.kotlin.descriptors.*
import org.jetbrains.kotlin.diagnostics.rendering.TabledDescriptorRenderer.newTable import org.jetbrains.kotlin.diagnostics.rendering.TabledDescriptorRenderer.newTable
import org.jetbrains.kotlin.diagnostics.rendering.TabledDescriptorRenderer.newText import org.jetbrains.kotlin.diagnostics.rendering.TabledDescriptorRenderer.newText
@@ -407,4 +410,15 @@ object Renderers {
} }
append("(").append(renderTypes(inferenceErrorData.valueArgumentsTypes)).append(")") append("(").append(renderTypes(inferenceErrorData.valueArgumentsTypes)).append(")")
} }
@JvmField val RENDER_WHEN_MISSING_CASES: Renderer<List<WhenMissingCase>> = Renderer {
if (!it.hasUnknown) {
val list = it.map { "'$it'" }.joinToString(", ")
val branches = if (it.size > 1) "branches" else "branch"
"$list $branches or 'else' branch instead"
}
else {
"'else' branch"
}
}
} }
+47
View File
@@ -0,0 +1,47 @@
fun nonExhaustiveInt(x: Int) = <error descr="[NO_ELSE_IN_WHEN] when expression must be exhaustive, add necessary 'else' branch">when</error>(x) {
0 -> false
}
fun nonExhaustiveBoolean(b: Boolean) = <error descr="[NO_ELSE_IN_WHEN] when expression must be exhaustive, add necessary 'true' branch or 'else' branch instead">when</error>(b) {
false -> 0
}
enum class Color {
RED,
GREEN,
BLUE
}
fun nonExhaustiveEnum(c: Color) = <error descr="[NO_ELSE_IN_WHEN] when expression must be exhaustive, add necessary 'RED', 'BLUE' branches or 'else' branch instead">when</error>(c) {
Color.GREEN -> 0xff00
}
fun nonExhaustiveNullable(c: Color?) = <error descr="[NO_ELSE_IN_WHEN] when expression must be exhaustive, add necessary 'GREEN', 'null' branches or 'else' branch instead">when</error>(c) {
Color.RED -> 0xff
Color.BLUE -> 0xff0000
}
fun whenOnEnum(c: Color) {
<warning descr="[NON_EXHAUSTIVE_WHEN] when expression on enum is recommended to be exhaustive, add 'RED' branch or 'else' branch instead">when</warning>(c) {
Color.BLUE -> {}
Color.GREEN -> {}
}
}
sealed class Variant {
object Singleton : Variant()
class Something : Variant()
object Another : Variant()
}
fun nonExhaustiveSealed(v: Variant) = <error descr="[NO_ELSE_IN_WHEN] when expression must be exhaustive, add necessary 'is Something', 'Another' branches or 'else' branch instead">when</error>(v) {
Variant.Singleton -> false
}
sealed class Empty
fun nonExhaustiveEmpty(e: Empty) = <error descr="[NO_ELSE_IN_WHEN] when expression must be exhaustive, add necessary 'else' branch">when</error>(<warning>e</warning>) {}
fun nonExhaustiveNullableEmpty(e: Empty?) = <error descr="[NO_ELSE_IN_WHEN] when expression must be exhaustive, add necessary 'else' branch">when</error>(<warning>e</warning>) {}
@@ -366,6 +366,12 @@ public class PsiCheckerTestGenerated extends AbstractPsiCheckerTest {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/checker/WhenInEnumInExtensionProperty.kt"); String fileName = KotlinTestUtils.navigationMetadata("idea/testData/checker/WhenInEnumInExtensionProperty.kt");
doTest(fileName); doTest(fileName);
} }
@TestMetadata("WhenNonExhaustive.kt")
public void testWhenNonExhaustive() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/checker/WhenNonExhaustive.kt");
doTest(fileName);
}
} }
@TestMetadata("idea/testData/checker/regression") @TestMetadata("idea/testData/checker/regression")