-
-
Notifications
You must be signed in to change notification settings - Fork 755
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We鈥檒l occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add ElseCaseInsteadOfExhaustiveWhen rule #4632
Merged
Merged
Changes from 6 commits
Commits
Show all changes
9 commits
Select commit
Hold shift + click to select a range
c14306c
Add ElseCaseInEnumOrSealedWhen rule
G00fY2 09a04ef
Adapt to new rule by replacing else cases
G00fY2 b93f631
Change rule name, support booleans, improve docs
G00fY2 5023869
Change variable name of boolean condition
G00fY2 0ab40c6
Rename rule to ElseCaseInsteadOfExhaustiveWhen
G00fY2 f9ee78d
Improve rule descriptions
G00fY2 14a7bbf
Suppress rule for outdated documentation method
G00fY2 b4a756e
Not report sealed class subjects that have an expect modifier
G00fY2 3e42301
Fix sample code syntax and not compile mulitplatform code
G00fY2 File filter
Filter by extension
Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
88 changes: 88 additions & 0 deletions
88
...src/main/kotlin/io/gitlab/arturbosch/detekt/rules/bugs/ElseCaseInsteadOfExhaustiveWhen.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,88 @@ | ||
package io.gitlab.arturbosch.detekt.rules.bugs | ||
|
||
import io.gitlab.arturbosch.detekt.api.CodeSmell | ||
import io.gitlab.arturbosch.detekt.api.Config | ||
import io.gitlab.arturbosch.detekt.api.Debt | ||
import io.gitlab.arturbosch.detekt.api.Entity | ||
import io.gitlab.arturbosch.detekt.api.Issue | ||
import io.gitlab.arturbosch.detekt.api.Rule | ||
import io.gitlab.arturbosch.detekt.api.Severity | ||
import io.gitlab.arturbosch.detekt.api.internal.RequiresTypeResolution | ||
import org.jetbrains.kotlin.cfg.WhenChecker | ||
import org.jetbrains.kotlin.psi.KtWhenExpression | ||
import org.jetbrains.kotlin.resolve.BindingContext | ||
import org.jetbrains.kotlin.resolve.calls.callUtil.getType | ||
import org.jetbrains.kotlin.types.typeUtil.isBooleanOrNullableBoolean | ||
|
||
/** | ||
* This rule reports `when` expressions that contain an `else` case even though they have an exhaustive set of cases. | ||
* | ||
* This occurs when the subject of the `when` expression is either an enum class, sealed class or of type boolean. | ||
* Using `else` cases for these expressions can lead to unintended behavior when adding new enum types, sealed subtypes | ||
* or changing the nullability of a boolean, since this will be implicitly handled by the `else` case. | ||
* | ||
* <noncompliant> | ||
* enum class Color { | ||
* RED, | ||
* GREEN, | ||
* BLUE | ||
* } | ||
* | ||
* when(c) { | ||
* Color.RED -> {} | ||
* Color.GREEN -> {} | ||
* else -> {} | ||
* } | ||
* </noncompliant> | ||
* | ||
* <compliant> | ||
* enum class Color { | ||
* RED, | ||
* GREEN, | ||
* BLUE | ||
* } | ||
* | ||
* when(c) { | ||
* Color.RED -> {} | ||
* Color.GREEN -> {} | ||
* Color.BLUE -> {} | ||
* } | ||
* </compliant> | ||
*/ | ||
@RequiresTypeResolution | ||
class ElseCaseInsteadOfExhaustiveWhen(config: Config = Config.empty) : Rule(config) { | ||
|
||
override val issue: Issue = Issue( | ||
"ElseCaseInsteadOfExhaustiveWhen", | ||
Severity.Warning, | ||
"A `when` expression that has an exhaustive set of cases should not contain an `else` case.", | ||
Debt.FIVE_MINS | ||
) | ||
|
||
override fun visitWhenExpression(whenExpression: KtWhenExpression) { | ||
super.visitWhenExpression(whenExpression) | ||
|
||
if (bindingContext == BindingContext.EMPTY) return | ||
checkForElseCaseInsteadOfExhaustiveWhenExpression(whenExpression) | ||
} | ||
|
||
private fun checkForElseCaseInsteadOfExhaustiveWhenExpression(whenExpression: KtWhenExpression) { | ||
val subjectExpression = whenExpression.subjectExpression ?: return | ||
if (whenExpression.elseExpression == null) return | ||
|
||
val subjectType = subjectExpression.getType(bindingContext) | ||
val isEnumSubject = WhenChecker.getClassDescriptorOfTypeIfEnum(subjectType) != null | ||
val isSealedSubject = WhenChecker.getClassDescriptorOfTypeIfSealed(subjectType) != null | ||
val isBooleanSubject = subjectType?.isBooleanOrNullableBoolean() == true | ||
|
||
if (isEnumSubject || isSealedSubject || isBooleanSubject) { | ||
val subjectTypeName = when { | ||
isEnumSubject -> "enum class" | ||
isSealedSubject -> "sealed class" | ||
else -> "boolean" | ||
} | ||
val message = "When expression with $subjectTypeName subject should not contain an `else` case." | ||
report(CodeSmell(issue, Entity.from(whenExpression), message)) | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
267 changes: 267 additions & 0 deletions
267
...test/kotlin/io/gitlab/arturbosch/detekt/rules/bugs/ElseCaseInsteadOfExhaustiveWhenSpec.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,267 @@ | ||
package io.gitlab.arturbosch.detekt.rules.bugs | ||
|
||
import io.gitlab.arturbosch.detekt.rules.KotlinCoreEnvironmentTest | ||
import io.gitlab.arturbosch.detekt.test.compileAndLintWithContext | ||
import org.assertj.core.api.Assertions.assertThat | ||
import org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreEnvironment | ||
import org.junit.jupiter.api.Nested | ||
import org.junit.jupiter.api.Test | ||
|
||
@KotlinCoreEnvironmentTest | ||
class ElseCaseInsteadOfExhaustiveWhenSpec(private val env: KotlinCoreEnvironment) { | ||
private val subject = ElseCaseInsteadOfExhaustiveWhen() | ||
|
||
@Nested | ||
inner class `ElseCaseInsteadOfExhaustiveWhen rule` { | ||
@Nested | ||
inner class Enum { | ||
@Test | ||
fun `reports when enum _when_ expression used as statement contains _else_ case`() { | ||
val code = """ | ||
enum class Color { | ||
RED, | ||
GREEN, | ||
BLUE | ||
} | ||
|
||
fun whenOnEnumFail(c: Color) { | ||
when (c) { | ||
Color.BLUE -> {} | ||
Color.GREEN -> {} | ||
else -> {} | ||
} | ||
} | ||
""" | ||
val actual = subject.compileAndLintWithContext(env, code) | ||
assertThat(actual).hasSize(1) | ||
} | ||
|
||
@Test | ||
fun `reports when enum _when_ expression contains _else_ case`() { | ||
val code = """ | ||
enum class Color { | ||
RED, | ||
GREEN, | ||
BLUE | ||
} | ||
|
||
fun whenOnEnumFail(c: Color) { | ||
val x = when (c) { | ||
Color.BLUE -> 1 | ||
Color.GREEN -> 2 | ||
else -> 100 | ||
} | ||
} | ||
""" | ||
val actual = subject.compileAndLintWithContext(env, code) | ||
assertThat(actual).hasSize(1) | ||
} | ||
|
||
@Test | ||
fun `does not report when enum _when_ expression does not contain _else_ case`() { | ||
val code = """ | ||
enum class Color { | ||
RED, | ||
GREEN, | ||
BLUE | ||
} | ||
|
||
fun whenOnEnumPassA(c: Color) { | ||
when (c) { | ||
Color.BLUE -> {} | ||
Color.GREEN -> {} | ||
Color.RED -> {} | ||
} | ||
|
||
val x = when (c) { | ||
Color.BLUE -> 1 | ||
Color.GREEN -> 2 | ||
Color.RED -> 3 | ||
} | ||
} | ||
|
||
fun whenOnEnumPassB(c: Color) { | ||
when (c) { | ||
Color.BLUE -> {} | ||
Color.GREEN -> {} | ||
} | ||
} | ||
""" | ||
assertThat(subject.compileAndLintWithContext(env, code)).isEmpty() | ||
} | ||
} | ||
|
||
@Nested | ||
inner class `Sealed class` { | ||
@Test | ||
fun `reports when sealed _when_ expression used as statement contains _else_ case`() { | ||
val code = """ | ||
sealed class Variant { | ||
object VariantA : Variant() | ||
class VariantB : Variant() | ||
object VariantC : Variant() | ||
} | ||
|
||
fun whenOnSealedFail(v: Variant) { | ||
when (v) { | ||
is Variant.VariantA -> {} | ||
is Variant.VariantB -> {} | ||
else -> {} | ||
} | ||
} | ||
""" | ||
assertThat(subject.compileAndLintWithContext(env, code)).hasSize(1) | ||
} | ||
|
||
@Test | ||
fun `reports when sealed _when_ expression contains _else_ case`() { | ||
val code = """ | ||
sealed class Variant { | ||
object VariantA : Variant() | ||
class VariantB : Variant() | ||
object VariantC : Variant() | ||
} | ||
|
||
fun whenOnSealedFail(v: Variant) { | ||
val x = when (v) { | ||
is Variant.VariantA -> "a" | ||
is Variant.VariantB -> "b" | ||
else -> "other" | ||
} | ||
} | ||
""" | ||
assertThat(subject.compileAndLintWithContext(env, code)).hasSize(1) | ||
} | ||
|
||
@Test | ||
fun `does not report when sealed _when_ expression does not contain _else_ case`() { | ||
val code = """ | ||
sealed class Variant { | ||
object VariantA : Variant() | ||
class VariantB : Variant() | ||
object VariantC : Variant() | ||
} | ||
|
||
fun whenOnSealedPass(v: Variant) { | ||
when (v) { | ||
is Variant.VariantA -> {} | ||
is Variant.VariantB -> {} | ||
} | ||
} | ||
""" | ||
assertThat(subject.compileAndLintWithContext(env, code)).isEmpty() | ||
} | ||
} | ||
|
||
@Nested | ||
inner class Boolean { | ||
@Test | ||
fun `reports when boolean _when_ expression used as statement contains _else_ case`() { | ||
val code = """ | ||
fun whenOnBooleanFail(b: Boolean) { | ||
when (b) { | ||
true -> {} | ||
else -> {} | ||
} | ||
} | ||
""" | ||
val actual = subject.compileAndLintWithContext(env, code) | ||
assertThat(actual).hasSize(1) | ||
} | ||
|
||
@Test | ||
fun `reports when nullable boolean _when_ expression contains _else_ case`() { | ||
val code = """ | ||
fun whenOnNullableBooleanFail(b: Boolean?) { | ||
val x = when (b) { | ||
true -> 1 | ||
false -> 2 | ||
else -> 100 | ||
} | ||
} | ||
""" | ||
val actual = subject.compileAndLintWithContext(env, code) | ||
assertThat(actual).hasSize(1) | ||
} | ||
|
||
@Test | ||
fun `does not report when boolean _when_ expression does not contain _else_ case`() { | ||
val code = """ | ||
fun whenOnBooleanPassA(b: Boolean) { | ||
when (b) { | ||
true -> {} | ||
false -> {} | ||
} | ||
|
||
val x = when (b) { | ||
true -> 1 | ||
false -> 2 | ||
} | ||
} | ||
""" | ||
assertThat(subject.compileAndLintWithContext(env, code)).isEmpty() | ||
} | ||
|
||
@Test | ||
fun `does not report when nullable boolean _when_ expression does not contain _else_ case`() { | ||
val code = """ | ||
fun whenOnNullableBooleanPassA(b: Boolean?) { | ||
when (b) { | ||
true -> {} | ||
false -> {} | ||
null -> {} | ||
} | ||
|
||
val x = when (b) { | ||
true -> 1 | ||
false -> 2 | ||
null -> 100 | ||
} | ||
} | ||
""" | ||
assertThat(subject.compileAndLintWithContext(env, code)).isEmpty() | ||
} | ||
} | ||
|
||
@Nested | ||
inner class `Standard when` { | ||
@Test | ||
fun `does not report when _else_ case is used for non enum or sealed _when_ expression`() { | ||
val code = """ | ||
fun whenChecks() { | ||
val x = 3 | ||
val s = "3" | ||
|
||
when (x) { | ||
0, 1 -> print("x == 0 or x == 1") | ||
else -> print("otherwise") | ||
} | ||
|
||
when (x) { | ||
Integer.parseInt(s) -> print("s encodes x") | ||
else -> print("s does not encode x") | ||
} | ||
|
||
when (x) { | ||
in 1..10 -> print("x is in the range") | ||
!in 10..20 -> print("x is outside the range") | ||
else -> print("none of the above") | ||
} | ||
|
||
val y = when(s) { | ||
is String -> s.startsWith("prefix") | ||
else -> false | ||
} | ||
|
||
when { | ||
x.equals(s) -> print("x equals s") | ||
x.plus(3) == 4 -> print("x is 1") | ||
else -> print("x is funny") | ||
} | ||
} | ||
""" | ||
assertThat(subject.compileAndLintWithContext(env, code)).isEmpty() | ||
} | ||
} | ||
} | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think that this case is a good place to add a
@Suppress("ElseCaseInsteadOfExhaustiveWhen")
.This rule is good in general (for example it have a lot of sense the change in
AnalysisResult
) but I think that in this case theelse
have sense.