Skip to content

GuardCluase also matches if-with-body that contains a return #2671

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

Merged
merged 4 commits into from
May 11, 2020

Conversation

pinkasey
Copy link
Contributor

@pinkasey pinkasey commented May 10, 2020

@arturbosch
I thought opening a PR was better than opening an Issue.
Although, I didn't properly test it yet - I'm new to gradle and Spek.
If you prefere, I can open an issue instead...

What I want -
to consider an if with a body as guard clause as well.
I've added a test-case to demonstrate.

In my company, we almost disabled ReturnCount because it catches guard-clauses, and then I saw guard-clauses - and was disappointed it applies only to ifs without a body.
Our code-style convention don't allow if's without body.

context("a file with an if condition guard clause with body and 2 returns") {
val code = """
fun test(x: Int): Int {
if (x < 4) {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I want this to count as a GuardClause.
We have many of these in our code.

In fact, many consider it a bad practice to have an if with no body, so they'll never enjoy the benefit of the GuardClause feature.

@codecov
Copy link

codecov bot commented May 10, 2020

Codecov Report

Merging #2671 into master will decrease coverage by 0.48%.
The diff coverage is 87.50%.

Impacted file tree graph

@@             Coverage Diff              @@
##             master    #2671      +/-   ##
============================================
- Coverage     79.98%   79.50%   -0.49%     
+ Complexity     2307     2293      -14     
============================================
  Files           380      380              
  Lines          6781     6782       +1     
  Branches       1229     1229              
============================================
- Hits           5424     5392      -32     
- Misses          740      774      +34     
+ Partials        617      616       -1     
Impacted Files Coverage Δ Complexity Δ
.../io/gitlab/arturbosch/detekt/rules/GuardClauses.kt 82.35% <80.00%> (+7.35%) 0.00 <0.00> (ø)
...kotlin/io/gitlab/arturbosch/detekt/DetektPlugin.kt 52.52% <100.00%> (-14.15%) 11.00 <1.00> (ø)
...tlab/arturbosch/detekt/DetektCreateBaselineTask.kt 0.00% <0.00%> (-48.39%) 0.00% <0.00%> (-12.00%)
...tlab/arturbosch/detekt/DetektGenerateConfigTask.kt 0.00% <0.00%> (-31.25%) 0.00% <0.00%> (-2.00%)

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 02bf569...33306e8. Read the comment docs.

@@ -24,8 +24,8 @@ fun KtNamedFunction.yieldStatementsSkippingGuardClauses(): Sequence<KtExpression

fun KtExpression.isGuardClause(): Boolean {

fun KtReturnExpression.isIfConditionGuardClause(): Boolean {
val ifExpr = this.parent?.parent as? KtIfExpression
fun KtReturnExpression.isIfConditionGuardClause(ancestorExpression: KtExpression): Boolean {
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
fun KtReturnExpression.isIfConditionGuardClause(ancestorExpression: KtExpression): Boolean {
fun isIfConditionGuardClause(ancestorExpression: KtExpression): Boolean {

This can probably be refactored to not be an extension function?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I actually like it that way,
and anyway I don't feel comfortable to make that change to someone else's code 🙂

Copy link
Member

Choose a reason for hiding this comment

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

+1

Copy link
Member

Choose a reason for hiding this comment

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

@cortinico is right here. As you are not using the receiver inside the nested method is it unused.
I would propose an additional change which checks if the return statement is actually the last statement inside the if:

fun KtExpression.isGuardClause(): Boolean {

    fun isIfConditionGuardClause(returnExpr: KtReturnExpression): Boolean {
        val ifExpr = this as? KtIfExpression
        return ifExpr != null &&
            ifExpr.`else` == null &&
            returnExpr === ifExpr.then?.lastBlockStatementOrThis()
    }

    fun KtReturnExpression.isElvisOperatorGuardClause(): Boolean {
        val elvisExpr = this.parent as? KtBinaryExpression
        return elvisExpr != null && elvisExpr.operationToken == KtTokens.ELVIS
    }

    val returnExpr = this.findDescendantOfType<KtReturnExpression>() ?: return false
    return isIfConditionGuardClause(returnExpr) || returnExpr.isElvisOperatorGuardClause()
}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

right
I didn't notice that I no longer use the receiver 😕
I'll change the code

…/style/ReturnCountSpec.kt

Co-authored-by: Nicola Corti <corti.nico@gmail.com>
Copy link
Member

@schalkms schalkms left a comment

Choose a reason for hiding this comment

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

I'm okay with that change, although we have to be careful here.
Therefore, this change should be properly tested. The following script is very useful in that regard.

Script reference

Copy link
Member

@arturbosch arturbosch left a comment

Choose a reason for hiding this comment

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

Very nice PR @pinkasey . We definitely missed out on checking the braces.
Please see my proposed change, thx!

@@ -24,8 +24,8 @@ fun KtNamedFunction.yieldStatementsSkippingGuardClauses(): Sequence<KtExpression

fun KtExpression.isGuardClause(): Boolean {

fun KtReturnExpression.isIfConditionGuardClause(): Boolean {
val ifExpr = this.parent?.parent as? KtIfExpression
fun KtReturnExpression.isIfConditionGuardClause(ancestorExpression: KtExpression): Boolean {
Copy link
Member

Choose a reason for hiding this comment

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

@cortinico is right here. As you are not using the receiver inside the nested method is it unused.
I would propose an additional change which checks if the return statement is actually the last statement inside the if:

fun KtExpression.isGuardClause(): Boolean {

    fun isIfConditionGuardClause(returnExpr: KtReturnExpression): Boolean {
        val ifExpr = this as? KtIfExpression
        return ifExpr != null &&
            ifExpr.`else` == null &&
            returnExpr === ifExpr.then?.lastBlockStatementOrThis()
    }

    fun KtReturnExpression.isElvisOperatorGuardClause(): Boolean {
        val elvisExpr = this.parent as? KtBinaryExpression
        return elvisExpr != null && elvisExpr.operationToken == KtTokens.ELVIS
    }

    val returnExpr = this.findDescendantOfType<KtReturnExpression>() ?: return false
    return isIfConditionGuardClause(returnExpr) || returnExpr.isElvisOperatorGuardClause()
}

@@ -89,7 +151,10 @@ class ReturnCountSpec : Spek({
if(a == null) return
val models = b as? Int ?: return
val position = c?.takeIf { it != -1 } ?: return
if(b !is String) return
if(b !is String) {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I thought -
why not include one with braces too

@pinkasey pinkasey requested review from schalkms and arturbosch May 11, 2020 15:46
@pinkasey
Copy link
Contributor Author

tests fail because my example code (which i copy-pasted from previous it) - did not compile.
I'll fix that, and the rest of the cases while I'm at it.
please wait with review...

@pinkasey
Copy link
Contributor Author

OK
I've fixed my tests,
migrated all tests but the last one from lint() to compileAndLint().
Hope checks pass this time.

4 -> return 4
}
return 6
Copy link
Contributor Author

Choose a reason for hiding this comment

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

These code-snippets didn't compile, because there wasn't a return in every execution path.

Copy link
Member

@arturbosch arturbosch left a comment

Choose a reason for hiding this comment

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

Thanks!

@arturbosch arturbosch merged commit 47ef2cf into detekt:master May 11, 2020
@pinkasey
Copy link
Contributor Author

pinkasey commented May 12, 2020

Thanks!

Thanks @arturbosch
for the good comments and encouragement!

@pinkasey pinkasey deleted the eyal_GuardClauses_with_body branch May 12, 2020 06:20
@arturbosch arturbosch added this to the 1.9.0 milestone May 12, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants