-
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
Open
amerikrainian
wants to merge
6
commits into
rust-lang:master
Choose a base branch
from
amerikrainian:manual-checked-div
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+457
−0
Open
Add manual_checked_div lint #16149
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
bea60f9
Add manual checked div lint
amerikrainian 34c252c
Drop manual_checked_div changelog edit
amerikrainian 1316c0e
Run cargo dev update_lints
amerikrainian 898e816
Make checked div work on signed ints and collect multiple divisions
amerikrainian 428d901
Enforced divisor purity and first use
amerikrainian aef068a
Handle min and -1 case
amerikrainian File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,234 @@ | ||
| use clippy_utils::consts::{ConstEvalCtxt, FullInt}; | ||
| use clippy_utils::diagnostics::span_lint_and_then; | ||
| use clippy_utils::sugg::Sugg; | ||
| use clippy_utils::visitors::{Descend, for_each_expr_without_closures}; | ||
| use clippy_utils::{SpanlessEq, is_integer_literal}; | ||
| use rustc_ast::{LitIntType, LitKind}; | ||
| use rustc_errors::{Applicability, MultiSpan}; | ||
| use rustc_hir::{BinOpKind, Block, Expr, ExprKind, UnOp}; | ||
| use rustc_lint::{LateContext, LateLintPass}; | ||
| use rustc_middle::ty; | ||
| use rustc_session::declare_lint_pass; | ||
| use rustc_span::source_map::Spanned; | ||
| use std::ops::ControlFlow; | ||
|
|
||
| declare_clippy_lint! { | ||
| /// ### What it does | ||
| /// Detects manual zero checks before dividing integers, such as `if x != 0 { y / x }`. | ||
| /// | ||
| /// ### Why is this bad? | ||
| /// `checked_div` already handles the zero case and makes the intent clearer while avoiding a | ||
| /// panic from a manual division. | ||
| /// | ||
| /// ### Example | ||
| /// ```no_run | ||
| /// # let (a, b) = (10u32, 5u32); | ||
| /// if b != 0 { | ||
| /// let result = a / b; | ||
| /// println!("{result}"); | ||
| /// } | ||
| /// ``` | ||
| /// Use instead: | ||
| /// ```no_run | ||
| /// # let (a, b) = (10u32, 5u32); | ||
| /// if let Some(result) = a.checked_div(b) { | ||
| /// println!("{result}"); | ||
| /// } | ||
| /// ``` | ||
| #[clippy::version = "1.93.0"] | ||
| pub MANUAL_CHECKED_DIV, | ||
| nursery, | ||
| "manual zero checks before dividing integers" | ||
| } | ||
| declare_lint_pass!(ManualCheckedDiv => [MANUAL_CHECKED_DIV]); | ||
|
|
||
| #[derive(Copy, Clone)] | ||
| enum NonZeroBranch { | ||
| Then, | ||
| Else, | ||
| } | ||
|
|
||
| impl LateLintPass<'_> for ManualCheckedDiv { | ||
| fn check_expr(&mut self, cx: &LateContext<'_>, expr: &Expr<'_>) { | ||
| if let ExprKind::If(cond, then, r#else) = expr.kind | ||
| && !expr.span.from_expansion() | ||
| && let Some((divisor, branch)) = divisor_from_condition(cond) | ||
| && is_integer(cx, divisor) | ||
| && let Some(block) = branch_block(then, r#else, branch) | ||
| { | ||
| let divisor_excludes_minus_one = excludes_minus_one(cx, cond, divisor); | ||
| let mut eq = SpanlessEq::new(cx).deny_side_effects().paths_by_resolution(); | ||
| if !eq.eq_expr(divisor, divisor) { | ||
| return; | ||
| } | ||
|
|
||
| let mut applicability = Applicability::MaybeIncorrect; | ||
| let mut divisions = Vec::new(); | ||
| let mut first_use = None; | ||
|
|
||
| let found_early_use = for_each_expr_without_closures(block, |e| { | ||
| if let ExprKind::Binary(binop, lhs, rhs) = e.kind | ||
| && binop.node == BinOpKind::Div | ||
| && eq.eq_expr(rhs, divisor) | ||
| && is_integer(cx, lhs) | ||
| { | ||
| let lhs_ty = cx.typeck_results().expr_ty(lhs); | ||
| if let ty::Int(int_ty) = lhs_ty.peel_refs().kind() | ||
| && !divisor_excludes_minus_one | ||
| && min_and_minus_one(cx, lhs, rhs, *int_ty) | ||
| { | ||
| return ControlFlow::Break(()); | ||
| } | ||
|
|
||
| match first_use { | ||
| None => first_use = Some(UseKind::Division), | ||
| Some(UseKind::Other) => return ControlFlow::Break(()), | ||
| Some(UseKind::Division) => {}, | ||
| } | ||
|
|
||
| let lhs_snip = Sugg::hir_with_applicability(cx, lhs, "_", &mut applicability); | ||
| let rhs_snip = Sugg::hir_with_applicability(cx, rhs, "_", &mut applicability); | ||
| let lhs_sugg = lhs_snip.maybe_paren().to_string(); | ||
| let type_suffix = if matches!( | ||
| lhs.kind, | ||
| ExprKind::Lit(Spanned { | ||
| node: LitKind::Int(_, LitIntType::Unsuffixed), | ||
| .. | ||
| }) | ExprKind::Unary( | ||
| UnOp::Neg, | ||
| Expr { | ||
| kind: ExprKind::Lit(Spanned { | ||
| node: LitKind::Int(_, LitIntType::Unsuffixed), | ||
| .. | ||
| }), | ||
| .. | ||
| } | ||
| ) | ||
| ) { | ||
| format!("_{lhs_ty}") | ||
| } else { | ||
| String::new() | ||
| }; | ||
| let suggestion = format!("{lhs_sugg}{type_suffix}.checked_div({rhs_snip})"); | ||
| divisions.push((e.span, suggestion)); | ||
|
|
||
| ControlFlow::<(), _>::Continue(Descend::No) | ||
| } else if eq.eq_expr(e, divisor) { | ||
| if first_use.is_none() { | ||
| first_use = Some(UseKind::Other); | ||
| } | ||
| ControlFlow::<(), _>::Continue(Descend::Yes) | ||
| } else { | ||
| ControlFlow::<(), _>::Continue(Descend::Yes) | ||
| } | ||
| }); | ||
|
|
||
| if found_early_use.is_some() || first_use != Some(UseKind::Division) || divisions.is_empty() { | ||
| return; | ||
| } | ||
|
|
||
| let mut spans: Vec<_> = divisions.iter().map(|(span, _)| *span).collect(); | ||
| spans.push(cond.span); | ||
| span_lint_and_then( | ||
| cx, | ||
| MANUAL_CHECKED_DIV, | ||
| MultiSpan::from_spans(spans), | ||
| "manual checked division", | ||
| |diag| { | ||
| diag.multipart_suggestion("consider using `checked_div`", divisions, applicability); | ||
| }, | ||
| ); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| #[derive(Copy, Clone, Eq, PartialEq)] | ||
| enum UseKind { | ||
| Division, | ||
| Other, | ||
| } | ||
|
|
||
| fn divisor_from_condition<'tcx>(cond: &'tcx Expr<'tcx>) -> Option<(&'tcx Expr<'tcx>, NonZeroBranch)> { | ||
| let ExprKind::Binary(binop, lhs, rhs) = cond.kind else { | ||
| return None; | ||
| }; | ||
|
|
||
| match binop.node { | ||
| BinOpKind::Ne | BinOpKind::Lt if is_zero(lhs) => Some((rhs, NonZeroBranch::Then)), | ||
| BinOpKind::Ne | BinOpKind::Gt if is_zero(rhs) => Some((lhs, NonZeroBranch::Then)), | ||
| BinOpKind::Eq if is_zero(lhs) => Some((rhs, NonZeroBranch::Else)), | ||
| BinOpKind::Eq if is_zero(rhs) => Some((lhs, NonZeroBranch::Else)), | ||
| _ => None, | ||
| } | ||
| } | ||
|
|
||
| fn branch_block<'tcx>( | ||
| then: &'tcx Expr<'tcx>, | ||
| r#else: Option<&'tcx Expr<'tcx>>, | ||
| branch: NonZeroBranch, | ||
| ) -> Option<&'tcx Block<'tcx>> { | ||
| match branch { | ||
| NonZeroBranch::Then => match then.kind { | ||
| ExprKind::Block(block, _) => Some(block), | ||
| _ => None, | ||
| }, | ||
| NonZeroBranch::Else => match r#else?.kind { | ||
| ExprKind::Block(block, _) => Some(block), | ||
| _ => None, | ||
| }, | ||
| } | ||
| } | ||
|
|
||
| fn is_zero(expr: &Expr<'_>) -> bool { | ||
| is_integer_literal(expr, 0) | ||
| } | ||
|
|
||
| fn min_and_minus_one(cx: &LateContext<'_>, lhs: &Expr<'_>, rhs: &Expr<'_>, int_ty: ty::IntTy) -> bool { | ||
| let lhs_val = int_value(cx, lhs); | ||
| let rhs_val = int_value(cx, rhs); | ||
|
|
||
| let lhs_maybe_min = lhs_val.is_none_or(|val| match val { | ||
| FullInt::S(signed) => signed == int_min_value(int_ty, cx), | ||
| FullInt::U(_) => false, | ||
| }); | ||
| let rhs_maybe_minus_one = rhs_val.is_none_or(|val| matches!(val, FullInt::S(-1))); | ||
|
|
||
| lhs_maybe_min && rhs_maybe_minus_one | ||
| } | ||
|
|
||
| fn int_min_value(int_ty: ty::IntTy, cx: &LateContext<'_>) -> i128 { | ||
| let bits = match int_ty { | ||
| ty::IntTy::Isize => cx.tcx.data_layout.pointer_size().bits(), | ||
| ty::IntTy::I8 => 8, | ||
| ty::IntTy::I16 => 16, | ||
| ty::IntTy::I32 => 32, | ||
| ty::IntTy::I64 => 64, | ||
| ty::IntTy::I128 => 128, | ||
| }; | ||
| -(1_i128 << (bits - 1)) | ||
| } | ||
|
|
||
| fn int_value(cx: &LateContext<'_>, expr: &Expr<'_>) -> Option<FullInt> { | ||
| let ecx = ConstEvalCtxt::new(cx); | ||
| ecx.eval_full_int(expr, expr.span.ctxt()) | ||
| } | ||
|
|
||
| fn excludes_minus_one(cx: &LateContext<'_>, cond: &Expr<'_>, divisor: &Expr<'_>) -> bool { | ||
| let ExprKind::Binary(binop, lhs, rhs) = cond.kind else { | ||
| return false; | ||
| }; | ||
|
|
||
| let mut eq = SpanlessEq::new(cx).deny_side_effects().paths_by_resolution(); | ||
| match binop.node { | ||
| BinOpKind::Gt if is_zero(rhs) && eq.eq_expr(lhs, divisor) => true, | ||
| BinOpKind::Lt if is_zero(lhs) && eq.eq_expr(rhs, divisor) => true, | ||
| _ => false, | ||
| } | ||
| } | ||
|
|
||
| fn is_integer(cx: &LateContext<'_>, expr: &Expr<'_>) -> bool { | ||
| matches!( | ||
| cx.typeck_results().expr_ty(expr).peel_refs().kind(), | ||
| ty::Uint(_) | ty::Int(_) | ||
| ) | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,81 @@ | ||
| #![warn(clippy::manual_checked_div)] | ||
|
|
||
| fn main() { | ||
| let a = 10u32; | ||
| let mut b = 5u32; | ||
|
|
||
| // Should trigger lint | ||
| if b != 0 { | ||
| //~^ manual_checked_div | ||
| let _result = a.checked_div(b); | ||
| let _another = (a + 1).checked_div(b); | ||
| } | ||
|
|
||
| if b > 0 { | ||
| //~^ manual_checked_div | ||
| let _result = a.checked_div(b); | ||
| } | ||
|
|
||
| if b == 0 { | ||
| //~^ manual_checked_div | ||
| println!("zero"); | ||
| } else { | ||
| let _result = a.checked_div(b); | ||
| } | ||
|
|
||
| // Should NOT trigger (already using checked_div) | ||
| if let Some(result) = b.checked_div(a) { | ||
| println!("{result}"); | ||
| } | ||
|
|
||
| let c = -5i32; | ||
| if c != 0 { | ||
| //~^ manual_checked_div | ||
| let _result = 10_i32.checked_div(c); | ||
| } | ||
|
|
||
| // Should NOT trigger (side effects in divisor) | ||
| if counter() > 0 { | ||
| let _ = 32 / counter(); | ||
| } | ||
|
|
||
| // Should NOT trigger (divisor used before division) | ||
| if b > 0 { | ||
| use_value(b); | ||
| let _ = a / b; | ||
| } | ||
|
|
||
| let signed_min = i32::MIN; | ||
| let mut signed_div: i32 = -1; | ||
|
|
||
| // Should NOT trigger (would change behavior for MIN / -1) | ||
| if signed_div != 0 { | ||
| let _ = signed_min / signed_div; | ||
| } | ||
|
|
||
| signed_div = 2; | ||
|
|
||
| if signed_div > 0 { | ||
| //~^ manual_checked_div | ||
| let _ = signed_min.checked_div(signed_div); | ||
| } | ||
|
|
||
| // Should NOT trigger (divisor may change during evaluation) | ||
| if b > 0 { | ||
| g(inc_and_return_value(&mut b), a / b); | ||
| } | ||
| } | ||
|
|
||
| fn counter() -> u32 { | ||
| println!("counter"); | ||
| 1 | ||
| } | ||
|
|
||
| fn use_value(_v: u32) {} | ||
|
|
||
| fn inc_and_return_value(x: &mut u32) -> u32 { | ||
| *x += 1; | ||
| *x | ||
| } | ||
|
|
||
| fn g(_lhs: u32, _rhs: u32) {} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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 / -1also returnsNoneinchecked_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
NoneisMIN / -1so 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.