Skip to content

Commit ca4c0a9

Browse files
committed
New lint: manual_is_multiple_of
1 parent 98e87cf commit ca4c0a9

15 files changed

+388
-0
lines changed

CHANGELOG.md

+2
Original file line numberDiff line numberDiff line change
@@ -5800,6 +5800,7 @@ Released 2018-09-13
58005800
[`manual_is_ascii_check`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_is_ascii_check
58015801
[`manual_is_finite`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_is_finite
58025802
[`manual_is_infinite`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_is_infinite
5803+
[`manual_is_multiple_of`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_is_multiple_of
58035804
[`manual_is_power_of_two`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_is_power_of_two
58045805
[`manual_is_variant_and`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_is_variant_and
58055806
[`manual_let_else`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_let_else
@@ -6365,6 +6366,7 @@ Released 2018-09-13
63656366
[`max-struct-bools`]: https://doc.rust-lang.org/clippy/lint_configuration.html#max-struct-bools
63666367
[`max-suggested-slice-pattern-length`]: https://doc.rust-lang.org/clippy/lint_configuration.html#max-suggested-slice-pattern-length
63676368
[`max-trait-bounds`]: https://doc.rust-lang.org/clippy/lint_configuration.html#max-trait-bounds
6369+
[`min-divisor`]: https://doc.rust-lang.org/clippy/lint_configuration.html#min-divisor
63686370
[`min-ident-chars-threshold`]: https://doc.rust-lang.org/clippy/lint_configuration.html#min-ident-chars-threshold
63696371
[`missing-docs-in-crate-items`]: https://doc.rust-lang.org/clippy/lint_configuration.html#missing-docs-in-crate-items
63706372
[`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
@@ -703,6 +703,16 @@ The maximum number of bounds a trait can have to be linted
703703
* [`type_repetition_in_bounds`](https://rust-lang.github.io/rust-clippy/master/index.html#type_repetition_in_bounds)
704704

705705

706+
## `min-divisor`
707+
The smallest divisor to propose `.is_multiple_of()` for.
708+
709+
**Default Value:** `4`
710+
711+
---
712+
**Affected lints:**
713+
* [`manual_is_multiple_of`](https://rust-lang.github.io/rust-clippy/master/index.html#manual_is_multiple_of)
714+
715+
706716
## `min-ident-chars-threshold`
707717
Minimum chars an ident can have, anything below or equal to this will be linted.
708718

clippy_config/src/conf.rs

+3
Original file line numberDiff line numberDiff line change
@@ -583,6 +583,9 @@ define_Conf! {
583583
/// The maximum number of bounds a trait can have to be linted
584584
#[lints(type_repetition_in_bounds)]
585585
max_trait_bounds: u64 = 3,
586+
/// The smallest divisor to propose `.is_multiple_of()` for.
587+
#[lints(manual_is_multiple_of)]
588+
min_divisor: u64 = 4,
586589
/// Minimum chars an ident can have, anything below or equal to this will be linted.
587590
#[lints(min_ident_chars)]
588591
min_ident_chars_threshold: u64 = 1,

clippy_lints/src/declared_lints.rs

+1
Original file line numberDiff line numberDiff line change
@@ -606,6 +606,7 @@ pub static LINTS: &[&crate::LintInfo] = &[
606606
crate::operators::IMPOSSIBLE_COMPARISONS_INFO,
607607
crate::operators::INEFFECTIVE_BIT_MASK_INFO,
608608
crate::operators::INTEGER_DIVISION_INFO,
609+
crate::operators::MANUAL_IS_MULTIPLE_OF_INFO,
609610
crate::operators::MISREFACTORED_ASSIGN_OP_INFO,
610611
crate::operators::MODULO_ARITHMETIC_INFO,
611612
crate::operators::MODULO_ONE_INFO,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
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_divisor: u64,
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+
const MSG: &str = "manual implementation of `.is_multiple_of()`";
28+
let min_divisor = u128::from(min_divisor);
29+
let invert = if op == BinOpKind::Eq { "" } else { "!" };
30+
let mut app = Applicability::MachineApplicable;
31+
32+
if lhs_op.node == BinOpKind::Rem {
33+
if integer_const(cx, lhs_right).is_some_and(|d| d < min_divisor) {
34+
return;
35+
}
36+
span_lint_and_sugg(
37+
cx,
38+
MANUAL_IS_MULTIPLE_OF,
39+
expr.span,
40+
MSG,
41+
"replace with",
42+
format!(
43+
"{}{}.is_multiple_of({})",
44+
invert,
45+
Sugg::hir_with_applicability(cx, lhs_left, "_", &mut app).maybe_par(),
46+
Sugg::hir_with_applicability(cx, lhs_right, "_", &mut app)
47+
),
48+
app,
49+
);
50+
} else if lhs_op.node == BinOpKind::BitAnd {
51+
let (dividend, divisor) = if let Some(divisor) = is_all_ones(cx, lhs_right, min_divisor, &mut app) {
52+
(lhs_left, divisor)
53+
} else if let Some(divisor) = is_all_ones(cx, lhs_left, min_divisor, &mut app) {
54+
(lhs_right, divisor)
55+
} else {
56+
return;
57+
};
58+
span_lint_and_sugg(
59+
cx,
60+
MANUAL_IS_MULTIPLE_OF,
61+
expr.span,
62+
MSG,
63+
"replace with",
64+
format!(
65+
"{}{}.is_multiple_of({divisor})",
66+
invert,
67+
Sugg::hir_with_applicability(cx, dividend, "_", &mut app).maybe_par()
68+
),
69+
app,
70+
);
71+
}
72+
}
73+
}
74+
75+
// If we have a `x == 0`, `x != 0` or `x > 0` (or the reverted ones), return the non-zero operand
76+
fn uint_compare_to_zero<'tcx>(
77+
cx: &LateContext<'tcx>,
78+
op: BinOpKind,
79+
lhs: &'tcx Expr<'tcx>,
80+
rhs: &'tcx Expr<'tcx>,
81+
) -> Option<&'tcx Expr<'tcx>> {
82+
let operand = if matches!(lhs.kind, ExprKind::Binary(..))
83+
&& matches!(op, BinOpKind::Eq | BinOpKind::Ne | BinOpKind::Gt)
84+
&& is_zero_integer_const(cx, rhs)
85+
{
86+
lhs
87+
} else if matches!(rhs.kind, ExprKind::Binary(..))
88+
&& matches!(op, BinOpKind::Eq | BinOpKind::Ne | BinOpKind::Lt)
89+
&& is_zero_integer_const(cx, lhs)
90+
{
91+
rhs
92+
} else {
93+
return None;
94+
};
95+
96+
matches!(cx.typeck_results().expr_ty_adjusted(operand).kind(), ty::Uint(_)).then_some(operand)
97+
}
98+
99+
/// If `expr` is made of all ones, return the representation of a value `v` such as
100+
/// `1 << v == expr + 1`. If it can be determined than `1 << v` is smaller than `min_divisor`,
101+
/// return `None`.
102+
fn is_all_ones<'tcx>(
103+
cx: &LateContext<'tcx>,
104+
expr: &'tcx Expr<'tcx>,
105+
min_divisor: u128,
106+
app: &mut Applicability,
107+
) -> Option<String> {
108+
if let ExprKind::Binary(op, lhs, rhs) = expr.kind
109+
&& op.node == BinOpKind::Sub
110+
&& let ExprKind::Binary(op, lhs_left, lhs_right) = lhs.kind
111+
&& op.node == BinOpKind::Shl
112+
&& let Some(1) = integer_const(cx, lhs_left)
113+
&& let Some(1) = integer_const(cx, rhs)
114+
&& integer_const(cx, lhs_right).is_none_or(|v| 1 << v >= min_divisor)
115+
{
116+
Some(Sugg::hir_with_applicability(cx, lhs, "_", app).to_string())
117+
} else if let Some(value) = integer_const(cx, expr)
118+
&& let Some(inc_value) = value.checked_add(1)
119+
&& inc_value.is_power_of_two()
120+
{
121+
let repr = if expr.span.check_source_text(cx, |s| s.starts_with("0x")) {
122+
format!("{inc_value:#x}")
123+
} else {
124+
inc_value.to_string()
125+
};
126+
(inc_value >= min_divisor).then_some(repr)
127+
} else {
128+
None
129+
}
130+
}

clippy_lints/src/operators/mod.rs

+47
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 misrefactored_assign_op;
1516
mod modulo_arithmetic;
1617
mod modulo_one;
@@ -24,6 +25,7 @@ mod verbose_bit_mask;
2425
pub(crate) mod arithmetic_side_effects;
2526

2627
use clippy_config::Conf;
28+
use clippy_utils::msrvs::Msrv;
2729
use rustc_hir::{Body, Expr, ExprKind, UnOp};
2830
use rustc_lint::{LateContext, LateLintPass};
2931
use rustc_session::impl_lint_pass;
@@ -834,17 +836,58 @@ declare_clippy_lint! {
834836
"explicit self-assignment"
835837
}
836838

839+
declare_clippy_lint! {
840+
/// ### What it does
841+
/// Checks for manual implementation of `.is_multiple_of()` on
842+
/// unsigned integer types.
843+
///
844+
/// ### Why is this bad?
845+
/// `a.is_multiple_of(b)` is a clearer way to check for divisibility
846+
/// of `a` by `b`. This expression can never panic.
847+
///
848+
/// ### Example
849+
/// ```no_run
850+
/// # let (a, b) = (3u64, 4u64);
851+
/// if a % b == 0 {
852+
/// println!("{a} is divisible by {b}");
853+
/// }
854+
/// if a & 3 != 0 {
855+
/// println!("{a} is not properly aligned");
856+
/// }
857+
/// ```
858+
/// Use instead:
859+
/// ```no_run
860+
/// # #![feature(unsigned_is_multiple_of)]
861+
/// # let (a, b) = (3u64, 4u64);
862+
/// if a.is_multiple_of(b) {
863+
/// println!("{a} is divisible by {b}");
864+
/// }
865+
/// if !a.is_multiple_of(4) {
866+
/// println!("{a} is not properly aligned");
867+
/// }
868+
/// ```
869+
#[clippy::version = "1.87.0"]
870+
pub MANUAL_IS_MULTIPLE_OF,
871+
complexity,
872+
"manual implementation of `.is_multiple_of()`"
873+
}
874+
837875
pub struct Operators {
838876
arithmetic_context: numeric_arithmetic::Context,
839877
verbose_bit_mask_threshold: u64,
840878
modulo_arithmetic_allow_comparison_to_zero: bool,
879+
min_divisor: u64,
880+
msrv: Msrv,
841881
}
882+
842883
impl Operators {
843884
pub fn new(conf: &'static Conf) -> Self {
844885
Self {
845886
arithmetic_context: numeric_arithmetic::Context::default(),
846887
verbose_bit_mask_threshold: conf.verbose_bit_mask_threshold,
847888
modulo_arithmetic_allow_comparison_to_zero: conf.allow_comparison_to_zero,
889+
min_divisor: conf.min_divisor,
890+
msrv: conf.msrv.clone(),
848891
}
849892
}
850893
}
@@ -876,6 +919,7 @@ impl_lint_pass!(Operators => [
876919
NEEDLESS_BITWISE_BOOL,
877920
PTR_EQ,
878921
SELF_ASSIGNMENT,
922+
MANUAL_IS_MULTIPLE_OF,
879923
]);
880924

881925
impl<'tcx> LateLintPass<'tcx> for Operators {
@@ -913,6 +957,7 @@ impl<'tcx> LateLintPass<'tcx> for Operators {
913957
rhs,
914958
self.modulo_arithmetic_allow_comparison_to_zero,
915959
);
960+
manual_is_multiple_of::check(cx, e, op.node, lhs, rhs, self.min_divisor, &self.msrv);
916961
},
917962
ExprKind::AssignOp(op, lhs, rhs) => {
918963
self.arithmetic_context.check_binary(cx, e, op.node, lhs, rhs);
@@ -943,6 +988,8 @@ impl<'tcx> LateLintPass<'tcx> for Operators {
943988
fn check_body_post(&mut self, cx: &LateContext<'tcx>, b: &Body<'_>) {
944989
self.arithmetic_context.body_post(cx, b);
945990
}
991+
992+
extract_msrv_attr!(LateContext);
946993
}
947994

948995
fn macro_with_not_op(e: &Expr<'_>) -> bool {

clippy_utils/src/msrvs.rs

+1
Original file line numberDiff line numberDiff line change
@@ -19,6 +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 { UNSIGNED_IS_MULTIPLE_OF }
2223
1,84,0 { CONST_OPTION_AS_SLICE }
2324
1,83,0 { CONST_EXTERN_FN, CONST_FLOAT_BITS_CONV, CONST_FLOAT_CLASSIFY, CONST_MUT_REFS, CONST_UNWRAP }
2425
1,82,0 { IS_NONE_OR, REPEAT_N, RAW_REF_OP }
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
min-divisor = 0
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
#![warn(clippy::manual_is_multiple_of)]
2+
#![feature(unsigned_is_multiple_of)]
3+
4+
fn main() {}
5+
6+
fn f(a: u64, b: u64) {
7+
let _ = a.is_multiple_of(2); //~ manual_is_multiple_of
8+
let _ = a.is_multiple_of(1 << 1); //~ manual_is_multiple_of
9+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
#![warn(clippy::manual_is_multiple_of)]
2+
#![feature(unsigned_is_multiple_of)]
3+
4+
fn main() {}
5+
6+
fn f(a: u64, b: u64) {
7+
let _ = a & 1 == 0; //~ manual_is_multiple_of
8+
let _ = a & ((1 << 1) - 1) == 0; //~ manual_is_multiple_of
9+
}
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:7: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:8: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-divisor
5960
min-ident-chars-threshold
6061
missing-docs-in-crate-items
6162
module-item-order-groupings
@@ -147,6 +148,7 @@ error: error reading Clippy's configuration file: unknown field `barfoo`, expect
147148
max-struct-bools
148149
max-suggested-slice-pattern-length
149150
max-trait-bounds
151+
min-divisor
150152
min-ident-chars-threshold
151153
missing-docs-in-crate-items
152154
module-item-order-groupings
@@ -238,6 +240,7 @@ error: error reading Clippy's configuration file: unknown field `allow_mixed_uni
238240
max-struct-bools
239241
max-suggested-slice-pattern-length
240242
max-trait-bounds
243+
min-divisor
241244
min-ident-chars-threshold
242245
missing-docs-in-crate-items
243246
module-item-order-groupings

tests/ui/manual_is_multiple_of.fixed

+33
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
#![warn(clippy::manual_is_multiple_of)]
2+
#![feature(unsigned_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-divisor`
25+
let _ = a & ((1 << 1) - 1) == 0; // Do not lint: below `min-divisor`
26+
let _ = a.is_multiple_of(4); //~ manual_is_multiple_of
27+
let _ = a.is_multiple_of(1 << 2); //~ manual_is_multiple_of
28+
}
29+
30+
#[clippy::msrv = "1.86"]
31+
fn g(a: u64, b: u64) {
32+
let _ = a % b == 0;
33+
}

0 commit comments

Comments
 (0)