Skip to content

Commit 2c1c746

Browse files
authored
New lint: manual_is_multiple_of (#14292)
~~I've added a `min_divisor` configuration option, default to 4, to not trigger on smaller divisibility operations. I would prefer not to lint `if a & 1 == 0` as `if a.is_multiple_of(2)` by default because the former is very idiomatic in systems (and embedded) programming.~~ ~~A `min_and_mask_size` option, defaulting to 3, sets the default bits to be and-masked before this lint triggers; that would be `n` in `v & ((1 << n) - 1) == 0`. The form `v % 10 == 0` is always linted.~~ ~~This PR will stay in draft mode until the next rustup which will mark `unsigned_is_multiple_of` stable for Rust 1.87.0, and the feature flags will be removed.~~ What should its category be? I've used "complexity", but "pedantic" might be suitable as well. Close #14289 changelog: [`manual_is_multiple_of`]: new lint r? ghost
2 parents 8ec6f1a + 6ffff5f commit 2c1c746

30 files changed

+303
-89
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5968,6 +5968,7 @@ Released 2018-09-13
59685968
[`manual_is_ascii_check`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_is_ascii_check
59695969
[`manual_is_finite`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_is_finite
59705970
[`manual_is_infinite`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_is_infinite
5971+
[`manual_is_multiple_of`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_is_multiple_of
59715972
[`manual_is_power_of_two`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_is_power_of_two
59725973
[`manual_is_variant_and`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_is_variant_and
59735974
[`manual_let_else`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_let_else

clippy_lints/src/casts/cast_sign_loss.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,7 @@ fn pow_call_result_sign(cx: &LateContext<'_>, base: &Expr<'_>, exponent: &Expr<'
168168

169169
// Rust's integer pow() functions take an unsigned exponent.
170170
let exponent_val = get_const_unsigned_int_eval(cx, exponent, None);
171-
let exponent_is_even = exponent_val.map(|val| val % 2 == 0);
171+
let exponent_is_even = exponent_val.map(|val| val.is_multiple_of(2));
172172

173173
match (base_sign, exponent_is_even) {
174174
// Non-negative bases always return non-negative results, ignoring overflow.

clippy_lints/src/declared_lints.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -591,6 +591,7 @@ pub static LINTS: &[&::declare_clippy_lint::LintInfo] = &[
591591
crate::operators::IMPOSSIBLE_COMPARISONS_INFO,
592592
crate::operators::INEFFECTIVE_BIT_MASK_INFO,
593593
crate::operators::INTEGER_DIVISION_INFO,
594+
crate::operators::MANUAL_IS_MULTIPLE_OF_INFO,
594595
crate::operators::MANUAL_MIDPOINT_INFO,
595596
crate::operators::MISREFACTORED_ASSIGN_OP_INFO,
596597
crate::operators::MODULO_ARITHMETIC_INFO,

clippy_lints/src/if_not_else.rs

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use clippy_utils::consts::{ConstEvalCtxt, Constant};
1+
use clippy_utils::consts::is_zero_integer_const;
22
use clippy_utils::diagnostics::{span_lint_and_help, span_lint_and_sugg};
33
use clippy_utils::is_else_clause;
44
use clippy_utils::source::{HasSession, indent_of, reindent_multiline, snippet};
@@ -48,13 +48,6 @@ declare_clippy_lint! {
4848

4949
declare_lint_pass!(IfNotElse => [IF_NOT_ELSE]);
5050

51-
fn is_zero_const(expr: &Expr<'_>, cx: &LateContext<'_>) -> bool {
52-
if let Some(value) = ConstEvalCtxt::new(cx).eval_simple(expr) {
53-
return Constant::Int(0) == value;
54-
}
55-
false
56-
}
57-
5851
impl LateLintPass<'_> for IfNotElse {
5952
fn check_expr(&mut self, cx: &LateContext<'_>, e: &Expr<'_>) {
6053
if let ExprKind::If(cond, cond_inner, Some(els)) = e.kind
@@ -68,7 +61,7 @@ impl LateLintPass<'_> for IfNotElse {
6861
),
6962
// Don't lint on `… != 0`, as these are likely to be bit tests.
7063
// For example, `if foo & 0x0F00 != 0 { … } else { … }` is already in the "proper" order.
71-
ExprKind::Binary(op, _, rhs) if op.node == BinOpKind::Ne && !is_zero_const(rhs, cx) => (
64+
ExprKind::Binary(op, _, rhs) if op.node == BinOpKind::Ne && !is_zero_integer_const(cx, rhs) => (
7265
"unnecessary `!=` operation",
7366
"change to `==` and swap the blocks of the `if`/`else`",
7467
),

clippy_lints/src/operators/identity_op.rs

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use clippy_utils::consts::{ConstEvalCtxt, Constant, FullInt};
1+
use clippy_utils::consts::{ConstEvalCtxt, Constant, FullInt, integer_const, is_zero_integer_const};
22
use clippy_utils::diagnostics::span_lint_and_sugg;
33
use clippy_utils::source::snippet_with_applicability;
44
use clippy_utils::{ExprUseNode, clip, expr_use_ctxt, peel_hir_expr_refs, unsext};
@@ -186,9 +186,7 @@ fn is_allowed<'tcx>(
186186
cx.typeck_results().expr_ty(left).peel_refs().is_integral()
187187
&& cx.typeck_results().expr_ty(right).peel_refs().is_integral()
188188
// `1 << 0` is a common pattern in bit manipulation code
189-
&& !(cmp == BinOpKind::Shl
190-
&& ConstEvalCtxt::new(cx).eval_simple(right) == Some(Constant::Int(0))
191-
&& ConstEvalCtxt::new(cx).eval_simple(left) == Some(Constant::Int(1)))
189+
&& !(cmp == BinOpKind::Shl && is_zero_integer_const(cx, right) && integer_const(cx, left) == Some(1))
192190
}
193191

194192
fn check_remainder(cx: &LateContext<'_>, left: &Expr<'_>, right: &Expr<'_>, span: Span, arg: Span) {
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
use clippy_utils::consts::is_zero_integer_const;
2+
use clippy_utils::diagnostics::span_lint_and_sugg;
3+
use clippy_utils::msrvs::{self, Msrv};
4+
use clippy_utils::sugg::Sugg;
5+
use rustc_ast::BinOpKind;
6+
use rustc_errors::Applicability;
7+
use rustc_hir::{Expr, ExprKind};
8+
use rustc_lint::LateContext;
9+
use rustc_middle::ty;
10+
11+
use super::MANUAL_IS_MULTIPLE_OF;
12+
13+
pub(super) fn check<'tcx>(
14+
cx: &LateContext<'tcx>,
15+
expr: &Expr<'_>,
16+
op: BinOpKind,
17+
lhs: &'tcx Expr<'tcx>,
18+
rhs: &'tcx Expr<'tcx>,
19+
msrv: Msrv,
20+
) {
21+
if msrv.meets(cx, msrvs::UNSIGNED_IS_MULTIPLE_OF)
22+
&& let Some(operand) = uint_compare_to_zero(cx, op, lhs, rhs)
23+
&& let ExprKind::Binary(operand_op, operand_left, operand_right) = operand.kind
24+
&& operand_op.node == BinOpKind::Rem
25+
{
26+
let mut app = Applicability::MachineApplicable;
27+
let divisor = Sugg::hir_with_applicability(cx, operand_right, "_", &mut app);
28+
span_lint_and_sugg(
29+
cx,
30+
MANUAL_IS_MULTIPLE_OF,
31+
expr.span,
32+
"manual implementation of `.is_multiple_of()`",
33+
"replace with",
34+
format!(
35+
"{}{}.is_multiple_of({divisor})",
36+
if op == BinOpKind::Eq { "" } else { "!" },
37+
Sugg::hir_with_applicability(cx, operand_left, "_", &mut app).maybe_paren()
38+
),
39+
app,
40+
);
41+
}
42+
}
43+
44+
// If we have a `x == 0`, `x != 0` or `x > 0` (or the reverted ones), return the non-zero operand
45+
fn uint_compare_to_zero<'tcx>(
46+
cx: &LateContext<'tcx>,
47+
op: BinOpKind,
48+
lhs: &'tcx Expr<'tcx>,
49+
rhs: &'tcx Expr<'tcx>,
50+
) -> Option<&'tcx Expr<'tcx>> {
51+
let operand = if matches!(lhs.kind, ExprKind::Binary(..))
52+
&& matches!(op, BinOpKind::Eq | BinOpKind::Ne | BinOpKind::Gt)
53+
&& is_zero_integer_const(cx, rhs)
54+
{
55+
lhs
56+
} else if matches!(rhs.kind, ExprKind::Binary(..))
57+
&& matches!(op, BinOpKind::Eq | BinOpKind::Ne | BinOpKind::Lt)
58+
&& is_zero_integer_const(cx, lhs)
59+
{
60+
rhs
61+
} else {
62+
return None;
63+
};
64+
65+
matches!(cx.typeck_results().expr_ty_adjusted(operand).kind(), ty::Uint(_)).then_some(operand)
66+
}

clippy_lints/src/operators/mod.rs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ mod float_cmp;
1111
mod float_equality_without_abs;
1212
mod identity_op;
1313
mod integer_division;
14+
mod manual_is_multiple_of;
1415
mod manual_midpoint;
1516
mod misrefactored_assign_op;
1617
mod modulo_arithmetic;
@@ -830,12 +831,42 @@ declare_clippy_lint! {
830831
"manual implementation of `midpoint` which can overflow"
831832
}
832833

834+
declare_clippy_lint! {
835+
/// ### What it does
836+
/// Checks for manual implementation of `.is_multiple_of()` on
837+
/// unsigned integer types.
838+
///
839+
/// ### Why is this bad?
840+
/// `a.is_multiple_of(b)` is a clearer way to check for divisibility
841+
/// of `a` by `b`. This expression can never panic.
842+
///
843+
/// ### Example
844+
/// ```no_run
845+
/// # let (a, b) = (3u64, 4u64);
846+
/// if a % b == 0 {
847+
/// println!("{a} is divisible by {b}");
848+
/// }
849+
/// ```
850+
/// Use instead:
851+
/// ```no_run
852+
/// # let (a, b) = (3u64, 4u64);
853+
/// if a.is_multiple_of(b) {
854+
/// println!("{a} is divisible by {b}");
855+
/// }
856+
/// ```
857+
#[clippy::version = "1.89.0"]
858+
pub MANUAL_IS_MULTIPLE_OF,
859+
complexity,
860+
"manual implementation of `.is_multiple_of()`"
861+
}
862+
833863
pub struct Operators {
834864
arithmetic_context: numeric_arithmetic::Context,
835865
verbose_bit_mask_threshold: u64,
836866
modulo_arithmetic_allow_comparison_to_zero: bool,
837867
msrv: Msrv,
838868
}
869+
839870
impl Operators {
840871
pub fn new(conf: &'static Conf) -> Self {
841872
Self {
@@ -874,6 +905,7 @@ impl_lint_pass!(Operators => [
874905
NEEDLESS_BITWISE_BOOL,
875906
SELF_ASSIGNMENT,
876907
MANUAL_MIDPOINT,
908+
MANUAL_IS_MULTIPLE_OF,
877909
]);
878910

879911
impl<'tcx> LateLintPass<'tcx> for Operators {
@@ -891,6 +923,7 @@ impl<'tcx> LateLintPass<'tcx> for Operators {
891923
identity_op::check(cx, e, op.node, lhs, rhs);
892924
needless_bitwise_bool::check(cx, e, op.node, lhs, rhs);
893925
manual_midpoint::check(cx, e, op.node, lhs, rhs, self.msrv);
926+
manual_is_multiple_of::check(cx, e, op.node, lhs, rhs, self.msrv);
894927
}
895928
self.arithmetic_context.check_binary(cx, e, op.node, lhs, rhs);
896929
bit_mask::check(cx, e, op.node, lhs, rhs);

clippy_utils/src/consts.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -958,3 +958,18 @@ fn field_of_struct<'tcx>(
958958
None
959959
}
960960
}
961+
962+
/// If `expr` evaluates to an integer constant, return its value.
963+
pub fn integer_const(cx: &LateContext<'_>, expr: &Expr<'_>) -> Option<u128> {
964+
if let Some(Constant::Int(value)) = ConstEvalCtxt::new(cx).eval_simple(expr) {
965+
Some(value)
966+
} else {
967+
None
968+
}
969+
}
970+
971+
/// Check if `expr` evaluates to an integer constant of 0.
972+
#[inline]
973+
pub fn is_zero_integer_const(cx: &LateContext<'_>, expr: &Expr<'_>) -> bool {
974+
integer_const(cx, expr) == Some(0)
975+
}

clippy_utils/src/msrvs.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ macro_rules! msrv_aliases {
2424
// names may refer to stabilized feature flags or library items
2525
msrv_aliases! {
2626
1,88,0 { LET_CHAINS }
27-
1,87,0 { OS_STR_DISPLAY, INT_MIDPOINT, CONST_CHAR_IS_DIGIT }
27+
1,87,0 { OS_STR_DISPLAY, INT_MIDPOINT, CONST_CHAR_IS_DIGIT, UNSIGNED_IS_MULTIPLE_OF }
2828
1,85,0 { UINT_FLOAT_MIDPOINT, CONST_SIZE_OF_VAL }
2929
1,84,0 { CONST_OPTION_AS_SLICE, MANUAL_DANGLING_PTR }
3030
1,83,0 { CONST_EXTERN_FN, CONST_FLOAT_BITS_CONV, CONST_FLOAT_CLASSIFY, CONST_MUT_REFS, CONST_UNWRAP }

tests/ui/box_default.fixed

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ fn issue_10381() {
126126
impl Bar for Foo {}
127127

128128
fn maybe_get_bar(i: u32) -> Option<Box<dyn Bar>> {
129-
if i % 2 == 0 {
129+
if i.is_multiple_of(2) {
130130
Some(Box::new(Foo::default()))
131131
} else {
132132
None

tests/ui/box_default.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ fn issue_10381() {
126126
impl Bar for Foo {}
127127

128128
fn maybe_get_bar(i: u32) -> Option<Box<dyn Bar>> {
129-
if i % 2 == 0 {
129+
if i.is_multiple_of(2) {
130130
Some(Box::new(Foo::default()))
131131
} else {
132132
None

tests/ui/infinite_iter.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ fn infinite_iters() {
3838
//~^ infinite_iter
3939

4040
// infinite iter
41-
(0_u64..).filter(|x| x % 2 == 0).last();
41+
(0_u64..).filter(|x| x.is_multiple_of(2)).last();
4242
//~^ infinite_iter
4343

4444
// not an infinite, because ranges are double-ended

tests/ui/infinite_iter.stderr

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,8 @@ LL | (0_usize..).flat_map(|x| 0..x).product::<usize>();
4242
error: infinite iteration detected
4343
--> tests/ui/infinite_iter.rs:41:5
4444
|
45-
LL | (0_u64..).filter(|x| x % 2 == 0).last();
46-
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
45+
LL | (0_u64..).filter(|x| x.is_multiple_of(2)).last();
46+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
4747

4848
error: possible infinite iteration detected
4949
--> tests/ui/infinite_iter.rs:53:5

tests/ui/iter_kv_map.fixed

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -30,15 +30,19 @@ fn main() {
3030

3131
let _ = map.clone().values().collect::<Vec<_>>();
3232
//~^ iter_kv_map
33-
let _ = map.keys().filter(|x| *x % 2 == 0).count();
33+
let _ = map.keys().filter(|x| x.is_multiple_of(2)).count();
3434
//~^ iter_kv_map
3535

3636
// Don't lint
37-
let _ = map.iter().filter(|(_, val)| *val % 2 == 0).map(|(key, _)| key).count();
37+
let _ = map
38+
.iter()
39+
.filter(|(_, val)| val.is_multiple_of(2))
40+
.map(|(key, _)| key)
41+
.count();
3842
let _ = map.iter().map(get_key).collect::<Vec<_>>();
3943

4044
// Linting the following could be an improvement to the lint
41-
// map.iter().filter_map(|(_, val)| (val % 2 == 0).then(val * 17)).count();
45+
// map.iter().filter_map(|(_, val)| (val.is_multiple_of(2)).then(val * 17)).count();
4246

4347
// Lint
4448
let _ = map.keys().map(|key| key * 9).count();
@@ -84,15 +88,19 @@ fn main() {
8488

8589
let _ = map.clone().values().collect::<Vec<_>>();
8690
//~^ iter_kv_map
87-
let _ = map.keys().filter(|x| *x % 2 == 0).count();
91+
let _ = map.keys().filter(|x| x.is_multiple_of(2)).count();
8892
//~^ iter_kv_map
8993

9094
// Don't lint
91-
let _ = map.iter().filter(|(_, val)| *val % 2 == 0).map(|(key, _)| key).count();
95+
let _ = map
96+
.iter()
97+
.filter(|(_, val)| val.is_multiple_of(2))
98+
.map(|(key, _)| key)
99+
.count();
92100
let _ = map.iter().map(get_key).collect::<Vec<_>>();
93101

94102
// Linting the following could be an improvement to the lint
95-
// map.iter().filter_map(|(_, val)| (val % 2 == 0).then(val * 17)).count();
103+
// map.iter().filter_map(|(_, val)| (val.is_multiple_of(2)).then(val * 17)).count();
96104

97105
// Lint
98106
let _ = map.keys().map(|key| key * 9).count();

tests/ui/iter_kv_map.rs

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -30,15 +30,19 @@ fn main() {
3030

3131
let _ = map.clone().iter().map(|(_, val)| val).collect::<Vec<_>>();
3232
//~^ iter_kv_map
33-
let _ = map.iter().map(|(key, _)| key).filter(|x| *x % 2 == 0).count();
33+
let _ = map.iter().map(|(key, _)| key).filter(|x| x.is_multiple_of(2)).count();
3434
//~^ iter_kv_map
3535

3636
// Don't lint
37-
let _ = map.iter().filter(|(_, val)| *val % 2 == 0).map(|(key, _)| key).count();
37+
let _ = map
38+
.iter()
39+
.filter(|(_, val)| val.is_multiple_of(2))
40+
.map(|(key, _)| key)
41+
.count();
3842
let _ = map.iter().map(get_key).collect::<Vec<_>>();
3943

4044
// Linting the following could be an improvement to the lint
41-
// map.iter().filter_map(|(_, val)| (val % 2 == 0).then(val * 17)).count();
45+
// map.iter().filter_map(|(_, val)| (val.is_multiple_of(2)).then(val * 17)).count();
4246

4347
// Lint
4448
let _ = map.iter().map(|(key, _value)| key * 9).count();
@@ -86,15 +90,19 @@ fn main() {
8690

8791
let _ = map.clone().iter().map(|(_, val)| val).collect::<Vec<_>>();
8892
//~^ iter_kv_map
89-
let _ = map.iter().map(|(key, _)| key).filter(|x| *x % 2 == 0).count();
93+
let _ = map.iter().map(|(key, _)| key).filter(|x| x.is_multiple_of(2)).count();
9094
//~^ iter_kv_map
9195

9296
// Don't lint
93-
let _ = map.iter().filter(|(_, val)| *val % 2 == 0).map(|(key, _)| key).count();
97+
let _ = map
98+
.iter()
99+
.filter(|(_, val)| val.is_multiple_of(2))
100+
.map(|(key, _)| key)
101+
.count();
94102
let _ = map.iter().map(get_key).collect::<Vec<_>>();
95103

96104
// Linting the following could be an improvement to the lint
97-
// map.iter().filter_map(|(_, val)| (val % 2 == 0).then(val * 17)).count();
105+
// map.iter().filter_map(|(_, val)| (val.is_multiple_of(2)).then(val * 17)).count();
98106

99107
// Lint
100108
let _ = map.iter().map(|(key, _value)| key * 9).count();

0 commit comments

Comments
 (0)