Skip to content

Commit e5cc3d7

Browse files
committed
Add manual checked div lint
1 parent e567e30 commit e5cc3d7

File tree

7 files changed

+233
-0
lines changed

7 files changed

+233
-0
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@ document.
88

99
[e9b7045...master](https://github.com/rust-lang/rust-clippy/compare/e9b7045...master)
1010

11+
### New Lints
12+
13+
* Added [`manual_checked_div`] to `nursery`
14+
1115
## Rust 1.91
1216

1317
Current stable, released 2025-10-30
@@ -6530,6 +6534,7 @@ Released 2018-09-13
65306534
[`manual_async_fn`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_async_fn
65316535
[`manual_bits`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_bits
65326536
[`manual_c_str_literals`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_c_str_literals
6537+
[`manual_checked_div`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_checked_div
65336538
[`manual_clamp`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_clamp
65346539
[`manual_contains`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_contains
65356540
[`manual_dangling_ptr`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_dangling_ptr

clippy_lints/src/declared_lints.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -295,6 +295,7 @@ pub static LINTS: &[&::declare_clippy_lint::LintInfo] = &[
295295
crate::manual_assert::MANUAL_ASSERT_INFO,
296296
crate::manual_async_fn::MANUAL_ASYNC_FN_INFO,
297297
crate::manual_bits::MANUAL_BITS_INFO,
298+
crate::manual_checked_div::MANUAL_CHECKED_DIV_INFO,
298299
crate::manual_clamp::MANUAL_CLAMP_INFO,
299300
crate::manual_float_methods::MANUAL_IS_FINITE_INFO,
300301
crate::manual_float_methods::MANUAL_IS_INFINITE_INFO,

clippy_lints/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,7 @@ mod manual_abs_diff;
197197
mod manual_assert;
198198
mod manual_async_fn;
199199
mod manual_bits;
200+
mod manual_checked_div;
200201
mod manual_clamp;
201202
mod manual_float_methods;
202203
mod manual_hash_one;
@@ -848,6 +849,7 @@ pub fn register_lint_passes(store: &mut rustc_lint::LintStore, conf: &'static Co
848849
Box::new(|_| Box::new(toplevel_ref_arg::ToplevelRefArg)),
849850
Box::new(|_| Box::new(volatile_composites::VolatileComposites)),
850851
Box::new(|_| Box::<replace_box::ReplaceBox>::default()),
852+
Box::new(|_| Box::new(manual_checked_div::ManualCheckedDiv)),
851853
// add late passes here, used by `cargo dev new_lint`
852854
];
853855
store.late_passes.extend(late_lints);
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
use clippy_utils::diagnostics::span_lint_and_sugg;
2+
use clippy_utils::sugg::Sugg;
3+
use clippy_utils::visitors::{Descend, for_each_expr_without_closures};
4+
use clippy_utils::{SpanlessEq, is_integer_literal};
5+
use rustc_errors::Applicability;
6+
use rustc_hir::{BinOpKind, Block, Expr, ExprKind};
7+
use rustc_lint::{LateContext, LateLintPass};
8+
use rustc_middle::ty;
9+
use rustc_session::declare_lint_pass;
10+
use std::ops::ControlFlow;
11+
12+
declare_clippy_lint! {
13+
/// ### What it does
14+
/// Detects manual zero checks before dividing unsigned integers, such as `if x != 0 { y / x }`.
15+
///
16+
/// ### Why is this bad?
17+
/// `checked_div` already handles the zero case and makes the intent clearer while avoiding a
18+
/// panic from a manual division.
19+
///
20+
/// ### Example
21+
/// ```no_run
22+
/// # let (a, b) = (10u32, 5u32);
23+
/// if b != 0 {
24+
/// let result = a / b;
25+
/// println!("{result}");
26+
/// }
27+
/// ```
28+
/// Use instead:
29+
/// ```no_run
30+
/// # let (a, b) = (10u32, 5u32);
31+
/// if let Some(result) = a.checked_div(b) {
32+
/// println!("{result}");
33+
/// }
34+
/// ```
35+
#[clippy::version = "1.93.0"]
36+
pub MANUAL_CHECKED_DIV,
37+
nursery,
38+
"manual zero checks before dividing unsigned integers"
39+
}
40+
declare_lint_pass!(ManualCheckedDiv => [MANUAL_CHECKED_DIV]);
41+
42+
#[derive(Copy, Clone)]
43+
enum NonZeroBranch {
44+
Then,
45+
Else,
46+
}
47+
48+
impl LateLintPass<'_> for ManualCheckedDiv {
49+
fn check_expr(&mut self, cx: &LateContext<'_>, expr: &Expr<'_>) {
50+
if expr.span.from_expansion() {
51+
return;
52+
}
53+
54+
if let ExprKind::If(cond, then, r#else) = expr.kind
55+
&& let Some((divisor, branch)) = divisor_from_condition(cond)
56+
&& is_unsigned(cx, divisor)
57+
{
58+
let Some(block) = branch_block(then, r#else, branch) else {
59+
return;
60+
};
61+
let mut eq = SpanlessEq::new(cx);
62+
63+
for_each_expr_without_closures(block, |e| {
64+
if let ExprKind::Binary(binop, lhs, rhs) = e.kind
65+
&& binop.node == BinOpKind::Div
66+
&& eq.eq_expr(rhs, divisor)
67+
&& is_unsigned(cx, lhs)
68+
{
69+
let mut applicability = Applicability::MaybeIncorrect;
70+
let lhs_snip = Sugg::hir_with_applicability(cx, lhs, "..", &mut applicability);
71+
let rhs_snip = Sugg::hir_with_applicability(cx, rhs, "..", &mut applicability);
72+
73+
span_lint_and_sugg(
74+
cx,
75+
MANUAL_CHECKED_DIV,
76+
e.span,
77+
"manual checked division",
78+
"consider using `checked_div`",
79+
format!("{}.checked_div({})", lhs_snip.maybe_paren(), rhs_snip),
80+
applicability,
81+
);
82+
83+
ControlFlow::<(), _>::Continue(Descend::No)
84+
} else {
85+
ControlFlow::<(), _>::Continue(Descend::Yes)
86+
}
87+
});
88+
}
89+
}
90+
}
91+
92+
fn divisor_from_condition<'tcx>(cond: &'tcx Expr<'tcx>) -> Option<(&'tcx Expr<'tcx>, NonZeroBranch)> {
93+
let ExprKind::Binary(binop, lhs, rhs) = cond.kind else {
94+
return None;
95+
};
96+
97+
match binop.node {
98+
BinOpKind::Ne | BinOpKind::Lt if is_zero(lhs) => Some((rhs, NonZeroBranch::Then)),
99+
BinOpKind::Ne | BinOpKind::Gt if is_zero(rhs) => Some((lhs, NonZeroBranch::Then)),
100+
BinOpKind::Eq if is_zero(lhs) => Some((rhs, NonZeroBranch::Else)),
101+
BinOpKind::Eq if is_zero(rhs) => Some((lhs, NonZeroBranch::Else)),
102+
_ => None,
103+
}
104+
}
105+
106+
fn branch_block<'tcx>(
107+
then: &'tcx Expr<'tcx>,
108+
r#else: Option<&'tcx Expr<'tcx>>,
109+
branch: NonZeroBranch,
110+
) -> Option<&'tcx Block<'tcx>> {
111+
match branch {
112+
NonZeroBranch::Then => {
113+
if let ExprKind::Block(block, _) = then.kind {
114+
Some(block)
115+
} else {
116+
None
117+
}
118+
},
119+
NonZeroBranch::Else => match r#else.map(|expr| &expr.kind) {
120+
Some(ExprKind::Block(block, _)) => Some(block),
121+
_ => None,
122+
},
123+
}
124+
}
125+
126+
fn is_zero(expr: &Expr<'_>) -> bool {
127+
is_integer_literal(expr, 0)
128+
}
129+
130+
fn is_unsigned(cx: &LateContext<'_>, expr: &Expr<'_>) -> bool {
131+
matches!(cx.typeck_results().expr_ty(expr).peel_refs().kind(), ty::Uint(_))
132+
}

tests/ui/manual_checked_div.fixed

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
#![warn(clippy::manual_checked_div)]
2+
3+
fn main() {
4+
let a = 10u32;
5+
let b = 5u32;
6+
7+
// Should trigger lint
8+
if b != 0 {
9+
let _result = a.checked_div(b);
10+
//~^ manual_checked_div
11+
}
12+
13+
if b > 0 {
14+
let _result = a.checked_div(b);
15+
//~^ manual_checked_div
16+
}
17+
18+
if b == 0 {
19+
println!("zero");
20+
} else {
21+
let _result = a.checked_div(b);
22+
//~^ manual_checked_div
23+
}
24+
25+
// Should NOT trigger (already using checked_div)
26+
if let Some(result) = b.checked_div(a) {
27+
println!("{result}");
28+
}
29+
30+
// Should NOT trigger (signed integers)
31+
let c = -5i32;
32+
if c != 0 {
33+
let _result = 10 / c;
34+
}
35+
}

tests/ui/manual_checked_div.rs

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
#![warn(clippy::manual_checked_div)]
2+
3+
fn main() {
4+
let a = 10u32;
5+
let b = 5u32;
6+
7+
// Should trigger lint
8+
if b != 0 {
9+
let _result = a / b;
10+
//~^ manual_checked_div
11+
}
12+
13+
if b > 0 {
14+
let _result = a / b;
15+
//~^ manual_checked_div
16+
}
17+
18+
if b == 0 {
19+
println!("zero");
20+
} else {
21+
let _result = a / b;
22+
//~^ manual_checked_div
23+
}
24+
25+
// Should NOT trigger (already using checked_div)
26+
if let Some(result) = b.checked_div(a) {
27+
println!("{result}");
28+
}
29+
30+
// Should NOT trigger (signed integers)
31+
let c = -5i32;
32+
if c != 0 {
33+
let _result = 10 / c;
34+
}
35+
}

tests/ui/manual_checked_div.stderr

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
error: manual checked division
2+
--> tests/ui/manual_checked_div.rs:9:23
3+
|
4+
LL | let _result = a / b;
5+
| ^^^^^ help: consider using `checked_div`: `a.checked_div(b)`
6+
|
7+
= note: `-D clippy::manual-checked-div` implied by `-D warnings`
8+
= help: to override `-D warnings` add `#[allow(clippy::manual_checked_div)]`
9+
10+
error: manual checked division
11+
--> tests/ui/manual_checked_div.rs:14:23
12+
|
13+
LL | let _result = a / b;
14+
| ^^^^^ help: consider using `checked_div`: `a.checked_div(b)`
15+
16+
error: manual checked division
17+
--> tests/ui/manual_checked_div.rs:21:23
18+
|
19+
LL | let _result = a / b;
20+
| ^^^^^ help: consider using `checked_div`: `a.checked_div(b)`
21+
22+
error: aborting due to 3 previous errors
23+

0 commit comments

Comments
 (0)