Skip to content
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’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CanBeNonNullable: fix false positives for parameterized types #4870

Merged
merged 1 commit into from May 30, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Expand Up @@ -14,6 +14,7 @@ import io.gitlab.arturbosch.detekt.rules.isNonNullCheck
import io.gitlab.arturbosch.detekt.rules.isNullCheck
import io.gitlab.arturbosch.detekt.rules.isOpen
import io.gitlab.arturbosch.detekt.rules.isOverride
import org.jetbrains.kotlin.com.intellij.codeInsight.NullableNotNullManager.isNullable
import org.jetbrains.kotlin.descriptors.CallableDescriptor
import org.jetbrains.kotlin.descriptors.DeclarationDescriptor
import org.jetbrains.kotlin.descriptors.PropertyDescriptor
Expand Down Expand Up @@ -52,6 +53,8 @@ import org.jetbrains.kotlin.resolve.calls.smartcasts.getKotlinTypeForComparison
import org.jetbrains.kotlin.resolve.calls.util.getResolvedCall
import org.jetbrains.kotlin.resolve.calls.util.getType
import org.jetbrains.kotlin.resolve.descriptorUtil.fqNameOrNull
import org.jetbrains.kotlin.types.KotlinType
import org.jetbrains.kotlin.types.TypeUtils
import org.jetbrains.kotlin.types.isNullable

/**
Expand Down Expand Up @@ -445,7 +448,8 @@ class CanBeNonNullable(config: Config = Config.empty) : Rule(config) {
}

override fun visitProperty(property: KtProperty) {
if (property.getKotlinTypeForComparison(bindingContext)?.isNullable() == true) {
val type = property.getKotlinTypeForComparison(bindingContext)
if (type?.isNullableAndCanBeNonNullable() == true) {
val fqName = property.fqName
if (property.isCandidate() && fqName != null) {
candidateProps[fqName] = property
Expand Down Expand Up @@ -474,6 +478,21 @@ class CanBeNonNullable(config: Config = Config.empty) : Rule(config) {
super.visitBinaryExpression(expression)
}

/**
* Determines whether a type is nullable and can be made non-nullable; for most properties
* this is simply whether they are nullable, but for type parameters they can only be made
* non-nullable when explicitly marked nullable.
*
* Note that [KotlinType.isNullable] for type parameter types is true unless it inherits
* from a non-nullable type, e.g.:
* - nullable: <T> or <T : Any?>
* - non-nullable: <T : Any>
* But even if T is nullable, a property `val t: T` cannot be made into a non-nullable type.
*/
private fun KotlinType.isNullableAndCanBeNonNullable(): Boolean {
return if (TypeUtils.isTypeParameter(this)) isMarkedNullable else isNullable()
}

private fun KtProperty.isCandidate(): Boolean {
if (isOpen() || isAbstract()) return false
val isSetToNonNullable = initializer?.isNullableType() != true &&
Expand Down Expand Up @@ -512,7 +531,11 @@ class CanBeNonNullable(config: Config = Config.empty) : Rule(config) {
)
}
else -> {
this?.getType(bindingContext)?.isNullable() == true
// only consider types which can be made non-nullable as nullable to warn on
// cases where a field has declared type `T?` but is only assigned as `T`; here
// `T` should not be considered nullable to enforce that the field could be
// declared as just `T`
this?.getType(bindingContext)?.isNullableAndCanBeNonNullable() == true
}
}
}
Expand Down
Expand Up @@ -370,6 +370,79 @@ class CanBeNonNullableSpec(val env: KotlinCoreEnvironment) {
"""
assertThat(subject.compileAndLintWithContext(env, code)).isEmpty()
}

@Test
fun `does not report on non-null properties of a parameterized type`() {
val code = """
class P<T>(foo: T) {
val a: T = foo
val b: T? = null
}
""".trimIndent()

assertThat(subject.compileAndLintWithContext(env, code)).isEmpty()
}

@Test
fun `reports on properties of an unnecessarily nullable parameterized type`() {
val code = """
class P<T>(foo: T) {
val bar: T? = foo
}
""".trimIndent()

assertThat(subject.compileAndLintWithContext(env, code)).hasSize(1)
}

@Test
fun `reports on properties of an unnecessarily nullable parameterized type extending Any`() {
val code = """
class P<T : Any>(foo: T) {
val bar: T? = foo
}
""".trimIndent()

assertThat(subject.compileAndLintWithContext(env, code)).hasSize(1)
}

@Test
fun `does not report on properties of a parameterized type which must be nullable`() {
val code = """
class P<T : Any>(private val foo: T) {
val a: T
get() = foo
val b: T? = foo.takeIf { Random.nextBoolean() }
val c: T?
get() = foo.takeIf { Random.nextBoolean() }
dzirbel marked this conversation as resolved.
Show resolved Hide resolved
}
""".trimIndent()

assertThat(subject.compileAndLintWithContext(env, code)).isEmpty()
}

@Test
fun `does not report on properties of a parameterized type which resolve to Nothing`() {
val code = """
class P<T> {
val foo: T
get() = error("")
dzirbel marked this conversation as resolved.
Show resolved Hide resolved
}
""".trimIndent()

assertThat(subject.compileAndLintWithContext(env, code)).isEmpty()
}

@Test
fun `reports on properties of an unnecessarily nullable parameterized type which resolve to Nothing`() {
val code = """
class P<T> {
val foo: T?
get() = error("")
}
""".trimIndent()

assertThat(subject.compileAndLintWithContext(env, code)).hasSize(1)
}
}

@Test
Expand Down