Skip to content

Commit 36bb8c5

Browse files
committed
New lint: manual_is_multiple_of
1 parent 9c54a1d commit 36bb8c5

16 files changed

+376
-2
lines changed

CHANGELOG.md

+2
Original file line numberDiff line numberDiff line change
@@ -5803,6 +5803,7 @@ Released 2018-09-13
58035803
[`manual_is_ascii_check`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_is_ascii_check
58045804
[`manual_is_finite`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_is_finite
58055805
[`manual_is_infinite`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_is_infinite
5806+
[`manual_is_multiple_of`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_is_multiple_of
58065807
[`manual_is_power_of_two`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_is_power_of_two
58075808
[`manual_is_variant_and`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_is_variant_and
58085809
[`manual_let_else`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_let_else
@@ -6372,6 +6373,7 @@ Released 2018-09-13
63726373
[`max-struct-bools`]: https://doc.rust-lang.org/clippy/lint_configuration.html#max-struct-bools
63736374
[`max-suggested-slice-pattern-length`]: https://doc.rust-lang.org/clippy/lint_configuration.html#max-suggested-slice-pattern-length
63746375
[`max-trait-bounds`]: https://doc.rust-lang.org/clippy/lint_configuration.html#max-trait-bounds
6376+
[`min-and-mask-size`]: https://doc.rust-lang.org/clippy/lint_configuration.html#min-and-mask-size
63756377
[`min-ident-chars-threshold`]: https://doc.rust-lang.org/clippy/lint_configuration.html#min-ident-chars-threshold
63766378
[`missing-docs-in-crate-items`]: https://doc.rust-lang.org/clippy/lint_configuration.html#missing-docs-in-crate-items
63776379
[`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
@@ -724,6 +724,16 @@ The maximum number of bounds a trait can have to be linted
724724
* [`type_repetition_in_bounds`](https://rust-lang.github.io/rust-clippy/master/index.html#type_repetition_in_bounds)
725725

726726

727+
## `min-and-mask-size`
728+
The smallest number of bits masked with `&` which will be replaced by `.is_multiple_of()`.
729+
730+
**Default Value:** `3`
731+
732+
---
733+
**Affected lints:**
734+
* [`manual_is_multiple_of`](https://rust-lang.github.io/rust-clippy/master/index.html#manual_is_multiple_of)
735+
736+
727737
## `min-ident-chars-threshold`
728738
Minimum chars an ident can have, anything below or equal to this will be linted.
729739

clippy_config/src/conf.rs

+3
Original file line numberDiff line numberDiff line change
@@ -603,6 +603,9 @@ define_Conf! {
603603
/// The maximum number of bounds a trait can have to be linted
604604
#[lints(type_repetition_in_bounds)]
605605
max_trait_bounds: u64 = 3,
606+
/// The smallest number of bits masked with `&` which will be replaced by `.is_multiple_of()`.
607+
#[lints(manual_is_multiple_of)]
608+
min_and_mask_size: u8 = 3,
606609
/// Minimum chars an ident can have, anything below or equal to this will be linted.
607610
#[lints(min_ident_chars)]
608611
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
@@ -610,6 +610,7 @@ pub static LINTS: &[&crate::LintInfo] = &[
610610
crate::operators::IMPOSSIBLE_COMPARISONS_INFO,
611611
crate::operators::INEFFECTIVE_BIT_MASK_INFO,
612612
crate::operators::INTEGER_DIVISION_INFO,
613+
crate::operators::MANUAL_IS_MULTIPLE_OF_INFO,
613614
crate::operators::MANUAL_MIDPOINT_INFO,
614615
crate::operators::MISREFACTORED_ASSIGN_OP_INFO,
615616
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 msrv.meets(cx, msrvs::UNSIGNED_IS_MULTIPLE_OF)
24+
&& let Some(operand) = uint_compare_to_zero(cx, op, lhs, rhs)
25+
&& let ExprKind::Binary(lhs_op, lhs_left, lhs_right) = operand.kind
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_paren()
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;
@@ -830,18 +831,56 @@ 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+
/// if a & 3 != 0 {
850+
/// println!("{a} is not properly aligned");
851+
/// }
852+
/// ```
853+
/// Use instead:
854+
/// ```no_run
855+
/// # let (a, b) = (3u64, 4u64);
856+
/// if a.is_multiple_of(b) {
857+
/// println!("{a} is divisible by {b}");
858+
/// }
859+
/// if !a.is_multiple_of(4) {
860+
/// println!("{a} is not properly aligned");
861+
/// }
862+
/// ```
863+
#[clippy::version = "1.87.0"]
864+
pub MANUAL_IS_MULTIPLE_OF,
865+
complexity,
866+
"manual implementation of `.is_multiple_of()`"
867+
}
868+
833869
pub struct Operators {
834870
arithmetic_context: numeric_arithmetic::Context,
835871
verbose_bit_mask_threshold: u64,
836872
modulo_arithmetic_allow_comparison_to_zero: bool,
873+
min_and_mask_size: u8,
837874
msrv: Msrv,
838875
}
876+
839877
impl Operators {
840878
pub fn new(conf: &'static Conf) -> Self {
841879
Self {
842880
arithmetic_context: numeric_arithmetic::Context::default(),
843881
verbose_bit_mask_threshold: conf.verbose_bit_mask_threshold,
844882
modulo_arithmetic_allow_comparison_to_zero: conf.allow_comparison_to_zero,
883+
min_and_mask_size: conf.min_and_mask_size,
845884
msrv: conf.msrv,
846885
}
847886
}
@@ -874,6 +913,7 @@ impl_lint_pass!(Operators => [
874913
NEEDLESS_BITWISE_BOOL,
875914
SELF_ASSIGNMENT,
876915
MANUAL_MIDPOINT,
916+
MANUAL_IS_MULTIPLE_OF,
877917
]);
878918

879919
impl<'tcx> LateLintPass<'tcx> for Operators {
@@ -891,6 +931,7 @@ impl<'tcx> LateLintPass<'tcx> for Operators {
891931
identity_op::check(cx, e, op.node, lhs, rhs);
892932
needless_bitwise_bool::check(cx, e, op.node, lhs, rhs);
893933
manual_midpoint::check(cx, e, op.node, lhs, rhs, self.msrv);
934+
manual_is_multiple_of::check(cx, e, op.node, lhs, rhs, self.min_and_mask_size, self.msrv);
894935
}
895936
self.arithmetic_context.check_binary(cx, e, op.node, lhs, rhs);
896937
bit_mask::check(cx, e, op.node, lhs, rhs);

clippy_utils/src/msrvs.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ macro_rules! msrv_aliases {
2222

2323
// names may refer to stabilized feature flags or library items
2424
msrv_aliases! {
25-
1,87,0 { OS_STR_DISPLAY, INT_MIDPOINT }
25+
1,87,0 { OS_STR_DISPLAY, INT_MIDPOINT, UNSIGNED_IS_MULTIPLE_OF }
2626
1,85,0 { UINT_FLOAT_MIDPOINT }
2727
1,84,0 { CONST_OPTION_AS_SLICE }
2828
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
@@ -56,6 +56,7 @@ error: error reading Clippy's configuration file: unknown field `foobar`, expect
5656
max-struct-bools
5757
max-suggested-slice-pattern-length
5858
max-trait-bounds
59+
min-and-mask-size
5960
min-ident-chars-threshold
6061
missing-docs-in-crate-items
6162
module-item-order-groupings
@@ -148,6 +149,7 @@ error: error reading Clippy's configuration file: unknown field `barfoo`, expect
148149
max-struct-bools
149150
max-suggested-slice-pattern-length
150151
max-trait-bounds
152+
min-and-mask-size
151153
min-ident-chars-threshold
152154
missing-docs-in-crate-items
153155
module-item-order-groupings
@@ -240,6 +242,7 @@ error: error reading Clippy's configuration file: unknown field `allow_mixed_uni
240242
max-struct-bools
241243
max-suggested-slice-pattern-length
242244
max-trait-bounds
245+
min-and-mask-size
243246
min-ident-chars-threshold
244247
missing-docs-in-crate-items
245248
module-item-order-groupings

tests/ui/manual_is_multiple_of.fixed

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

0 commit comments

Comments
 (0)