Skip to content

Commit fc95a21

Browse files
committed
Handle min and -1 case
1 parent 04e9ae5 commit fc95a21

File tree

4 files changed

+93
-2
lines changed

4 files changed

+93
-2
lines changed

clippy_lints/src/manual_checked_div.rs

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use clippy_utils::consts::{ConstEvalCtxt, FullInt};
12
use clippy_utils::diagnostics::span_lint_and_then;
23
use clippy_utils::sugg::Sugg;
34
use clippy_utils::visitors::{Descend, for_each_expr_without_closures};
@@ -55,6 +56,7 @@ impl LateLintPass<'_> for ManualCheckedDiv {
5556
&& is_integer(cx, divisor)
5657
&& let Some(block) = branch_block(then, r#else, branch)
5758
{
59+
let divisor_excludes_minus_one = excludes_minus_one(cx, cond, divisor);
5860
let mut eq = SpanlessEq::new(cx).deny_side_effects().paths_by_resolution();
5961
if !eq.eq_expr(divisor, divisor) {
6062
return;
@@ -70,6 +72,14 @@ impl LateLintPass<'_> for ManualCheckedDiv {
7072
&& eq.eq_expr(rhs, divisor)
7173
&& is_integer(cx, lhs)
7274
{
75+
let lhs_ty = cx.typeck_results().expr_ty(lhs);
76+
if let ty::Int(int_ty) = lhs_ty.peel_refs().kind()
77+
&& !divisor_excludes_minus_one
78+
&& min_and_minus_one(cx, lhs, rhs, *int_ty)
79+
{
80+
return ControlFlow::Break(());
81+
}
82+
7383
match first_use {
7484
None => first_use = Some(UseKind::Division),
7585
Some(UseKind::Other) => return ControlFlow::Break(()),
@@ -78,7 +88,6 @@ impl LateLintPass<'_> for ManualCheckedDiv {
7888

7989
let lhs_snip = Sugg::hir_with_applicability(cx, lhs, "_", &mut applicability);
8090
let rhs_snip = Sugg::hir_with_applicability(cx, rhs, "_", &mut applicability);
81-
let lhs_ty = cx.typeck_results().expr_ty(lhs);
8291
let lhs_sugg = lhs_snip.maybe_paren().to_string();
8392
let type_suffix = if matches!(
8493
lhs.kind,
@@ -174,6 +183,49 @@ fn is_zero(expr: &Expr<'_>) -> bool {
174183
is_integer_literal(expr, 0)
175184
}
176185

186+
fn min_and_minus_one(cx: &LateContext<'_>, lhs: &Expr<'_>, rhs: &Expr<'_>, int_ty: ty::IntTy) -> bool {
187+
let lhs_val = int_value(cx, lhs);
188+
let rhs_val = int_value(cx, rhs);
189+
190+
let lhs_maybe_min = lhs_val.is_none_or(|val| match val {
191+
FullInt::S(signed) => signed == int_min_value(int_ty, cx),
192+
FullInt::U(_) => false,
193+
});
194+
let rhs_maybe_minus_one = rhs_val.is_none_or(|val| matches!(val, FullInt::S(-1)));
195+
196+
lhs_maybe_min && rhs_maybe_minus_one
197+
}
198+
199+
fn int_min_value(int_ty: ty::IntTy, cx: &LateContext<'_>) -> i128 {
200+
let bits = match int_ty {
201+
ty::IntTy::Isize => cx.tcx.data_layout.pointer_size().bits(),
202+
ty::IntTy::I8 => 8,
203+
ty::IntTy::I16 => 16,
204+
ty::IntTy::I32 => 32,
205+
ty::IntTy::I64 => 64,
206+
ty::IntTy::I128 => 128,
207+
};
208+
-(1_i128 << (bits - 1))
209+
}
210+
211+
fn int_value(cx: &LateContext<'_>, expr: &Expr<'_>) -> Option<FullInt> {
212+
let ecx = ConstEvalCtxt::new(cx);
213+
ecx.eval_full_int(expr, expr.span.ctxt())
214+
}
215+
216+
fn excludes_minus_one(cx: &LateContext<'_>, cond: &Expr<'_>, divisor: &Expr<'_>) -> bool {
217+
let ExprKind::Binary(binop, lhs, rhs) = cond.kind else {
218+
return false;
219+
};
220+
221+
let mut eq = SpanlessEq::new(cx).deny_side_effects().paths_by_resolution();
222+
match binop.node {
223+
BinOpKind::Gt if is_zero(rhs) && eq.eq_expr(lhs, divisor) => true,
224+
BinOpKind::Lt if is_zero(lhs) && eq.eq_expr(rhs, divisor) => true,
225+
_ => false,
226+
}
227+
}
228+
177229
fn is_integer(cx: &LateContext<'_>, expr: &Expr<'_>) -> bool {
178230
matches!(
179231
cx.typeck_results().expr_ty(expr).peel_refs().kind(),

tests/ui/manual_checked_div.fixed

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,21 @@ fn main() {
4545
let _ = a / b;
4646
}
4747

48+
let signed_min = i32::MIN;
49+
let mut signed_div: i32 = -1;
50+
51+
// Should NOT trigger (would change behavior for MIN / -1)
52+
if signed_div != 0 {
53+
let _ = signed_min / signed_div;
54+
}
55+
56+
signed_div = 2;
57+
58+
if signed_div > 0 {
59+
//~^ manual_checked_div
60+
let _ = signed_min.checked_div(signed_div);
61+
}
62+
4863
// Should NOT trigger (divisor may change during evaluation)
4964
if b > 0 {
5065
g(inc_and_return_value(&mut b), a / b);

tests/ui/manual_checked_div.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,21 @@ fn main() {
4545
let _ = a / b;
4646
}
4747

48+
let signed_min = i32::MIN;
49+
let mut signed_div: i32 = -1;
50+
51+
// Should NOT trigger (would change behavior for MIN / -1)
52+
if signed_div != 0 {
53+
let _ = signed_min / signed_div;
54+
}
55+
56+
signed_div = 2;
57+
58+
if signed_div > 0 {
59+
//~^ manual_checked_div
60+
let _ = signed_min / signed_div;
61+
}
62+
4863
// Should NOT trigger (divisor may change during evaluation)
4964
if b > 0 {
5065
g(inc_and_return_value(&mut b), a / b);

tests/ui/manual_checked_div.stderr

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,5 +44,14 @@ LL |
4444
LL | let _result = 10 / c;
4545
| ^^^^^^ help: consider using `checked_div`: `10_i32.checked_div(c)`
4646

47-
error: aborting due to 4 previous errors
47+
error: manual checked division
48+
--> tests/ui/manual_checked_div.rs:58:8
49+
|
50+
LL | if signed_div > 0 {
51+
| ^^^^^^^^^^^^^^
52+
LL |
53+
LL | let _ = signed_min / signed_div;
54+
| ^^^^^^^^^^^^^^^^^^^^^^^ help: consider using `checked_div`: `signed_min.checked_div(signed_div)`
55+
56+
error: aborting due to 5 previous errors
4857

0 commit comments

Comments
 (0)