Skip to content

Commit 3c7f816

Browse files
committed
feat(lints): add unsigned_subtraction_gt_zero lint for comparisons against zero after subtraction
1 parent 2a9eb78 commit 3c7f816

File tree

7 files changed

+192
-0
lines changed

7 files changed

+192
-0
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6759,6 +6759,7 @@ Released 2018-09-13
67596759
[`unsafe_removed_from_name`]: https://rust-lang.github.io/rust-clippy/master/index.html#unsafe_removed_from_name
67606760
[`unsafe_vector_initialization`]: https://rust-lang.github.io/rust-clippy/master/index.html#unsafe_vector_initialization
67616761
[`unseparated_literal_suffix`]: https://rust-lang.github.io/rust-clippy/master/index.html#unseparated_literal_suffix
6762+
[`unsigned_subtraction_gt_zero`]: https://rust-lang.github.io/rust-clippy/master/index.html#unsigned_subtraction_gt_zero
67626763
[`unsound_collection_transmute`]: https://rust-lang.github.io/rust-clippy/master/index.html#unsound_collection_transmute
67636764
[`unstable_as_mut_slice`]: https://rust-lang.github.io/rust-clippy/master/index.html#unstable_as_mut_slice
67646765
[`unstable_as_slice`]: https://rust-lang.github.io/rust-clippy/master/index.html#unstable_as_slice

