Skip to content

Add new lint [manual_checked_sub] #14236

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 26 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
5d1d182
Add new lint [] init
benacq Feb 16, 2025
0b9eba2
Lint test case false positives corrected
benacq Mar 1, 2025
392c3b4
Lint test case false positives corrected
benacq Mar 1, 2025
f7c2e5a
Lint test case false positives corrected
benacq Mar 1, 2025
60b5c16
Lint test case false positives corrected
benacq Mar 2, 2025
bdf9ac8
Lint test case false positives corrected
benacq Mar 2, 2025
2e85d1a
Lint test case false positives corrected
benacq Mar 2, 2025
e0b131a
Lint test case false positives corrected
benacq Mar 2, 2025
797075a
Lint test case false positives corrected
benacq Mar 2, 2025
2669fc9
Lint test case false positives corrected
benacq Mar 2, 2025
06383da
Lint test case false positives corrected
benacq Mar 2, 2025
6b034a7
Lint test case false positives corrected
benacq Mar 2, 2025
0e66fd9
In progress
benacq Mar 28, 2025
99be19d
manual_checked_sub implementation review feedback fixes
benacq Apr 7, 2025
e3b0262
manual_checked_sub implementation review feedback fixes
benacq Apr 7, 2025
cc58db7
manual_checked_sub implementation review feedback fixes
benacq Apr 7, 2025
9d21893
manual_checked_sub implementation review feedback fixes - Improved ru…
benacq Apr 7, 2025
46d448f
manual_checked_sub implementation review feedback fixes - Improved ru…
benacq Apr 7, 2025
31aca94
manual_checked_sub implementation review feedback fixes - Variable ge…
benacq Apr 8, 2025
510d847
manual_checked_sub implementation review feedback fixes - Variable ge…
benacq Apr 8, 2025
8d8dece
manual_checked_sub implementation review feedback fixes - Variable ge…
benacq Apr 8, 2025
3f42120
manual_checked_sub implementation review feedback fixes - Variable ge…
benacq Apr 8, 2025
3cb4e86
Macro call guard and lint suggestion refactor
benacq Apr 23, 2025
b1f3340
Revert "Macro guard implementation and lint suggestion refector"
benacq Apr 24, 2025
e61986c
Macro guard implementation and lint suggestion refactor
benacq Apr 24, 2025
090496e
Macro guard implementation and lint suggestion refactor
benacq Apr 24, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5847,6 +5847,7 @@ Released 2018-09-13
[`manual_async_fn`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_async_fn
[`manual_bits`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_bits
[`manual_c_str_literals`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_c_str_literals
[`manual_checked_sub`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_checked_sub
[`manual_clamp`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_clamp
[`manual_contains`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_contains
[`manual_dangling_ptr`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_dangling_ptr
Expand Down
Binary file added clippy_lints/.DS_Store
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file should be removed

Binary file not shown.
Binary file added clippy_lints/src/.DS_Store
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file should be removed

Binary file not shown.
1 change: 1 addition & 0 deletions clippy_lints/src/declared_lints.rs
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,7 @@ pub static LINTS: &[&crate::LintInfo] = &[
crate::manual_assert::MANUAL_ASSERT_INFO,
crate::manual_async_fn::MANUAL_ASYNC_FN_INFO,
crate::manual_bits::MANUAL_BITS_INFO,
crate::manual_checked_sub::MANUAL_CHECKED_SUB_INFO,
crate::manual_clamp::MANUAL_CLAMP_INFO,
crate::manual_div_ceil::MANUAL_DIV_CEIL_INFO,
crate::manual_float_methods::MANUAL_IS_FINITE_INFO,
Expand Down
4 changes: 2 additions & 2 deletions clippy_lints/src/dereference.rs
Original file line number Diff line number Diff line change
Expand Up @@ -433,12 +433,12 @@ impl<'tcx> LateLintPass<'tcx> for Dereferencing<'tcx> {
(2, deref_msg)
};

if deref_count >= required_refs {
if let Some(result) = deref_count.checked_sub(required_refs) {
self.state = Some((
State::DerefedBorrow(DerefedBorrow {
// One of the required refs is for the current borrow expression, the remaining ones
// can't be removed without breaking the code. See earlier comment.
count: deref_count - required_refs,
count: result,
msg,
stability,
for_field_access: if let ExprUseNode::FieldAccess(name) = use_node
Expand Down
3 changes: 3 additions & 0 deletions clippy_lints/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,7 @@ mod manual_abs_diff;
mod manual_assert;
mod manual_async_fn;
mod manual_bits;
mod manual_checked_sub;
mod manual_clamp;
mod manual_div_ceil;
mod manual_float_methods;
Expand Down Expand Up @@ -942,6 +943,8 @@ pub fn register_lints(store: &mut rustc_lint::LintStore, conf: &'static Conf) {
store.register_late_pass(move |_| Box::new(non_std_lazy_statics::NonStdLazyStatic::new(conf)));
store.register_late_pass(|_| Box::new(manual_option_as_slice::ManualOptionAsSlice::new(conf)));
store.register_late_pass(|_| Box::new(single_option_map::SingleOptionMap));
store.register_late_pass(move |_| Box::new(manual_checked_sub::ManualCheckedSub::new(conf)));
store.register_late_pass(move |_| Box::new(redundant_test_prefix::RedundantTestPrefix));

// add lints here, do not remove this comment, it's used in `new_lint`
}
193 changes: 193 additions & 0 deletions clippy_lints/src/manual_checked_sub.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
use clippy_config::Conf;
use clippy_utils::diagnostics::span_lint;
use clippy_utils::msrvs::{self, Msrv};

use clippy_utils::{is_mutable, path_to_local};
use rustc_ast::{BinOpKind, LitIntType, LitKind};
use rustc_data_structures::packed::Pu128;
use rustc_hir::intravisit::{Visitor, walk_expr};
use rustc_hir::{Expr, ExprKind};
use rustc_lint::{LateContext, LateLintPass};
use rustc_middle::ty::{self};
use rustc_session::impl_lint_pass;

declare_clippy_lint! {
/// ### What it does
/// Checks for manual re-implementations of checked subtraction.
///
/// ### Why is this bad?
/// Manually re-implementing checked subtraction can be error-prone and less readable.
/// Using the standard library method `.checked_sub()` is clearer and less likely to contain bugs.
///
/// ### Example
/// ```rust
/// // Bad: Manual implementation of checked subtraction
/// fn get_remaining_items(total: u32, used: u32) -> Option<u32> {
/// if total >= used {
/// Some(total - used)
/// } else {
/// None
/// }
/// }
/// ```
///
/// Use instead:
/// ```rust
/// // Good: Using the standard library's checked_sub
/// fn get_remaining_items(total: u32, used: u32) -> Option<u32> {
/// total.checked_sub(used)
/// }
/// ```
#[clippy::version = "1.86.0"]
pub MANUAL_CHECKED_SUB,
complexity,
"Checks for manual re-implementations of checked subtraction."
}

pub struct ManualCheckedSub {
msrv: Msrv,
}

impl ManualCheckedSub {
#[must_use]
pub fn new(conf: &'static Conf) -> Self {
Self { msrv: conf.msrv }
}
}

impl_lint_pass!(ManualCheckedSub => [MANUAL_CHECKED_SUB]);

impl<'tcx> LateLintPass<'tcx> for ManualCheckedSub {
fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) {
if !self.msrv.meets(cx, msrvs::CHECKED_SUB) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MSRV checking is more expensive than checking for the presence of an if expression or the fact that there is an unsigned subtraction, so it should rather be moved at the end of the subsequent if let check.

return;
}

if let Some(if_expr) = clippy_utils::higher::If::hir(expr)
&& let ExprKind::Binary(op, lhs, rhs) = if_expr.cond.kind
&& !(matches!(lhs.kind, ExprKind::Lit(_)) && matches!(rhs.kind, ExprKind::Lit(_)))
&& is_unsigned_int(cx, lhs)
&& is_unsigned_int(cx, rhs)
{
// Skip if either non-literal operand is mutable
if (!matches!(lhs.kind, ExprKind::Lit(_)) && is_mutable(cx, lhs))
|| (!matches!(rhs.kind, ExprKind::Lit(_)) && is_mutable(cx, rhs))
{
return;
}

// Skip if either lhs or rhs is a macro call
if lhs.span.from_expansion() || rhs.span.from_expansion() {
return;
}

if let BinOpKind::Ge | BinOpKind::Gt | BinOpKind::Le | BinOpKind::Lt = op.node {
Comment on lines +73 to +84
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Those checks can probably be merged into the earlier if let test.

SubExprVisitor {
cx,
if_expr: expr,

condition_lhs: lhs,
condition_rhs: rhs,
condition_op: op.node,
}
.visit_expr(if_expr.then);
}
}
}
}

struct SubExprVisitor<'cx, 'tcx> {
cx: &'cx LateContext<'tcx>,
if_expr: &'tcx Expr<'tcx>,

condition_lhs: &'tcx Expr<'tcx>,
condition_rhs: &'tcx Expr<'tcx>,
condition_op: BinOpKind,
}

impl<'tcx> Visitor<'tcx> for SubExprVisitor<'_, 'tcx> {
fn visit_expr(&mut self, expr: &'tcx Expr<'_>) {
if let ExprKind::Binary(op, sub_lhs, sub_rhs) = expr.kind
&& let BinOpKind::Sub = op.node
{
// // Skip if either sub_lhs or sub_rhs is a macro call
if sub_lhs.span.from_expansion() || sub_rhs.span.from_expansion() {
return;
}

if let ExprKind::Lit(lit) = self.condition_lhs.kind
&& self.condition_op == BinOpKind::Lt
&& let LitKind::Int(Pu128(0), _) = lit.node
&& (is_referencing_same_variable(sub_lhs, self.condition_rhs))
{
self.emit_lint();
}

if let ExprKind::Lit(lit) = self.condition_rhs.kind
&& self.condition_op == BinOpKind::Gt
&& let LitKind::Int(Pu128(0), _) = lit.node
&& (is_referencing_same_variable(sub_lhs, self.condition_lhs))
{
self.emit_lint();
}

if self.condition_op == BinOpKind::Ge
&& (is_referencing_same_variable(sub_lhs, self.condition_lhs)
|| are_literals_equal(sub_lhs, self.condition_lhs))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This code:

if a > 0 {
    let b = a - 3;
}

will be replaced by a

if let Some(result) = a.checked_sub(0) {
    let b = result;
}

&& (is_referencing_same_variable(sub_rhs, self.condition_rhs)
|| are_literals_equal(sub_rhs, self.condition_rhs))
{
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This code, when a is mutable,

if a > b {
    a *= 2;
    let c = a - b;
}

will be replaced by

if let Some(result) = a.checked_sub(b) {
    a *= 2;
    let c = result;
}

self.emit_lint();
}

if self.condition_op == BinOpKind::Le
&& (is_referencing_same_variable(sub_lhs, self.condition_rhs)
|| are_literals_equal(sub_lhs, self.condition_rhs))
&& (is_referencing_same_variable(sub_rhs, self.condition_lhs)
|| are_literals_equal(sub_rhs, self.condition_lhs))
{
self.emit_lint();
}
}

walk_expr(self, expr);
}
}

impl SubExprVisitor<'_, '_> {
fn emit_lint(&mut self) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
fn emit_lint(&mut self) {
fn emit_lint(&self) {

span_lint(
self.cx,
MANUAL_CHECKED_SUB,
self.if_expr.span,
"manual re-implementation of checked subtraction - consider using `.checked_sub()`",
);
}
}

fn is_unsigned_int<'tcx>(cx: &LateContext<'tcx>, expr: &Expr<'tcx>) -> bool {
let expr_type = cx.typeck_results().expr_ty(expr).peel_refs();
if matches!(expr_type.kind(), ty::Uint(_)) {
return true;
}

false
}
Comment on lines +168 to +175
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can use the shorter

Suggested change
fn is_unsigned_int<'tcx>(cx: &LateContext<'tcx>, expr: &Expr<'tcx>) -> bool {
let expr_type = cx.typeck_results().expr_ty(expr).peel_refs();
if matches!(expr_type.kind(), ty::Uint(_)) {
return true;
}
false
}
fn is_unsigned_int(cx: &LateContext<'_>, expr: &Expr<'_>) -> bool {
matches!(cx.typeck_results().expr_ty(expr).peel_refs().kind(), ty::Uint(_))
}

Also, please add tests with references since you seem to need to peel them.


fn are_literals_equal<'tcx>(expr1: &Expr<'tcx>, expr2: &Expr<'tcx>) -> bool {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
fn are_literals_equal<'tcx>(expr1: &Expr<'tcx>, expr2: &Expr<'tcx>) -> bool {
fn are_literals_equal(expr1: &Expr<'_>, expr2: &Expr<'_>) -> bool {

if let (ExprKind::Lit(lit1), ExprKind::Lit(lit2)) = (&expr1.kind, &expr2.kind)
&& let (LitKind::Int(val1, suffix1), LitKind::Int(val2, suffix2)) = (&lit1.node, &lit2.node)
{
return val1 == val2 && suffix1 == suffix2 && *suffix1 != LitIntType::Unsuffixed;
}

false
}

fn is_referencing_same_variable<'tcx>(expr1: &'tcx Expr<'_>, expr2: &'tcx Expr<'_>) -> bool {
if let (Some(id1), Some(id2)) = (path_to_local(expr1), path_to_local(expr2)) {
return id1 == id2;
}

false
}
Comment on lines +187 to +193
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can use the more compact

Suggested change
fn is_referencing_same_variable<'tcx>(expr1: &'tcx Expr<'_>, expr2: &'tcx Expr<'_>) -> bool {
if let (Some(id1), Some(id2)) = (path_to_local(expr1), path_to_local(expr2)) {
return id1 == id2;
}
false
}
fn is_referencing_same_variable(expr1: &Expr<'_>, expr2: &Expr<'_>) -> bool {
path_to_local(expr1).is_some_and(|id1| path_to_local(expr2) == Some(id1))
}

Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,10 @@ fn extract_count_with_applicability(
&& let LitKind::Int(Pu128(upper_bound), _) = lit.node
{
// Here we can explicitly calculate the number of iterations
let count = if upper_bound >= lower_bound {
let count = if let Some(result) = upper_bound.checked_sub(lower_bound) {
match range.limits {
RangeLimits::HalfOpen => upper_bound - lower_bound,
RangeLimits::Closed => (upper_bound - lower_bound).checked_add(1)?,
RangeLimits::HalfOpen => result,
RangeLimits::Closed => (result).checked_add(1)?,
}
} else {
0
Expand Down
2 changes: 1 addition & 1 deletion clippy_utils/src/msrvs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,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, SLICE_FILL }
1,47,0 { TAU, IS_ASCII_DIGIT_CONST, ARRAY_IMPL_ANY_LEN, SATURATING_SUB_CONST }
1,47,0 { TAU, IS_ASCII_DIGIT_CONST, ARRAY_IMPL_ANY_LEN, SATURATING_SUB_CONST, CHECKED_SUB }
1,46,0 { CONST_IF_MATCH }
1,45,0 { STR_STRIP_PREFIX }
1,43,0 { LOG2_10, LOG10_2, NUMERIC_ASSOCIATED_CONSTANTS }
Expand Down
7 changes: 6 additions & 1 deletion tests/ui/implicit_saturating_sub.fixed
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
#![allow(unused_assignments, unused_mut, clippy::assign_op_pattern)]
#![allow(
unused_assignments,
unused_mut,
clippy::assign_op_pattern,
clippy::manual_checked_sub
)]
#![warn(clippy::implicit_saturating_sub)]

use std::cmp::PartialEq;
Expand Down
7 changes: 6 additions & 1 deletion tests/ui/implicit_saturating_sub.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
#![allow(unused_assignments, unused_mut, clippy::assign_op_pattern)]
#![allow(
unused_assignments,
unused_mut,
clippy::assign_op_pattern,
clippy::manual_checked_sub
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure modifying this test file is needed, as implicit_saturating_sub requires the modified variable to be mutable, which you filter out.

)]
#![warn(clippy::implicit_saturating_sub)]

use std::cmp::PartialEq;
Expand Down
Loading