diff --git a/CHANGELOG.md b/CHANGELOG.md index 91d793489be2..ad2c264021ae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6607,6 +6607,7 @@ Released 2018-09-13 [`manual_async_fn`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_async_fn [`manual_bits`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_bits [`manual_c_str_literals`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_c_str_literals +[`manual_checked_div`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_checked_div [`manual_clamp`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_clamp [`manual_contains`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_contains [`manual_dangling_ptr`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_dangling_ptr diff --git a/clippy_lints/src/declared_lints.rs b/clippy_lints/src/declared_lints.rs index 6b68940c6423..6e9baf9ed1e0 100644 --- a/clippy_lints/src/declared_lints.rs +++ b/clippy_lints/src/declared_lints.rs @@ -296,6 +296,7 @@ pub static LINTS: &[&::declare_clippy_lint::LintInfo] = &[ crate::manual_assert::MANUAL_ASSERT_INFO, crate::manual_async_fn::MANUAL_ASYNC_FN_INFO, crate::manual_bits::MANUAL_BITS_INFO, + crate::manual_checked_div::MANUAL_CHECKED_DIV_INFO, crate::manual_clamp::MANUAL_CLAMP_INFO, crate::manual_float_methods::MANUAL_IS_FINITE_INFO, crate::manual_float_methods::MANUAL_IS_INFINITE_INFO, diff --git a/clippy_lints/src/lib.rs b/clippy_lints/src/lib.rs index a957afdb1910..172525291a82 100644 --- a/clippy_lints/src/lib.rs +++ b/clippy_lints/src/lib.rs @@ -198,6 +198,7 @@ mod manual_abs_diff; mod manual_assert; mod manual_async_fn; mod manual_bits; +mod manual_checked_div; mod manual_clamp; mod manual_float_methods; mod manual_hash_one; @@ -857,6 +858,7 @@ pub fn register_lint_passes(store: &mut rustc_lint::LintStore, conf: &'static Co Box::new(|_| Box::::default()), Box::new(move |_| Box::new(manual_ilog2::ManualIlog2::new(conf))), Box::new(|_| Box::new(same_length_and_capacity::SameLengthAndCapacity)), + Box::new(|_| Box::new(manual_checked_div::ManualCheckedDiv)), // add late passes here, used by `cargo dev new_lint` ]; store.late_passes.extend(late_lints); diff --git a/clippy_lints/src/manual_checked_div.rs b/clippy_lints/src/manual_checked_div.rs new file mode 100644 index 000000000000..427fa9417565 --- /dev/null +++ b/clippy_lints/src/manual_checked_div.rs @@ -0,0 +1,155 @@ +use clippy_utils::diagnostics::span_lint_and_help; +use clippy_utils::visitors::{Descend, for_each_expr_without_closures}; +use clippy_utils::{SpanlessEq, is_integer_literal}; +use rustc_errors::MultiSpan; +use rustc_hir::{BinOpKind, Block, Expr, ExprKind}; +use rustc_lint::{LateContext, LateLintPass}; +use rustc_middle::ty; +use rustc_session::declare_lint_pass; +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_unsigned_integer(cx, divisor) + && let Some(block) = branch_block(then, r#else, branch) + { + // This lint is intended for unsigned integers only. + // + // For signed integers, the most direct refactor to `checked_div` is often not + // semantically equivalent to the original guard. For example, `rhs > 0` deliberately + // excludes negative divisors, while `checked_div` would return `Some` for `rhs = -2`. + // Also, `checked_div` can return `None` for `MIN / -1`, which requires additional handling beyond + // the zero check. + let mut eq = SpanlessEq::new(cx).deny_side_effects().paths_by_resolution(); + if !eq.eq_expr(divisor, divisor) { + return; + } + + let mut division_spans = 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_unsigned_integer(cx, lhs) + { + match first_use { + None => first_use = Some(UseKind::Division), + Some(UseKind::Other) => return ControlFlow::Break(()), + Some(UseKind::Division) => {}, + } + + division_spans.push(e.span); + + 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) || division_spans.is_empty() { + return; + } + + division_spans.push(cond.span); + span_lint_and_help( + cx, + MANUAL_CHECKED_DIV, + MultiSpan::from_spans(division_spans), + "manual checked division", + None, + "consider using `checked_div`", + ); + } + } +} + +#[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 is_unsigned_integer(cx: &LateContext<'_>, expr: &Expr<'_>) -> bool { + matches!(cx.typeck_results().expr_ty(expr).peel_refs().kind(), ty::Uint(_)) +} diff --git a/tests/ui/manual_checked_div.rs b/tests/ui/manual_checked_div.rs new file mode 100644 index 000000000000..d22c5c9f2360 --- /dev/null +++ b/tests/ui/manual_checked_div.rs @@ -0,0 +1,80 @@ +#![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 / b; + let _another = (a + 1) / b; + } + + if b > 0 { + //~^ manual_checked_div + let _result = a / b; + } + + if b == 0 { + //~^ manual_checked_div + println!("zero"); + } else { + let _result = a / b; + } + + // Should NOT trigger (already using checked_div) + if let Some(result) = b.checked_div(a) { + println!("{result}"); + } + + // Should NOT trigger (signed integers are not linted) + let c = -5i32; + if c != 0 { + let _result = 10 / 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 (signed integers are not linted) + if signed_div != 0 { + let _ = signed_min / signed_div; + } + + signed_div = 2; + + if signed_div > 0 { + let _ = signed_min / 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) {} diff --git a/tests/ui/manual_checked_div.stderr b/tests/ui/manual_checked_div.stderr new file mode 100644 index 000000000000..24bdd5078b5d --- /dev/null +++ b/tests/ui/manual_checked_div.stderr @@ -0,0 +1,39 @@ +error: manual checked division + --> tests/ui/manual_checked_div.rs:8:8 + | +LL | if b != 0 { + | ^^^^^^ +LL | +LL | let _result = a / b; + | ^^^^^ +LL | let _another = (a + 1) / b; + | ^^^^^^^^^^^ + | + = help: consider using `checked_div` + = note: `-D clippy::manual-checked-div` implied by `-D warnings` + = help: to override `-D warnings` add `#[allow(clippy::manual_checked_div)]` + +error: manual checked division + --> tests/ui/manual_checked_div.rs:14:8 + | +LL | if b > 0 { + | ^^^^^ +LL | +LL | let _result = a / b; + | ^^^^^ + | + = help: consider using `checked_div` + +error: manual checked division + --> tests/ui/manual_checked_div.rs:19:8 + | +LL | if b == 0 { + | ^^^^^^ +... +LL | let _result = a / b; + | ^^^^^ + | + = help: consider using `checked_div` + +error: aborting due to 3 previous errors +