Skip to content

Commit

Permalink
Added rule CanBeNonNullableProperty (#4379)
Browse files Browse the repository at this point in the history
* Added rule CanBeNonNullableProperty

* Added processing for open properties and delegated properties

* Fixed check for nullable types that didn't cover bang-punctuated types; Reorganized tests and split up basic var/val evaluation tests

* Fix Detekt issues

* Added check to exclude evaluating interfaces

* Set rule to not be active by default

* Fixed Detekt issue

* Changed name of rule to make it potentially applicable to local variables in the future; Re-enabled usage of rule for Detekt codebase; Fixed test code snippets that weren't compiling
  • Loading branch information
severn-everett committed Dec 26, 2021
1 parent aef4cf9 commit f5d2a17
Show file tree
Hide file tree
Showing 5 changed files with 577 additions and 0 deletions.
2 changes: 2 additions & 0 deletions config/detekt/detekt.yml
Expand Up @@ -143,6 +143,8 @@ potential-bugs:
active: true

style:
CanBeNonNullable:
active: true
ClassOrdering:
active: true
CollapsibleIfStatements:
Expand Down
2 changes: 2 additions & 0 deletions detekt-core/src/main/resources/default-detekt-config.yml
Expand Up @@ -603,6 +603,8 @@ potential-bugs:

style:
active: true
CanBeNonNullable:
active: false
ClassOrdering:
active: false
CollapsibleIfStatements:
Expand Down
@@ -0,0 +1,185 @@
package io.gitlab.arturbosch.detekt.rules.style

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.DetektVisitor
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.isOpen
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.KtExpression
import org.jetbrains.kotlin.psi.KtFile
import org.jetbrains.kotlin.psi.KtIfExpression
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.psiUtil.collectDescendantsOfType
import org.jetbrains.kotlin.psi.psiUtil.isPrivate
import org.jetbrains.kotlin.resolve.BindingContext
import org.jetbrains.kotlin.resolve.calls.callUtil.getResolvedCall
import org.jetbrains.kotlin.resolve.calls.callUtil.getType
import org.jetbrains.kotlin.resolve.calls.smartcasts.getKotlinTypeForComparison
import org.jetbrains.kotlin.resolve.descriptorUtil.fqNameOrNull
import org.jetbrains.kotlin.types.isNullable

/**
* This rule inspects variables marked as nullable and reports which could be
* declared as non-nullable instead.
*
* <noncompliant>
* class A {
* var a: Int? = 5
*
* fun foo() {
* a = 6
* }
* }
*
* class A {
* val a: Int?
* get() = 5
* }
* </noncompliant>
*
* <compliant>
* class A {
* var a: Int = 5
*
* fun foo() {
* a = 6
* }
* }
*
* class A {
* val a: Int
* get() = 5
* }
* </compliant>
*/
@RequiresTypeResolution
class CanBeNonNullable(config: Config = Config.empty) : Rule(config) {
override val issue = Issue(
javaClass.simpleName,
Severity.Style,
"Variable can be changed to non-nullable, as it is never set to null.",
Debt.TEN_MINS
)

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

private inner class NonNullableCheckVisitor : 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
// if the file does not encounter these properties being assigned
// a nullable value.
private val candidateProps = mutableMapOf<FqName, KtProperty>()

override fun visitKtFile(file: KtFile) {
super.visitKtFile(file)
// Any candidate properties that were not removed during the inspection
// of the Kotlin file were never assigned nullable values in the code,
// thus they can be converted to non-nullable.
candidateProps.forEach { (_, property) ->
report(
CodeSmell(
issue,
Entity.from(property),
"The nullable variable '${property.name}' can be made non-nullable."
)
)
}
}

override fun visitClass(klass: KtClass) {
if (!klass.isInterface()) {
super.visitClass(klass)
}
}

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

override fun visitBinaryExpression(expression: KtBinaryExpression) {
if (expression.operationToken == KtTokens.EQ) {
val fqName = expression.left
?.getResolvedCall(bindingContext)
?.resultingDescriptor
?.fqNameOrNull()
if (
fqName != null &&
candidateProps.containsKey(fqName) &&
expression.right?.isNullableType() == true
) {
// A candidate property has been assigned a nullable value
// in the file's code, so it can be removed from the map of
// candidates for flagging.
candidateProps.remove(fqName)
}
}
super.visitBinaryExpression(expression)
}

private fun KtProperty.isCandidate(): Boolean {
if (isOpen()) return false
val isSetToNonNullable = initializer?.isNullableType() != true &&
getter?.isNullableType() != true &&
delegate?.returnsNullable() != true
val cannotSetViaNonPrivateMeans = !isVar || (isPrivate() || (setter?.isPrivate() == true))
return isSetToNonNullable && cannotSetViaNonPrivateMeans
}

private fun KtPropertyDelegate?.returnsNullable(): Boolean {
val property = this?.parent as? KtProperty ?: return false
val propertyDescriptor =
bindingContext[BindingContext.DECLARATION_TO_DESCRIPTOR, property] as? PropertyDescriptor
return propertyDescriptor?.getter?.let {
bindingContext[BindingContext.DELEGATED_PROPERTY_RESOLVED_CALL, it]
?.resultingDescriptor
?.returnType
?.isNullable() == true
} ?: false
}

private fun KtExpression?.isNullableType(): Boolean {
return when (this) {
is KtConstantExpression -> {
this.text == "null"
}
is KtIfExpression -> {
this.then.isNullableType() || this.`else`.isNullableType()
}
is KtPropertyAccessor -> {
(initializer?.getType(bindingContext)?.isNullable() == true) ||
(
bodyExpression
?.collectDescendantsOfType<KtReturnExpression>()
?.any { it.returnedExpression.isNullableType() } == true
)
}
else -> {
this?.getType(bindingContext)?.isNullable() == true
}
}
}
}
}
Expand Up @@ -23,6 +23,7 @@ class StyleGuideProvider : DefaultRuleSetProvider {
override fun instance(config: Config): RuleSet = RuleSet(
ruleSetId,
listOf(
CanBeNonNullable(config),
ClassOrdering(config),
CollapsibleIfStatements(config),
DestructuringDeclarationWithTooManyEntries(config),
Expand Down

0 comments on commit f5d2a17

Please sign in to comment.