fix missing_asserts_for_indexing ignores match for length - #17400
fix missing_asserts_for_indexing ignores match for length#17400wasd243 wants to merge 5 commits into
missing_asserts_for_indexing ignores match for length#17400Conversation
…arantees the index is in bounds
|
Thanks for the pull request, and welcome!You should hear from one of our reviewers after this PR is reviewed by at least 2 reviewers from the community Please see the contribution instructions for more information. Namely, in order to ensure the minimum review times lag, PR authors and assigned reviewers should ensure that the review label (
|
|
Lintcheck changes for cd5f376
This comment will be updated if you push new changes |
| && is_slice_len_expr(cx, scrutinee, slice) | ||
| && match_arms_bound_len(cx, index_expr, arms) | ||
| { | ||
| return arms.iter().any(|arm| is_wild(arm.pat)); |
There was a problem hiding this comment.
This assumes that if we have a wildcard anywhere, then the entire match is safe, even if we are accessing indexes we can't safely assume.
fn foo(supported: &[u8]) {
match supported.len() {
0 => {},
1 => {},
2 => println!("{} {}", supported[0], supported[2]),
_ => println!("{} {}", supported[0], supported[2]),
}
}There was a problem hiding this comment.
Fixed -- literal arms now require index < n (the arm pins len to exactly n), instead of the shared index <= max check.
Lints now:
match supported.len() {
0 => {},
1 => {},
2 => println!("{} {}", supported[2], supported[3]), // both out of bounds for len == 2 → lints
_ => {},
}The example doesn't lint but for a pre-existing reason unrelated to the suppression:
match supported.len() {
0 => {},
1 => {},
2 => println!("{} {}", supported[0], supported[2]), // [0] in bounds → suppressed; [2] NOT suppressed
_ => println!("{} {}", supported[0], supported[2]), // both in bounds (len >= 3) → suppressed
}Co-authored-by: Gri-ffin <82527700+Gri-ffin@users.noreply.github.com>
| fn fix_match_case(supported: &[u8]) { | ||
| match supported.len() { | ||
| 0 => {}, | ||
| 1 => println!("{}", supported[0]), | ||
| _ => println!("{} or {}", supported[0], supported[1]), | ||
| } |
There was a problem hiding this comment.
I think this lint suggestion can be tightened by using the structural pattern matching on slices.
| fn fix_match_case(supported: &[u8]) { | |
| match supported.len() { | |
| 0 => {}, | |
| 1 => println!("{}", supported[0]), | |
| _ => println!("{} or {}", supported[0], supported[1]), | |
| } | |
| fn fix_match_case(supported: &[u8]) { | |
| match supported { | |
| [] => {}, | |
| [first] => println!("{}", first), | |
| [first, second, ...] => println!("{} or {}", first, second), | |
| } |
Much more work though and might be better in another lint.
There was a problem hiding this comment.
@CommanderStorm pattern matching is not always available: https://play.rust-lang.org/?version=stable&mode=debug&edition=2024&gist=0afb61b6183cf5bd030edea69c8a52e9
There was a problem hiding this comment.
Yes, you neee to typecheck that it is an slice, if the alternative suggestion makes sense.
69029e4 to
cd5f376
Compare
|
☔ The latest upstream changes (possibly #17607) made this pull request unmergeable. Please resolve the merge conflicts. |
|
This PR may be out of scope. It's difficult to reliably reason about potential panics in Although the issue is a valid false positive, addressing it more generally would conflict with the current scope and behavior of the lint, and some of the related work would likely be better suited for a separate lint. I think it would be better to close this PR temporarily. |
|
While I agree on generalization issue, is there a possibility to cover at least some cases. I don't even ask about suggestions to replace with, which can land in a documentation rather than be automatic replacements. |
Can we boil this down to very simple patterns where the lint is save? Is there something that we can learn from your effort for this different lint? |
Maybe we could, but if we only use HIR's patterns and hardcode each specific cases, it would be complex to review the code and fix other cases FP/FN. Even if we only treat this as a false-positive case, the amount of code would be difficult to keep under control. I tried to handle this as a narrow false positive in this PR, but it's 100-200 lines added only for a fn foo(supported: &[u8]) {
match supported.len() {
0 => {},
1 => {},
2 => println!("{} {}", supported[0], supported[2]),
_ => println!("{} {}", supported[0], supported[2]),
}
}and y21 already suggested on #17399 an even better solution:
In
This kind of false positive is already recorded in the lint's docs as a known limitation, and the docs explicitly note that these cases can be difficult to reason about. For this, my personal answer is no; it may be out of scope. Is the suggestion maybe incorrect?Yes. I tried this code, the same reproducer in issue #17398 fn issue17398(supported: &[u8]) {
match supported.len() { // `match` without assert
0 => {},
1 => println!("{}", supported[0]),
_ => println!("{} or {}", supported[0], supported[1]),
//~^^ missing_asserts_for_indexing
}
}In this false-positive case, rustfix does not apply the suggestion to the Is the current value being If not, it would be better to change it into |
|
It's worth a while to at least list all false positives you've met and struggled with, so there would be an extensive test scenarios for the future. This also would provide an overview, how problematic would be an implementation for general cases. As for the fix, without knowledge o an exact type (e.g must be a slice for pattern matching for elements), it's impossible to suggest any fix, but it's worth a while to mention some in documentation, otherwise, this assert could become an annoyance. |
FPs edge cases found or mentioned in #17400lint: [ In this PR, we found these cases that cause false-positives. I summarized them into this markdown file to record these cases which could be used as a checklist for future implementations. I run the FP-1:fn match_len(supported: &[u8]) {
match supported.len() {
0 => {}
1 => println!("{}", supported[0]),
_ => println!("{} or {}", supported[0], supported[1]),
}
}stderr outputReason: usize is not negative and value 0 was explicitly checked, lint should never fire. FP-2:fn if_let_len(supported: &[u8]) {
if let 0 | 1 = supported.len() {
println!("checks for supported len previously");
} else {
println!("{} {}", supported[0], supported[1]);
}
}stderr outputReason: The FP-3fn let_else_len(supported: &[u8]) {
let (0 | 1) = supported.len() else {
println!("{} {}", supported[0], supported[1]);
return;
};
}stderr outputReason: The lint requires adding an FP-4 serde-1.0.204/src/de/mod.rs:2285A minor reproduction for #![warn(clippy::missing_asserts_for_indexing, reason="show-off")]
use std::fmt::{self, Display};
struct OneOf {
names: &'static [&'static str],
}
impl Display for OneOf {
fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
match self.names.len() {
0 => panic!(),
1 => write!(formatter, "`{}`", self.names[0]),
2 => write!(formatter, "`{}` or `{}`", self.names[0], self.names[1]),
_ => {
formatter.write_str("one of ")?;
for (i, alt) in self.names.iter().enumerate() {
if i > 0 {
formatter.write_str(", ")?;
}
write!(formatter, "`{}`", alt)?;
}
Ok(())
}
}
}
}
fn main() {
let one_of = OneOf { names: &["a", "b", "c"] };
println!("{}", one_of);
}stderr outputReason: FP-5fn guard_len(supported: &[u8]) {
match supported.len() {
n if n < 2 => {}
_ => println!("{} {}", supported[0], supported[1]),
}
}stderr outputReason: The guard already ensured FP-6fn range_arm_len(supported: &[u8]) {
match supported.len() {
0..=1 => {}
_ => println!("{} {}", supported[0], supported[1]),
}
}stderr outputReason: The range already ensured the FP-7 prettypretty/crates/prettytty/src/util.rsA minor reproduction based on fn render_pairs(chunk: &[u8]) {
for pair in chunk.chunks(2) {
// Allow for uneven number of bytes in final chunk.
print!("{:02x}", pair[0]);
if pair.len() == 1 {
print!(" ");
} else {
print!("{:02x} ", pair[1]);
}
}
}stderr outputReason: the FP-8fn some_value_inside_and_do_some_calc() {
let mut buf = [0u8; 13];
let word = &mut buf[6..=10];
word[0] = b'e';
word[1] = b'a';
word[2] = b'r';
word[3] = b't';
word[4] = b'h';
}stderr outputReason: FP-9fn is_sorted_strict(slice: &[u8]) -> bool {
slice.windows(2).all(|w| w[0] < w[1])
}stderr outputReason: If you are reading this and trying to fix this lint's FP, I definitely suggest you try to use MIR find a general solution rather than hardcode some specific cases, there are lots of #17398 and #17399 are similar issue and in my opinion both of them maybe could be fixed with MIR but not HIR. I left this as a type of checklist, though we have lint check in CI, if you successfully fix this, you probably should see a lots of FPs have been removed (or not, because almost everybody use NotesThis record ONLY for All above only represent my PERSONAL suggestion, if you want to fix it in other ways I appreciated that and please only treat this as an normal examples. |
|
Thank you for the comprehensive list of your findings. My comments: FP-7: There's no guarantee, that pair has at least 1 element as per slice.chunks documentation. Documentation advises to use FP-9: I agree here, as documentation for side note: I'm curious why there's no FP-8: we have all lengths checked by construction. FP-5: complex match, where MIR is required. and at least some of the rest examples were covered by this PR. |
Fixes #17398
missing_asserts_for_indexingpreviously ignoredmatchexpressions, so indexing inside amatchon the slice's length was linted even when the match arms already guarantee the indices are in bounds. For example:Here the
0and1arms rule out all lengths below 2, so the indexing in the wildcard arm cannot fail and the bounds checks are already elided without anassert!.This PR suppresses the lint for an index inside a
matchonslice.len()when the non-wildcard arms are integer literals contiguously covering0..=max(so the wildcard arm implieslen > max) and the index is at mostmax.The check is deliberately conservative and falls back to linting as before when:
0..=max(e.g.5 => .., _ => .., where_can still be0)0 | 1 => ..is safe in principle but not analyzed)One behavioral note: suppression is per-index. If a
matchguaranteeslen > 1but the wildcard arm indexes[0],[1]and[2], only[2]remains unsuppressed, which is equivalent to a single unchecked access and therefore doesn't lint (consistent with the lint never firing on single accesses).changelog: [
missing_asserts_for_indexing]: fix false positive when amatchon the slice length guarantees the indices are in bounds