-
Notifications
You must be signed in to change notification settings - Fork 1.9k
Add manual_checked_div lint #16149
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
base: master
Are you sure you want to change the base?
Add manual_checked_div lint #16149
Conversation
|
rustbot has assigned @samueltardieu. Use |
|
No changes for 428d901 |
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.
Extremely impressive for a "complete newbie at Clippy"! Left a couple starting comments
| NonZeroBranch::Else => match r#else.map(|expr| &expr.kind) { | ||
| Some(ExprKind::Block(block, _)) => Some(block), | ||
| _ => None, | ||
| }, |
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.
| NonZeroBranch::Else => match r#else.map(|expr| &expr.kind) { | |
| Some(ExprKind::Block(block, _)) => Some(block), | |
| _ => None, | |
| }, | |
| NonZeroBranch::Else => match r#else?.kind { | |
| ExprKind::Block(block, _) => Some(block), | |
| _ => None, | |
| }, |
| let Some(block) = branch_block(then, r#else, branch) else { | ||
| return; | ||
| }; |
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.
this could be moved into the let-chain as well
| if expr.span.from_expansion() { | ||
| return; | ||
| } |
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.
this could be moved into the let-chain as well
| let lhs_snip = Sugg::hir_with_applicability(cx, lhs, "..", &mut applicability); | ||
| let rhs_snip = Sugg::hir_with_applicability(cx, rhs, "..", &mut applicability); |
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.
For single expressions, the preferred placeholder is "_"
| let lhs_snip = Sugg::hir_with_applicability(cx, lhs, "..", &mut applicability); | |
| let rhs_snip = Sugg::hir_with_applicability(cx, rhs, "..", &mut applicability); | |
| let lhs_snip = Sugg::hir_with_applicability(cx, lhs, "_", &mut applicability); | |
| let rhs_snip = Sugg::hir_with_applicability(cx, rhs, "_", &mut applicability); |
| let Some(block) = branch_block(then, r#else, branch) else { | ||
| return; | ||
| }; | ||
| let mut eq = SpanlessEq::new(cx); |
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.
This is fine to inline
| span_lint_and_sugg( | ||
| cx, | ||
| MANUAL_CHECKED_DIV, | ||
| e.span, |
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.
It could be nice to also highlight the span where the variable (let's call it b) is checked for being non-zero, which you could do by passing MultiSpan::new(vec![cond.span, e.span]).
But with that, if there were multiple instances of a / b (however unlikely that is), each firing of the lint would show the said checking place, which would be suboptimal. Because of this, a better solution could be to first collect all the instances of a / b (where each one might have a different a!), then lint on all of them at once (if there are any, of course). Now that I've written all this, it does sound like a bit too much work -- if you want to give it a try, feel free, but otherwise just highlighting both the condition and the division would be a good start.
tests/ui/manual_checked_div.rs
Outdated
| // Should NOT trigger (signed integers) | ||
| let c = -5i32; | ||
| if c != 0 { | ||
| let _result = 10 / c; | ||
| } |
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.
Sorry, why exactly do we not lint here?... I see that the original issue only talks about unsigned integers, but I think signed ones should be fine as well?
This comment has been minimized.
This comment has been minimized.
|
@amerikrainian Could you look at, and probably apply, @ada4a's suggestions? |
|
Reminder, once the PR becomes ready for a review, use |
Yes. I'm sorry. I'm a student and we're in the finals season, so I am deep in the trenches until tomorrow; will do my best to get this done before Friday. |
|
No hurry, I just wanted to make sure you noticed them. Take your time. |
bf07325 to
1316c0e
Compare
|
This PR was rebased onto a different master commit. Here's a range-diff highlighting what actually changed. Rebasing is a normal part of keeping PRs up to date, so no action is needed—this note is just to help reviewers. |
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.
This is a good initial shot. Some risks of false positives need to be fixed though.
As a general rule, dividend() / divisor() can be linted only if you can prove that divisor() is identical between the check and the division, and if divisor() has no side-effect. For example, the following code should not be linted as it would not give the same result:
fn counter() -> u32 { /* Some code which increments and return a counter */ }
if counter() > 0 {
let a = 32 / counter();
println!("{}", a); // This divides by the value of the counter as incremented twice
}Also, if the divisor is used separately from the division, it might not be easy to refactor with .checked_div(), as in:
if b > 0 {
do_something_with(b);
f(a / b);
}Another, more tortuous, example, would be, with a mutable b:
if b > 0 {
g(inc_and_return_value(&mut b), a / b);
}In the latest example, the value of b may have changed while evaluating the first argument of g(), so computing the division first using .checked_div() might not be appropriate.
In short, I think you should trigger the lint only if the division is the first use of the divisor in the "then" part of the if, and if the divisor is an expression that cannot change between the check and the division (checking for the absence of function calls should be fine).
Also, it would be great if /= could be checked the same way. And maybe % and %=, with checked_rem(), since the same logic applies.
|
I'm happy to update this for the /=, %, and %= cases, I just wanted to ensure that this is what you had in mind. I updated the tests with the listed negative examples and enforced first use/purity of divisor expressions since I think it's more trickier and extension to other ops should be mechanical and straightforward. Is this what you intended? @rustbot ready |
| fn is_integer(cx: &LateContext<'_>, expr: &Expr<'_>) -> bool { | ||
| matches!( | ||
| cx.typeck_results().expr_ty(expr).peel_refs().kind(), | ||
| ty::Uint(_) | ty::Int(_) | ||
| ) | ||
| } |
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 can't lint signed and unsigned values with the same values since MIN / -1 also returns None in checked_div.
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.
So I gave this some thought, and we can make it so that for signed, only accept conditions of the form b > 0 or 0 < b (depending on which side zero is on). We would then skip linting if the condition is b != 0, b == 0, or anything else. Again, this is for signed. Is this what you had in mind? Any other cases I should be aware of?
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 only additional case returning None is MIN / -1 so you'll have to check for that in addition to the rhs being zero. You can't change the meaning of the code for a lint like this if it's enabled by default.
changelog: [
manual_checked_div]: new lint suggestingchecked_divinstead of manual zero checks before unsigned divisionImplements the manual checked div from #12894. I'm relatively new to Rust and a complete newbie at Clippy, so my apologies if I forgot anything. I looked through the linked issue and could not find anything talking about the above lint.