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

[NullCheckOnMutableProperty] Created rule for when a null-check is performed on a mutable property #4353

Merged

Conversation

severn-everett
Copy link
Contributor

This is for issue #3353. Any advice on how to improve this one is welcome, as this was a bit of a stabbing-in-the-dark solution for me.

@codecov
Copy link

codecov bot commented Dec 2, 2021

Codecov Report

Merging #4353 (85edb20) into main (92433ea) will increase coverage by 0.08%.
The diff coverage is 85.07%.

Impacted file tree graph

@@             Coverage Diff              @@
##               main    #4353      +/-   ##
============================================
+ Coverage     84.26%   84.35%   +0.08%     
- Complexity     3263     3273      +10     
============================================
  Files           473      474       +1     
  Lines         10336    10405      +69     
  Branches       1827     1845      +18     
============================================
+ Hits           8710     8777      +67     
- Misses          667      668       +1     
- Partials        959      960       +1     
Impacted Files Coverage Δ
...tlab/arturbosch/detekt/rules/KtBinaryExpression.kt 0.00% <0.00%> (ø)
...ch/detekt/rules/bugs/NullCheckOnMutableProperty.kt 87.50% <87.50%> (ø)
...turbosch/detekt/rules/bugs/PotentialBugProvider.kt 100.00% <100.00%> (ø)
...etekt/rules/style/optional/MandatoryBracesLoops.kt 73.33% <0.00%> (-2.53%) ⬇️
...ab/arturbosch/detekt/core/config/ValidateConfig.kt 98.59% <0.00%> (-1.41%) ⬇️
...n/kotlin/io/gitlab/arturbosch/detekt/api/Entity.kt 75.00% <0.00%> (-1.20%) ⬇️
.../detekt/rules/exceptions/ObjectExtendsThrowable.kt 80.95% <0.00%> (ø)
...t/generator/collection/RuleSetProviderCollector.kt 75.94% <0.00%> (+1.26%) ⬆️
...tlab/arturbosch/detekt/rules/style/UseDataClass.kt 80.28% <0.00%> (+1.40%) ⬆️
...osch/detekt/rules/exceptions/SwallowedException.kt 77.04% <0.00%> (+1.63%) ⬆️
... and 4 more

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 92433ea...85edb20. Read the comment docs.

Copy link
Member

@BraisGabin BraisGabin left a comment

Choose a reason for hiding this comment

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

Could you add these tests too:

class A(private var a: Int?) {
    fun foo() {
        if (a != null) {
            println(2 + 2) // this null check should be fine
        } 
    }
}
class A(private val a: Int?) {
    class B(private var a: Int?) {
        fun foo() {
            if (a != null) {
                println(2 + a!!) // should report
            } 
        }
    }

    fun foo() {
        if (a != null) {
            println(2 + a!!) // shouldn't report
        } 
    }
}
class A(private var a: Int?) {
    inner class B {
        fun foo() {
            if (a != null) {
                println(2 + a!!) // should report
            } 
        }
    }
}

I think that you are handling correctly the last two, but just to have tests that ensure it.

Comment on lines 47 to 49
* </compliant>
*
* <compliant>
Copy link
Member

Choose a reason for hiding this comment

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

I think that we only support one "compliant" block, can you merge both?

Suggested change
* </compliant>
*
* <compliant>
*

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The code wouldn't pass the first test case proposed here, as doing so would mean the rule would have to detect both a null-check for the mutable property and a double-bang operation on the variable. This, essentially, would turn the rule into a sub-rule for UnsafeCallOnNullableType: anything that NullCheckOnMutableProperty would pick up would be picked up by that rule as well. It's not a perfectly symmetrical relationship, but I'm not sure that the use case wherein a developer wants to be warned about double-bang operations only if they're being conducted after a null-check is that prevalent.

@BraisGabin
Copy link
Member

The code wouldn't pass the first test case proposed here, as doing so would mean the rule would have to detect both a null-check for the mutable property and a double-bang operation on the variable. This, essentially, would turn the rule into a sub-rule for UnsafeCallOnNullableType: anything that NullCheckOnMutableProperty would pick up would be picked up by that rule as well. It's not a perfectly symmetrical relationship, but I'm not sure that the use case wherein a developer wants to be warned about double-bang operations only if they're being conducted after a null-check is that prevalent.

I'm going to give more examples because I think that it will be more clear:

