Skip to content

Commit 160daec

Browse files
committed
New lint: manual_is_multiple_of
1 parent 1c54f5a commit 160daec

16 files changed

+364
-2
lines changed

CHANGELOG.md

+2
Original file line numberDiff line numberDiff line change
@@ -5801,6 +5801,7 @@ Released 2018-09-13
58015801
[`manual_is_ascii_check`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_is_ascii_check
58025802
[`manual_is_finite`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_is_finite
58035803
[`manual_is_infinite`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_is_infinite
5804+
[`manual_is_multiple_of`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_is_multiple_of
58045805
[`manual_is_power_of_two`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_is_power_of_two
58055806
[`manual_is_variant_and`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_is_variant_and
58065807
[`manual_let_else`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_let_else
@@ -6369,6 +6370,7 @@ Released 2018-09-13
63696370
[`max-struct-bools`]: https://doc.rust-lang.org/clippy/lint_configuration.html#max-struct-bools
63706371
[`max-suggested-slice-pattern-length`]: https://doc.rust-lang.org/clippy/lint_configuration.html#max-suggested-slice-pattern-length
63716372
[`max-trait-bounds`]: https://doc.rust-lang.org/clippy/lint_configuration.html#max-trait-bounds
6373+
[`min-and-mask-size`]: https://doc.rust-lang.org/clippy/lint_configuration.html#min-and-mask-size
63726374
[`min-ident-chars-threshold`]: https://doc.rust-lang.org/clippy/lint_configuration.html#min-ident-chars-threshold
63736375
[`missing-docs-in-crate-items`]: https://doc.rust-lang.org/clippy/lint_configuration.html#missing-docs-in-crate-items
63746376
[`module-item-order-groupings`]: https://doc.rust-lang.org/clippy/lint_configuration.html#module-item-order-groupings

book/src/lint_configuration.md

+10
Original file line numberDiff line numberDiff line change
@@ -713,6 +713,16 @@ The maximum number of bounds a trait can have to be linted
713713
* [`type_repetition_in_bounds`](https://rust-lang.github.io/rust-clippy/master/index.html#type_repetition_in_bounds)
714714

715715

716+
## `min-and-mask-size`
717+
The smallest number of bits masked with `&` which will be replaced by `.is_multiple_of()`.
718+
719+
**Default Value:** `3`
720+
721+
---
722+
**Affected lints:**
723+
* [`manual_is_multiple_of`](https://rust-lang.github.io/rust-clippy/master/index.html#manual_is_multiple_of)
724+
725+
716726
## `min-ident-chars-threshold`
717727
Minimum chars an ident can have, anything below or equal to this will be linted.
718728

clippy_config/src/conf.rs

+3
Original file line numberDiff line numberDiff line change
@@ -586,6 +586,9 @@ define_Conf! {
586586
/// The maximum number of bounds a trait can have to be linted
587587
#[lints(type_repetition_in_bounds)]
588588
max_trait_bounds: u64 = 3,
589+
/// The smallest number of bits masked with `&` which will be replaced by `.is_multiple_of()`.
590+
#[lints(manual_is_multiple_of)]
591+
min_and_mask_size: u8 = 3,
589592
/// Minimum chars an ident can have, anything below or equal to this will be linted.
590593
#[lints(min_ident_chars)]
591594
min_ident_chars_threshold: u64 = 1,

clippy_lints/src/casts/cast_sign_loss.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,7 @@ fn pow_call_result_sign(cx: &LateContext<'_>, base: &Expr<'_>, exponent: &Expr<'
167167

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

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

clippy_lints/src/declared_lints.rs

+1
Original file line numberDiff line numberDiff line change
@@ -608,6 +608,7 @@ pub static LINTS: &[&crate::LintInfo] = &[
608608
crate::operators::IMPOSSIBLE_COMPARISONS_INFO,
609609
crate::operators::INEFFECTIVE_BIT_MASK_INFO,
610610
crate::operators::INTEGER_DIVISION_INFO,
611+
crate::operators::MANUAL_IS_MULTIPLE_OF_INFO,
611612
crate::operators::MANUAL_MIDPOINT_INFO,
612613
crate::operators::MISREFACTORED_ASSIGN_OP_INFO,
613614
crate::operators::MODULO_ARITHMETIC_INFO,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
use clippy_utils::consts::{integer_const, is_zero_integer_const};
2+
use clippy_utils::diagnostics::span_lint_and_sugg;
3+
use clippy_utils::msrvs::{self, Msrv};
4+
use clippy_utils::source::SpanRangeExt;
5+
use clippy_utils::sugg::Sugg;
6+
use rustc_ast::BinOpKind;
7+
use rustc_errors::Applicability;
8+
use rustc_hir::{Expr, ExprKind};
9+
use rustc_lint::LateContext;
10+
use rustc_middle::ty;
11+
12+
use super::MANUAL_IS_MULTIPLE_OF;
13+
14+
pub(super) fn check<'tcx>(
15+
cx: &LateContext<'tcx>,
16+
expr: &Expr<'_>,
17+
op: BinOpKind,
18+
lhs: &'tcx Expr<'tcx>,
19+
rhs: &'tcx Expr<'tcx>,
20+
min_and_mask_size: u8,
21+
msrv: &Msrv,
22+
) {
23+
if let Some(operand) = uint_compare_to_zero(cx, op, lhs, rhs)
24+
&& let ExprKind::Binary(lhs_op, lhs_left, lhs_right) = operand.kind
25+
&& msrv.meets(msrvs::UNSIGNED_IS_MULTIPLE_OF)
26+
{
27+
let mut app = Applicability::MachineApplicable;
28+
let (dividend, divisor) = if lhs_op.node == BinOpKind::Rem {
29+
(
30+
lhs_left,
31+
Sugg::hir_with_applicability(cx, lhs_right, "_", &mut app).into_string(),
32+
)
33+
} else if lhs_op.node == BinOpKind::BitAnd {
34+
let min_divisor = 1 << u128::from(min_and_mask_size);
35+
if let Some(divisor) = is_all_ones(cx, lhs_right, min_divisor, &mut app) {
36+
(lhs_left, divisor)
37+
} else if let Some(divisor) = is_all_ones(cx, lhs_left, min_divisor, &mut app) {
38+
(lhs_right, divisor)
39+
} else {
40+
return;
41+
}
42+
} else {
43+
return;
44+
};
45+
span_lint_and_sugg(
46+
cx,
47+
MANUAL_IS_MULTIPLE_OF,
48+
expr.span,
49+
"manual implementation of `.is_multiple_of()`",
50+
"replace with",
51+
format!(
52+
"{}{}.is_multiple_of({divisor})",
53+
if op == BinOpKind::Eq { "" } else { "!" },
54+
Sugg::hir_with_applicability(cx, dividend, "_", &mut app).maybe_par()
55+
),
56+
app,
57+
);
58+
}
59+
}
60+
61+
// If we have a `x == 0`, `x != 0` or `x > 0` (or the reverted ones), return the non-zero operand
62+
fn uint_compare_to_zero<'tcx>(
63+
cx: &LateContext<'tcx>,
64+
op: BinOpKind,
65+
lhs: &'tcx Expr<'tcx>,
66+
rhs: &'tcx Expr<'tcx>,
67+
) -> Option<&'tcx Expr<'tcx>> {
68+
let operand = if matches!(lhs.kind, ExprKind::Binary(..))
69+
&& matches!(op, BinOpKind::Eq | BinOpKind::Ne | BinOpKind::Gt)
70+
&& is_zero_integer_const(cx, rhs)
71+
{
72+
lhs
73+
} else if matches!(rhs.kind, ExprKind::Binary(..))
74+
&& matches!(op, BinOpKind::Eq | BinOpKind::Ne | BinOpKind::Lt)
75+
&& is_zero_integer_const(cx, lhs)
76+
{
77+
rhs
78+
} else {
79+
return None;
80+
};
81+
82+
matches!(cx.typeck_results().expr_ty_adjusted(operand).kind(), ty::Uint(_)).then_some(operand)
83+
}
84+
85+
/// If `expr` is made of all ones, return the representation of `expr+1` if it is no smaller than
86+
/// `min_divisor`.
87+
fn is_all_ones<'tcx>(
88+
cx: &LateContext<'tcx>,
89+
expr: &'tcx Expr<'tcx>,
90+
min_divisor: u128,
91+
app: &mut Applicability,
92+
) -> Option<String> {
93+
if let ExprKind::Binary(op, lhs, rhs) = expr.kind
94+
&& op.node == BinOpKind::Sub
95+
&& let ExprKind::Binary(op, lhs_left, lhs_right) = lhs.kind
96+
&& op.node == BinOpKind::Shl
97+
&& let Some(1) = integer_const(cx, lhs_left)
98+
&& let Some(1) = integer_const(cx, rhs)
99+
&& integer_const(cx, lhs_right).is_none_or(|v| 1 << v >= min_divisor)
100+
{
101+
Some(Sugg::hir_with_applicability(cx, lhs, "_", app).to_string())
102+
} else if let Some(value) = integer_const(cx, expr)
103+
&& let Some(inc_value) = value.checked_add(1)
104+
&& inc_value.is_power_of_two()
105+
{
106+
let repr = if expr.span.check_source_text(cx, |s| s.starts_with("0x")) {
107+
format!("{inc_value:#x}")
108+
} else {
109+
inc_value.to_string()
110+
};
111+
(inc_value >= min_divisor).then_some(repr)
112+
} else {
113+
None
114+
}
115+
}

clippy_lints/src/operators/mod.rs

+41
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;
@@ -860,18 +861,56 @@ declare_clippy_lint! {
860861
"manual implementation of `midpoint` which can overflow"
861862
}
862863

864+
declare_clippy_lint! {
865+
/// ### What it does
866+
/// Checks for manual implementation of `.is_multiple_of()` on
867+
/// unsigned integer types.
868+
///
869+
/// ### Why is this bad?
870+
/// `a.is_multiple_of(b)` is a clearer way to check for divisibility
871+
/// of `a` by `b`. This expression can never panic.
872+
///
873+
/// ### Example
874+
/// ```no_run
875+
/// # let (a, b) = (3u64, 4u64);
876+
/// if a % b == 0 {
877+
/// println!("{a} is divisible by {b}");
878+
/// }
879+
/// if a & 3 != 0 {
880+
/// println!("{a} is not properly aligned");
881+
/// }
882+
/// ```
883+
/// Use instead:
884+
/// ```no_run
885+
/// # let (a, b) = (3u64, 4u64);
886+
/// if a.is_multiple_of(b) {
887+
/// println!("{a} is divisible by {b}");
888+
/// }
889+
/// if !a.is_multiple_of(4) {
890+
/// println!("{a} is not properly aligned");
891+
/// }
892+
/// ```
893+
#[clippy::version = "1.87.0"]
894+
pub MANUAL_IS_MULTIPLE_OF,
895+
complexity,
896+
"manual implementation of `.is_multiple_of()`"
897+
}
898+
863899
pub struct Operators {
864900
arithmetic_context: numeric_arithmetic::Context,
865901
verbose_bit_mask_threshold: u64,
866902
modulo_arithmetic_allow_comparison_to_zero: bool,
903+
min_and_mask_size: u8,
867904
msrv: Msrv,
868905
}
906+
869907
impl Operators {
870908
pub fn new(conf: &'static Conf) -> Self {
871909
Self {
872910
arithmetic_context: numeric_arithmetic::Context::default(),
873911
verbose_bit_mask_threshold: conf.verbose_bit_mask_threshold,
874912
modulo_arithmetic_allow_comparison_to_zero: conf.allow_comparison_to_zero,
913+
min_and_mask_size: conf.min_and_mask_size,
875914
msrv: conf.msrv.clone(),
876915
}
877916
}
@@ -905,6 +944,7 @@ impl_lint_pass!(Operators => [
905944
PTR_EQ,
906945
SELF_ASSIGNMENT,
907946
MANUAL_MIDPOINT,
947+
MANUAL_IS_MULTIPLE_OF,
908948
]);
909949

910950
impl<'tcx> LateLintPass<'tcx> for Operators {
@@ -943,6 +983,7 @@ impl<'tcx> LateLintPass<'tcx> for Operators {
943983
rhs,
944984
self.modulo_arithmetic_allow_comparison_to_zero,
945985
);
986+
manual_is_multiple_of::check(cx, e, op.node, lhs, rhs, self.min_and_mask_size, &self.msrv);
946987
},
947988
ExprKind::AssignOp(op, lhs, rhs) => {
948989
self.arithmetic_context.check_binary(cx, e, op.node, lhs, rhs);

clippy_utils/src/msrvs.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ macro_rules! msrv_aliases {
1919

2020
// names may refer to stabilized feature flags or library items
2121
msrv_aliases! {
22-
1,87,0 { OS_STR_DISPLAY, INT_MIDPOINT }
22+
1,87,0 { OS_STR_DISPLAY, INT_MIDPOINT, UNSIGNED_IS_MULTIPLE_OF }
2323
1,85,0 { UINT_FLOAT_MIDPOINT }
2424
1,84,0 { CONST_OPTION_AS_SLICE }
2525
1,83,0 { CONST_EXTERN_FN, CONST_FLOAT_BITS_CONV, CONST_FLOAT_CLASSIFY, CONST_MUT_REFS, CONST_UNWRAP }
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
min-and-mask-size = 0
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
#![warn(clippy::manual_is_multiple_of)]
2+
3+
fn main() {}
4+
5+
fn f(a: u64, b: u64) {
6+
let _ = a.is_multiple_of(2); //~ manual_is_multiple_of
7+
let _ = a.is_multiple_of(1 << 1); //~ manual_is_multiple_of
8+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
#![warn(clippy::manual_is_multiple_of)]
2+
3+
fn main() {}
4+
5+
fn f(a: u64, b: u64) {
6+
let _ = a & 1 == 0; //~ manual_is_multiple_of
7+
let _ = a & ((1 << 1) - 1) == 0; //~ manual_is_multiple_of
8+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
error: manual implementation of `.is_multiple_of()`
2+
--> tests/ui-toml/manual_is_multiple_of/manual_is_multiple_of.rs:6:13
3+
|
4+
LL | let _ = a & 1 == 0;
5+
| ^^^^^^^^^^ help: replace with: `a.is_multiple_of(2)`
6+
|
7+
= note: `-D clippy::manual-is-multiple-of` implied by `-D warnings`
8+
= help: to override `-D warnings` add `#[allow(clippy::manual_is_multiple_of)]`
9+
10+
error: manual implementation of `.is_multiple_of()`
11+
--> tests/ui-toml/manual_is_multiple_of/manual_is_multiple_of.rs:7:13
12+
|
13+
LL | let _ = a & ((1 << 1) - 1) == 0;
14+
| ^^^^^^^^^^^^^^^^^^^^^^^ help: replace with: `a.is_multiple_of(1 << 1)`
15+
16+
error: aborting due to 2 previous errors
17+

tests/ui-toml/toml_unknown_key/conf_unknown_key.stderr

+3
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ error: error reading Clippy's configuration file: unknown field `foobar`, expect
5757
max-struct-bools
5858
max-suggested-slice-pattern-length
5959
max-trait-bounds
60+
min-and-mask-size
6061
min-ident-chars-threshold
6162
missing-docs-in-crate-items
6263
module-item-order-groupings
@@ -149,6 +150,7 @@ error: error reading Clippy's configuration file: unknown field `barfoo`, expect
149150
max-struct-bools
150151
max-suggested-slice-pattern-length
151152
max-trait-bounds
153+
min-and-mask-size
152154
min-ident-chars-threshold
153155
missing-docs-in-crate-items
154156
module-item-order-groupings
@@ -241,6 +243,7 @@ error: error reading Clippy's configuration file: unknown field `allow_mixed_uni
241243
max-struct-bools
242244
max-suggested-slice-pattern-length
243245
max-trait-bounds
246+
min-and-mask-size
244247
min-ident-chars-threshold
245248
missing-docs-in-crate-items
246249
module-item-order-groupings

tests/ui/manual_is_multiple_of.fixed

+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
#![warn(clippy::manual_is_multiple_of)]
2+
3+
fn main() {}
4+
5+
#[clippy::msrv = "1.87"]
6+
fn f(a: u64, b: u64) {
7+
let _ = a.is_multiple_of(b); //~ manual_is_multiple_of
8+
let _ = (a + 1).is_multiple_of(b + 1); //~ manual_is_multiple_of
9+
let _ = !a.is_multiple_of(b); //~ manual_is_multiple_of
10+
let _ = !(a + 1).is_multiple_of(b + 1); //~ manual_is_multiple_of
11+
12+
let _ = a.is_multiple_of(4096); //~ manual_is_multiple_of
13+
let _ = !a.is_multiple_of(4096); //~ manual_is_multiple_of
14+
let _ = a.is_multiple_of(1 << b); //~ manual_is_multiple_of
15+
let _ = !a.is_multiple_of(1 << b); //~ manual_is_multiple_of
16+
let _ = a.is_multiple_of(1 << b); //~ manual_is_multiple_of
17+
18+
let _ = !a.is_multiple_of(b); //~ manual_is_multiple_of
19+
let _ = !a.is_multiple_of(b); //~ manual_is_multiple_of
20+
21+
let _ = a.is_multiple_of(0x100); //~ manual_is_multiple_of
22+
23+
let _ = a & 1 == 0; // Do not lint: below `min-and-mask-size`
24+
let _ = a & ((1 << 1) - 1) == 0; // Do not lint: below `min-and-mask-size`
25+
let _ = a.is_multiple_of(8); //~ manual_is_multiple_of
26+
let _ = a.is_multiple_of(1 << 3); //~ manual_is_multiple_of
27+
}
28+
29+
#[clippy::msrv = "1.86"]
30+
fn g(a: u64, b: u64) {
31+
let _ = a % b == 0;
32+
}

tests/ui/manual_is_multiple_of.rs

+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
#![warn(clippy::manual_is_multiple_of)]
2+
3+
fn main() {}
4+
5+
#[clippy::msrv = "1.87"]
6+
fn f(a: u64, b: u64) {
7+
let _ = a % b == 0; //~ manual_is_multiple_of
8+
let _ = (a + 1) % (b + 1) == 0; //~ manual_is_multiple_of
9+
let _ = a % b != 0; //~ manual_is_multiple_of
10+
let _ = (a + 1) % (b + 1) != 0; //~ manual_is_multiple_of
11+
12+
let _ = a & 4095 == 0; //~ manual_is_multiple_of
13+
let _ = a & 4095 != 0; //~ manual_is_multiple_of
14+
let _ = a & ((1 << b) - 1) == 0; //~ manual_is_multiple_of
15+
let _ = a & ((1 << b) - 1) != 0; //~ manual_is_multiple_of
16+
let _ = ((1 << b) - 1) & a == 0; //~ manual_is_multiple_of
17+
18+
let _ = a % b > 0; //~ manual_is_multiple_of
19+
let _ = 0 < a % b; //~ manual_is_multiple_of
20+
21+
let _ = a & 0xff == 0; //~ manual_is_multiple_of
22+
23+
let _ = a & 1 == 0; // Do not lint: below `min-and-mask-size`
24+
let _ = a & ((1 << 1) - 1) == 0; // Do not lint: below `min-and-mask-size`
25+
let _ = a & 7 == 0; //~ manual_is_multiple_of
26+
let _ = a & ((1 << 3) - 1) == 0; //~ manual_is_multiple_of
27+
}
28+
29+
#[clippy::msrv = "1.86"]
30+
fn g(a: u64, b: u64) {
31+
let _ = a % b == 0;
32+
}

0 commit comments

Comments
 (0)