Skip to content

New lint: manual_is_multiple_of #14292

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5863,6 +5863,7 @@ Released 2018-09-13
[`manual_is_ascii_check`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_is_ascii_check
[`manual_is_finite`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_is_finite
[`manual_is_infinite`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_is_infinite
[`manual_is_multiple_of`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_is_multiple_of
[`manual_is_power_of_two`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_is_power_of_two
[`manual_is_variant_and`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_is_variant_and
[`manual_let_else`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_let_else
Expand Down
2 changes: 1 addition & 1 deletion clippy_lints/src/casts/cast_sign_loss.rs
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ fn pow_call_result_sign(cx: &LateContext<'_>, base: &Expr<'_>, exponent: &Expr<'

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

match (base_sign, exponent_is_even) {
// Non-negative bases always return non-negative results, ignoring overflow.
Expand Down
1 change: 1 addition & 0 deletions clippy_lints/src/declared_lints.rs
Original file line number Diff line number Diff line change
Expand Up @@ -583,6 +583,7 @@ pub static LINTS: &[&crate::LintInfo] = &[
crate::operators::IMPOSSIBLE_COMPARISONS_INFO,
crate::operators::INEFFECTIVE_BIT_MASK_INFO,
crate::operators::INTEGER_DIVISION_INFO,
crate::operators::MANUAL_IS_MULTIPLE_OF_INFO,
crate::operators::MANUAL_MIDPOINT_INFO,
crate::operators::MISREFACTORED_ASSIGN_OP_INFO,
crate::operators::MODULO_ARITHMETIC_INFO,
Expand Down
11 changes: 2 additions & 9 deletions clippy_lints/src/if_not_else.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use clippy_utils::consts::{ConstEvalCtxt, Constant};
use clippy_utils::consts::is_zero_integer_const;
use clippy_utils::diagnostics::{span_lint_and_help, span_lint_and_sugg};
use clippy_utils::is_else_clause;
use clippy_utils::source::{HasSession, indent_of, reindent_multiline, snippet};
Expand Down Expand Up @@ -48,13 +48,6 @@ declare_clippy_lint! {

declare_lint_pass!(IfNotElse => [IF_NOT_ELSE]);

fn is_zero_const(expr: &Expr<'_>, cx: &LateContext<'_>) -> bool {
if let Some(value) = ConstEvalCtxt::new(cx).eval_simple(expr) {
return Constant::Int(0) == value;
}
false
}

impl LateLintPass<'_> for IfNotElse {
fn check_expr(&mut self, cx: &LateContext<'_>, e: &Expr<'_>) {
if let ExprKind::If(cond, cond_inner, Some(els)) = e.kind
Expand All @@ -68,7 +61,7 @@ impl LateLintPass<'_> for IfNotElse {
),
// Don't lint on `… != 0`, as these are likely to be bit tests.
// For example, `if foo & 0x0F00 != 0 { … } else { … }` is already in the "proper" order.
ExprKind::Binary(op, _, rhs) if op.node == BinOpKind::Ne && !is_zero_const(rhs, cx) => (
ExprKind::Binary(op, _, rhs) if op.node == BinOpKind::Ne && !is_zero_integer_const(cx, rhs) => (
"unnecessary `!=` operation",
"change to `==` and swap the blocks of the `if`/`else`",
),
Expand Down
6 changes: 2 additions & 4 deletions clippy_lints/src/operators/identity_op.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use clippy_utils::consts::{ConstEvalCtxt, Constant, FullInt};
use clippy_utils::consts::{ConstEvalCtxt, Constant, FullInt, integer_const, is_zero_integer_const};
use clippy_utils::diagnostics::span_lint_and_sugg;
use clippy_utils::source::snippet_with_applicability;
use clippy_utils::{clip, peel_hir_expr_refs, unsext};
Expand Down Expand Up @@ -170,9 +170,7 @@ fn is_allowed(cx: &LateContext<'_>, cmp: BinOpKind, left: &Expr<'_>, right: &Exp
cx.typeck_results().expr_ty(left).peel_refs().is_integral()
&& cx.typeck_results().expr_ty(right).peel_refs().is_integral()
// `1 << 0` is a common pattern in bit manipulation code
&& !(cmp == BinOpKind::Shl
&& ConstEvalCtxt::new(cx).eval_simple(right) == Some(Constant::Int(0))
&& ConstEvalCtxt::new(cx).eval_simple(left) == Some(Constant::Int(1)))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Couldn't these lines be replaced with the functions you moved into clippy_utils? 👀

Copy link
Contributor Author

@samueltardieu samueltardieu Mar 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here also, this comment points at code which already does exactly what you are suggesting.

&& !(cmp == BinOpKind::Shl && is_zero_integer_const(cx, right) && integer_const(cx, left) == Some(1))
}

fn check_remainder(cx: &LateContext<'_>, left: &Expr<'_>, right: &Expr<'_>, span: Span, arg: Span) {
Expand Down
66 changes: 66 additions & 0 deletions clippy_lints/src/operators/manual_is_multiple_of.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
use clippy_utils::consts::is_zero_integer_const;
use clippy_utils::diagnostics::span_lint_and_sugg;
use clippy_utils::msrvs::{self, Msrv};
use clippy_utils::sugg::Sugg;
use rustc_ast::BinOpKind;
use rustc_errors::Applicability;
use rustc_hir::{Expr, ExprKind};
use rustc_lint::LateContext;
use rustc_middle::ty;

use super::MANUAL_IS_MULTIPLE_OF;

pub(super) fn check<'tcx>(
cx: &LateContext<'tcx>,
expr: &Expr<'_>,
op: BinOpKind,
lhs: &'tcx Expr<'tcx>,
rhs: &'tcx Expr<'tcx>,
msrv: Msrv,
) {
if msrv.meets(cx, msrvs::UNSIGNED_IS_MULTIPLE_OF)
&& let Some(operand) = uint_compare_to_zero(cx, op, lhs, rhs)
&& let ExprKind::Binary(operand_op, operand_left, operand_right) = operand.kind
&& operand_op.node == BinOpKind::Rem
{
let mut app = Applicability::MachineApplicable;
let divisor = Sugg::hir_with_applicability(cx, operand_right, "_", &mut app);
span_lint_and_sugg(
cx,
MANUAL_IS_MULTIPLE_OF,
expr.span,
"manual implementation of `.is_multiple_of()`",
"replace with",
format!(
"{}{}.is_multiple_of({divisor})",
if op == BinOpKind::Eq { "" } else { "!" },
Sugg::hir_with_applicability(cx, operand_left, "_", &mut app).maybe_paren()
),
app,
);
}
}

// If we have a `x == 0`, `x != 0` or `x > 0` (or the reverted ones), return the non-zero operand
fn uint_compare_to_zero<'tcx>(
cx: &LateContext<'tcx>,
op: BinOpKind,
lhs: &'tcx Expr<'tcx>,
rhs: &'tcx Expr<'tcx>,
) -> Option<&'tcx Expr<'tcx>> {
let operand = if matches!(lhs.kind, ExprKind::Binary(..))
&& matches!(op, BinOpKind::Eq | BinOpKind::Ne | BinOpKind::Gt)
&& is_zero_integer_const(cx, rhs)
{
lhs
} else if matches!(rhs.kind, ExprKind::Binary(..))
&& matches!(op, BinOpKind::Eq | BinOpKind::Ne | BinOpKind::Lt)
&& is_zero_integer_const(cx, lhs)
{
rhs
} else {
return None;
};

matches!(cx.typeck_results().expr_ty_adjusted(operand).kind(), ty::Uint(_)).then_some(operand)
}
33 changes: 33 additions & 0 deletions clippy_lints/src/operators/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ mod float_cmp;
mod float_equality_without_abs;
mod identity_op;
mod integer_division;
mod manual_is_multiple_of;
mod manual_midpoint;
mod misrefactored_assign_op;
mod modulo_arithmetic;
Expand Down Expand Up @@ -830,12 +831,42 @@ declare_clippy_lint! {
"manual implementation of `midpoint` which can overflow"
}

declare_clippy_lint! {
/// ### What it does
/// Checks for manual implementation of `.is_multiple_of()` on
/// unsigned integer types.
///
/// ### Why is this bad?
/// `a.is_multiple_of(b)` is a clearer way to check for divisibility
/// of `a` by `b`. This expression can never panic.
///
/// ### Example
/// ```no_run
/// # let (a, b) = (3u64, 4u64);
/// if a % b == 0 {
/// println!("{a} is divisible by {b}");
/// }
/// ```
/// Use instead:
/// ```no_run
/// # let (a, b) = (3u64, 4u64);
/// if a.is_multiple_of(b) {
/// println!("{a} is divisible by {b}");
/// }
/// ```
#[clippy::version = "1.89.0"]
pub MANUAL_IS_MULTIPLE_OF,
complexity,
"manual implementation of `.is_multiple_of()`"
}

pub struct Operators {
arithmetic_context: numeric_arithmetic::Context,
verbose_bit_mask_threshold: u64,
modulo_arithmetic_allow_comparison_to_zero: bool,
msrv: Msrv,
}

impl Operators {
pub fn new(conf: &'static Conf) -> Self {
Self {
Expand Down Expand Up @@ -874,6 +905,7 @@ impl_lint_pass!(Operators => [
NEEDLESS_BITWISE_BOOL,
SELF_ASSIGNMENT,
MANUAL_MIDPOINT,
MANUAL_IS_MULTIPLE_OF,
]);

impl<'tcx> LateLintPass<'tcx> for Operators {
Expand All @@ -891,6 +923,7 @@ impl<'tcx> LateLintPass<'tcx> for Operators {
identity_op::check(cx, e, op.node, lhs, rhs);
needless_bitwise_bool::check(cx, e, op.node, lhs, rhs);
manual_midpoint::check(cx, e, op.node, lhs, rhs, self.msrv);
manual_is_multiple_of::check(cx, e, op.node, lhs, rhs, self.msrv);
}
self.arithmetic_context.check_binary(cx, e, op.node, lhs, rhs);
bit_mask::check(cx, e, op.node, lhs, rhs);
Expand Down
15 changes: 15 additions & 0 deletions clippy_utils/src/consts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -960,3 +960,18 @@ fn field_of_struct<'tcx>(
None
}
}

/// If `expr` evaluates to an integer constant, return its value.
pub fn integer_const(cx: &LateContext<'_>, expr: &Expr<'_>) -> Option<u128> {
if let Some(Constant::Int(value)) = ConstEvalCtxt::new(cx).eval_simple(expr) {
Some(value)
} else {
None
}
}

/// Check if `expr` evaluates to an integer constant of 0.
#[inline]
pub fn is_zero_integer_const(cx: &LateContext<'_>, expr: &Expr<'_>) -> bool {
integer_const(cx, expr) == Some(0)
}
2 changes: 1 addition & 1 deletion clippy_utils/src/msrvs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ macro_rules! msrv_aliases {
// names may refer to stabilized feature flags or library items
msrv_aliases! {
1,88,0 { LET_CHAINS }
1,87,0 { OS_STR_DISPLAY, INT_MIDPOINT, CONST_CHAR_IS_DIGIT }
1,87,0 { OS_STR_DISPLAY, INT_MIDPOINT, CONST_CHAR_IS_DIGIT, UNSIGNED_IS_MULTIPLE_OF }
1,85,0 { UINT_FLOAT_MIDPOINT }
1,84,0 { CONST_OPTION_AS_SLICE, MANUAL_DANGLING_PTR }
1,83,0 { CONST_EXTERN_FN, CONST_FLOAT_BITS_CONV, CONST_FLOAT_CLASSIFY, CONST_MUT_REFS, CONST_UNWRAP }
Expand Down
2 changes: 1 addition & 1 deletion tests/ui/box_default.fixed
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ fn issue_10381() {
impl Bar for Foo {}

fn maybe_get_bar(i: u32) -> Option<Box<dyn Bar>> {
if i % 2 == 0 {
if i.is_multiple_of(2) {
Some(Box::new(Foo::default()))
} else {
None
Expand Down
2 changes: 1 addition & 1 deletion tests/ui/box_default.rs
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ fn issue_10381() {
impl Bar for Foo {}

fn maybe_get_bar(i: u32) -> Option<Box<dyn Bar>> {
if i % 2 == 0 {
if i.is_multiple_of(2) {
Some(Box::new(Foo::default()))
} else {
None
Expand Down
2 changes: 1 addition & 1 deletion tests/ui/infinite_iter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ fn infinite_iters() {
//~^ infinite_iter

// infinite iter
(0_u64..).filter(|x| x % 2 == 0).last();
(0_u64..).filter(|x| x.is_multiple_of(2)).last();
//~^ infinite_iter

// not an infinite, because ranges are double-ended
Expand Down
4 changes: 2 additions & 2 deletions tests/ui/infinite_iter.stderr
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,8 @@ LL | (0_usize..).flat_map(|x| 0..x).product::<usize>();
error: infinite iteration detected
--> tests/ui/infinite_iter.rs:41:5
|
LL | (0_u64..).filter(|x| x % 2 == 0).last();
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
LL | (0_u64..).filter(|x| x.is_multiple_of(2)).last();
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

error: possible infinite iteration detected
--> tests/ui/infinite_iter.rs:53:5
Expand Down
20 changes: 14 additions & 6 deletions tests/ui/iter_kv_map.fixed
Original file line number Diff line number Diff line change
Expand Up @@ -30,15 +30,19 @@ fn main() {

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

// Don't lint
let _ = map.iter().filter(|(_, val)| *val % 2 == 0).map(|(key, _)| key).count();
let _ = map
.iter()
.filter(|(_, val)| val.is_multiple_of(2))
.map(|(key, _)| key)
.count();
let _ = map.iter().map(get_key).collect::<Vec<_>>();

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

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

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

// Don't lint
let _ = map.iter().filter(|(_, val)| *val % 2 == 0).map(|(key, _)| key).count();
let _ = map
.iter()
.filter(|(_, val)| val.is_multiple_of(2))
.map(|(key, _)| key)
.count();
let _ = map.iter().map(get_key).collect::<Vec<_>>();

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

// Lint
let _ = map.keys().map(|key| key * 9).count();
Expand Down
20 changes: 14 additions & 6 deletions tests/ui/iter_kv_map.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,15 +30,19 @@ fn main() {

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

// Don't lint
let _ = map.iter().filter(|(_, val)| *val % 2 == 0).map(|(key, _)| key).count();
let _ = map
.iter()
.filter(|(_, val)| val.is_multiple_of(2))
.map(|(key, _)| key)
.count();
let _ = map.iter().map(get_key).collect::<Vec<_>>();

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

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

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

// Don't lint
let _ = map.iter().filter(|(_, val)| *val % 2 == 0).map(|(key, _)| key).count();
let _ = map
.iter()
.filter(|(_, val)| val.is_multiple_of(2))
.map(|(key, _)| key)
.count();
let _ = map.iter().map(get_key).collect::<Vec<_>>();

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

// Lint
let _ = map.iter().map(|(key, _value)| key * 9).count();
Expand Down
Loading