class A(private var a: Int?) {
    fun foo() {
        if (a != null) {
            println(a) // report! This could print an unexpected `null`
        }
    }
}
class A(private var a: Int?) {
    fun foo() {
        if (a != null) {
            println(2 + requireNotNull(a)) // report! This could crash
        } 
    }
}
class A(private var a: Int?) {
    fun foo() {
        if (a != null) {
            println("a is not null") // this code is completely fine
        }
    }
}

This code is not related with the usage of !!. We just need to check if the property is used or not inside the if. If it's used, we should flag the check. If it is not used, we shouldn't flag it.


Other missing test:

class A(private var a: Int?) {
    fun foo() {
        if (a != null && true) {
            println("a is not null") // this code is completely fine
        }
    }
}

@severn-everett
Copy link
Contributor Author

Gotcha, I'll see what I can do

Copy link
Member

@BraisGabin BraisGabin left a comment

Choose a reason for hiding this comment

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

🎉🎉🎉 Great job! I really like this rule. This will flag a lot of difficult to spot issues. 👏👏👏

Do you think this is ready to be merged?

}
}
}
"""
Copy link
Member

Choose a reason for hiding this comment

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

It would be nice to have another rule to check something like this. But it would be difficult and with lots of false positives.

it("should not report a null-check on a non-mutable class property") {
val code = """
class A {
private val a: Int? = 5
Copy link
Member

Choose a reason for hiding this comment

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

private val a: Int?
  get() = 5

Maybe this could be flagged too.

@severn-everett
Copy link
Contributor Author

The code should be good to go now. The other two points could be made into other issues/tasks to be tackled later.

@BraisGabin
Copy link
Member

Ok, I'll wait a bit to get the review form other maintainer.

I think that this case should be handled:

private val a: Int?
  get() = 5

But I agree that it could be handled in other PR. Can you do it? Or should we open an issue so we don't forget about it?

@severn-everett
Copy link
Contributor Author

I'd say that it's a different issue, as it wouldn't be "error-prone": the value is going to be non-null, no matter how many times it's checked. Maybe this would be a style issue, e.g. some rule like UnnecessaryNullable?

@BraisGabin
Copy link
Member

BraisGabin commented Dec 15, 2021

Sorry, I wasn't clear with my example. It was far too simplified. I mean something like this:

private val a: Int?
  get() = foo()

a is val but its value could change between calls so it is a mutable property.

@severn-everett severn-everett deleted the create_null_check_on_mutable_property branch December 16, 2021 19:23
@BraisGabin
Copy link
Member

Why did you closed and removed it?

@severn-everett severn-everett restored the create_null_check_on_mutable_property branch December 16, 2021 19:36
@severn-everett
Copy link
Contributor Author

My apologies, I thought it was merged - thanks for the heads-up!

Copy link
Member

@cortinico cortinico left a comment

Choose a reason for hiding this comment

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

Great work :)

*
* class A(private var a: Int?) {
* fun foo() {
* val a = a
Copy link
Member

Choose a reason for hiding this comment

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

That's not the best snippet to put in the compliant block. Is doing name shadowing and should not be suggested.

* }
* </compliant>
*/

Copy link
Member

Choose a reason for hiding this comment

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

Extra white line?

)

override fun visitKtFile(file: KtFile) {
if (bindingContext == BindingContext.EMPTY) return
Copy link
Member

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

candidateProperties.getOrPut(candidateFqName) { ArrayDeque() }.apply { add(expression) }
}
}
super.visitIfExpression(expression)
Copy link
Member

Choose a reason for hiding this comment

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

Commenting here but valid for the whole NullCheckVisitor:

Is there a reason why you're calling super. methods at arbitrary locations inside the function blocks? Ideally you should always call the super method as first statement and then add your logic.

In this line you're calling super.visitIfExpression(expression) in the middle of the block.

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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 visitIfExpression() removing any if-statements added to candidateProperties afterwards. I've added a comment that explains as much in the code.

CodeSmell(
issue,
Entity.from(ifExpression),
"Null-check is being called on a mutable property."
Copy link
Member

Choose a reason for hiding this comment

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

nit: if you could customize the message with the name of the property would be 👌

@cortinico
Copy link
Member

Are we good to merge this?

@cortinico cortinico added this to the 1.20.0 milestone Dec 31, 2021
@cortinico cortinico changed the title Created rule for when a null-check is performed on a mutable property [NullCheckOnMutableProperty] Created rule for when a null-check is performed on a mutable property Dec 31, 2021
@BraisGabin BraisGabin merged commit f6440e3 into detekt:main Dec 31, 2021
@cortinico cortinico added the notable changes Marker for notable changes in the changelog label Jan 3, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
notable changes Marker for notable changes in the changelog rules
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants