-
-
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’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[NullCheckOnMutableProperty] Created rule for when a null-check is performed on a mutable property #4353
[NullCheckOnMutableProperty] Created rule for when a null-check is performed on a mutable property #4353
Changes from all commits
556166d
c83d3da
1beced5
d3995e5
db26bac
c6e6137
1e7b0cf
60c747d
7373b14
dee8d9c
0b1ba46
614657b
4713eb9
2b80d7a
625afc1
609faa5
85edb20
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,159 @@ | ||
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.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.isNonNullCheck | ||
import io.gitlab.arturbosch.detekt.rules.isNullCheck | ||
import org.jetbrains.kotlin.backend.common.peek | ||
import org.jetbrains.kotlin.backend.common.pop | ||
import org.jetbrains.kotlin.name.FqName | ||
import org.jetbrains.kotlin.psi.KtBinaryExpression | ||
import org.jetbrains.kotlin.psi.KtConstantExpression | ||
import org.jetbrains.kotlin.psi.KtFile | ||
import org.jetbrains.kotlin.psi.KtIfExpression | ||
import org.jetbrains.kotlin.psi.KtNameReferenceExpression | ||
import org.jetbrains.kotlin.psi.KtPrimaryConstructor | ||
import org.jetbrains.kotlin.psi.KtProperty | ||
import org.jetbrains.kotlin.psi.KtReferenceExpression | ||
import org.jetbrains.kotlin.resolve.BindingContext | ||
import org.jetbrains.kotlin.resolve.calls.callUtil.getResolvedCall | ||
import org.jetbrains.kotlin.resolve.descriptorUtil.fqNameOrNull | ||
|
||
/** | ||
* Reports null-checks on mutable properties, as these properties' value can be | ||
* changed - and thus make the null-check invalid - after the execution of the | ||
* if-statement. | ||
* | ||
* <noncompliant> | ||
* class A(private var a: Int?) { | ||
* fun foo() { | ||
* if (a != null) { | ||
* println(2 + a!!) | ||
* } | ||
* } | ||
* } | ||
* </noncompliant> | ||
* | ||
* <compliant> | ||
* class A(private val a: Int?) { | ||
* fun foo() { | ||
* if (a != null) { | ||
* println(2 + a) | ||
* } | ||
* } | ||
* } | ||
* </compliant> | ||
*/ | ||
@RequiresTypeResolution | ||
class NullCheckOnMutableProperty(config: Config) : Rule(config) { | ||
override val issue = Issue( | ||
javaClass.simpleName, | ||
Severity.Defect, | ||
"Checking nullability on a mutable property is not useful because the " + | ||
"property may be set to null afterwards.", | ||
Debt.TEN_MINS | ||
) | ||
|
||
override fun visitKtFile(file: KtFile) { | ||
if (bindingContext == BindingContext.EMPTY) return | ||
super.visitKtFile(file) | ||
NullCheckVisitor().visitKtFile(file) | ||
} | ||
|
||
private inner class NullCheckVisitor : DetektVisitor() { | ||
private val mutableProperties = mutableSetOf<FqName>() | ||
private val candidateProperties = mutableMapOf<FqName, MutableList<KtIfExpression>>() | ||
|
||
override fun visitPrimaryConstructor(constructor: KtPrimaryConstructor) { | ||
super.visitPrimaryConstructor(constructor) | ||
constructor.valueParameters.asSequence() | ||
.filter { it.isMutable } | ||
.mapNotNull { it.fqName } | ||
.forEach(mutableProperties::add) | ||
} | ||
|
||
override fun visitProperty(property: KtProperty) { | ||
super.visitProperty(property) | ||
val fqName = property.fqName | ||
if (fqName != null && (property.isVar || property.getter != null)) { | ||
mutableProperties.add(fqName) | ||
} | ||
} | ||
|
||
override fun visitIfExpression(expression: KtIfExpression) { | ||
// Extract all possible null-checks within the if-expression. | ||
val nonNullChecks = (expression.condition as? KtBinaryExpression) | ||
?.collectNonNullChecks() | ||
.orEmpty() | ||
|
||
val modifiedCandidateQueues = nonNullChecks.mapNotNull { nonNullCondition -> | ||
if (nonNullCondition.left is KtConstantExpression) { | ||
nonNullCondition.right as? KtNameReferenceExpression | ||
} else { | ||
nonNullCondition.left as? KtNameReferenceExpression | ||
}?.let { referenceExpression -> | ||
referenceExpression.getResolvedCall(bindingContext) | ||
?.resultingDescriptor | ||
?.let { | ||
it.fqNameOrNull()?.takeIf(mutableProperties::contains) | ||
} | ||
}?.let { candidateFqName -> | ||
// A candidate mutable property is present, so attach the current | ||
// if-expression to it in the property candidates map. | ||
candidateProperties.getOrPut(candidateFqName) { ArrayDeque() }.apply { add(expression) } | ||
} | ||
} | ||
// Visit descendant expressions to see whether candidate properties | ||
// identified in this if-expression are being referenced. | ||
super.visitIfExpression(expression) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Commenting here but valid for the whole Is there a reason why you're calling In this line you're calling There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The line needs to be in the middle of the block in order to analyze whether the descendant expressions use any candidate properties identified in the if-expression, with the |
||
// Remove the if-expression after having iterated out of its code block. | ||
modifiedCandidateQueues.forEach { it.pop() } | ||
} | ||
|
||
override fun visitReferenceExpression(expression: KtReferenceExpression) { | ||
super.visitReferenceExpression(expression) | ||
expression.getResolvedCall(bindingContext) | ||
?.resultingDescriptor | ||
?.fqNameOrNull() | ||
?.let { fqName -> | ||
val expressionParent = expression.parent | ||
// Don't check the reference expression if it's being invoked in the if-expression | ||
// where it's being null-checked. | ||
if ( | ||
expressionParent !is KtBinaryExpression || | ||
(!expressionParent.isNonNullCheck() && !expressionParent.isNullCheck()) | ||
) { | ||
// If there's an if-expression attached to the candidate property, a null-checked | ||
// mutable property is being referenced. | ||
candidateProperties[fqName]?.peek()?.let { ifExpression -> | ||
report( | ||
CodeSmell( | ||
issue, | ||
Entity.from(ifExpression), | ||
"Null-check is being called on mutable property '$fqName'." | ||
) | ||
) | ||
} | ||
} | ||
} | ||
} | ||
|
||
private fun KtBinaryExpression.collectNonNullChecks(): List<KtBinaryExpression> { | ||
return if (isNonNullCheck()) { | ||
listOf(this) | ||
} else { | ||
val nonNullChecks = mutableListOf<KtBinaryExpression>() | ||
(left as? KtBinaryExpression)?.let { nonNullChecks.addAll(it.collectNonNullChecks()) } | ||
(right as? KtBinaryExpression)?.let { nonNullChecks.addAll(it.collectNonNullChecks()) } | ||
nonNullChecks | ||
} | ||
} | ||
} | ||
} |
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.
You're missing a
super.visitKtFile(file)
here