Skip to content

Conversation

@Artur-Sulej
Copy link
Contributor

@Artur-Sulej Artur-Sulej commented Jul 6, 2025

Add a new lint that detects the use of decimal literals as bit masks in bitwise operations. Using decimal literals for bit masks can obscure the intended bit pattern and reduce code readability. This lint encourages the use of binary (0b...) or hexadecimal (0x...) notation to make bit patterns explicit and easier to understand at a glance.

Example:

let masked = x & 14; // Bad: decimal literal as bit mask
let masked = x & 0b1110; // Good: bit pattern is explicit

changelog: [decimal_bitwise_operands]: new lint

Fixes #1775

@github-actions
Copy link

github-actions bot commented Jul 6, 2025

Lintcheck changes for 9cb6306

Lint Added Removed Changed
clippy::decimal_bitwise_operands 28 0 0

This comment will be updated if you push new changes

@rustbot rustbot added S-blocked Status: marked as blocked ❌ on something else such as an RFC or other implementation work A-lint Area: New lints labels Jul 6, 2025
@Artur-Sulej
Copy link
Contributor Author

Although I annotated my test with //~ decimal_bit_mask, Clippy still reports the lint as an unmatched diagnostic and the test fails.
Any idea what I might be missing?

@Artur-Sulej Artur-Sulej marked this pull request as ready for review September 3, 2025 20:27
@rustbot
Copy link
Collaborator

rustbot commented Sep 3, 2025

r? @llogiq

rustbot has assigned @llogiq.
They will have a look at your PR within the next two weeks and either review your PR or reassign to another reviewer.

Use r? to explicitly pick a reviewer

@rustbot rustbot added the S-waiting-on-review Status: Awaiting review from the assignee but also interested parties label Sep 3, 2025
@rustbot

This comment has been minimized.

@rustbot

This comment has been minimized.

@samueltardieu samueltardieu added S-waiting-on-author Status: This is awaiting some action from the author. (Use `@rustbot ready` to update this status) and removed S-blocked Status: marked as blocked ❌ on something else such as an RFC or other implementation work S-waiting-on-review Status: Awaiting review from the assignee but also interested parties labels Sep 17, 2025
99 | 0b1010; //~ decimal_bit_mask
99 ^ 0b1010; //~ decimal_bit_mask
0xD | 99; //~ decimal_bit_mask
88 & 99; //~ decimal_bit_mask
Copy link
Contributor

Choose a reason for hiding this comment

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

Although I annotated my test with //~ decimal_bit_mask, Clippy still reports the lint as an unmatched diagnostic and the test fails.
Any idea what I might be missing?

That's because in this case, you're firing the lint twice, once for each operand, so you'll need to add a second error mark. That's the easiest to do by bringing them to under the linted expression:

Suggested change
88 & 99; //~ decimal_bit_mask
88 & 99;
//~^ decimal_bit_mask
//~| decimal_bit_mask
  • ^ means the lint should happen on the line above the comment
  • | "attaches" the error mark to another one on a neighbouring line (in this case the //~^ decimal_bit_mask directly above), and will expect a lint at the same location (in this case, the line containing 88 & 99;)

Comment on lines 39 to 49
Expr {
kind: kind1,
span: span1,
..
},
Expr {
kind: kind2,
span: span2,
..
},
) = &e.kind
Copy link
Contributor

Choose a reason for hiding this comment

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

It would be a bit more concise to have these simply as left and right, and then use left.span, right.kind etc. throughout the code

