Skip to content

Commit 98b1666

Browse files
committed
Add new lint [] init
1 parent 0f20a12 commit 98b1666

File tree

8 files changed

+470
-1
lines changed

8 files changed

+470
-1
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5742,6 +5742,7 @@ Released 2018-09-13
57425742
[`manual_async_fn`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_async_fn
57435743
[`manual_bits`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_bits
57445744
[`manual_c_str_literals`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_c_str_literals
5745+
[`manual_checked_sub`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_checked_sub
57455746
[`manual_clamp`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_clamp
57465747
[`manual_contains`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_contains
57475748
[`manual_div_ceil`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_div_ceil

clippy_lints/src/declared_lints.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -312,6 +312,7 @@ pub static LINTS: &[&crate::LintInfo] = &[
312312
crate::manual_assert::MANUAL_ASSERT_INFO,
313313
crate::manual_async_fn::MANUAL_ASYNC_FN_INFO,
314314
crate::manual_bits::MANUAL_BITS_INFO,
315+
crate::manual_checked_sub::MANUAL_CHECKED_SUB_INFO,
315316
crate::manual_clamp::MANUAL_CLAMP_INFO,
316317
crate::manual_div_ceil::MANUAL_DIV_CEIL_INFO,
317318
crate::manual_float_methods::MANUAL_IS_FINITE_INFO,

clippy_lints/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,7 @@ mod main_recursion;
207207
mod manual_assert;
208208
mod manual_async_fn;
209209
mod manual_bits;
210+
mod manual_checked_sub;
210211
mod manual_clamp;
211212
mod manual_div_ceil;
212213
mod manual_float_methods;
@@ -980,5 +981,6 @@ pub fn register_lints(store: &mut rustc_lint::LintStore, conf: &'static Conf) {
980981
store.register_late_pass(move |_| Box::new(non_std_lazy_statics::NonStdLazyStatic::new(conf)));
981982
store.register_late_pass(|_| Box::new(manual_option_as_slice::ManualOptionAsSlice::new(conf)));
982983
store.register_late_pass(|_| Box::new(single_option_map::SingleOptionMap));
984+
store.register_late_pass(move |_| Box::new(manual_checked_sub::ManualCheckedSub::new(conf)));
983985
// add lints here, do not remove this comment, it's used in `new_lint`
984986
}
Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
use clippy_config::Conf;
2+
use clippy_utils::diagnostics::span_lint_and_sugg;
3+
use clippy_utils::msrvs::{self, Msrv};
4+
use clippy_utils::source::snippet_with_applicability;
5+
use rustc_ast::{BinOpKind, LitIntType, LitKind};
6+
use rustc_data_structures::packed::Pu128;
7+
use rustc_errors::Applicability;
8+
use rustc_hir::def::Res;
9+
use rustc_hir::intravisit::{Visitor, walk_expr};
10+
use rustc_hir::{Expr, ExprKind, QPath};
11+
use rustc_lint::{LateContext, LateLintPass};
12+
use rustc_middle::ty::{self};
13+
use rustc_session::impl_lint_pass;
14+
15+
declare_clippy_lint! {
16+
/// ### What it does
17+
/// Checks for manual re-implementations of checked subtraction.
18+
///
19+
/// ### Why is this bad?
20+
/// Manually re-implementing checked subtraction can be error-prone and less readable.
21+
/// Using the standard library method `.checked_sub()` is clearer and less likely to contain bugs.
22+
///
23+
/// ### Example
24+
/// ```no_run
25+
/// if a >= b {
26+
/// a - b
27+
/// }
28+
/// ```
29+
/// Use instead:
30+
/// ```no_run
31+
/// a.checked_sub(b)
32+
/// ```
33+
#[clippy::version = "1.86.0"]
34+
pub MANUAL_CHECKED_SUB,
35+
complexity,
36+
"default lint description"
37+
}
38+
39+
pub struct ManualCheckedSub {
40+
msrv: Msrv,
41+
}
42+
43+
impl ManualCheckedSub {
44+
#[must_use]
45+
pub fn new(conf: &'static Conf) -> Self {
46+
Self {
47+
msrv: conf.msrv.clone(),
48+
}
49+
}
50+
}
51+
52+
impl_lint_pass!(ManualCheckedSub => [MANUAL_CHECKED_SUB]);
53+
54+
impl<'tcx> LateLintPass<'tcx> for ManualCheckedSub {
55+
fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) {
56+
if !self.msrv.meets(msrvs::MANUAL_CHECKED_SUB) {
57+
return;
58+
}
59+
60+
let applicability = Applicability::MachineApplicable;
61+
62+
if let ExprKind::If(drop_temp_expr, body_block, _) = expr.kind
63+
&& let ExprKind::DropTemps(condition_expr) = drop_temp_expr.kind
64+
&& let ExprKind::Binary(op, lhs, rhs) = condition_expr.kind
65+
&& is_unsigned_int(cx, &lhs)
66+
&& is_unsigned_int(cx, &rhs)
67+
{
68+
if let BinOpKind::Ge = op.node {
69+
SubExprVisitor {
70+
cx,
71+
applicability,
72+
condition_lhs: &lhs,
73+
condition_rhs: &rhs,
74+
}
75+
.visit_expr(body_block)
76+
}
77+
78+
if let BinOpKind::Gt = op.node {
79+
if let ExprKind::Lit(lit) = &rhs.kind
80+
&& let LitKind::Int(Pu128(0), _) = lit.node
81+
{
82+
SubExprVisitor {
83+
cx,
84+
applicability,
85+
condition_lhs: &lhs,
86+
condition_rhs: &rhs,
87+
}
88+
.visit_expr(body_block)
89+
}
90+
}
91+
}
92+
}
93+
}
94+
95+
struct SubExprVisitor<'a, 'tcx> {
96+
cx: &'a LateContext<'tcx>,
97+
applicability: Applicability,
98+
condition_lhs: &'tcx Expr<'tcx>,
99+
condition_rhs: &'tcx Expr<'tcx>,
100+
}
101+
102+
impl<'tcx> Visitor<'tcx> for SubExprVisitor<'_, 'tcx> {
103+
fn visit_expr(&mut self, expr: &'tcx Expr<'_>) {
104+
if let ExprKind::Binary(op, sub_lhs, sub_rhs) = expr.kind
105+
&& let BinOpKind::Sub = op.node
106+
{
107+
if let ExprKind::Lit(lit) = self.condition_rhs.kind
108+
&& let LitKind::Int(Pu128(0), _) = lit.node
109+
&& (is_referencing_same_variable(&sub_lhs, self.condition_lhs)
110+
|| are_literals_equal(&sub_lhs, self.condition_lhs))
111+
{
112+
self.build_suggession(expr);
113+
} else {
114+
if (is_referencing_same_variable(&sub_lhs, self.condition_lhs)
115+
|| are_literals_equal(&sub_lhs, self.condition_lhs))
116+
&& (is_referencing_same_variable(&sub_rhs, self.condition_rhs)
117+
|| are_literals_equal(&sub_rhs, self.condition_rhs))
118+
{
119+
self.build_suggession(expr);
120+
}
121+
}
122+
}
123+
124+
walk_expr(self, expr);
125+
}
126+
}
127+
128+
impl<'a, 'tcx> SubExprVisitor<'a, 'tcx> {
129+
fn build_suggession(&mut self, expr: &Expr<'tcx>) {
130+
let lhs_snippet = snippet_with_applicability(self.cx, self.condition_lhs.span, "..", &mut self.applicability);
131+
let rhs_snippet = snippet_with_applicability(self.cx, self.condition_rhs.span, "..", &mut self.applicability);
132+
133+
let lhs_needs_parens = matches!(self.condition_lhs.kind, ExprKind::Cast(..));
134+
135+
let suggestion = if lhs_needs_parens {
136+
format!("({}).checked_sub({})", lhs_snippet, rhs_snippet)
137+
} else {
138+
format!("{}.checked_sub({})", lhs_snippet, rhs_snippet)
139+
};
140+
141+
span_lint_and_sugg(
142+
self.cx,
143+
MANUAL_CHECKED_SUB,
144+
expr.span,
145+
"manual re-implementation of checked subtraction",
146+
"consider using `.checked_sub()`",
147+
suggestion,
148+
self.applicability,
149+
);
150+
}
151+
}
152+
153+
fn is_unsigned_int<'tcx>(cx: &LateContext<'tcx>, expr: &Expr<'_>) -> bool {
154+
let expr_type = cx.typeck_results().expr_ty(expr).peel_refs();
155+
if matches!(expr_type.kind(), ty::Uint(_)) {
156+
return true;
157+
}
158+
159+
if let ExprKind::Lit(lit) = &expr.kind {
160+
if let LitKind::Int(_, suffix) = &lit.node {
161+
if let LitIntType::Unsuffixed = suffix {
162+
return true;
163+
}
164+
}
165+
}
166+
false
167+
}
168+
169+
fn are_literals_equal<'tcx>(expr1: &Expr<'tcx>, expr2: &Expr<'tcx>) -> bool {
170+
if let (ExprKind::Lit(lit1), ExprKind::Lit(lit2)) = (&expr1.kind, &expr2.kind) {
171+
if let (LitKind::Int(val1, suffix1), LitKind::Int(val2, suffix2)) = (&lit1.node, &lit2.node) {
172+
return val1 == val2
173+
&& suffix1 == suffix2
174+
&& *suffix1 != LitIntType::Unsuffixed
175+
&& *suffix2 != LitIntType::Unsuffixed;
176+
}
177+
}
178+
false
179+
}
180+
fn is_referencing_same_variable<'tcx>(expr1: &'tcx Expr<'_>, expr2: &'tcx Expr<'_>) -> bool {
181+
if let ExprKind::Cast(cast_expr1, _) = &expr1.kind {
182+
if let Some(cast_expr1_path) = get_resolved_path_id(cast_expr1) {
183+
if let ExprKind::Cast(cast_expr2, _) = &expr2.kind {
184+
if let Some(cast_expr2_path) = get_resolved_path_id(cast_expr2) {
185+
return cast_expr1_path == cast_expr2_path;
186+
}
187+
} else {
188+
if let Some(expr2_path) = get_resolved_path_id(expr2) {
189+
return cast_expr1_path == expr2_path;
190+
};
191+
return false;
192+
}
193+
}
194+
} else if let ExprKind::Cast(cast_expr2, _) = &expr2.kind {
195+
if let Some(cast_expr2_path) = get_resolved_path_id(cast_expr2) {
196+
if let Some(expr1_path) = get_resolved_path_id(expr1) {
197+
return expr1_path == cast_expr2_path;
198+
};
199+
return false;
200+
}
201+
}
202+
203+
let expr1_path = get_resolved_path_id(expr1);
204+
let expr2_path = get_resolved_path_id(expr2);
205+
206+
if let (Some(expr1_path), Some(expr2_path)) = (expr1_path, expr2_path) {
207+
expr1_path == expr2_path
208+
} else {
209+
false
210+
}
211+
}
212+
213+
fn get_resolved_path_id<'tcx>(expr: &'tcx Expr<'_>) -> Option<Res> {
214+
if let ExprKind::Path(QPath::Resolved(_, path)) = &expr.kind {
215+
path.segments.first().map(|segment| segment.res)
216+
} else {
217+
None
218+
}
219+
}

clippy_utils/src/msrvs.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ msrv_aliases! {
4545
1,52,0 { STR_SPLIT_ONCE, REM_EUCLID_CONST }
4646
1,51,0 { BORROW_AS_PTR, SEEK_FROM_CURRENT, UNSIGNED_ABS }
4747
1,50,0 { BOOL_THEN, CLAMP, SLICE_FILL }
48-
1,47,0 { TAU, IS_ASCII_DIGIT_CONST, ARRAY_IMPL_ANY_LEN, SATURATING_SUB_CONST }
48+
1,47,0 { TAU, IS_ASCII_DIGIT_CONST, ARRAY_IMPL_ANY_LEN, SATURATING_SUB_CONST, MANUAL_CHECKED_SUB }
4949
1,46,0 { CONST_IF_MATCH }
5050
1,45,0 { STR_STRIP_PREFIX }
5151
1,43,0 { LOG2_10, LOG10_2, NUMERIC_ASSOCIATED_CONSTANTS }

tests/ui/manual_checked_sub.fixed

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
#![allow(clippy::collapsible_if)]
2+
#![warn(clippy::manual_checked_sub)]
3+
4+
//Positive test, lint
5+
fn positive_tests() {
6+
let a: u32 = 10;
7+
let b: u32 = 5;
8+
9+
// Case 1: a - b inside an if a >= b
10+
if a >= b {
11+
let _ = a.checked_sub(b);
12+
}
13+
14+
// Case 2: a - 1 inside an if a > 0
15+
if a > 0 {
16+
let _ = a.checked_sub(0);
17+
}
18+
19+
// Case 3: Both suffixed, should lint
20+
if 10u32 >= 5u32 {
21+
let _ = 10u32.checked_sub(5u32);
22+
}
23+
24+
let x: i32 = 10;
25+
let y: i32 = 5;
26+
27+
// Case 4: casted variables inside an if, should lint
28+
if x as u32 >= y as u32 {
29+
let _ = (x as u32).checked_sub(y as u32);
30+
}
31+
32+
// Case 5: casted variable and literal inside an if, should lint
33+
if x as u32 >= 5u32 {
34+
let _ = (x as u32).checked_sub(5u32);
35+
}
36+
}
37+
38+
// Negative tests, no lint
39+
fn negative_tests() {
40+
let a: u32 = 10;
41+
let b: u32 = 5;
42+
43+
// Case 1: Uses checked_sub() correctly, no lint
44+
let _ = a.checked_sub(b);
45+
46+
// Case 2: Works with signed integers (i32), no lint
47+
let x: i32 = 10;
48+
let y: i32 = 5;
49+
if x >= y {
50+
let _ = x - y;
51+
}
52+
53+
// Case 3: If condition doesn't match, no lint
54+
if a == b {
55+
let _ = a - b;
56+
}
57+
58+
// Case 4: a - b inside an if a > b
59+
if a > b {
60+
let _ = a - b;
61+
}
62+
63+
// Case 5: Unsuffixed literals, no lint
64+
if 10 >= 5 {
65+
let _ = 10 - 5;
66+
}
67+
68+
// Case 6: Suffixed lhs, unsuffixed rhs, no lint
69+
if 10u32 >= 5 {
70+
let _ = 10u32 - 5;
71+
}
72+
73+
// Case 7: Unsuffixed lhs, suffixed rhs, no lint
74+
if 10 >= 5u32 {
75+
let _ = 10 - 5u32;
76+
}
77+
}
78+
79+
fn edge_cases() {
80+
let a: u32 = 10;
81+
let b: u32 = 5;
82+
let c = 3;
83+
84+
// Edge Case 1: Subtraction inside a nested if
85+
if a >= b {
86+
if c > 0 {
87+
let _ = a.checked_sub(b);
88+
}
89+
}
90+
91+
if a > b {
92+
if a - b > c {
93+
let _ = a - b; // This subtraction is safe because `a > b` is checked
94+
}
95+
}
96+
}
97+
98+
fn main() {
99+
positive_tests();
100+
negative_tests();
101+
edge_cases();
102+
}

0 commit comments

Comments
 (0)