clippy_lints/src/declared_lints.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -600,6 +600,7 @@ pub static LINTS: &[&::declare_clippy_lint::LintInfo] = &[
600600
crate::operators::OP_REF_INFO,
601601
crate::operators::REDUNDANT_COMPARISONS_INFO,
602602
crate::operators::SELF_ASSIGNMENT_INFO,
603+
crate::operators::UNSIGNED_SUBTRACTION_GT_ZERO_INFO,
603604
crate::operators::VERBOSE_BIT_MASK_INFO,
604605
crate::option_env_unwrap::OPTION_ENV_UNWRAP_INFO,
605606
crate::option_if_let_else::OPTION_IF_LET_ELSE_INFO,

clippy_lints/src/operators/mod.rs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ mod needless_bitwise_bool;
2020
mod numeric_arithmetic;
2121
mod op_ref;
2222
mod self_assignment;
23+
mod unsigned_subtraction_gt_zero;
2324
mod verbose_bit_mask;
2425

2526
pub(crate) mod arithmetic_side_effects;
@@ -62,6 +63,34 @@ declare_clippy_lint! {
6263
"a comparison with a maximum or minimum value that is always true or false"
6364
}
6465

66+
declare_clippy_lint! {
67+
/// ### What it does
68+
/// Lints comparisons of an unsigned integer subtraction against zero, like `(a - b) > 0`.
69+
///
70+
/// ### Why is this bad?
71+
/// If `a < b`, `a - b` will panic in debug builds and wrap in release builds, making the
72+
/// comparison effectively behave like `a != b`. This is likely unintended; `a > b` is clearer
73+
/// and avoids potential panics and wraparound.
74+
///
75+
/// ### Example
76+
/// ```no_run
77+
/// let (a, b): (u32, u32) = (1, 2);
78+
/// if a - b > 0 {}
79+
/// ```
80+
///
81+
/// Use instead:
82+
/// ```no_run
83+
/// let (a, b): (u32, u32) = (1, 2);
84+
/// if a > b {}
85+
/// // Or, if you meant inequality:
86+
/// if a != b {}
87+
/// ```
88+
#[clippy::version = "1.92.0"]
89+
pub UNSIGNED_SUBTRACTION_GT_ZERO,
90+
correctness,
91+
"suspicious comparison of unsigned subtraction to zero"
92+
}
93+
6594
declare_clippy_lint! {
6695
/// ### What it does
6796
/// Checks any kind of arithmetic operation of any type.
@@ -906,6 +935,7 @@ impl_lint_pass!(Operators => [
906935
SELF_ASSIGNMENT,
907936
MANUAL_MIDPOINT,
908937
MANUAL_IS_MULTIPLE_OF,
938+
UNSIGNED_SUBTRACTION_GT_ZERO,
909939
]);
910940

911941
impl<'tcx> LateLintPass<'tcx> for Operators {
@@ -932,6 +962,7 @@ impl<'tcx> LateLintPass<'tcx> for Operators {
932962
const_comparisons::check(cx, op, lhs, rhs, e.span);
933963
duration_subsec::check(cx, e, op.node, lhs, rhs);
934964
float_equality_without_abs::check(cx, e, op.node, lhs, rhs);
965+
unsigned_subtraction_gt_zero::check(cx, e, op.node, lhs, rhs);
935966
integer_division::check(cx, e, op.node, lhs, rhs);
936967
cmp_owned::check(cx, op.node, lhs, rhs);
937968
float_cmp::check(cx, e, op.node, lhs, rhs);
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
use clippy_utils::consts::is_zero_integer_const;
2+
use clippy_utils::diagnostics::span_lint_and_then;
3+
use clippy_utils::sugg::Sugg;
4+
use rustc_errors::Applicability;
5+
use rustc_hir::{BinOpKind, Expr, ExprKind};
6+
use rustc_lint::LateContext;
7+
use rustc_middle::ty;
8+
9+
use super::UNSIGNED_SUBTRACTION_GT_ZERO;
10+
11+
pub(super) fn check<'tcx>(
12+
cx: &LateContext<'tcx>,
13+
expr: &'tcx Expr<'tcx>,
14+
op: BinOpKind,
15+
lhs: &'tcx Expr<'tcx>,
16+
rhs: &'tcx Expr<'tcx>,
17+
) {
18+
// Avoid linting macro-generated code to reduce noise
19+
if expr.span.from_expansion() {
20+
return;
21+
}
22+
23+
// Only consider strict relational comparisons where one side is zero and the other is a subtraction
24+
let sub_expr = match op {
25+
BinOpKind::Gt if is_zero_integer_const(cx, rhs, expr.span.ctxt()) => lhs,
26+
BinOpKind::Lt if is_zero_integer_const(cx, lhs, expr.span.ctxt()) => rhs,
27+
_ => return,
28+
};
29+
30+
// Ensure the compared expression is a subtraction
31+
let (a, b) = match sub_expr.kind {
32+
ExprKind::Binary(sub_op, a, b) if sub_op.node == BinOpKind::Sub => (a, b),
33+
_ => return,
34+
};
35+
36+
// Subtraction result type must be an unsigned primitive
37+
if !matches!(cx.typeck_results().expr_ty(sub_expr).peel_refs().kind(), ty::Uint(_)) {
38+
return;
39+
}
40+
41+
// Suggest `a > b` preserving user formatting with parentheses as needed
42+
let mut app = Applicability::MaybeIncorrect;
43+
let (left_sugg, right_sugg) = (
44+
Sugg::hir_with_applicability(cx, a, "_", &mut app).maybe_paren(),
45+
Sugg::hir_with_applicability(cx, b, "_", &mut app).maybe_paren(),
46+
);
47+
let replacement = format!("{left_sugg} > {right_sugg}");
48+
let neq_suggestion = format!("{left_sugg} != {right_sugg}");
49+
50+
span_lint_and_then(
51+
cx,
52+
UNSIGNED_SUBTRACTION_GT_ZERO,
53+
expr.span,
54+
"suspicious comparison of unsigned subtraction to zero",
55+
|diag| {
56+
diag.help("`a - b > 0` will panic in debug when a < b and wrap in release; `a > b` is clearer");
57+
diag.help(format!("if you meant inequality, use {neq_suggestion}"));
58+
diag.span_suggestion(expr.span, "try", replacement, app);
59+
},
60+
);
61+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
#![warn(clippy::unsigned_subtraction_gt_zero)]
2+
#![allow(clippy::needless_if)]
3+
4+
fn main() {
5+
let (a, b): (u32, u32) = (1, 2);
6+
if a > b {}
7+
//~^ unsigned_subtraction_gt_zero
8+
9+
if a > b {}
10+
//~^ unsigned_subtraction_gt_zero
11+
12+
let (x, y): (usize, usize) = (10, 3);
13+
if x > y {}
14+
//~^ unsigned_subtraction_gt_zero
15+
16+
if x > y {}
17+
//~^ unsigned_subtraction_gt_zero
18+
19+
// signed: should not lint
20+
let (i, j): (i32, i32) = (1, 2);
21+
if i - j > 0 {}
22+
23+
// float: should not lint
24+
let (f, g): (f32, f32) = (1.0, 2.0);
25+
if f - g > 0.0 {}
26+
27+
// using saturating_sub: should not lint
28+
if a.saturating_sub(b) > 0 {}
29+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
#![warn(clippy::unsigned_subtraction_gt_zero)]
2+
#![allow(clippy::needless_if)]
3+
4+
fn main() {
5+
let (a, b): (u32, u32) = (1, 2);
6+
if a - b > 0 {}
7+
//~^ unsigned_subtraction_gt_zero
8+
9+
if 0 < a - b {}
10+
//~^ unsigned_subtraction_gt_zero
11+
12+
let (x, y): (usize, usize) = (10, 3);
13+
if x - y > 0 {}
14+
//~^ unsigned_subtraction_gt_zero
15+
16+
if 0 < x - y {}
17+
//~^ unsigned_subtraction_gt_zero
18+
19+
// signed: should not lint
20+
let (i, j): (i32, i32) = (1, 2);
21+
if i - j > 0 {}
22+
23+
// float: should not lint
24+
let (f, g): (f32, f32) = (1.0, 2.0);
25+
if f - g > 0.0 {}
26+
27+
// using saturating_sub: should not lint
28+
if a.saturating_sub(b) > 0 {}
29+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
error: suspicious comparison of unsigned subtraction to zero
2+
--> tests/ui/unsigned_subtraction_gt_zero.rs:6:8
3+
|
4+
LL | if a - b > 0 {}
5+
| ^^^^^^^^^ help: try: `a > b`
6+
|
7+
= help: `a - b > 0` will panic in debug when a < b and wrap in release; `a > b` is clearer
8+
= help: if you meant inequality, use a != b
9+
= note: `-D clippy::unsigned-subtraction-gt-zero` implied by `-D warnings`
10+
= help: to override `-D warnings` add `#[allow(clippy::unsigned_subtraction_gt_zero)]`
11+
12+
error: suspicious comparison of unsigned subtraction to zero
13+
--> tests/ui/unsigned_subtraction_gt_zero.rs:9:8
14+
|
15+
LL | if 0 < a - b {}
16+
| ^^^^^^^^^ help: try: `a > b`
17+
|
18+
= help: `a - b > 0` will panic in debug when a < b and wrap in release; `a > b` is clearer
19+
= help: if you meant inequality, use a != b
20+
21+
error: suspicious comparison of unsigned subtraction to zero
22+
--> tests/ui/unsigned_subtraction_gt_zero.rs:13:8
23+
|
24+
LL | if x - y > 0 {}
25+
| ^^^^^^^^^ help: try: `x > y`
26+
|
27+
= help: `a - b > 0` will panic in debug when a < b and wrap in release; `a > b` is clearer
28+
= help: if you meant inequality, use x != y
29+
30+
error: suspicious comparison of unsigned subtraction to zero
31+
--> tests/ui/unsigned_subtraction_gt_zero.rs:16:8
32+
|
33+
LL | if 0 < x - y {}
34+
| ^^^^^^^^^ help: try: `x > y`
35+
|
36+
= help: `a - b > 0` will panic in debug when a < b and wrap in release; `a > b` is clearer
37+
= help: if you meant inequality, use x != y
38+
39+
error: aborting due to 4 previous errors
40+

0 commit comments

Comments
 (0)