Skip to content

Fixes a collapsible_match false positive where the suggested if-into-guard collapse produces code that fails the borrow checker (E0502).#16912

Open
Souradip121 wants to merge 3 commits intorust-lang:masterfrom
Souradip121:fix-collapsible-match-borrow-conflict
Open

Fixes a collapsible_match false positive where the suggested if-into-guard collapse produces code that fails the borrow checker (E0502).#16912
Souradip121 wants to merge 3 commits intorust-lang:masterfrom
Souradip121:fix-collapsible-match-borrow-conflict

Conversation

@Souradip121
Copy link
Copy Markdown
Contributor

@Souradip121 Souradip121 commented Apr 25, 2026

When the inner if cond { ... } lives in an arm body whose match scrutinee mutably
borrows some place P, moving the condition into a guard makes that guard run while
the scrutinee borrow is still live. If cond reads from P, the rewrite is unsound.
NLL hides this in the original because the scrutinee borrow can end before the arm
body runs, but a guard executes during pattern matching, not after.

Closes #16903

What the lint catches

// Before -- compiles. `mode` is unused in the arm body, so NLL ends the borrow
// before `self.modes.len()` runs.
let Some(mode) = self.modes.last_mut() else { return };
match (mode, key) {
    (&mut Mode::A, 0) => {},
    (_, 1) => {
        if self.modes.len() >= 2 {
            self.modes.pop();
        }
    },
    _ => {},
}

// After clippy --fix (broken) -- guard runs while `mode` still mut-borrows
// `self.modes`, so reading `self.modes.len()` is E0502.
match (mode, key) {
    (&mut Mode::A, 0) => {},
    (_, 1) if self.modes.len() >= 2 => {
        self.modes.pop();
    },
    _ => {},
}

After the fix, the lint stays silent on this shape. It still fires when the inner
condition reads only places disjoint from the scrutinee's borrow.

How the gate works

A new helper, inner_cond_conflicts_with_scrutinee_borrows, runs in front of the
existing Path-B suggestion. It returns true when collapsing would introduce a
borrow conflict, in which case the suggestion is suppressed.

  1. Run ExprUseVisitor on the match scrutinee with a delegate that records every
    place taken under a mutable borrow (or written to). This catches direct cases
    like match v.iter_mut().next() { ... } and auto-deref of match self.x { ... }
    when self.x: &mut T.
  2. For every local of type &mut T referenced in the scrutinee, walk up to its
    let initializer once and merge its mutable borrows into the same set. This
    catches the let-else pattern from the reproducer where the binding carries the
    borrow into the scrutinee transparently.
  3. Run ExprUseVisitor on the inner condition with a delegate that records every
    place read or borrowed (any kind).
  4. Compare the two sets. Two places alias when they share a base local and one's
    projection list is a prefix of the other's. Upvar bases are treated
    conservatively as aliasing.

Conservative conditions (no false positives)

The lint is now skipped when:

  • The scrutinee mutably borrows a place P (directly, or transitively through a
    one-level let binding of &mut T), AND the inner if condition reads or
    borrows P or a sub-place of P.

Existing gates still apply on top of this:

Out of scope

  • Function-parameter &mut T bindings are not traced back through the call
    boundary; their borrow origin is unknown so we conservatively don't expand
    them. If a real-world case surfaces this can be tightened in a follow-up.
  • let chains beyond one level (let a = &mut x; let b = a; match b { ... })
    are not followed. The dominant pattern in the wild is one-level let-else, which
    the v1 covers.

Tests

Negative cases (must NOT lint) in tests/ui/collapsible_match.rs:

  • issue16903::handle -- the cackle reproducer verbatim, with let-else binding
    &mut Mode from self.modes.last_mut() and a guard that would read
    self.modes.
  • A direct v.iter_mut().next() scrutinee with an inner if v.is_empty().

Positive control (must STILL lint) in tests/ui/collapsible_match_fixable.rs:

  • issue16903_no_conflict -- scrutinee mutably borrows v, but the inner if
    reads only an unrelated local_flag. The lint fires; the resulting .fixed
    output compiles.

changelog: [collapsible_match]: no longer suggests an if-into-guard collapse
when the inner condition would conflict with a mutable borrow held by the match
scrutinee.

@rustbot rustbot added the S-waiting-on-review Status: Awaiting review from the assignee but also interested parties label Apr 25, 2026
@rustbot
Copy link
Copy Markdown
Collaborator

rustbot commented Apr 25, 2026

r? @dswij

rustbot has assigned @dswij.
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

Why was this reviewer chosen?

The reviewer was selected based on:

  • Owners of files modified in this PR: 7 candidates
  • 7 candidates expanded to 7 candidates
  • Random selection from Jarcho, dswij, llogiq

@Gri-ffin
Copy link
Copy Markdown
Contributor

Gri-ffin commented Apr 25, 2026

this seems way too complex for a fix, and partial at that, the analysis currently has gaps around multi-level reborrow chains (e.g., let x = &mut *y; let z = &mut *x;) and ignoring cases where the borrow might have originated from a param or complex assignment as you noted, it's just adding significant complexity that still doesn't fully solve the problem it's supposed to fix.
i was originally thinking to suppress the lint if we hold a mutable borrow before the match, though that still introduce trade-offs about ignoring valid suggestions,but since the actual issue is --fix blindly applying the lint suggestion, i think the simplest route to take from here is to simply allow the lint to fire and inform the user, by changing it from MachineApplicable to MaybeIncorrect, allowing the users to see the warning and make the call on whether to to apply it or not.

Comment on lines +393 to +395
fn places_alias(a: &Place<'_>, b: &Place<'_>) -> bool {
match (a.base, b.base) {
(PlaceBase::Upvar(_), _) | (_, PlaceBase::Upvar(_)) => true,
Copy link
Copy Markdown
Contributor

@Gri-ffin Gri-ffin Apr 25, 2026

Choose a reason for hiding this comment

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

Closures appear frequently near match arms. Treating any Upvar as aliasing will make the lint silently stop firing in closure heavy code.

View changes since the review

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

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.

cargo clippy --fix for collapsible_match sometimes causes compiler error with borrow mutability

4 participants