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

Conversation

samueltardieu
Copy link
Contributor

@samueltardieu samueltardieu commented Feb 25, 2025

I've added a min_divisor configuration option, default to 4, to not trigger on smaller divisibility operations. I would prefer not to lint if a & 1 == 0 as if a.is_multiple_of(2) by default because the former is very idiomatic in systems (and embedded) programming.

A min_and_mask_size option, defaulting to 3, sets the default bits to be and-masked before this lint triggers; that would be n in v & ((1 << n) - 1) == 0. The form v % 10 == 0 is always linted.

This PR will stay in draft mode until the next rustup which will mark unsigned_is_multiple_of stable for Rust 1.87.0, and the feature flags will be removed.

What should its category be? I've used "complexity", but "pedantic" might be suitable as well.

Close #14289
changelog: [manual_is_multiple_of]: new lint
r? ghost

@rustbot rustbot added the S-waiting-on-review Status: Awaiting review from the assignee but also interested parties label Feb 25, 2025
@samueltardieu samueltardieu force-pushed the manual-is-multiple-of branch 3 times, most recently from 5cfb480 to ca4c0a9 Compare February 25, 2025 12:43
@joshtriplett
Copy link
Member

Having a min_divisor configuration option seems reasonable, but I think that option should default to flagging every instance of this. Large codebases that want to keep the & 1 == 0 pattern can set that option.

@samueltardieu
Copy link
Contributor Author

Having a min_divisor configuration option seems reasonable, but I think that option should default to flagging every instance of this. Large codebases that want to keep the & 1 == 0 pattern can set that option.

I've added an extra commit to test this with lintcheck. There was only one hit in Clippy sources (except for tests).

@samueltardieu
Copy link
Contributor Author

@joshtriplett The lintcheck output seems quite reasonable indeed.

@y21
Copy link
Member

y21 commented Feb 25, 2025

I agree with linting the a % b == 0 pattern, but I'm unsure about the & one... Something like (x & 0b111) == 0 feels like a very general expression.
I'm a bit concerned that linting here has the opposite effect: emitting warnings on code that has a totally different semantic meaning where x.is_multiple_of(8) would be a very obfuscated way of checking that the last 3 bits aren't set.

@joshtriplett
Copy link
Member

joshtriplett commented Feb 25, 2025

First of all, please don't treat that as a blocker; it's not as important as the % N == 0 case.

That said: I've seen many, many codebases which use & (PAGE_SIZE - 1) == 0 and similar instead of % PAGE_SIZE == 0, when they know the value is a power of two.

We may need to tune the heuristic to make sure it doesn't produce false positives, and for instance we may want a different threshold for & vs % (& 1 == 0 might be a flag test), but I think things like & 4095 == 0 or & (4096 - 1) == 0 or & (CONST_EQUAL_TO_4096 - 1) == 0 are pretty clear.

Looking at the lintcheck output, I think I agree with not flagging & 1 == 0 by default.

@samueltardieu
Copy link
Contributor Author

What about applying the min-divisor option (maybe using another name, like min-one-bits to represent the number of ones) only to the &(2^n-1) form of the lint, with a sensible default such as 5 bits? The % form would be linted unconditionally.

@joshtriplett
Copy link
Member

joshtriplett commented Feb 25, 2025

@samueltardieu Sounds reasonable. I would say the default should flag 7 and 15 by default, and maybe 3, but not 1. Because 1 might be a flag, but 3 is definitely two bits.

@samueltardieu samueltardieu force-pushed the manual-is-multiple-of branch 2 times, most recently from 0569fb8 to b5db45e Compare February 25, 2025 21:55
@samueltardieu
Copy link
Contributor Author

Done, see the toplevel comment for the updated option.

@joshtriplett
Copy link
Member

Looking at the lintcheck output, this looks great!

@samueltardieu samueltardieu marked this pull request as ready for review February 27, 2025 22:43
@samueltardieu
Copy link
Contributor Author

rustup happened, PR ready.
r? clippy

@samueltardieu samueltardieu force-pushed the manual-is-multiple-of branch 2 times, most recently from 0423c58 to 160daec Compare February 28, 2025 17:35
@samueltardieu
Copy link
Contributor Author