span_lint(
cx,
DECIMAL_BIT_MASK,
e.span,
Copy link
Contributor

Choose a reason for hiding this comment

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

You can improve the output by pointing the lint at the exact operand that is decimal -- for example, here you would use span1 (or left.span, as per previous comment)

cx,
DECIMAL_BIT_MASK,
e.span,
"Using decimal literal for bit mask. Consider using binary (0b...) or hexadecimal (0x...) notation for better readability.",
Copy link
Contributor

Choose a reason for hiding this comment

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

The second sentence should be emitted separately, as a help message -- see https://doc.rust-lang.org/clippy/development/emitting_lints.html#emitting-a-lint-1 for more info.

Try using span_lint_and_help here

Comment on lines 65 to 67
&& let Some(snippet) = snippet_opt(cx, *span2)
&& !snippet.starts_with("0b")
&& !snippet.starts_with("0x")
Copy link
Contributor

Choose a reason for hiding this comment

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

You can use SpanRangeExt::get_source_code here to avoid allocating a String (which is what snippet_opt does)

                && let Some(snippet) = span2.get_source_text(cx)

In fact, since you're only using the snippet to check its contents, you can get away with SpanRangeExt::check_source_code:

Suggested change
&& let Some(snippet) = snippet_opt(cx, *span2)
&& !snippet.starts_with("0b")
&& !snippet.starts_with("0x")
&& span2.check_source_text(cx, |src| !src.starts_with("0b") && !src.starts_with("0x"))

@rustbot

This comment has been minimized.

@ada4a
Copy link
Contributor

ada4a commented Nov 6, 2025

@rustbot note remove Feature-freeze

@ada4a
Copy link
Contributor

ada4a commented Nov 6, 2025

Could you please change the PR description to something like this?

"""
Add a new lint that detects the use of decimal literals as bit masks in bitwise operations. Using decimal literals for bit masks can obscure the intended bit pattern and reduce code readability. This lint encourages the use of binary (0b...) or hexadecimal (0x...) notation to make bit patterns explicit and easier to understand at a glance.

Example:

let masked = x & 15; // Bad: decimal literal as bit mask
let masked = x & 0b1111; // Good: bit pattern is explicit

changelog: [decimal_bit_mask]: new lint

Fixes #1775
"""

Motivation for the changes:

  • the changelog message should fit on one line
  • by using "Fixes", you make it so that when this PR is merged, the corresponding issue is automatically closed

@ada4a
Copy link
Contributor

ada4a commented Nov 6, 2025

If you look at Lintcheck results (displayed over at #15215 (comment)), you can see a couple of problems with the current implementation:

warning: Using decimal literal for bit mask. Consider using binary (0b...) or hexadecimal (0x...) notation for better readability.
   --> target/lintcheck/sources/sha2-0.11.0-pre.3/src/sha256/x86.rs:100:1
    |
100 | cpufeatures::new!(shani_cpuid, "sha", "sse2", "ssse3", "sse4.1");
    | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    |
    = note: `--force-warn clippy::decimal-bit-mask` implied by `--force-warn clippy::nursery`
    = note: this warning originates in the macro `$crate::__detect_target_features` which comes from the expansion of the macro `cpufeatures::new` (in Nightly builds, run with -Z macro-backtrace for more info)

Here, the lint fires on some binary operation coming deep from an expansion of cpufeatures::new!. Typically, you want to avoid linting things coming from expansion -- please use !e.span.from_expansion() to filter those out. See https://doc.rust-lang.org/clippy/development/macro_expansions.html#the-spanfrom_expansion-method for more information

warning: Using decimal literal for bit mask. Consider using binary (0b...) or hexadecimal (0x...) notation for better readability.
    --> target/lintcheck/sources/chrono-0.4.38/src/naive/date/mod.rs:1344:9
     |
1344 |         self.yof() & (0b1000) == 0
     |         ^^^^^^^^^^^^^^^^^^^^^

Here, 0b1000 isn't recognized as being a binary literal, because the way you check for that is by looking at the span of the literal, but here the span actually contains the parentheses surrounding the literal. Because of that, you'll need to first trim starting paren(s), then possibly some whitespace, and only then performing the actual checks.

warning: Using decimal literal for bit mask. Consider using binary (0b...) or hexadecimal (0x...) notation for better readability.
   --> target/lintcheck/sources/ahash-0.8.11/src/fallback_hash.rs:197:19
    |
197 |         let rot = (self.buffer & 63) as u32;
    |                   ^^^^^^^^^^^^^^^^^^
    |
    = note: `--force-warn clippy::decimal-bit-mask` implied by `--force-warn clippy::nursery`

Here, and in a couple more cases, the decimal operand is a power of 2, or (power of 2 - 1). I think it'd be appropriate to give them a pass. To get the value of a literal, you can go

if let ExprKind::Lit(lit) = left
    && let LitKind::Int(val, _) = lit

with val being your value.

Note that this will also include operands 0 and 1, which is desirable.

warning: Using decimal literal for bit mask. Consider using binary (0b...) or hexadecimal (0x...) notation for better readability.
   --> target/lintcheck/sources/libc-0.2.155/src/unix/linux_like/linux/gnu/b64/x86_64/mod.rs:416:32
    |
416 | pub const O_TMPFILE: ::c_int = 0o20000000 | O_DIRECTORY;
    |                                ^^^^^^^^^^^^^^^^^^^^^^^^

Octal operands probably also make sense in some contexts, so I'd give them a pass as well.

@rustbot

This comment has been minimized.

@rustbot

This comment has been minimized.

@rustbot rustbot added the has-merge-commits PR has merge commits, merge with caution. label Nov 12, 2025
@Artur-Sulej Artur-Sulej force-pushed the 1775-decimal_bit_mask branch from 212fe6e to a33e249 Compare November 12, 2025 23:10
@rustbot rustbot removed has-merge-commits PR has merge commits, merge with caution. S-waiting-on-author Status: This is awaiting some action from the author. (Use `@rustbot ready` to update this status) labels Nov 12, 2025
@Artur-Sulej
Copy link
Contributor Author

@ada4a Thank you for the very thorough review and the clear explanations of all the details! I’ve addressed all the issues, and the PR is ready for another round of review.

@Artur-Sulej Artur-Sulej requested a review from ada4a November 12, 2025 23:50
@Artur-Sulej Artur-Sulej changed the title New lint: decimal_bit_mask New lint: decimal_bitwise_operands Nov 19, 2025
Copy link
Contributor

@llogiq llogiq left a comment

Choose a reason for hiding this comment

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

This is a good start, but I think the lint has some ways to go to become really useful.

View changes since this review


fn is_power_of_twoish(node: LitKind) -> bool {
if let LitKind::Int(Pu128(val), _) = node {
return val.is_power_of_two() || val.wrapping_add(1).is_power_of_two();
Copy link
Contributor

Choose a reason for hiding this comment

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

I think that if we accept hex, we should also accept decimals up to 9.

There is little benefit to require a 0x prefix for e.g. x & 5.

Copy link
Contributor Author

@Artur-Sulej Artur-Sulej Nov 20, 2025

Choose a reason for hiding this comment

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

Very good idea!

Comment on lines 107 to 121
error: using decimal literal for bitwise operation
--> tests/ui/decimal_bitwise_operands.rs:31:5
|
LL | 88 & 99;
| ^^
|
= help: use binary (0b...), hex (0x...), or octal (0o...) notation for better readability

error: using decimal literal for bitwise operation
--> tests/ui/decimal_bitwise_operands.rs:31:10
|
LL | 88 & 99;
| ^^
|
= help: use binary (0b...), hex (0x...), or octal (0o...) notation for better readability
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm ok with doing that in a follow-up PR.

x & SOME_CONST;
x |= SOME_CONST;

// GOOD: Parenthesized binary/hex literal (should not trigger lint)
Copy link
Contributor

Choose a reason for hiding this comment

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

We could also add checks for blocks with binary/hex literal, e.g. x & { 0x24 }.

LL | x & 99;
| ^^
|
= help: use binary (0b...), hex (0x...), or octal (0o...) notation for better readability
Copy link
Contributor

Choose a reason for hiding this comment

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

Could we insert the actual value as binary, hex or octal here?

It doesn't seem too helpful to let the user do the calculation.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

True, that's definitely better devex.

@Artur-Sulej
Copy link
Contributor Author

All comments addressed, ready for review.

@Artur-Sulej Artur-Sulej requested a review from llogiq November 20, 2025 13:36
@rustbot rustbot added the S-waiting-on-review Status: Awaiting review from the assignee but also interested parties label Nov 20, 2025
Copy link
Contributor

@llogiq llogiq left a comment

Choose a reason for hiding this comment

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

One question remains, otherwise this looks ok.

I'll start the Final Comments Period shortly.

View changes since this review

LL | x | (/* comment */99);
| ^^
|
= help: use binary (0b1100011), hex (0x63), or octal (0o143) notation for better readability
Copy link
Contributor

Choose a reason for hiding this comment

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

Would it make sense to group 4 or at least 8 binary digits by underscore? Makes it easier to read and avoids having another lint triggering on applying this one.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good point, these hints should be copy-pasteable without any issues.

@Artur-Sulej Artur-Sulej requested a review from llogiq November 22, 2025 01:50
@Artur-Sulej
Copy link
Contributor Author

Artur-Sulej commented Nov 22, 2025

I addressed the comment and updated the lint message to group digits with underscores. I used numeric_literal::format, which picks a reasonable digit group size for each radix.

Additional changes:

  • Reused the existing logic for detecting decimals (NumericLiteral), instead of custom parsing
  • Extended handling of bitwise operations to cover unary and cast expressions

Copy link
Contributor

@llogiq llogiq left a comment

Choose a reason for hiding this comment

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

I think the code is fine now, and will start the FCP when I get around to it (I recently only narrowly avoided being buried by a ceiling heating falling down and have still to deal with the aftermath).

Can you please squash your commits?

View changes since this review

@Artur-Sulej Artur-Sulej force-pushed the 1775-decimal_bit_mask branch from e3b3dad to 273deea Compare November 23, 2025 22:02
@rustbot

This comment has been minimized.

@Artur-Sulej
Copy link
Contributor Author

@llogiq Sorry to hear that, good luck with everything!

@llogiq @ada4a Commits squashed and rebased. Thanks for the review!

Copy link
Member

@samueltardieu samueltardieu left a comment

Choose a reason for hiding this comment

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

It would be interesting to preserve the suffix (for example 254_u32 should suggest 0xfe_u32, etc.).

View changes since this review

Comment on lines 38 to 40
&& is_decimal_number(cx, lit.span, &lit.node)
&& !is_single_digit(val)
&& !is_power_of_twoish(val)
Copy link
Member

Choose a reason for hiding this comment

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

Checks on source text are more expansive than arithmetic operations.

Suggested change
&& is_decimal_number(cx, lit.span, &lit.node)
&& !is_single_digit(val)
&& !is_power_of_twoish(val)
&& !is_single_digit(val)
&& !is_power_of_twoish(val)
&& is_decimal_number(cx, lit.span, &lit.node)

Comment on lines 72 to 74
numeric_literal::format(&format!("0b{val:b}"), None, false),
numeric_literal::format(&format!("0x{val:x}"), None, false),
numeric_literal::format(&format!("0o{val:o}"), None, false),
Copy link
Member

Choose a reason for hiding this comment

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

Rust knows how to format bin/oct/hex with a prefix:

Suggested change
numeric_literal::format(&format!("0b{val:b}"), None, false),
numeric_literal::format(&format!("0x{val:x}"), None, false),
numeric_literal::format(&format!("0o{val:o}"), None, false),
numeric_literal::format(&format!("{val:#b}"), None, false),
numeric_literal::format(&format!("{val:#x}"), None, false),
numeric_literal::format(&format!("{val:#o}"), None, false),

@Artur-Sulej
Copy link
Contributor Author

@samueltardieu Thanks for your comments. I've addressed them – plus I added handling a literal with a reference (example: x ^ &100).

LL | 0b1010 & 99;
| ^^
|
= help: use binary (0b110_0011), hex (0x0063), or octal (0o143) notation for better readability
Copy link
Contributor

Choose a reason for hiding this comment

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

Why is the hex output always padded to a width of 4?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Technically this is because the lint uses the shared numeric_literal helper, which is used throughout Clippy to format literals in a consistent way. For hex it groups digits by 4 and adds zero padding.

My understanding is that this was done for readability. Each hex digit represents 4 bits, so 4 digits are 16 bits, which matches common integer sizes (u16, i32, u64 etc).
Padding to 4 digits (2 bytes) also avoids a dangling half-byte when you think in terms of 16‑bit.

To sum up, the benefits are: consistency (same formatting as elsewhere) and readability (visual grouping of bytes).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Copy link
Contributor

Choose a reason for hiding this comment

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

That's fine for now. I might come back later and change it to handle 1-2 digits better. No reason to delay this though.

Copy link
Contributor

Choose a reason for hiding this comment

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

That's fine for now. I might come back later and change it to handle 1-2 digits better. No reason to delay this though.

@Artur-Sulej Artur-Sulej requested a review from pitaj November 28, 2025 13:15
/// ```
#[clippy::version = "1.93.0"]
pub DECIMAL_BITWISE_OPERANDS,
nursery,
Copy link
Contributor

Choose a reason for hiding this comment

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

During the FCP, it was decided that this lint should belong in the pedantic category.

Suggested change
nursery,
pedantic,

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done and rebased ✅

@Artur-Sulej Artur-Sulej force-pushed the 1775-decimal_bit_mask branch from fc65382 to f972a7c Compare November 29, 2025 18:53
@rustbot
Copy link
Collaborator

rustbot commented Nov 29, 2025

This PR was rebased onto a different master commit. Here's a range-diff highlighting what actually changed.

Rebasing is a normal part of keeping PRs up to date, so no action is needed—this note is just to help reviewers.

@llogiq
Copy link
Contributor

llogiq commented Nov 29, 2025

Alright. Can you please squash your commits?

@Artur-Sulej Artur-Sulej force-pushed the 1775-decimal_bit_mask branch from f972a7c to 9cb6306 Compare November 29, 2025 20:39
@llogiq llogiq added this pull request to the merge queue Nov 29, 2025
@Artur-Sulej
Copy link
Contributor Author

🔥

Merged via the queue into rust-lang:master with commit 2c95550 Nov 29, 2025
11 checks passed
@rustbot rustbot removed the S-waiting-on-review Status: Awaiting review from the assignee but also interested parties label Nov 29, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-lint Area: New lints needs-fcp

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Lint bit operations with decimal constants

6 participants