Skip to content

Commit

Permalink
Cast nullable to non nullable type (#5653)
Browse files Browse the repository at this point in the history
* New rule: report cast of nullable variable to non-null type

* Add test case for CastNullableToNonNullableType rule

* Refactor KtExpression utility to TypeUtils.kt

Move isNullable and descriptor common utility that was common across
NullableToStringCall.kt and CastNullableToNonNullableType.kt rules

* Add test for unnecessary safe check chain

* Make the parameters of isNullable non nullable

Handle null compilerResources at the call site

* Add apis to detekt-psi-utils.api

* Make KtExpression.descriptor private

* Add alternate compliant code
  • Loading branch information
atulgpt committed Jan 26, 2023
1 parent 3310798 commit 8822f04
Show file tree
Hide file tree
Showing 7 changed files with 285 additions and 26 deletions.
2 changes: 2 additions & 0 deletions detekt-core/src/main/resources/default-detekt-config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -406,6 +406,8 @@ potential-bugs:
active: true
forbiddenTypePatterns:
- 'kotlin.String'
CastNullableToNonNullableType:
active: false
CastToNullableType:
active: false
Deprecation:
Expand Down
1 change: 1 addition & 0 deletions detekt-psi-utils/api/detekt-psi-utils.api
Original file line number Diff line number Diff line change
Expand Up @@ -147,5 +147,6 @@ public final class io/gitlab/arturbosch/detekt/rules/TypeUtilsKt {
public static final fun fqNameOrNull (Lorg/jetbrains/kotlin/types/KotlinType;)Lorg/jetbrains/kotlin/name/FqName;
public static final fun getDataFlowAwareTypes (Lorg/jetbrains/kotlin/psi/KtExpression;Lorg/jetbrains/kotlin/resolve/BindingContext;Lorg/jetbrains/kotlin/config/LanguageVersionSettings;Lorg/jetbrains/kotlin/resolve/calls/smartcasts/DataFlowValueFactory;Lorg/jetbrains/kotlin/types/KotlinType;)Ljava/util/Set;
public static synthetic fun getDataFlowAwareTypes$default (Lorg/jetbrains/kotlin/psi/KtExpression;Lorg/jetbrains/kotlin/resolve/BindingContext;Lorg/jetbrains/kotlin/config/LanguageVersionSettings;Lorg/jetbrains/kotlin/resolve/calls/smartcasts/DataFlowValueFactory;Lorg/jetbrains/kotlin/types/KotlinType;ILjava/lang/Object;)Ljava/util/Set;
public static final fun isNullable (Lorg/jetbrains/kotlin/psi/KtExpression;Lorg/jetbrains/kotlin/resolve/BindingContext;Lorg/jetbrains/kotlin/config/LanguageVersionSettings;Lorg/jetbrains/kotlin/resolve/calls/smartcasts/DataFlowValueFactory;Z)Z
}

Original file line number Diff line number Diff line change
@@ -1,13 +1,20 @@
package io.gitlab.arturbosch.detekt.rules

import org.jetbrains.kotlin.com.intellij.psi.PsiElement
import org.jetbrains.kotlin.config.LanguageVersionSettings
import org.jetbrains.kotlin.descriptors.CallableDescriptor
import org.jetbrains.kotlin.diagnostics.Errors
import org.jetbrains.kotlin.name.FqName
import org.jetbrains.kotlin.psi.KtExpression
import org.jetbrains.kotlin.psi.KtSafeQualifiedExpression
import org.jetbrains.kotlin.resolve.BindingContext
import org.jetbrains.kotlin.resolve.calls.smartcasts.DataFlowValueFactory
import org.jetbrains.kotlin.resolve.calls.util.getResolvedCall
import org.jetbrains.kotlin.resolve.descriptorUtil.fqNameOrNull
import org.jetbrains.kotlin.types.KotlinType
import org.jetbrains.kotlin.types.TypeUtils
import org.jetbrains.kotlin.types.isFlexible
import org.jetbrains.kotlin.types.isNullable
import org.jetbrains.kotlin.util.containingNonLocalDeclaration

fun KotlinType.fqNameOrNull(): FqName? {
Expand Down Expand Up @@ -52,3 +59,44 @@ fun KtExpression.getDataFlowAwareTypes(
return dataFlowInfo.getStableTypes(dataFlowValue, languageVersionSettings)
.ifEmpty { setOf(originalType) }
}

/**
* Return if overall expression is nullable or not nullable
*
* ```kotlin
* var a: Int? = null
* val b = a // RHS expression will be nullable
* val c = a!! // RHS expression will be not nullable
* val d = (a ?: error("null assertion message")) // RHS expression will be not nullable
* val c = 1?.and(2) // RHS expression will be not nullable
* ```
* [shouldConsiderPlatformTypeAsNullable] determines the behaviour of platform type. Passing true considers
* the platform type as nullable otherwise it is not considered nullable in case of false
*/
@Suppress("ReturnCount")
fun KtExpression.isNullable(
bindingContext: BindingContext,
languageVersionSettings: LanguageVersionSettings,
dataFlowValueFactory: DataFlowValueFactory,
shouldConsiderPlatformTypeAsNullable: Boolean,
): Boolean {
val safeAccessOperation = safeAs<KtSafeQualifiedExpression>()?.operationTokenNode?.safeAs<PsiElement>()
if (safeAccessOperation != null) {
return bindingContext.diagnostics.forElement(safeAccessOperation).none {
it.factory == Errors.UNNECESSARY_SAFE_CALL
}
}
val originalType = descriptor(bindingContext)?.returnType?.takeIf {
it.isNullable() && (shouldConsiderPlatformTypeAsNullable || !it.isFlexible())
} ?: return false
val dataFlowTypes = getDataFlowAwareTypes(
bindingContext,
languageVersionSettings,
dataFlowValueFactory,
originalType
)
return dataFlowTypes.all { it.isNullable() }
}

private fun KtExpression.descriptor(bindingContext: BindingContext): CallableDescriptor? =
getResolvedCall(bindingContext)?.resultingDescriptor
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
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 io.gitlab.arturbosch.detekt.rules.isNullable
import org.jetbrains.kotlin.lexer.KtTokens
import org.jetbrains.kotlin.psi.KtBinaryExpressionWithTypeRHS
import org.jetbrains.kotlin.psi.KtNullableType
import org.jetbrains.kotlin.utils.addToStdlib.ifFalse
import org.jetbrains.kotlin.utils.addToStdlib.ifTrue

/**
* Reports cast of nullable variable to non-null type. Cast like this can hide `null`
* problems in your code. The compliant code would be that which will correctly check
* for two things (nullability and type) and not just one (cast).
*
* <noncompliant>
* fun foo(bar: Any?) {
* val x = bar as String
* }
* </noncompliant>
*
* <compliant>
* fun foo(bar: Any?) {
* val x = checkNotNull(bar) as String
* }
*
* // Alternative
* fun foo(bar: Any?) {
* val x = (bar ?: error("null assertion message")) as String
* }
* </compliant>
*/
@RequiresTypeResolution
class CastNullableToNonNullableType(config: Config = Config.empty) : Rule(config) {
override val issue: Issue = Issue(
javaClass.simpleName,
Severity.Defect,
"Nullable type to non-null type cast is found. Consider using two assertions, " +
"`null` assertions and type cast",
Debt.FIVE_MINS
)

@Suppress("ReturnCount")
override fun visitBinaryWithTypeRHSExpression(expression: KtBinaryExpressionWithTypeRHS) {
super.visitBinaryWithTypeRHSExpression(expression)

val operationReference = expression.operationReference
if (operationReference.getReferencedNameElementType() != KtTokens.AS_KEYWORD) return
if (expression.left.text == KtTokens.NULL_KEYWORD.value) return
val typeElement = expression.right?.typeElement ?: return
(typeElement is KtNullableType).ifTrue { return }
val compilerResourcesNonNull = compilerResources ?: return
expression.left.isNullable(
bindingContext,
compilerResourcesNonNull.languageVersionSettings,
compilerResourcesNonNull.dataFlowValueFactory,
shouldConsiderPlatformTypeAsNullable = true,
).ifFalse { return }

val message =
"Use separate `null` assertion and type cast like ('(${expression.left.text} ?: " +
"error(\"null assertion message\")) as ${typeElement.text}') instead of '${expression.text}'."
report(CodeSmell(issue, Entity.from(operationReference), message))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,23 +8,19 @@ 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 io.gitlab.arturbosch.detekt.rules.getDataFlowAwareTypes
import io.gitlab.arturbosch.detekt.rules.isNullable
import io.gitlab.arturbosch.detekt.rules.safeAs
import org.jetbrains.kotlin.com.intellij.psi.PsiElement
import org.jetbrains.kotlin.descriptors.CallableDescriptor
import org.jetbrains.kotlin.diagnostics.Errors
import org.jetbrains.kotlin.name.FqName
import org.jetbrains.kotlin.psi.KtCallExpression
import org.jetbrains.kotlin.psi.KtExpression
import org.jetbrains.kotlin.psi.KtQualifiedExpression
import org.jetbrains.kotlin.psi.KtSafeQualifiedExpression
import org.jetbrains.kotlin.psi.KtSimpleNameExpression
import org.jetbrains.kotlin.psi.KtStringTemplateEntry
import org.jetbrains.kotlin.psi.psiUtil.getStrictParentOfType
import org.jetbrains.kotlin.resolve.calls.util.getResolvedCall
import org.jetbrains.kotlin.resolve.descriptorUtil.fqNameOrNull
import org.jetbrains.kotlin.types.isFlexible
import org.jetbrains.kotlin.types.isNullable

/**
* Reports `toString()` calls with a nullable receiver that may return the string "null".
Expand Down Expand Up @@ -69,8 +65,18 @@ class NullableToStringCall(config: Config = Config.empty) : Rule(config) {
simpleOrCallExpression.descriptor()?.fqNameOrNull() == toString
) {
report(targetExpression)
} else if (targetExpression.parent is KtStringTemplateEntry && targetExpression.isNullable()) {
report(targetExpression.parent)
} else if (targetExpression.parent is KtStringTemplateEntry) {
compilerResources?.let { compilerResources ->
if (targetExpression.isNullable(
bindingContext,
compilerResources.languageVersionSettings,
compilerResources.dataFlowValueFactory,
shouldConsiderPlatformTypeAsNullable = false,
)
) {
report(targetExpression.parent)
}
}
}
}

Expand All @@ -85,25 +91,6 @@ class NullableToStringCall(config: Config = Config.empty) : Rule(config) {
return targetExpression
}

private fun KtExpression.isNullable(): Boolean {
val compilerResources = compilerResources ?: return false

val safeAccessOperation = safeAs<KtSafeQualifiedExpression>()?.operationTokenNode?.safeAs<PsiElement>()
if (safeAccessOperation != null) {
return bindingContext.diagnostics.forElement(safeAccessOperation).none {
it.factory == Errors.UNNECESSARY_SAFE_CALL
}
}
val originalType = descriptor()?.returnType?.takeIf { it.isNullable() && !it.isFlexible() } ?: return false
val dataFlowTypes = getDataFlowAwareTypes(
bindingContext,
compilerResources.languageVersionSettings,
compilerResources.dataFlowValueFactory,
originalType
)
return dataFlowTypes.all { it.isNullable() }
}

private fun report(element: PsiElement) {
val codeSmell = CodeSmell(
issue,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ class PotentialBugProvider : DefaultRuleSetProvider {
NullableToStringCall(config),
UnreachableCatchBlock(config),
CastToNullableType(config),
CastNullableToNonNullableType(config),
UnusedUnaryOperator(config)
)
)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
package io.gitlab.arturbosch.detekt.rules.bugs

import io.gitlab.arturbosch.detekt.rules.KotlinCoreEnvironmentTest
import io.gitlab.arturbosch.detekt.test.assertThat
import io.gitlab.arturbosch.detekt.test.compileAndLintWithContext
import org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreEnvironment
import org.junit.jupiter.api.Test

@KotlinCoreEnvironmentTest
class CastNullableToNonNullableTypeSpec(private val env: KotlinCoreEnvironment) {
private val subject = CastNullableToNonNullableType()

@Test
fun `reports casting Nullable type to NonNullable type`() {
val code = """
fun foo(bar: Any?) {
val x = bar as String
}
""".trimIndent()
val findings = subject.compileAndLintWithContext(env, code)
assertThat(findings).hasSize(1)
assertThat(findings).hasStartSourceLocation(2, 17)
assertThat(findings[0]).hasMessage(
"Use separate `null` assertion and type cast like " +
"('(bar ?: error(\"null assertion message\")) as String') instead of 'bar as String'."
)
}

@Test
fun `reports casting Nullable value returned from a function call to NonNullable type`() {
val code = """
fun foo(bar: Any?) {
bar() as Int
}
fun bar(): Int? {
return null
}
""".trimIndent()
val findings = subject.compileAndLintWithContext(env, code)
assertThat(findings).hasSize(1)
assertThat(findings).hasStartSourceLocation(2, 11)
assertThat(findings[0]).hasMessage(
"Use separate `null` assertion and type cast like " +
"('(bar() ?: error(\"null assertion message\")) as Int') instead of 'bar() as Int'."
)
}

@Test
fun `reports casting of platform type to NonNullable type`() {
val code = """
class Foo {
fun test() {
// getSimpleName() is not annotated with nullability information in the JDK, so compiler treats
// it as a platform type with unknown nullability.
val y = javaClass.simpleName as String
}
}
""".trimIndent()
val findings = subject.compileAndLintWithContext(env, code)
assertThat(findings).hasSize(1)
assertThat(findings).hasStartSourceLocation(5, 38)
assertThat(findings[0]).hasMessage(
"Use separate `null` assertion and type cast like " +
"('(javaClass.simpleName ?: error(\"null assertion message\")) as String') instead of " +
"'javaClass.simpleName as String'."
)
}

@Test
fun `does not report unnecessary safe check chain to NonNullable type`() {
val code = """
class Foo {
fun test() {
val z = 1?.and(2) as Int
}
}
""".trimIndent()
val findings = subject.compileAndLintWithContext(env, code)
assertThat(findings).isEmpty()
}

@Test
fun `does not report casting of Nullable type to NonNullable expression with assertion to NonNullable type`() {
val code = """
fun foo(bar: Any?) {
val x = (bar ?: error("null assertion message")) as String
}
""".trimIndent()
val findings = subject.compileAndLintWithContext(env, code)
assertThat(findings).isEmpty()
}

@Test
fun `does not report casting of Nullable type to NonNullable expression with !! assertion to NonNullable type`() {
val code = """
fun foo(bar: Any?) {
val x = bar!! as String
}
""".trimIndent()
val findings = subject.compileAndLintWithContext(env, code)
assertThat(findings).isEmpty()
}

@Test
fun `does not report casting of Nullable type to NonNullable smart casted variable to NonNullable type`() {
val code = """
fun foo(bar: Any?) {
val x = bar?.let { bar as String }
}
""".trimIndent()
val findings = subject.compileAndLintWithContext(env, code)
assertThat(findings).isEmpty()
}

@Test
fun `does not report casting of NonNullable type to NonNullable type`() {
val code = """
fun foo(bar: Any?) {
val x = bar as String?
}
""".trimIndent()
val findings = subject.compileAndLintWithContext(env, code)
assertThat(findings).isEmpty()
}

@Test
fun `does not report safe casting of Nullable type to NonNullable type`() {
val code = """
fun foo(bar: Any?) {
val x = bar as? String
}
""".trimIndent()
val findings = subject.compileAndLintWithContext(env, code)
assertThat(findings).isEmpty()
}

@Test
fun `does not report as compile error will happen when null to NonNullable type`() {
val code = """
fun foo(bar: Any?) {
val x = null as String
}
""".trimIndent()
val findings = subject.compileAndLintWithContext(env, code)
assertThat(findings).isEmpty()
}
}

0 comments on commit 8822f04

Please sign in to comment.