Skip to content

Commit 2f08e6b

Browse files
committed
New lint: manual_is_multiple_of
1 parent a2af0bc commit 2f08e6b

27 files changed

+461
-99
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
@@ -6367,6 +6368,7 @@ Released 2018-09-13
63676368
[`max-struct-bools`]: https://doc.rust-lang.org/clippy/lint_configuration.html#max-struct-bools
63686369
[`max-suggested-slice-pattern-length`]: https://doc.rust-lang.org/clippy/lint_configuration.html#max-suggested-slice-pattern-length
63696370
[`max-trait-bounds`]: https://doc.rust-lang.org/clippy/lint_configuration.html#max-trait-bounds
6371+
[`min-and-mask-size`]: https://doc.rust-lang.org/clippy/lint_configuration.html#min-and-mask-size
63706372
[`min-ident-chars-threshold`]: https://doc.rust-lang.org/clippy/lint_configuration.html#min-ident-chars-threshold
63716373
[`missing-docs-in-crate-items`]: https://doc.rust-lang.org/clippy/lint_configuration.html#missing-docs-in-crate-items
63726374
[`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-and-mask-size`
707+
The smallest number of bits masked with `&` which will be replaced by `.is_multiple_of()`.
708+
709+
**Default Value:** `3`
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 number of bits masked with `&` which will be replaced by `.is_multiple_of()`.
587+
#[lints(manual_is_multiple_of)]
588+
min_and_mask_size: u8 = 3,
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/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::MISREFACTORED_ASSIGN_OP_INFO,
612613
crate::operators::MODULO_ARITHMETIC_INFO,
613614
crate::operators::MODULO_ONE_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

+46
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,57 @@ 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+
/// # let (a, b) = (3u64, 4u64);
861+
/// if a.is_multiple_of(b) {
862+
/// println!("{a} is divisible by {b}");
863+
/// }
864+
/// if !a.is_multiple_of(4) {
865+
/// println!("{a} is not properly aligned");
866+
/// }
867+
/// ```
868+
#[clippy::version = "1.87.0"]
869+
pub MANUAL_IS_MULTIPLE_OF,
870+
complexity,
871+
"manual implementation of `.is_multiple_of()`"
872+
}
873+
837874
pub struct Operators {
838875
arithmetic_context: numeric_arithmetic::Context,
839876
verbose_bit_mask_threshold: u64,
840877
modulo_arithmetic_allow_comparison_to_zero: bool,
878+
min_and_mask_size: u8,
879+
msrv: Msrv,
841880
}
881+
842882
impl Operators {
843883
pub fn new(conf: &'static Conf) -> Self {
844884
Self {
845885
arithmetic_context: numeric_arithmetic::Context::default(),
846886
verbose_bit_mask_threshold: conf.verbose_bit_mask_threshold,
847887
modulo_arithmetic_allow_comparison_to_zero: conf.allow_comparison_to_zero,
888+
min_and_mask_size: conf.min_and_mask_size,
889+
msrv: conf.msrv.clone(),
848890
}
849891
}
850892
}
@@ -876,6 +918,7 @@ impl_lint_pass!(Operators => [
876918
NEEDLESS_BITWISE_BOOL,
877919
PTR_EQ,
878920
SELF_ASSIGNMENT,
921+
MANUAL_IS_MULTIPLE_OF,
879922
]);
880923

881924
impl<'tcx> LateLintPass<'tcx> for Operators {
@@ -913,6 +956,7 @@ impl<'tcx> LateLintPass<'tcx> for Operators {
913956
rhs,
914957
self.modulo_arithmetic_allow_comparison_to_zero,
915958
);
959+
manual_is_multiple_of::check(cx, e, op.node, lhs, rhs, self.min_and_mask_size, &self.msrv);
916960
},
917961
ExprKind::AssignOp(op, lhs, rhs) => {
918962
self.arithmetic_context.check_binary(cx, e, op.node, lhs, rhs);
@@ -943,6 +987,8 @@ impl<'tcx> LateLintPass<'tcx> for Operators {
943987
fn check_body_post(&mut self, cx: &LateContext<'tcx>, b: &Body<'_>) {
944988
self.arithmetic_context.body_post(cx, b);
945989
}
990+
991+
extract_msrv_attr!(LateContext);
946992
}
947993

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

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 }
22+
1,87,0 { OS_STR_DISPLAY, UNSIGNED_IS_MULTIPLE_OF }
2323
1,84,0 { CONST_OPTION_AS_SLICE }
2424
1,83,0 { CONST_EXTERN_FN, CONST_FLOAT_BITS_CONV, CONST_FLOAT_CLASSIFY, CONST_MUT_REFS, CONST_UNWRAP }
2525
1,82,0 { IS_NONE_OR, REPEAT_N, RAW_REF_OP }
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
@@ -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-and-mask-size
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-and-mask-size
241244
min-ident-chars-threshold
242245
missing-docs-in-crate-items
243246
module-item-order-groupings

tests/ui/box_default.fixed

-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
#![warn(clippy::box_default)]
22
#![allow(clippy::boxed_local, clippy::default_constructed_unit_structs)]
3-
#![feature(unsigned_is_multiple_of)]
43

54
#[derive(Default)]
65
struct ImplementsDefault;

0 commit comments

Comments
 (0)