From 74a2344dc12b5659357272f4cd8b6e837195ab24 Mon Sep 17 00:00:00 2001 From: Guillaume Gomez Date: Tue, 19 Mar 2024 16:51:40 +0100 Subject: [PATCH 1/6] Extend `implicit_saturation_sub` lint --- clippy_lints/src/implicit_saturating_sub.rs | 284 +++++++++++++++----- 1 file changed, 221 insertions(+), 63 deletions(-) diff --git a/clippy_lints/src/implicit_saturating_sub.rs b/clippy_lints/src/implicit_saturating_sub.rs index 127c73ed637..453ff09bf4f 100644 --- a/clippy_lints/src/implicit_saturating_sub.rs +++ b/clippy_lints/src/implicit_saturating_sub.rs @@ -1,11 +1,13 @@ -use clippy_utils::diagnostics::span_lint_and_sugg; -use clippy_utils::{higher, is_integer_literal, peel_blocks_with_stmt, SpanlessEq}; +use clippy_utils::diagnostics::{span_lint_and_sugg, span_lint_and_then}; +use clippy_utils::source::snippet_opt; +use clippy_utils::{higher, is_integer_literal, path_to_local, peel_blocks, peel_blocks_with_stmt, SpanlessEq}; use rustc_ast::ast::LitKind; use rustc_data_structures::packed::Pu128; use rustc_errors::Applicability; -use rustc_hir::{BinOpKind, Expr, ExprKind, QPath}; +use rustc_hir::{BinOp, BinOpKind, Expr, ExprKind, HirId, QPath}; use rustc_lint::{LateContext, LateLintPass}; use rustc_session::declare_lint_pass; +use rustc_span::Span; declare_clippy_lint! { /// ### What it does @@ -50,73 +52,229 @@ impl<'tcx> LateLintPass<'tcx> for ImplicitSaturatingSub { // Check if the conditional expression is a binary operation && let ExprKind::Binary(ref cond_op, cond_left, cond_right) = cond.kind - - // Ensure that the binary operator is >, !=, or < - && (BinOpKind::Ne == cond_op.node || BinOpKind::Gt == cond_op.node || BinOpKind::Lt == cond_op.node) - - // Check if assign operation is done - && let Some(target) = subtracts_one(cx, then) - - // Extracting out the variable name - && let ExprKind::Path(QPath::Resolved(_, ares_path)) = target.kind { - // Handle symmetric conditions in the if statement - let (cond_var, cond_num_val) = if SpanlessEq::new(cx).eq_expr(cond_left, target) { - if BinOpKind::Gt == cond_op.node || BinOpKind::Ne == cond_op.node { - (cond_left, cond_right) - } else { - return; - } - } else if SpanlessEq::new(cx).eq_expr(cond_right, target) { - if BinOpKind::Lt == cond_op.node || BinOpKind::Ne == cond_op.node { - (cond_right, cond_left) - } else { - return; - } + check_with_condition(cx, expr, cond_op.node, cond_left, cond_right, then); + } else if let Some(higher::If { + cond, + then: if_block, + r#else: Some(else_block), + }) = higher::If::hir(expr) + && let ExprKind::Binary(ref cond_op, cond_left, cond_right) = cond.kind + { + check_manual_check(cx, expr, cond_op, cond_left, cond_right, if_block, else_block); + } + } +} + +fn check_manual_check<'tcx>( + cx: &LateContext<'tcx>, + expr: &Expr<'tcx>, + condition: &BinOp, + left_hand: &Expr<'tcx>, + right_hand: &Expr<'tcx>, + if_block: &Expr<'tcx>, + else_block: &Expr<'tcx>, +) { + let ty = cx.typeck_results().expr_ty(left_hand); + if ty.is_numeric() && !ty.is_signed() { + match condition.node { + BinOpKind::Gt | BinOpKind::Ge => check_gt( + cx, + condition.span, + expr.span, + left_hand, + right_hand, + if_block, + else_block, + ), + BinOpKind::Lt | BinOpKind::Le => check_gt( + cx, + condition.span, + expr.span, + right_hand, + left_hand, + if_block, + else_block, + ), + _ => {}, + } + } +} + +fn check_gt( + cx: &LateContext<'_>, + condition_span: Span, + expr_span: Span, + big_var: &Expr<'_>, + little_var: &Expr<'_>, + if_block: &Expr<'_>, + else_block: &Expr<'_>, +) { + if let Some(big_var) = Var::new(big_var) + && let Some(little_var) = Var::new(little_var) + { + check_subtraction(cx, condition_span, expr_span, big_var, little_var, if_block, else_block); + } +} + +struct Var { + span: Span, + hir_id: HirId, +} + +impl Var { + fn new(expr: &Expr<'_>) -> Option { + path_to_local(expr).map(|hir_id| Self { + span: expr.span, + hir_id, + }) + } +} + +fn check_subtraction( + cx: &LateContext<'_>, + condition_span: Span, + expr_span: Span, + big_var: Var, + little_var: Var, + if_block: &Expr<'_>, + else_block: &Expr<'_>, +) { + let if_block = peel_blocks(if_block); + let else_block = peel_blocks(else_block); + if is_integer_literal(if_block, 0) { + // We need to check this case as well to prevent infinite recursion. + if is_integer_literal(else_block, 0) { + // Well, seems weird but who knows? + return; + } + // If the subtraction is done in the `else` block, then we need to also revert the two + // variables as it means that the check was reverted too. + check_subtraction(cx, condition_span, expr_span, little_var, big_var, else_block, if_block); + return; + } + if is_integer_literal(else_block, 0) + && let ExprKind::Binary(op, left, right) = if_block.kind + && let BinOpKind::Sub = op.node + { + let local_left = path_to_local(left); + let local_right = path_to_local(right); + if Some(big_var.hir_id) == local_left && Some(little_var.hir_id) == local_right { + // This part of the condition is voluntarily split from the one before to ensure that + // if `snippet_opt` fails, it won't try the next conditions. + if let Some(big_var_snippet) = snippet_opt(cx, big_var.span) + && let Some(little_var_snippet) = snippet_opt(cx, little_var.span) + { + span_lint_and_sugg( + cx, + IMPLICIT_SATURATING_SUB, + expr_span, + "manual arithmetic check found", + "replace it with", + format!("{big_var_snippet}.saturating_sub({little_var_snippet})"), + Applicability::MachineApplicable, + ); + } + } else if Some(little_var.hir_id) == local_left + && Some(big_var.hir_id) == local_right + && let Some(big_var_snippet) = snippet_opt(cx, big_var.span) + && let Some(little_var_snippet) = snippet_opt(cx, little_var.span) + { + span_lint_and_then( + cx, + IMPLICIT_SATURATING_SUB, + condition_span, + "inverted arithmetic check before subtraction", + |diag| { + diag.span_note( + if_block.span, + format!("this subtraction underflows when `{little_var_snippet} < {big_var_snippet}`"), + ); + diag.span_suggestion( + if_block.span, + "try replacing it with", + format!("{big_var_snippet} - {little_var_snippet}"), + Applicability::MaybeIncorrect, + ); + }, + ); + } + } +} + +fn check_with_condition<'tcx>( + cx: &LateContext<'tcx>, + expr: &Expr<'tcx>, + cond_op: BinOpKind, + cond_left: &Expr<'tcx>, + cond_right: &Expr<'tcx>, + then: &Expr<'tcx>, +) { + // Ensure that the binary operator is >, !=, or < + if (BinOpKind::Ne == cond_op || BinOpKind::Gt == cond_op || BinOpKind::Lt == cond_op) + + // Check if assign operation is done + && let Some(target) = subtracts_one(cx, then) + + // Extracting out the variable name + && let ExprKind::Path(QPath::Resolved(_, ares_path)) = target.kind + { + // Handle symmetric conditions in the if statement + let (cond_var, cond_num_val) = if SpanlessEq::new(cx).eq_expr(cond_left, target) { + if BinOpKind::Gt == cond_op || BinOpKind::Ne == cond_op { + (cond_left, cond_right) } else { return; - }; - - // Check if the variable in the condition statement is an integer - if !cx.typeck_results().expr_ty(cond_var).is_integral() { + } + } else if SpanlessEq::new(cx).eq_expr(cond_right, target) { + if BinOpKind::Lt == cond_op || BinOpKind::Ne == cond_op { + (cond_right, cond_left) + } else { return; } + } else { + return; + }; - // Get the variable name - let var_name = ares_path.segments[0].ident.name.as_str(); - match cond_num_val.kind { - ExprKind::Lit(cond_lit) => { - // Check if the constant is zero - if let LitKind::Int(Pu128(0), _) = cond_lit.node { - if cx.typeck_results().expr_ty(cond_left).is_signed() { - } else { - print_lint_and_sugg(cx, var_name, expr); - }; - } - }, - ExprKind::Path(QPath::TypeRelative(_, name)) => { - if name.ident.as_str() == "MIN" - && let Some(const_id) = cx.typeck_results().type_dependent_def_id(cond_num_val.hir_id) - && let Some(impl_id) = cx.tcx.impl_of_method(const_id) - && let None = cx.tcx.impl_trait_ref(impl_id) // An inherent impl - && cx.tcx.type_of(impl_id).instantiate_identity().is_integral() - { + // Check if the variable in the condition statement is an integer + if !cx.typeck_results().expr_ty(cond_var).is_integral() { + return; + } + + // Get the variable name + let var_name = ares_path.segments[0].ident.name.as_str(); + match cond_num_val.kind { + ExprKind::Lit(cond_lit) => { + // Check if the constant is zero + if let LitKind::Int(Pu128(0), _) = cond_lit.node { + if cx.typeck_results().expr_ty(cond_left).is_signed() { + } else { print_lint_and_sugg(cx, var_name, expr); - } - }, - ExprKind::Call(func, []) => { - if let ExprKind::Path(QPath::TypeRelative(_, name)) = func.kind - && name.ident.as_str() == "min_value" - && let Some(func_id) = cx.typeck_results().type_dependent_def_id(func.hir_id) - && let Some(impl_id) = cx.tcx.impl_of_method(func_id) - && let None = cx.tcx.impl_trait_ref(impl_id) // An inherent impl - && cx.tcx.type_of(impl_id).instantiate_identity().is_integral() - { - print_lint_and_sugg(cx, var_name, expr); - } - }, - _ => (), - } + }; + } + }, + ExprKind::Path(QPath::TypeRelative(_, name)) => { + if name.ident.as_str() == "MIN" + && let Some(const_id) = cx.typeck_results().type_dependent_def_id(cond_num_val.hir_id) + && let Some(impl_id) = cx.tcx.impl_of_method(const_id) + && let None = cx.tcx.impl_trait_ref(impl_id) // An inherent impl + && cx.tcx.type_of(impl_id).instantiate_identity().is_integral() + { + print_lint_and_sugg(cx, var_name, expr); + } + }, + ExprKind::Call(func, []) => { + if let ExprKind::Path(QPath::TypeRelative(_, name)) = func.kind + && name.ident.as_str() == "min_value" + && let Some(func_id) = cx.typeck_results().type_dependent_def_id(func.hir_id) + && let Some(impl_id) = cx.tcx.impl_of_method(func_id) + && let None = cx.tcx.impl_trait_ref(impl_id) // An inherent impl + && cx.tcx.type_of(impl_id).instantiate_identity().is_integral() + { + print_lint_and_sugg(cx, var_name, expr); + } + }, + _ => (), } } } From 2622a87587ada00d6aac1d036da8a2f579a675ba Mon Sep 17 00:00:00 2001 From: Guillaume Gomez Date: Tue, 19 Mar 2024 16:59:08 +0100 Subject: [PATCH 2/6] Add ui regression tests for `implicit_saturation_sub` lint extension --- tests/ui/implicit_saturating_sub.rs | 7 +++ tests/ui/manual_arithmetic_check-2.rs | 35 +++++++++++ tests/ui/manual_arithmetic_check-2.stderr | 76 +++++++++++++++++++++++ tests/ui/manual_arithmetic_check.fixed | 16 +++++ tests/ui/manual_arithmetic_check.rs | 20 ++++++ tests/ui/manual_arithmetic_check.stderr | 29 +++++++++ 6 files changed, 183 insertions(+) create mode 100644 tests/ui/manual_arithmetic_check-2.rs create mode 100644 tests/ui/manual_arithmetic_check-2.stderr create mode 100644 tests/ui/manual_arithmetic_check.fixed create mode 100644 tests/ui/manual_arithmetic_check.rs create mode 100644 tests/ui/manual_arithmetic_check.stderr diff --git a/tests/ui/implicit_saturating_sub.rs b/tests/ui/implicit_saturating_sub.rs index 5d7b95d2c65..05f7d852383 100644 --- a/tests/ui/implicit_saturating_sub.rs +++ b/tests/ui/implicit_saturating_sub.rs @@ -260,4 +260,11 @@ fn main() { } else if u_32 > 0 { u_32 -= 1; } + + let result = if a < b { + println!("we shouldn't remove this"); + 0 + } else { + a - b + }; } diff --git a/tests/ui/manual_arithmetic_check-2.rs b/tests/ui/manual_arithmetic_check-2.rs new file mode 100644 index 00000000000..e97e3bdfef7 --- /dev/null +++ b/tests/ui/manual_arithmetic_check-2.rs @@ -0,0 +1,35 @@ +//@no-rustfix +#![warn(clippy::implicit_saturating_sub)] +#![allow(arithmetic_overflow)] + +fn main() { + let a = 12u32; + let b = 13u32; + + let result = if a > b { b - a } else { 0 }; + //~^ ERROR: inverted arithmetic check before subtraction + let result = if b < a { b - a } else { 0 }; + //~^ ERROR: inverted arithmetic check before subtraction + + let result = if a > b { 0 } else { a - b }; + //~^ ERROR: inverted arithmetic check before subtraction + let result = if a >= b { 0 } else { a - b }; + //~^ ERROR: inverted arithmetic check before subtraction + let result = if b < a { 0 } else { a - b }; + //~^ ERROR: inverted arithmetic check before subtraction + let result = if b <= a { 0 } else { a - b }; + //~^ ERROR: inverted arithmetic check before subtraction + + let af = 12f32; + let bf = 13f32; + // Should not lint! + let result = if bf < af { 0. } else { af - bf }; + + // Should not lint! + let result = if a < b { + println!("we shouldn't remove this"); + 0 + } else { + a - b + }; +} diff --git a/tests/ui/manual_arithmetic_check-2.stderr b/tests/ui/manual_arithmetic_check-2.stderr new file mode 100644 index 00000000000..d49e536ce3f --- /dev/null +++ b/tests/ui/manual_arithmetic_check-2.stderr @@ -0,0 +1,76 @@ +error: inverted arithmetic check before subtraction + --> tests/ui/manual_arithmetic_check-2.rs:9:23 + | +LL | let result = if a > b { b - a } else { 0 }; + | ^ ----- help: try replacing it with: `a - b` + | +note: this subtraction underflows when `b < a` + --> tests/ui/manual_arithmetic_check-2.rs:9:29 + | +LL | let result = if a > b { b - a } else { 0 }; + | ^^^^^ + = note: `-D clippy::implicit-saturating-sub` implied by `-D warnings` + = help: to override `-D warnings` add `#[allow(clippy::implicit_saturating_sub)]` + +error: inverted arithmetic check before subtraction + --> tests/ui/manual_arithmetic_check-2.rs:11:23 + | +LL | let result = if b < a { b - a } else { 0 }; + | ^ ----- help: try replacing it with: `a - b` + | +note: this subtraction underflows when `b < a` + --> tests/ui/manual_arithmetic_check-2.rs:11:29 + | +LL | let result = if b < a { b - a } else { 0 }; + | ^^^^^ + +error: inverted arithmetic check before subtraction + --> tests/ui/manual_arithmetic_check-2.rs:14:23 + | +LL | let result = if a > b { 0 } else { a - b }; + | ^ ----- help: try replacing it with: `b - a` + | +note: this subtraction underflows when `a < b` + --> tests/ui/manual_arithmetic_check-2.rs:14:40 + | +LL | let result = if a > b { 0 } else { a - b }; + | ^^^^^ + +error: inverted arithmetic check before subtraction + --> tests/ui/manual_arithmetic_check-2.rs:16:23 + | +LL | let result = if a >= b { 0 } else { a - b }; + | ^^ ----- help: try replacing it with: `b - a` + | +note: this subtraction underflows when `a < b` + --> tests/ui/manual_arithmetic_check-2.rs:16:41 + | +LL | let result = if a >= b { 0 } else { a - b }; + | ^^^^^ + +error: inverted arithmetic check before subtraction + --> tests/ui/manual_arithmetic_check-2.rs:18:23 + | +LL | let result = if b < a { 0 } else { a - b }; + | ^ ----- help: try replacing it with: `b - a` + | +note: this subtraction underflows when `a < b` + --> tests/ui/manual_arithmetic_check-2.rs:18:40 + | +LL | let result = if b < a { 0 } else { a - b }; + | ^^^^^ + +error: inverted arithmetic check before subtraction + --> tests/ui/manual_arithmetic_check-2.rs:20:23 + | +LL | let result = if b <= a { 0 } else { a - b }; + | ^^ ----- help: try replacing it with: `b - a` + | +note: this subtraction underflows when `a < b` + --> tests/ui/manual_arithmetic_check-2.rs:20:41 + | +LL | let result = if b <= a { 0 } else { a - b }; + | ^^^^^ + +error: aborting due to 6 previous errors + diff --git a/tests/ui/manual_arithmetic_check.fixed b/tests/ui/manual_arithmetic_check.fixed new file mode 100644 index 00000000000..afb2abcbe90 --- /dev/null +++ b/tests/ui/manual_arithmetic_check.fixed @@ -0,0 +1,16 @@ +#![warn(clippy::implicit_saturating_sub)] + +fn main() { + let a = 12u32; + let b = 13u32; + + let result = a.saturating_sub(b); + //~^ ERROR: manual arithmetic check found + let result = a.saturating_sub(b); + //~^ ERROR: manual arithmetic check found + + let result = a.saturating_sub(b); + //~^ ERROR: manual arithmetic check found + let result = a.saturating_sub(b); + //~^ ERROR: manual arithmetic check found +} diff --git a/tests/ui/manual_arithmetic_check.rs b/tests/ui/manual_arithmetic_check.rs new file mode 100644 index 00000000000..c11ac7097fc --- /dev/null +++ b/tests/ui/manual_arithmetic_check.rs @@ -0,0 +1,20 @@ +#![warn(clippy::implicit_saturating_sub)] + +fn main() { + let a = 12u32; + let b = 13u32; + let c = 8u32; + + let result = if a > b { a - b } else { 0 }; + //~^ ERROR: manual arithmetic check found + let result = if b < a { a - b } else { 0 }; + //~^ ERROR: manual arithmetic check found + + let result = if a < b { 0 } else { a - b }; + //~^ ERROR: manual arithmetic check found + let result = if b > a { 0 } else { a - b }; + //~^ ERROR: manual arithmetic check found + + // Should not warn! + let result = if a > b { a - b } else { a - c }; +} diff --git a/tests/ui/manual_arithmetic_check.stderr b/tests/ui/manual_arithmetic_check.stderr new file mode 100644 index 00000000000..153a8bb5734 --- /dev/null +++ b/tests/ui/manual_arithmetic_check.stderr @@ -0,0 +1,29 @@ +error: manual arithmetic check found + --> tests/ui/manual_arithmetic_check.rs:7:18 + | +LL | let result = if a > b { a - b } else { 0 }; + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: replace it with: `a.saturating_sub(b)` + | + = note: `-D clippy::implicit-saturating-sub` implied by `-D warnings` + = help: to override `-D warnings` add `#[allow(clippy::implicit_saturating_sub)]` + +error: manual arithmetic check found + --> tests/ui/manual_arithmetic_check.rs:9:18 + | +LL | let result = if b < a { a - b } else { 0 }; + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: replace it with: `a.saturating_sub(b)` + +error: manual arithmetic check found + --> tests/ui/manual_arithmetic_check.rs:12:18 + | +LL | let result = if a < b { 0 } else { a - b }; + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: replace it with: `a.saturating_sub(b)` + +error: manual arithmetic check found + --> tests/ui/manual_arithmetic_check.rs:14:18 + | +LL | let result = if b > a { 0 } else { a - b }; + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: replace it with: `a.saturating_sub(b)` + +error: aborting due to 4 previous errors + From 27c63433659773c70dca86a3d6f3b9bd7654c2c3 Mon Sep 17 00:00:00 2001 From: Guillaume Gomez Date: Tue, 19 Mar 2024 17:00:31 +0100 Subject: [PATCH 3/6] Add ui test to ensure that if `0` is returned from both `if` and `else`, it will not break clippy --- tests/ui/implicit_saturating_sub.fixed | 13 ++++++++++--- tests/ui/implicit_saturating_sub.rs | 6 +++--- tests/ui/manual_arithmetic_check.fixed | 8 ++++++++ tests/ui/manual_arithmetic_check.rs | 4 ++++ tests/ui/manual_arithmetic_check.stderr | 8 ++++---- 5 files changed, 29 insertions(+), 10 deletions(-) diff --git a/tests/ui/implicit_saturating_sub.fixed b/tests/ui/implicit_saturating_sub.fixed index 27f679797dd..81cc1494914 100644 --- a/tests/ui/implicit_saturating_sub.fixed +++ b/tests/ui/implicit_saturating_sub.fixed @@ -184,18 +184,18 @@ fn main() { let mut m = Mock; let mut u_32 = 3000; let a = 200; - let mut _b = 8; + let mut b = 8; if m != 0 { m -= 1; } if a > 0 { - _b -= 1; + b -= 1; } if 0 > a { - _b -= 1; + b -= 1; } if u_32 > 0 { @@ -214,4 +214,11 @@ fn main() { } else if u_32 > 0 { u_32 -= 1; } + + let result = if a < b { + println!("we shouldn't remove this"); + 0 + } else { + a - b + }; } diff --git a/tests/ui/implicit_saturating_sub.rs b/tests/ui/implicit_saturating_sub.rs index 05f7d852383..f73396ebd27 100644 --- a/tests/ui/implicit_saturating_sub.rs +++ b/tests/ui/implicit_saturating_sub.rs @@ -230,18 +230,18 @@ fn main() { let mut m = Mock; let mut u_32 = 3000; let a = 200; - let mut _b = 8; + let mut b = 8; if m != 0 { m -= 1; } if a > 0 { - _b -= 1; + b -= 1; } if 0 > a { - _b -= 1; + b -= 1; } if u_32 > 0 { diff --git a/tests/ui/manual_arithmetic_check.fixed b/tests/ui/manual_arithmetic_check.fixed index afb2abcbe90..600fcd3765d 100644 --- a/tests/ui/manual_arithmetic_check.fixed +++ b/tests/ui/manual_arithmetic_check.fixed @@ -1,8 +1,10 @@ #![warn(clippy::implicit_saturating_sub)] +#![allow(clippy::if_same_then_else)] fn main() { let a = 12u32; let b = 13u32; + let c = 8u32; let result = a.saturating_sub(b); //~^ ERROR: manual arithmetic check found @@ -13,4 +15,10 @@ fn main() { //~^ ERROR: manual arithmetic check found let result = a.saturating_sub(b); //~^ ERROR: manual arithmetic check found + + // Should not warn! + let result = if a > b { a - b } else { a - c }; + + // Just to check it won't break clippy. + let result = if b > a { 0 } else { 0 }; } diff --git a/tests/ui/manual_arithmetic_check.rs b/tests/ui/manual_arithmetic_check.rs index c11ac7097fc..33f5adafe7e 100644 --- a/tests/ui/manual_arithmetic_check.rs +++ b/tests/ui/manual_arithmetic_check.rs @@ -1,4 +1,5 @@ #![warn(clippy::implicit_saturating_sub)] +#![allow(clippy::if_same_then_else)] fn main() { let a = 12u32; @@ -17,4 +18,7 @@ fn main() { // Should not warn! let result = if a > b { a - b } else { a - c }; + + // Just to check it won't break clippy. + let result = if b > a { 0 } else { 0 }; } diff --git a/tests/ui/manual_arithmetic_check.stderr b/tests/ui/manual_arithmetic_check.stderr index 153a8bb5734..b0cf73cd915 100644 --- a/tests/ui/manual_arithmetic_check.stderr +++ b/tests/ui/manual_arithmetic_check.stderr @@ -1,5 +1,5 @@ error: manual arithmetic check found - --> tests/ui/manual_arithmetic_check.rs:7:18 + --> tests/ui/manual_arithmetic_check.rs:9:18 | LL | let result = if a > b { a - b } else { 0 }; | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: replace it with: `a.saturating_sub(b)` @@ -8,19 +8,19 @@ LL | let result = if a > b { a - b } else { 0 }; = help: to override `-D warnings` add `#[allow(clippy::implicit_saturating_sub)]` error: manual arithmetic check found - --> tests/ui/manual_arithmetic_check.rs:9:18 + --> tests/ui/manual_arithmetic_check.rs:11:18 | LL | let result = if b < a { a - b } else { 0 }; | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: replace it with: `a.saturating_sub(b)` error: manual arithmetic check found - --> tests/ui/manual_arithmetic_check.rs:12:18 + --> tests/ui/manual_arithmetic_check.rs:14:18 | LL | let result = if a < b { 0 } else { a - b }; | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: replace it with: `a.saturating_sub(b)` error: manual arithmetic check found - --> tests/ui/manual_arithmetic_check.rs:14:18 + --> tests/ui/manual_arithmetic_check.rs:16:18 | LL | let result = if b > a { 0 } else { a - b }; | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: replace it with: `a.saturating_sub(b)` From d20fc38f0a17561166fb57c83ff5660b3cd41392 Mon Sep 17 00:00:00 2001 From: Guillaume Gomez Date: Fri, 19 Jul 2024 14:51:56 +0200 Subject: [PATCH 4/6] Create new `inverted_saturating_sub` lint --- CHANGELOG.md | 1 + clippy_lints/src/declared_lints.rs | 1 + clippy_lints/src/implicit_saturating_sub.rs | 34 +++++++++++++++++++-- tests/ui/manual_arithmetic_check-2.stderr | 3 +- tests/ui/manual_arithmetic_check.fixed | 2 +- tests/ui/manual_arithmetic_check.rs | 2 +- 6 files changed, 37 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fddc2fd994e..0bef3552966 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5500,6 +5500,7 @@ Released 2018-09-13 [`invalid_regex`]: https://rust-lang.github.io/rust-clippy/master/index.html#invalid_regex [`invalid_upcast_comparisons`]: https://rust-lang.github.io/rust-clippy/master/index.html#invalid_upcast_comparisons [`invalid_utf8_in_unchecked`]: https://rust-lang.github.io/rust-clippy/master/index.html#invalid_utf8_in_unchecked +[`inverted_saturating_sub`]: https://rust-lang.github.io/rust-clippy/master/index.html#inverted_saturating_sub [`invisible_characters`]: https://rust-lang.github.io/rust-clippy/master/index.html#invisible_characters [`is_digit_ascii_radix`]: https://rust-lang.github.io/rust-clippy/master/index.html#is_digit_ascii_radix [`items_after_statements`]: https://rust-lang.github.io/rust-clippy/master/index.html#items_after_statements diff --git a/clippy_lints/src/declared_lints.rs b/clippy_lints/src/declared_lints.rs index 8754a4dff87..a6dd6ae7312 100644 --- a/clippy_lints/src/declared_lints.rs +++ b/clippy_lints/src/declared_lints.rs @@ -216,6 +216,7 @@ pub static LINTS: &[&crate::LintInfo] = &[ crate::implicit_return::IMPLICIT_RETURN_INFO, crate::implicit_saturating_add::IMPLICIT_SATURATING_ADD_INFO, crate::implicit_saturating_sub::IMPLICIT_SATURATING_SUB_INFO, + crate::implicit_saturating_sub::INVERTED_SATURATING_SUB_INFO, crate::implied_bounds_in_impls::IMPLIED_BOUNDS_IN_IMPLS_INFO, crate::incompatible_msrv::INCOMPATIBLE_MSRV_INFO, crate::inconsistent_struct_constructor::INCONSISTENT_STRUCT_CONSTRUCTOR_INFO, diff --git a/clippy_lints/src/implicit_saturating_sub.rs b/clippy_lints/src/implicit_saturating_sub.rs index 453ff09bf4f..04c89062039 100644 --- a/clippy_lints/src/implicit_saturating_sub.rs +++ b/clippy_lints/src/implicit_saturating_sub.rs @@ -41,7 +41,37 @@ declare_clippy_lint! { "Perform saturating subtraction instead of implicitly checking lower bound of data type" } -declare_lint_pass!(ImplicitSaturatingSub => [IMPLICIT_SATURATING_SUB]); +declare_clippy_lint! { + /// ### What it does + /// Checks for comparisons between integers, followed by subtracting the greater value from the + /// lower one. + /// + /// ### Why is this bad? + /// This could result in an underflow and is most likely not what the user wants. If this was + /// intended to be a saturated subtraction, consider using the `saturating_sub` method directly. + /// + /// ### Example + /// ```no_run + /// let a = 12u32; + /// let b = 13u32; + /// + /// let result = if a > b { b - a } else { 0 }; + /// ``` + /// + /// Use instead: + /// ```no_run + /// let a = 12u32; + /// let b = 13u32; + /// + /// let result = a.saturating_sub(b); + /// ``` + #[clippy::version = "1.44.0"] + pub INVERTED_SATURATING_SUB, + correctness, + "Check if a variable is smaller than another one and still subtract from it even if smaller" +} + +declare_lint_pass!(ImplicitSaturatingSub => [IMPLICIT_SATURATING_SUB, INVERTED_SATURATING_SUB]); impl<'tcx> LateLintPass<'tcx> for ImplicitSaturatingSub { fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'tcx>) { @@ -182,7 +212,7 @@ fn check_subtraction( { span_lint_and_then( cx, - IMPLICIT_SATURATING_SUB, + INVERTED_SATURATING_SUB, condition_span, "inverted arithmetic check before subtraction", |diag| { diff --git a/tests/ui/manual_arithmetic_check-2.stderr b/tests/ui/manual_arithmetic_check-2.stderr index d49e536ce3f..4121aa7464f 100644 --- a/tests/ui/manual_arithmetic_check-2.stderr +++ b/tests/ui/manual_arithmetic_check-2.stderr @@ -9,8 +9,7 @@ note: this subtraction underflows when `b < a` | LL | let result = if a > b { b - a } else { 0 }; | ^^^^^ - = note: `-D clippy::implicit-saturating-sub` implied by `-D warnings` - = help: to override `-D warnings` add `#[allow(clippy::implicit_saturating_sub)]` + = note: `#[deny(clippy::inverted_saturating_sub)]` on by default error: inverted arithmetic check before subtraction --> tests/ui/manual_arithmetic_check-2.rs:11:23 diff --git a/tests/ui/manual_arithmetic_check.fixed b/tests/ui/manual_arithmetic_check.fixed index 600fcd3765d..29ecbb9ad2a 100644 --- a/tests/ui/manual_arithmetic_check.fixed +++ b/tests/ui/manual_arithmetic_check.fixed @@ -1,4 +1,4 @@ -#![warn(clippy::implicit_saturating_sub)] +#![warn(clippy::implicit_saturating_sub, clippy::inverted_saturating_sub)] #![allow(clippy::if_same_then_else)] fn main() { diff --git a/tests/ui/manual_arithmetic_check.rs b/tests/ui/manual_arithmetic_check.rs index 33f5adafe7e..69554c6b61c 100644 --- a/tests/ui/manual_arithmetic_check.rs +++ b/tests/ui/manual_arithmetic_check.rs @@ -1,4 +1,4 @@ -#![warn(clippy::implicit_saturating_sub)] +#![warn(clippy::implicit_saturating_sub, clippy::inverted_saturating_sub)] #![allow(clippy::if_same_then_else)] fn main() { From e845366c82ada0466a9801363064357ad22daf12 Mon Sep 17 00:00:00 2001 From: Guillaume Gomez Date: Mon, 22 Jul 2024 11:44:40 +0200 Subject: [PATCH 5/6] Add MSRV check for `saturating_sub` lints in const contexts --- clippy_config/src/msrvs.rs | 2 +- clippy_lints/src/implicit_saturating_sub.rs | 27 ++++++++++++++++++--- clippy_lints/src/lib.rs | 2 +- 3 files changed, 26 insertions(+), 5 deletions(-) diff --git a/clippy_config/src/msrvs.rs b/clippy_config/src/msrvs.rs index 5b9707efd26..db4c13fe33a 100644 --- a/clippy_config/src/msrvs.rs +++ b/clippy_config/src/msrvs.rs @@ -35,7 +35,7 @@ msrv_aliases! { 1,52,0 { STR_SPLIT_ONCE, REM_EUCLID_CONST } 1,51,0 { BORROW_AS_PTR, SEEK_FROM_CURRENT, UNSIGNED_ABS } 1,50,0 { BOOL_THEN, CLAMP } - 1,47,0 { TAU, IS_ASCII_DIGIT_CONST, ARRAY_IMPL_ANY_LEN } + 1,47,0 { TAU, IS_ASCII_DIGIT_CONST, ARRAY_IMPL_ANY_LEN, SATURATING_SUB_CONST } 1,46,0 { CONST_IF_MATCH } 1,45,0 { STR_STRIP_PREFIX } 1,43,0 { LOG2_10, LOG10_2, NUMERIC_ASSOCIATED_CONSTANTS } diff --git a/clippy_lints/src/implicit_saturating_sub.rs b/clippy_lints/src/implicit_saturating_sub.rs index 04c89062039..5a42802e6bb 100644 --- a/clippy_lints/src/implicit_saturating_sub.rs +++ b/clippy_lints/src/implicit_saturating_sub.rs @@ -1,12 +1,16 @@ +use clippy_config::msrvs::{self, Msrv}; +use clippy_config::Conf; use clippy_utils::diagnostics::{span_lint_and_sugg, span_lint_and_then}; use clippy_utils::source::snippet_opt; -use clippy_utils::{higher, is_integer_literal, path_to_local, peel_blocks, peel_blocks_with_stmt, SpanlessEq}; +use clippy_utils::{ + higher, in_constant, is_integer_literal, path_to_local, peel_blocks, peel_blocks_with_stmt, SpanlessEq, +}; use rustc_ast::ast::LitKind; use rustc_data_structures::packed::Pu128; use rustc_errors::Applicability; use rustc_hir::{BinOp, BinOpKind, Expr, ExprKind, HirId, QPath}; use rustc_lint::{LateContext, LateLintPass}; -use rustc_session::declare_lint_pass; +use rustc_session::impl_lint_pass; use rustc_span::Span; declare_clippy_lint! { @@ -71,13 +75,28 @@ declare_clippy_lint! { "Check if a variable is smaller than another one and still subtract from it even if smaller" } -declare_lint_pass!(ImplicitSaturatingSub => [IMPLICIT_SATURATING_SUB, INVERTED_SATURATING_SUB]); +pub struct ImplicitSaturatingSub { + msrv: Msrv, +} + +impl_lint_pass!(ImplicitSaturatingSub => [IMPLICIT_SATURATING_SUB, INVERTED_SATURATING_SUB]); + +impl ImplicitSaturatingSub { + pub fn new(conf: &'static Conf) -> Self { + Self { + msrv: conf.msrv.clone(), + } + } +} impl<'tcx> LateLintPass<'tcx> for ImplicitSaturatingSub { fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'tcx>) { if expr.span.from_expansion() { return; } + if in_constant(cx, expr.hir_id) && !self.msrv.meets(msrvs::SATURATING_SUB_CONST) { + return; + } if let Some(higher::If { cond, then, r#else: None }) = higher::If::hir(expr) // Check if the conditional expression is a binary operation @@ -94,6 +113,8 @@ impl<'tcx> LateLintPass<'tcx> for ImplicitSaturatingSub { check_manual_check(cx, expr, cond_op, cond_left, cond_right, if_block, else_block); } } + + extract_msrv_attr!(LateContext); } fn check_manual_check<'tcx>( diff --git a/clippy_lints/src/lib.rs b/clippy_lints/src/lib.rs index 2ac06b360be..80e314dd402 100644 --- a/clippy_lints/src/lib.rs +++ b/clippy_lints/src/lib.rs @@ -623,7 +623,7 @@ pub fn register_lints(store: &mut rustc_lint::LintStore, conf: &'static Conf) { store.register_late_pass(|_| Box::new(unit_return_expecting_ord::UnitReturnExpectingOrd)); store.register_late_pass(|_| Box::new(strings::StringAdd)); store.register_late_pass(|_| Box::new(implicit_return::ImplicitReturn)); - store.register_late_pass(|_| Box::new(implicit_saturating_sub::ImplicitSaturatingSub)); + store.register_late_pass(move |_| Box::new(implicit_saturating_sub::ImplicitSaturatingSub::new(conf))); store.register_late_pass(|_| Box::new(default_numeric_fallback::DefaultNumericFallback)); store.register_late_pass(|_| Box::new(inconsistent_struct_constructor::InconsistentStructConstructor)); store.register_late_pass(|_| Box::new(non_octal_unix_permissions::NonOctalUnixPermissions)); From 2832faf8959932a25fda883ed3f7dfde8b2d2817 Mon Sep 17 00:00:00 2001 From: Guillaume Gomez Date: Fri, 2 Aug 2024 11:17:14 +0200 Subject: [PATCH 6/6] Move MSRV check later in `implicit_saturating_sub` --- clippy_lints/src/implicit_saturating_sub.rs | 40 +++++++++++++++++---- 1 file changed, 33 insertions(+), 7 deletions(-) diff --git a/clippy_lints/src/implicit_saturating_sub.rs b/clippy_lints/src/implicit_saturating_sub.rs index 5a42802e6bb..3536309d83c 100644 --- a/clippy_lints/src/implicit_saturating_sub.rs +++ b/clippy_lints/src/implicit_saturating_sub.rs @@ -3,7 +3,7 @@ use clippy_config::Conf; use clippy_utils::diagnostics::{span_lint_and_sugg, span_lint_and_then}; use clippy_utils::source::snippet_opt; use clippy_utils::{ - higher, in_constant, is_integer_literal, path_to_local, peel_blocks, peel_blocks_with_stmt, SpanlessEq, + higher, is_in_const_context, is_integer_literal, path_to_local, peel_blocks, peel_blocks_with_stmt, SpanlessEq, }; use rustc_ast::ast::LitKind; use rustc_data_structures::packed::Pu128; @@ -94,9 +94,6 @@ impl<'tcx> LateLintPass<'tcx> for ImplicitSaturatingSub { if expr.span.from_expansion() { return; } - if in_constant(cx, expr.hir_id) && !self.msrv.meets(msrvs::SATURATING_SUB_CONST) { - return; - } if let Some(higher::If { cond, then, r#else: None }) = higher::If::hir(expr) // Check if the conditional expression is a binary operation @@ -110,13 +107,16 @@ impl<'tcx> LateLintPass<'tcx> for ImplicitSaturatingSub { }) = higher::If::hir(expr) && let ExprKind::Binary(ref cond_op, cond_left, cond_right) = cond.kind { - check_manual_check(cx, expr, cond_op, cond_left, cond_right, if_block, else_block); + check_manual_check( + cx, expr, cond_op, cond_left, cond_right, if_block, else_block, &self.msrv, + ); } } extract_msrv_attr!(LateContext); } +#[allow(clippy::too_many_arguments)] fn check_manual_check<'tcx>( cx: &LateContext<'tcx>, expr: &Expr<'tcx>, @@ -125,6 +125,7 @@ fn check_manual_check<'tcx>( right_hand: &Expr<'tcx>, if_block: &Expr<'tcx>, else_block: &Expr<'tcx>, + msrv: &Msrv, ) { let ty = cx.typeck_results().expr_ty(left_hand); if ty.is_numeric() && !ty.is_signed() { @@ -137,6 +138,7 @@ fn check_manual_check<'tcx>( right_hand, if_block, else_block, + msrv, ), BinOpKind::Lt | BinOpKind::Le => check_gt( cx, @@ -146,12 +148,14 @@ fn check_manual_check<'tcx>( left_hand, if_block, else_block, + msrv, ), _ => {}, } } } +#[allow(clippy::too_many_arguments)] fn check_gt( cx: &LateContext<'_>, condition_span: Span, @@ -160,11 +164,21 @@ fn check_gt( little_var: &Expr<'_>, if_block: &Expr<'_>, else_block: &Expr<'_>, + msrv: &Msrv, ) { if let Some(big_var) = Var::new(big_var) && let Some(little_var) = Var::new(little_var) { - check_subtraction(cx, condition_span, expr_span, big_var, little_var, if_block, else_block); + check_subtraction( + cx, + condition_span, + expr_span, + big_var, + little_var, + if_block, + else_block, + msrv, + ); } } @@ -182,6 +196,7 @@ impl Var { } } +#[allow(clippy::too_many_arguments)] fn check_subtraction( cx: &LateContext<'_>, condition_span: Span, @@ -190,6 +205,7 @@ fn check_subtraction( little_var: Var, if_block: &Expr<'_>, else_block: &Expr<'_>, + msrv: &Msrv, ) { let if_block = peel_blocks(if_block); let else_block = peel_blocks(else_block); @@ -201,7 +217,16 @@ fn check_subtraction( } // If the subtraction is done in the `else` block, then we need to also revert the two // variables as it means that the check was reverted too. - check_subtraction(cx, condition_span, expr_span, little_var, big_var, else_block, if_block); + check_subtraction( + cx, + condition_span, + expr_span, + little_var, + big_var, + else_block, + if_block, + msrv, + ); return; } if is_integer_literal(else_block, 0) @@ -215,6 +240,7 @@ fn check_subtraction( // if `snippet_opt` fails, it won't try the next conditions. if let Some(big_var_snippet) = snippet_opt(cx, big_var.span) && let Some(little_var_snippet) = snippet_opt(cx, little_var.span) + && (!is_in_const_context(cx) || msrv.meets(msrvs::SATURATING_SUB_CONST)) { span_lint_and_sugg( cx,