-
-
Notifications
You must be signed in to change notification settings - Fork 794
Potential new rule: DoubleNegativeLambda #5937
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
Conversation
…egative with a lambda block also containing a negative
This rule is definitely a good addition to the standard style ruleset! I like it. |
Codecov Report
@@ Coverage Diff @@
## main #5937 +/- ##
============================================
+ Coverage 84.46% 84.52% +0.06%
- Complexity 3784 3833 +49
============================================
Files 546 548 +2
Lines 12923 13052 +129
Branches 2268 2302 +34
============================================
+ Hits 10915 11032 +117
- Misses 877 879 +2
- Partials 1131 1141 +10
... and 10 files with indirect coverage changes Help us with your feedback. Take ten seconds to tell us how you rate us. Have a feature suggestion? Share it here. |
* Random.Default.nextInt().takeUnless { !it.isEven() } | ||
* </noncompliant> | ||
* <compliant> | ||
* Random.Default.nextInt().takeIf { it.isOdd() } |
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.
The code in the non-compliant and the compliant doesn't behave the same. That doesn't help to understand the rule.
* Random.Default.nextInt().takeIf { it.isOdd() } | |
* Random.Default.nextInt().takeIf { it.isEven() } |
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.
Thank you, I tried to make it clearer in e7b22f1
private val negatingFunctionNameParts = listOf("not", "non") | ||
|
||
@Configuration("Function names expressed in the negative that can form double negatives with their lambda blocks.") | ||
private val negativeFunctions: Set<String> by config(listOf("takeUnless")) { it.toSet() } |
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.
We could use here valueWithReason so in the reason we could say which is the positive form of that function. For example, for takeUnless
it would be takeIf
.
You can see examples of this at forbiddenMethodCall.
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.
Thanks, didn't know about this feature. I attempted this in 38a187a
KtTokens.NOT_IS, | ||
) | ||
|
||
private val negatingFunctionNameParts = listOf("not", "non") |
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.
Should we make this configurable?
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.
Thanks, I think they should be configurable too. Attempted this in df6a662
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.
I really like the idea behind this rule 👍. And if you agree with the change I propose on the comments it will be a LGTM.
) | ||
private val negativeFunctions: List<NegativeFunction> by config( | ||
valuesWithReason( | ||
"takeUnless" to "takeIf", |
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.
"takeUnless" to "takeIf", | |
"takeUnless" to "Use `takeIf` instead.", |
append("Rewrite in the positive") | ||
if (negativeFunction.positiveCounterpart != null) { | ||
append(" with `${negativeFunction.positiveCounterpart}`.") | ||
} else { | ||
append(".") | ||
} |
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.
append("Rewrite in the positive") | |
if (negativeFunction.positiveCounterpart != null) { | |
append(" with `${negativeFunction.positiveCounterpart}`.") | |
} else { | |
append(".") | |
} | |
if (negativeFunction.positiveCounterpart != null) { | |
append(negativeFunction.positiveCounterpart") | |
} else { | |
append("Rewrite in the positive.") | |
} |
I think that reason
should contain a reason, not just the counter part. And that should also make this code a bit simplier. Don't commit exactly my code, I assume that further rework is needed to land this change.
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.
Thanks, that makes a lot more sense. I did the change in 2ded6a4
I called it a "recommendation" ("Use takeIf
instead") rather than a "reason" because that seems to be what it's called in the docs:
https://github.com/detekt/detekt/blob/main/.github/CONTRIBUTING.md
but I can rename it back to "reason" if you prefer.
) | ||
private val negativeFunctions: List<NegativeFunction> by config( | ||
valuesWithReason( | ||
"takeUnless" to "Use `takeIf` instead.", |
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.
what's the reason why you haven't included none
and filterNot
in the default here?
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.
Thank you for the review.
I think there is a good case to add none
as a default. I searched a few big Kotlin codebases, including Detekt and KotlinPoet. I couldn't find any examples of none
with a negation in its lambda. So it seems it would be something we would catch in a code review. Adding none
would help explain the rule to consumers too.
I did not include filterNot
because it seems there are a few cases where it seems more readable leaving it in the negative. I found this example in KotlinPoet:
addModifiers(
flags.modalities
.filterNot { it == FINAL && !isOverride } // Final is the default
.filterNot { it == OPEN && isOverride } // Overrides are implicitly open
.filterNot { it == OPEN && isInInterface }, // interface methods are implicitly open
)
Rewriting this, we'd end up with:
addModifiers(
flags.modalities
.filter { it != FINAL && isOverride } // Final is the default
.filterNot { it == OPEN && isOverride } // Overrides are implicitly open
.filterNot { it == OPEN && isInInterface }, // interface methods are implicitly open
)
What do you think about adding none
as a default and leaving out filterNot
?
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.
What do you think about adding none as a default and leaving out filterNot?
Yup makes sense, thanks for clarifying 👍 none
feels also like the most natural use case for this rule
Potential new rule: DoubleNegativeLambda
Rationale
There was recently a popular post on Y-Combinator Hacker News arguing against the use of
unless
in Ruby.https://news.ycombinator.com/item?id=33965933
It is arguing that:
takeUnless { !it.isOdd() }
vstakeIf { it.isEven() }
)takeUnless { it.isEven() }
to become complex when some edge case is found and an extra condition is added so it becomes something liketakeUnless { it.isEven() && !it.isAlreadyAccountedFor() }
. Now the reader needs to unpack this expression using DeMorgan's lawsI am wondering if you would be interested in a rule that checks for double negatives like this using
takeUnless
? (it's okay if 'no', can keep it private)Considerations
filterNot
andnone
by default as well?To me, negatives in a
filterNot
block seem less harmful than in atakeUnless
block. I looked for examples and I found this one in KotlinPoet:It doesn't seem to benefit a lot from being rewritten in the positive as
filter { it != FINAL || isOverride }
One could argue that in addition to detecting simple function names with "not" and "non" in them, it should also try and catch negation in functions with back ticks to report cases like
takeUnless { it.
isn't a word() }
. I thought it would add a bit of complexity to the rule and detecting negation in a backtick function name would make it a harder problem where we would be trying to solve the problem of detecting negation in English which has so many different ways (isn't, can't, won't etc.)To me, functions like
takeUnless
are quite rare. I think it would be unusual to want to distinguish between one or the other.Disclosure
I used ChatGPT4 to help me write the rule and was thinking of writing an article about my experience (Chat GPT was not that helpful here despite Detekt code seemingly being in the training data).
Prompts and responses for transparency:
https://chat.openai.com/share/48f1ebbb-5761-4e0b-a232-d3ac6f808f89