Skip to content

Commit e17864e

Browse files
committed
Auto merge of #9031 - evantypanski:manual_rem_euclid, r=Jarcho
Add [`manual_rem_euclid`] lint Closes #8883 Adds a lint for checking manual use of `rem_euclid(n)` changelog: Add [`manual_rem_euclid`] lint
2 parents 1d1ae10 + df26c3f commit e17864e

13 files changed

+325
-6
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -3527,6 +3527,7 @@ Released 2018-09-13
35273527
[`manual_non_exhaustive`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_non_exhaustive
35283528
[`manual_ok_or`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_ok_or
35293529
[`manual_range_contains`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_range_contains
3530+
[`manual_rem_euclid`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_rem_euclid
35303531
[`manual_saturating_arithmetic`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_saturating_arithmetic
35313532
[`manual_split_once`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_split_once
35323533
[`manual_str_repeat`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_str_repeat

clippy_lints/src/lib.register_all.rs

+1
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,7 @@ store.register_group(true, "clippy::all", Some("clippy_all"), vec![
135135
LintId::of(manual_async_fn::MANUAL_ASYNC_FN),
136136
LintId::of(manual_bits::MANUAL_BITS),
137137
LintId::of(manual_non_exhaustive::MANUAL_NON_EXHAUSTIVE),
138+
LintId::of(manual_rem_euclid::MANUAL_REM_EUCLID),
138139
LintId::of(manual_strip::MANUAL_STRIP),
139140
LintId::of(map_clone::MAP_CLONE),
140141
LintId::of(map_unit_fn::OPTION_MAP_UNIT_FN),

clippy_lints/src/lib.register_complexity.rs

+1
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ store.register_group(true, "clippy::complexity", Some("clippy_complexity"), vec!
2424
LintId::of(loops::MANUAL_FLATTEN),
2525
LintId::of(loops::SINGLE_ELEMENT_LOOP),
2626
LintId::of(loops::WHILE_LET_LOOP),
27+
LintId::of(manual_rem_euclid::MANUAL_REM_EUCLID),
2728
LintId::of(manual_strip::MANUAL_STRIP),
2829
LintId::of(map_unit_fn::OPTION_MAP_UNIT_FN),
2930
LintId::of(map_unit_fn::RESULT_MAP_UNIT_FN),

clippy_lints/src/lib.register_lints.rs

+1
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,7 @@ store.register_lints(&[
254254
manual_bits::MANUAL_BITS,
255255
manual_non_exhaustive::MANUAL_NON_EXHAUSTIVE,
256256
manual_ok_or::MANUAL_OK_OR,
257+
manual_rem_euclid::MANUAL_REM_EUCLID,
257258
manual_strip::MANUAL_STRIP,
258259
map_clone::MAP_CLONE,
259260
map_err_ignore::MAP_ERR_IGNORE,

clippy_lints/src/lib.rs

+2
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,7 @@ mod manual_async_fn;
282282
mod manual_bits;
283283
mod manual_non_exhaustive;
284284
mod manual_ok_or;
285+
mod manual_rem_euclid;
285286
mod manual_strip;
286287
mod map_clone;
287288
mod map_err_ignore;
@@ -912,6 +913,7 @@ pub fn register_plugins(store: &mut rustc_lint::LintStore, sess: &Session, conf:
912913
store.register_late_pass(|| Box::new(as_underscore::AsUnderscore));
913914
store.register_late_pass(|| Box::new(read_zero_byte_vec::ReadZeroByteVec));
914915
store.register_late_pass(|| Box::new(default_instead_of_iter_empty::DefaultIterEmpty));
916+
store.register_late_pass(move || Box::new(manual_rem_euclid::ManualRemEuclid::new(msrv)));
915917
// add lints here, do not remove this comment, it's used in `new_lint`
916918
}
917919

clippy_lints/src/manual_rem_euclid.rs

+123
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
use clippy_utils::consts::{constant_full_int, FullInt};
2+
use clippy_utils::diagnostics::span_lint_and_sugg;
3+
use clippy_utils::source::snippet_with_applicability;
4+
use clippy_utils::{in_constant, meets_msrv, msrvs, path_to_local};
5+
use rustc_errors::Applicability;
6+
use rustc_hir::{BinOpKind, Expr, ExprKind, Node, TyKind};
7+
use rustc_lint::{LateContext, LateLintPass, LintContext};
8+
use rustc_middle::lint::in_external_macro;
9+
use rustc_semver::RustcVersion;
10+
use rustc_session::{declare_tool_lint, impl_lint_pass};
11+
12+
declare_clippy_lint! {
13+
/// ### What it does
14+
/// Checks for an expression like `((x % 4) + 4) % 4` which is a common manual reimplementation
15+
/// of `x.rem_euclid(4)`.
16+
///
17+
/// ### Why is this bad?
18+
/// It's simpler and more readable.
19+
///
20+
/// ### Example
21+
/// ```rust
22+
/// let x: i32 = 24;
23+
/// let rem = ((x % 4) + 4) % 4;
24+
/// ```
25+
/// Use instead:
26+
/// ```rust
27+
/// let x: i32 = 24;
28+
/// let rem = x.rem_euclid(4);
29+
/// ```
30+
#[clippy::version = "1.63.0"]
31+
pub MANUAL_REM_EUCLID,
32+
complexity,
33+
"manually reimplementing `rem_euclid`"
34+
}
35+
36+
pub struct ManualRemEuclid {
37+
msrv: Option<RustcVersion>,
38+
}
39+
40+
impl ManualRemEuclid {
41+
#[must_use]
42+
pub fn new(msrv: Option<RustcVersion>) -> Self {
43+
Self { msrv }
44+
}
45+
}
46+
47+
impl_lint_pass!(ManualRemEuclid => [MANUAL_REM_EUCLID]);
48+
49+
impl<'tcx> LateLintPass<'tcx> for ManualRemEuclid {
50+
fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) {
51+
if !meets_msrv(self.msrv, msrvs::REM_EUCLID) {
52+
return;
53+
}
54+
55+
if in_constant(cx, expr.hir_id) && !meets_msrv(self.msrv, msrvs::REM_EUCLID_CONST) {
56+
return;
57+
}
58+
59+
if in_external_macro(cx.sess(), expr.span) {
60+
return;
61+
}
62+
63+
if let ExprKind::Binary(op1, expr1, right) = expr.kind
64+
&& op1.node == BinOpKind::Rem
65+
&& let Some(const1) = check_for_unsigned_int_constant(cx, right)
66+
&& let ExprKind::Binary(op2, left, right) = expr1.kind
67+
&& op2.node == BinOpKind::Add
68+
&& let Some((const2, expr2)) = check_for_either_unsigned_int_constant(cx, left, right)
69+
&& let ExprKind::Binary(op3, expr3, right) = expr2.kind
70+
&& op3.node == BinOpKind::Rem
71+
&& let Some(const3) = check_for_unsigned_int_constant(cx, right)
72+
// Also ensures the const is nonzero since zero can't be a divisor
73+
&& const1 == const2 && const2 == const3
74+
&& let Some(hir_id) = path_to_local(expr3)
75+
&& let Some(Node::Binding(_)) = cx.tcx.hir().find(hir_id) {
76+
// Apply only to params or locals with annotated types
77+
match cx.tcx.hir().find(cx.tcx.hir().get_parent_node(hir_id)) {
78+
Some(Node::Param(..)) => (),
79+
Some(Node::Local(local)) => {
80+
let Some(ty) = local.ty else { return };
81+
if matches!(ty.kind, TyKind::Infer) {
82+
return;
83+
}
84+
}
85+
_ => return,
86+
};
87+
88+
let mut app = Applicability::MachineApplicable;
89+
let rem_of = snippet_with_applicability(cx, expr3.span, "_", &mut app);
90+
span_lint_and_sugg(
91+
cx,
92+
MANUAL_REM_EUCLID,
93+
expr.span,
94+
"manual `rem_euclid` implementation",
95+
"consider using",
96+
format!("{rem_of}.rem_euclid({const1})"),
97+
app,
98+
);
99+
}
100+
}
101+
102+
extract_msrv_attr!(LateContext);
103+
}
104+
105+
// Checks if either the left or right expressions can be an unsigned int constant and returns that
106+
// constant along with the other expression unchanged if so
107+
fn check_for_either_unsigned_int_constant<'a>(
108+
cx: &'a LateContext<'_>,
109+
left: &'a Expr<'_>,
110+
right: &'a Expr<'_>,
111+
) -> Option<(u128, &'a Expr<'a>)> {
112+
check_for_unsigned_int_constant(cx, left)
113+
.map(|int_const| (int_const, right))
114+
.or_else(|| check_for_unsigned_int_constant(cx, right).map(|int_const| (int_const, left)))
115+
}
116+
117+
fn check_for_unsigned_int_constant<'a>(cx: &'a LateContext<'_>, expr: &'a Expr<'_>) -> Option<u128> {
118+
let Some(int_const) = constant_full_int(cx, cx.typeck_results(), expr) else { return None };
119+
match int_const {
120+
FullInt::S(s) => s.try_into().ok(),
121+
FullInt::U(u) => Some(u),
122+
}
123+
}

clippy_utils/src/msrvs.rs

+2-2
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ macro_rules! msrv_aliases {
1313
// names may refer to stabilized feature flags or library items
1414
msrv_aliases! {
1515
1,53,0 { OR_PATTERNS, MANUAL_BITS }
16-
1,52,0 { STR_SPLIT_ONCE }
16+
1,52,0 { STR_SPLIT_ONCE, REM_EUCLID_CONST }
1717
1,51,0 { BORROW_AS_PTR, UNSIGNED_ABS }
1818
1,50,0 { BOOL_THEN }
1919
1,47,0 { TAU }
@@ -23,7 +23,7 @@ msrv_aliases! {
2323
1,42,0 { MATCHES_MACRO, SLICE_PATTERNS, PTR_SLICE_RAW_PARTS }
2424
1,41,0 { RE_REBALANCING_COHERENCE, RESULT_MAP_OR_ELSE }
2525
1,40,0 { MEM_TAKE, NON_EXHAUSTIVE, OPTION_AS_DEREF }
26-
1,38,0 { POINTER_CAST }
26+
1,38,0 { POINTER_CAST, REM_EUCLID }
2727
1,37,0 { TYPE_ALIAS_ENUM_VARIANTS }
2828
1,36,0 { ITERATOR_COPIED }
2929
1,35,0 { OPTION_COPIED, RANGE_CONTAINS }

tests/ui/auxiliary/macro_rules.rs

+8
Original file line numberDiff line numberDiff line change
@@ -127,3 +127,11 @@ macro_rules! ptr_as_ptr_cast {
127127
$ptr as *const i32
128128
};
129129
}
130+
131+
#[macro_export]
132+
macro_rules! manual_rem_euclid {
133+
() => {
134+
let value: i32 = 5;
135+
let _: i32 = ((value % 4) + 4) % 4;
136+
};
137+
}

tests/ui/manual_rem_euclid.fixed

+55
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
// run-rustfix
2+
// aux-build:macro_rules.rs
3+
4+
#![warn(clippy::manual_rem_euclid)]
5+
6+
#[macro_use]
7+
extern crate macro_rules;
8+
9+
macro_rules! internal_rem_euclid {
10+
() => {
11+
let value: i32 = 5;
12+
let _: i32 = value.rem_euclid(4);
13+
};
14+
}
15+
16+
fn main() {
17+
let value: i32 = 5;
18+
19+
let _: i32 = value.rem_euclid(4);
20+
let _: i32 = value.rem_euclid(4);
21+
let _: i32 = value.rem_euclid(4);
22+
let _: i32 = value.rem_euclid(4);
23+
let _: i32 = 1 + value.rem_euclid(4);
24+
25+
let _: i32 = (3 + value % 4) % 4;
26+
let _: i32 = (-4 + value % -4) % -4;
27+
let _: i32 = ((5 % 4) + 4) % 4;
28+
29+
// Make sure the lint does not trigger if it would cause an error, like with an ambiguous
30+
// integer type
31+
let not_annotated = 24;
32+
let _ = ((not_annotated % 4) + 4) % 4;
33+
let inferred: _ = 24;
34+
let _ = ((inferred % 4) + 4) % 4;
35+
36+
// For lint to apply the constant must always be on the RHS of the previous value for %
37+
let _: i32 = 4 % ((value % 4) + 4);
38+
let _: i32 = ((4 % value) + 4) % 4;
39+
40+
// Lint in internal macros
41+
internal_rem_euclid!();
42+
43+
// Do not lint in external macros
44+
manual_rem_euclid!();
45+
}
46+
47+
// Should lint for params too
48+
pub fn rem_euclid_4(num: i32) -> i32 {
49+
num.rem_euclid(4)
50+
}
51+
52+
// Constant version came later, should still lint
53+
pub const fn const_rem_euclid_4(num: i32) -> i32 {
54+
num.rem_euclid(4)
55+
}

tests/ui/manual_rem_euclid.rs

+55
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
// run-rustfix
2+
// aux-build:macro_rules.rs
3+
4+
#![warn(clippy::manual_rem_euclid)]
5+
6+
#[macro_use]
7+
extern crate macro_rules;
8+
9+
macro_rules! internal_rem_euclid {
10+
() => {
11+
let value: i32 = 5;
12+
let _: i32 = ((value % 4) + 4) % 4;
13+
};
14+
}
15+
16+
fn main() {
17+
let value: i32 = 5;
18+
19+
let _: i32 = ((value % 4) + 4) % 4;
20+
let _: i32 = (4 + (value % 4)) % 4;
21+
let _: i32 = (value % 4 + 4) % 4;
22+
let _: i32 = (4 + value % 4) % 4;
23+
let _: i32 = 1 + (4 + value % 4) % 4;
24+
25+
let _: i32 = (3 + value % 4) % 4;
26+
let _: i32 = (-4 + value % -4) % -4;
27+
let _: i32 = ((5 % 4) + 4) % 4;
28+
29+
// Make sure the lint does not trigger if it would cause an error, like with an ambiguous
30+
// integer type
31+
let not_annotated = 24;
32+
let _ = ((not_annotated % 4) + 4) % 4;
33+
let inferred: _ = 24;
34+
let _ = ((inferred % 4) + 4) % 4;
35+
36+
// For lint to apply the constant must always be on the RHS of the previous value for %
37+
let _: i32 = 4 % ((value % 4) + 4);
38+
let _: i32 = ((4 % value) + 4) % 4;
39+
40+
// Lint in internal macros
41+
internal_rem_euclid!();
42+
43+
// Do not lint in external macros
44+
manual_rem_euclid!();
45+
}
46+
47+
// Should lint for params too
48+
pub fn rem_euclid_4(num: i32) -> i32 {
49+
((num % 4) + 4) % 4
50+
}
51+
52+
// Constant version came later, should still lint
53+
pub const fn const_rem_euclid_4(num: i32) -> i32 {
54+
((num % 4) + 4) % 4
55+
}

tests/ui/manual_rem_euclid.stderr

+57
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
error: manual `rem_euclid` implementation
2+
--> $DIR/manual_rem_euclid.rs:19:18
3+
|
4+
LL | let _: i32 = ((value % 4) + 4) % 4;
5+
| ^^^^^^^^^^^^^^^^^^^^^ help: consider using: `value.rem_euclid(4)`
6+
|
7+
= note: `-D clippy::manual-rem-euclid` implied by `-D warnings`
8+
9+
error: manual `rem_euclid` implementation
10+
--> $DIR/manual_rem_euclid.rs:20:18
11+
|
12+
LL | let _: i32 = (4 + (value % 4)) % 4;
13+
| ^^^^^^^^^^^^^^^^^^^^^ help: consider using: `value.rem_euclid(4)`
14+
15+
error: manual `rem_euclid` implementation
16+
--> $DIR/manual_rem_euclid.rs:21:18
17+
|
18+
LL | let _: i32 = (value % 4 + 4) % 4;
19+
| ^^^^^^^^^^^^^^^^^^^ help: consider using: `value.rem_euclid(4)`
20+
21+
error: manual `rem_euclid` implementation
22+
--> $DIR/manual_rem_euclid.rs:22:18
23+
|
24+
LL | let _: i32 = (4 + value % 4) % 4;
25+
| ^^^^^^^^^^^^^^^^^^^ help: consider using: `value.rem_euclid(4)`
26+
27+
error: manual `rem_euclid` implementation
28+
--> $DIR/manual_rem_euclid.rs:23:22
29+
|
30+
LL | let _: i32 = 1 + (4 + value % 4) % 4;
31+
| ^^^^^^^^^^^^^^^^^^^ help: consider using: `value.rem_euclid(4)`
32+
33+
error: manual `rem_euclid` implementation
34+
--> $DIR/manual_rem_euclid.rs:12:22
35+
|
36+
LL | let _: i32 = ((value % 4) + 4) % 4;
37+
| ^^^^^^^^^^^^^^^^^^^^^ help: consider using: `value.rem_euclid(4)`
38+
...
39+
LL | internal_rem_euclid!();
40+
| ---------------------- in this macro invocation
41+
|
42+
= note: this error originates in the macro `internal_rem_euclid` (in Nightly builds, run with -Z macro-backtrace for more info)
43+
44+
error: manual `rem_euclid` implementation
45+
--> $DIR/manual_rem_euclid.rs:49:5
46+
|
47+
LL | ((num % 4) + 4) % 4
48+
| ^^^^^^^^^^^^^^^^^^^ help: consider using: `num.rem_euclid(4)`
49+
50+
error: manual `rem_euclid` implementation
51+
--> $DIR/manual_rem_euclid.rs:54:5
52+
|
53+
LL | ((num % 4) + 4) % 4
54+
| ^^^^^^^^^^^^^^^^^^^ help: consider using: `num.rem_euclid(4)`
55+
56+
error: aborting due to 8 previous errors
57+

0 commit comments

Comments
 (0)