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

Extend CanBeNonNullable rule to check function params #4431

Merged
merged 24 commits into from Jan 12, 2022
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
47e858e
Expanded CanBeNonNullable rule to check for function parameters that …
severn-everett Dec 30, 2021
7abf6ae
Added code comment
severn-everett Dec 30, 2021
9197f1b
Addressing Detekt issue
severn-everett Dec 30, 2021
7a50a9a
Reworked CanBeNonNullable check for params based on PR discussion
severn-everett Jan 1, 2022
0992e7d
Merge branch 'master' into function_parameters_non_nullable
severn-everett Jan 1, 2022
b642aa9
Address Detekt issues
severn-everett Jan 1, 2022
e5ecb4a
Added check to ignore override function; Added tests to improve code …
severn-everett Jan 1, 2022
b0a3cda
Enabled nullable params to be passed through to non-local variables a…
severn-everett Jan 1, 2022
82f5f85
Addressing Detekt issue
severn-everett Jan 1, 2022
2d48819
Fixed Detekt issue
severn-everett Jan 1, 2022
6a2bdc8
Reworked CanBeNonNullable so that it only calls on a specific heurist…
severn-everett Jan 3, 2022
408e7ec
Merge branch 'master' into function_parameters_non_nullable
severn-everett Jan 3, 2022
a260193
Fixed merge conflict
severn-everett Jan 3, 2022
8a381b5
CanBeNonNullable will only flag a param that has been checked for non…
severn-everett Jan 3, 2022
d8aef95
Added check for when a single-expression function finishes on a null-…
severn-everett Jan 3, 2022
e95d7ab
Addressing Detekt issues in SplitPattern.kt
severn-everett Jan 3, 2022
73790f1
Single expression check now recursively evaluates lambdas
severn-everett Jan 3, 2022
b3445d9
Address Detekt issues
severn-everett Jan 3, 2022
9d22662
CanBeNonNullable will now ignore params that are involved in an elvis…
severn-everett Jan 3, 2022
c69f3c4
CanBeNonNullable will now ignore single expression calls where the re…
severn-everett Jan 3, 2022
f9e938e
Simplified checks for single-expression functions; Consolidated param…
severn-everett Jan 4, 2022
08bc3fe
Address Detekt issues
severn-everett Jan 4, 2022
9f07ecc
Clean up CanBeNonNullableSpec tests
severn-everett Jan 4, 2022
9875284
Addressing PR comments
severn-everett Jan 4, 2022
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 @@ -9,20 +9,30 @@ 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.isNonNullCheck
import io.gitlab.arturbosch.detekt.rules.isNullCheck
import io.gitlab.arturbosch.detekt.rules.isOpen
import org.jetbrains.kotlin.descriptors.DeclarationDescriptor
import org.jetbrains.kotlin.descriptors.PropertyDescriptor
import org.jetbrains.kotlin.lexer.KtTokens
import org.jetbrains.kotlin.name.FqName
import org.jetbrains.kotlin.psi.KtBinaryExpression
import org.jetbrains.kotlin.psi.KtClass
import org.jetbrains.kotlin.psi.KtConstantExpression
import org.jetbrains.kotlin.psi.KtDotQualifiedExpression
import org.jetbrains.kotlin.psi.KtExpression
import org.jetbrains.kotlin.psi.KtFile
import org.jetbrains.kotlin.psi.KtIfExpression
import org.jetbrains.kotlin.psi.KtNameReferenceExpression
import org.jetbrains.kotlin.psi.KtNamedFunction
import org.jetbrains.kotlin.psi.KtNullableType
import org.jetbrains.kotlin.psi.KtParameter
import org.jetbrains.kotlin.psi.KtPostfixExpression
import org.jetbrains.kotlin.psi.KtProperty
import org.jetbrains.kotlin.psi.KtPropertyAccessor
import org.jetbrains.kotlin.psi.KtPropertyDelegate
import org.jetbrains.kotlin.psi.KtReturnExpression
import org.jetbrains.kotlin.psi.KtSafeQualifiedExpression
import org.jetbrains.kotlin.psi.psiUtil.collectDescendantsOfType
import org.jetbrains.kotlin.psi.psiUtil.isPrivate
import org.jetbrains.kotlin.resolve.BindingContext
Expand All @@ -49,6 +59,10 @@ import org.jetbrains.kotlin.types.isNullable
* val a: Int?
* get() = 5
* }
*
* fun foo(a: Int?) {
* val b = a!! + 2
* }
* </noncompliant>
*
* <compliant>
Expand All @@ -64,6 +78,14 @@ import org.jetbrains.kotlin.types.isNullable
* val a: Int
* get() = 5
* }
*
* fun foo(a: Int?) {
* val b = (a ?: 0) + 2
* }
BraisGabin marked this conversation as resolved.
Show resolved Hide resolved
*
* fun foo(a: Int) {
* val b = a + 2
* }
* </compliant>
*/
@RequiresTypeResolution
Expand All @@ -77,11 +99,115 @@ class CanBeNonNullable(config: Config = Config.empty) : Rule(config) {

override fun visitKtFile(file: KtFile) {
if (bindingContext == BindingContext.EMPTY) return
NonNullableCheckVisitor().visitKtFile(file)
PropertyCheckVisitor().visitKtFile(file)
ParameterCheckVisitor().visitKtFile(file)
super.visitKtFile(file)
}

private inner class NonNullableCheckVisitor : DetektVisitor() {
private inner class ParameterCheckVisitor : DetektVisitor() {
private val candidateParams = mutableMapOf<DeclarationDescriptor, KtParameter>()
private val referencedParams = mutableSetOf<DeclarationDescriptor>()

override fun visitKtFile(file: KtFile) {
super.visitKtFile(file)
// Any candidate params that were not removed during the inspection
// of the Kotlin file and were not added to referenceParams were never
// treated as nullable params in the code, thus they can be converted
// to non-nullable.
candidateParams.forEach { (descriptor, param) ->
if (referencedParams.contains(descriptor)) {
report(
CodeSmell(
issue,
Entity.from(param),
"The nullable parameter '${param.name}' can be made non-nullable."
)
)
}
}
}

override fun visitNamedFunction(function: KtNamedFunction) {
function.valueParameters.asSequence()
.filter {
it.typeReference?.typeElement is KtNullableType
}.mapNotNull { parameter ->
bindingContext[BindingContext.DECLARATION_TO_DESCRIPTOR, parameter]?.let {
it to parameter
}
}.forEach { (k, v) -> candidateParams[k] = v }
super.visitNamedFunction(function)
}

override fun visitPostfixExpression(expression: KtPostfixExpression) {
val descriptor = expression.baseExpression
?.getResolvedCall(bindingContext)
?.resultingDescriptor
if (descriptor != null && expression.operationToken == KtTokens.EXCLEXCL) {
referencedParams.add(descriptor)
}
super.visitPostfixExpression(expression)
}

override fun visitSafeQualifiedExpression(expression: KtSafeQualifiedExpression) {
expression.receiverExpression
.getResolvedCall(bindingContext)
?.resultingDescriptor
?.let(candidateParams::remove)
super.visitSafeQualifiedExpression(expression)
}

override fun visitDotQualifiedExpression(expression: KtDotQualifiedExpression) {
val isExtensionForNullable = expression.getResolvedCall(bindingContext)
?.resultingDescriptor
?.extensionReceiverParameter
?.type
?.isMarkedNullable
if (isExtensionForNullable == true) {
expression.receiverExpression
.getResolvedCall(bindingContext)
?.resultingDescriptor
?.let(candidateParams::remove)
}
super.visitDotQualifiedExpression(expression)
}

override fun visitBinaryExpression(expression: KtBinaryExpression) {
if (expression.operationToken == KtTokens.ELVIS) {
expression.left
?.getResolvedCall(bindingContext)
?.resultingDescriptor
?.let(candidateParams::remove)
}
super.visitBinaryExpression(expression)
}

override fun visitIfExpression(expression: KtIfExpression) {
expression.condition?.let { it as? KtBinaryExpression }?.evaluateIfExpression()
super.visitIfExpression(expression)
}

private fun KtBinaryExpression.evaluateIfExpression() {
val leftExpression = left
val rightExpression = right
if (isNullCheck() || isNonNullCheck()) {
// Determine whether either side of the expression is a variable
// that could be removed from candidateParams
when {
leftExpression is KtNameReferenceExpression -> leftExpression
rightExpression is KtNameReferenceExpression -> rightExpression
else -> null
}?.getResolvedCall(bindingContext)
?.resultingDescriptor
?.let(candidateParams::remove)
}
// Recursively iterate into the if-check if possible
(leftExpression as? KtBinaryExpression)?.evaluateIfExpression()
(rightExpression as? KtBinaryExpression)?.evaluateIfExpression()
}
}

private inner class PropertyCheckVisitor : DetektVisitor() {
// A list of properties that are marked as nullable during their
// declaration but do not explicitly receive a nullable value in
// the declaration, so they could potentially be marked as non-nullable
Expand Down Expand Up @@ -112,11 +238,13 @@ class CanBeNonNullable(config: Config = Config.empty) : Rule(config) {
}

override fun visitProperty(property: KtProperty) {
if (property.getKotlinTypeForComparison(bindingContext)?.isNullable() != true) return
val fqName = property.fqName ?: return
if (property.isCandidate()) {
candidateProps[fqName] = property
if (property.getKotlinTypeForComparison(bindingContext)?.isNullable() == true) {
val fqName = property.fqName
if (property.isCandidate() && fqName != null) {
candidateProps[fqName] = property
}
}
super.visitProperty(property)
}

override fun visitBinaryExpression(expression: KtBinaryExpression) {
Expand Down
Expand Up @@ -383,5 +383,107 @@ class CanBeNonNullableSpec : Spek({
"""
Assertions.assertThat(subject.compileAndLintWithContext(env, code)).isEmpty()
}

context("evaluating nullable function parameters") {
context("reports when they are never treated as nullable") {
it("and use a double-bang de-nullifier") {
val code = """
fun foo(a: Int?) {
val b = a!! + 2
}
""".trimIndent()
Assertions.assertThat(subject.compileAndLintWithContext(env, code)).hasSize(1)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Assertions.assertThat(subject.compileAndLintWithContext(env, code)).hasSize(1)
assertThat(subject.compileAndLintWithContext(env, code)).hasSize(1)

}

it("and use a double-bang qualified expression") {
val code = """
fun foo(a: Int?) {
val b = a!!.plus(2)
}
""".trimIndent()
Assertions.assertThat(subject.compileAndLintWithContext(env, code)).hasSize(1)
}
}

context("does not report when they are checked with an if-statement") {
it("on nullity") {
val code = """
fun foo(a: Int?) {
if (a == null) {
println("'a' is null")
}
val b = a!! + 2
}
""".trimIndent()
Assertions.assertThat(subject.compileAndLintWithContext(env, code)).isEmpty()
}

it("on non-nullity") {
val code = """
fun foo(a: Int?) {
if (a != null) {
println(a + 5)
}
val b = a!! + 2
}
""".trimIndent()
Assertions.assertThat(subject.compileAndLintWithContext(env, code)).isEmpty()
}

it("in a reversed manner") {
val code = """
fun foo(a: Int?) {
if (null != a) {
println(a + 5)
}
val b = a!! + 2
}
""".trimIndent()
Assertions.assertThat(subject.compileAndLintWithContext(env, code)).isEmpty()
}

it("with multiple clauses") {
val code = """
fun foo(a: Int?, other: Int) {
if (a != null && other % 2 == 0) {
println(a + 5)
}
val b = a!! + 2
}
""".trimIndent()
Assertions.assertThat(subject.compileAndLintWithContext(env, code)).isEmpty()
}
}

it("does not report when they are used in an assignment with an Elvis operator") {
val code = """
fun foo(a: Int?) {
val b = (a ?: 0) + 2
val c = a!! + 2
}
""".trimIndent()
Assertions.assertThat(subject.compileAndLintWithContext(env, code)).isEmpty()
}

it("does not report when they are accessed using a null-safe call") {
val code = """
fun foo(a: Int?) {
val b = a?.plus(2)
val c = a!! + 2
}
""".trimIndent()
Assertions.assertThat(subject.compileAndLintWithContext(env, code)).isEmpty()
}

it("does not report when they are accessed using a null-safe extension function") {
val code = """
fun foo(a: List<String>?) {
val b = a.orEmpty()
val c = a!! + "TEST"
}
""".trimIndent()
Assertions.assertThat(subject.compileAndLintWithContext(env, code)).isEmpty()
}
}
BraisGabin marked this conversation as resolved.
Show resolved Hide resolved
}
})