blob: 5fd1e27d5b9997e2c5217fff9f46aacfba5682c4 [file] [log] [blame] [edit]
use clippy_utils::diagnostics::span_lint_and_then;
use clippy_utils::visitors::{Descend, for_each_expr_without_closures};
use clippy_utils::{SpanlessEq, is_integer_literal};
use rustc_hir::{AssignOpKind, 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.95.0"]
pub MANUAL_CHECKED_OPS,
complexity,
"manual zero checks before dividing integers"
}
declare_lint_pass!(ManualCheckedOps => [MANUAL_CHECKED_OPS]);
#[derive(Copy, Clone)]
enum NonZeroBranch {
Then,
Else,
}
impl LateLintPass<'_> for ManualCheckedOps {
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)
// 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.
&& is_unsigned_integer(cx, divisor)
&& let Some(block) = branch_block(then, r#else, branch)
{
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 let ExprKind::AssignOp(op, lhs, rhs) = e.kind
&& op.node == AssignOpKind::DivAssign
&& 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);
return ControlFlow::Break(());
}
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;
}
span_lint_and_then(cx, MANUAL_CHECKED_OPS, cond.span, "manual checked division", |diag| {
diag.span_label(cond.span, "check performed here");
if let Some((first, rest)) = division_spans.split_first() {
diag.span_label(*first, "division performed here");
if !rest.is_empty() {
diag.span_labels(rest.to_vec(), "... and here");
}
}
diag.help("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(_))
}