Rebased.

@samueltardieu
Copy link
Contributor Author

Rebased

Copy link
Member

@blyxyas blyxyas left a comment

Choose a reason for hiding this comment

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

Very good lint and initial implementation, some nits in this initial reviewing round (some slip-ups)

@@ -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.

@samueltardieu samueltardieu force-pushed the manual-is-multiple-of branch 3 times, most recently from 8555fb4 to 36bb8c5 Compare March 25, 2025 17:23
@samueltardieu
Copy link
Contributor Author

@blyxyas Should I reassign?

@blyxyas
Copy link
Member

blyxyas commented Apr 16, 2025

Oh, I got here I review I never submitted ;(

Sugg::hir_with_applicability(cx, lhs_right, "_", &mut app).into_string(),
)
} else if lhs_op.node == BinOpKind::BitAnd {
let min_divisor = 1 << u128::from(min_and_mask_size);
Copy link
Member

Choose a reason for hiding this comment

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

Is there a reason why this is an u128, it will never be greater than 256 because it's parsed from conf as a u8.
Testing, seems that changing it to u8 doesn't throw Clippy into an eternal pit of sadness. Is it u128 because of possible future compatibility?

Copy link
Contributor Author

@samueltardieu samueltardieu Apr 17, 2025

Choose a reason for hiding this comment

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

It's because min_divisor needs to be a u128 (someone might want to not trigger the lint except for constants greater than 0x100 for example — I'll add a test). I could write it as 1u128 << min_and_mask_size though.

Or did I not understand your remark?

}

/// If `expr` is made of all ones, return the representation of `expr+1` if it is no smaller than
/// `min_divisor`.
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
/// `min_divisor`.
/// `min_divisor`. Made to catch ((1 << A) - 1) where A is either not a constant, or >= 2^min_divisor

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I've used the more complete:

/// If `expr` is provably made of all ones, return the representation of `expr+1` if it is no smaller than
/// `min_divisor`. This will catch expressions of the following forms:
/// - `(1 << A) - 1` where `A` is a constant
/// - integer literals — if it uses hexadecimal, the return value will as well
///
/// The function will not attempt to evaluate non-literal constant expressions, as those may depend on
/// conditional compilation.

@samueltardieu samueltardieu force-pushed the manual-is-multiple-of branch from 36bb8c5 to fa180dd Compare April 17, 2025 07:24
@samueltardieu samueltardieu requested a review from blyxyas April 17, 2025 07:26
@blyxyas
Copy link
Member

blyxyas commented Apr 19, 2025

@samueltardieu samueltardieu force-pushed the manual-is-multiple-of branch from fa180dd to 541345c Compare May 5, 2025 23:11
@samueltardieu
Copy link
Contributor Author

Removed the & part as well as the min-and-mask-size option, as discussed in the FCP.

Also updated the clippy version to 1.88.

@rustbot label +S-final-comment-period

@rustbot rustbot added the S-final-comment-period Status: final comment period it will be merged unless new objections are raised (~1 week) label May 5, 2025
@samueltardieu samueltardieu force-pushed the manual-is-multiple-of branch from 541345c to eae5a6d Compare May 5, 2025 23:21
@samueltardieu
Copy link
Contributor Author

Rebased to fix UI test error in iter_kv_map.rs.

@rustbot

This comment has been minimized.

@samueltardieu samueltardieu force-pushed the manual-is-multiple-of branch from eae5a6d to 04c0a4f Compare May 6, 2025 06:07
@samueltardieu
Copy link
Contributor Author

Rebased

@rustbot

This comment has been minimized.

@samueltardieu samueltardieu force-pushed the manual-is-multiple-of branch from 04c0a4f to 313ec7d Compare May 10, 2025 21:06
@samueltardieu
Copy link
Contributor Author

Rebased and set Clippy version to 1.89.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
S-final-comment-period Status: final comment period it will be merged unless new objections are raised (~1 week) S-waiting-on-review Status: Awaiting review from the assignee but also interested parties
Projects
None yet
Development

Successfully merging this pull request may close these issues.

suggest a.is_multiple_of(b) for a % b == 0
5 participants