-
Notifications
You must be signed in to change notification settings - Fork 1.9k
New lint: decimal_bitwise_operands
#15215
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
Conversation
|
Lintcheck changes for 9cb6306
This comment will be updated if you push new changes |
|
Although I annotated my test with |
This comment has been minimized.
This comment has been minimized.
013adec to
12ab9f6
Compare
This comment has been minimized.
This comment has been minimized.
tests/ui/decimal_bit_mask.rs
Outdated
| 99 | 0b1010; //~ decimal_bit_mask | ||
| 99 ^ 0b1010; //~ decimal_bit_mask | ||
| 0xD | 99; //~ decimal_bit_mask | ||
| 88 & 99; //~ decimal_bit_mask |
There was a problem hiding this comment.
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:
| 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_maskdirectly above), and will expect a lint at the same location (in this case, the line containing88 & 99;)
clippy_lints/src/decimal_bit_mask.rs
Outdated
| Expr { | ||
| kind: kind1, | ||
| span: span1, | ||
| .. | ||
| }, | ||
| Expr { | ||
| kind: kind2, | ||
| span: span2, | ||
| .. | ||
| }, | ||
| ) = &e.kind |
There was a problem hiding this comment.
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
clippy_lints/src/decimal_bit_mask.rs
Outdated
| span_lint( | ||
| cx, | ||
| DECIMAL_BIT_MASK, | ||
| e.span, |
There was a problem hiding this comment.
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)
clippy_lints/src/decimal_bit_mask.rs
Outdated
| cx, | ||
| DECIMAL_BIT_MASK, | ||
| e.span, | ||
| "Using decimal literal for bit mask. Consider using binary (0b...) or hexadecimal (0x...) notation for better readability.", |
There was a problem hiding this comment.
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
clippy_lints/src/decimal_bit_mask.rs
Outdated
| && let Some(snippet) = snippet_opt(cx, *span2) | ||
| && !snippet.starts_with("0b") | ||
| && !snippet.starts_with("0x") |
There was a problem hiding this comment.
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:
| && 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")) |
12ab9f6 to
580fe78
Compare
This comment has been minimized.
This comment has been minimized.
|
@rustbot note remove Feature-freeze |
|
Could you please change the PR description to something like this? """ Example: let masked = x & 15; // Bad: decimal literal as bit mask
let masked = x & 0b1111; // Good: bit pattern is explicitchangelog: [ Fixes #1775 Motivation for the changes:
|
|
If you look at Lintcheck results (displayed over at #15215 (comment)), you can see a couple of problems with the current implementation: Here, the lint fires on some binary operation coming deep from an expansion of Here, 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 with Note that this will also include operands 0 and 1, which is desirable. Octal operands probably also make sense in some contexts, so I'd give them a pass as well. |
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
212fe6e to
a33e249
Compare
|
@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. |
decimal_bit_maskdecimal_bitwise_operands
There was a problem hiding this 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.
|
|
||
| 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(); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Very good idea!
| 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 |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
|
All comments addressed, ready for review. |
There was a problem hiding this 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.
| LL | x | (/* comment */99); | ||
| | ^^ | ||
| | | ||
| = help: use binary (0b1100011), hex (0x63), or octal (0o143) notation for better readability |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
|
I addressed the comment and updated the lint message to group digits with underscores. I used Additional changes:
|
There was a problem hiding this 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?
e3b3dad to
273deea
Compare
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this 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.).
| && is_decimal_number(cx, lit.span, &lit.node) | ||
| && !is_single_digit(val) | ||
| && !is_power_of_twoish(val) |
There was a problem hiding this comment.
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.
| && 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) |
| 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), |
There was a problem hiding this comment.
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:
| 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), |
|
@samueltardieu Thanks for your comments. I've addressed them – plus I added handling a literal with a reference (example: |
| LL | 0b1010 & 99; | ||
| | ^^ | ||
| | | ||
| = help: use binary (0b110_0011), hex (0x0063), or octal (0o143) notation for better readability |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I did some archeology and found when it was introduced:
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
clippy_lints/src/operators/mod.rs
Outdated
| /// ``` | ||
| #[clippy::version = "1.93.0"] | ||
| pub DECIMAL_BITWISE_OPERANDS, | ||
| nursery, |
There was a problem hiding this comment.
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.
| nursery, | |
| pedantic, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done and rebased ✅
fc65382 to
f972a7c
Compare
|
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. |
|
Alright. Can you please squash your commits? |
f972a7c to
9cb6306
Compare
|
🔥 |
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:
changelog: [
decimal_bitwise_operands]: new lintFixes #1775