Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions compiler/rustc_parse/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
#![feature(assert_matches)]
#![feature(box_patterns)]
#![feature(debug_closure_helpers)]
#![feature(default_field_values)]
#![feature(if_let_guard)]
#![feature(iter_intersperse)]
#![recursion_limit = "256"]
Expand Down
94 changes: 94 additions & 0 deletions compiler/rustc_parse/src/parser/stmt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ use ast::Label;
use rustc_ast as ast;
use rustc_ast::token::{self, Delimiter, InvisibleOrigin, MetaVarKind, TokenKind};
use rustc_ast::util::classify::{self, TrailingBrace};
use rustc_ast::visit::{Visitor, walk_expr};
use rustc_ast::{
AttrStyle, AttrVec, Block, BlockCheckMode, DUMMY_NODE_ID, Expr, ExprKind, HasAttrs, Local,
LocalKind, MacCall, MacCallStmt, MacStmtStyle, Recovered, Stmt, StmtKind,
Expand Down Expand Up @@ -783,6 +784,71 @@ impl<'a> Parser<'a> {
Ok(self.mk_block(stmts, s, lo.to(self.prev_token.span)))
}

fn recover_missing_let_else(&mut self, err: &mut Diag<'_>, pat: &ast::Pat, stmt_span: Span) {
if self.token.kind != token::OpenBrace {
return;
}
match pat.kind {
ast::PatKind::Ident(..) | ast::PatKind::Missing | ast::PatKind::Wild => {
// Not if let or let else
return;
}
_ => {}
}
let snapshot = self.create_snapshot_for_diagnostic();
let block_span = self.token.span;
let (if_let, let_else) = match self.parse_block() {
Ok(block) => {
let mut idents = vec![];
pat.walk(&mut |pat: &ast::Pat| {
if let ast::PatKind::Ident(_, ident, _) = pat.kind {
idents.push(ident);
}
true
});
// Collect all bindings in pattern and see if they appear in the block. Likely meant
// to write `if let`. See if the block has a return. Likely meant to write
// `let else`.
let mut visitor = IdentFinder { idents, .. };
visitor.visit_block(&block);

(visitor.references_ident, visitor.has_return)
}
Err(e) => {
e.cancel();
self.restore_snapshot(snapshot);
(false, false)
}
};

let mut alternatively = "";
if if_let || !let_else {
alternatively = "alternatively, ";
err.span_suggestion_verbose(
stmt_span.shrink_to_lo(),
"you might have meant to use `if let`",
"if ".to_string(),
if if_let {
Applicability::MachineApplicable
} else {
Applicability::MaybeIncorrect
},
);
}
if let_else || !if_let {
err.span_suggestion_verbose(
block_span.shrink_to_lo(),
format!("{alternatively}you might have meant to use `let else`"),
"else ".to_string(),
if let_else {
Applicability::MachineApplicable
} else {
Applicability::MaybeIncorrect
},
);
}
}

fn recover_missing_dot(&mut self, err: &mut Diag<'_>) {
let Some((ident, _)) = self.token.ident() else {
return;
Expand Down Expand Up @@ -977,6 +1043,7 @@ impl<'a> Parser<'a> {
self.check_mistyped_turbofish_with_multiple_type_params(e, expr).map_err(
|mut e| {
self.recover_missing_dot(&mut e);
self.recover_missing_let_else(&mut e, &local.pat, stmt.span);
e
},
)?;
Expand Down Expand Up @@ -1065,3 +1132,30 @@ impl<'a> Parser<'a> {
self.mk_block(thin_vec![self.mk_stmt_err(span, guar)], BlockCheckMode::Default, span)
}
}

struct IdentFinder {
idents: Vec<Ident>,
/// If a block references one of the bindings introduced by the let pattern, we likely meant to
/// use `if let`.
/// This is pre-expansion, so if we encounter `let Some(x) = foo() { println!("{x}") }` we won't
/// find it.
references_ident: bool = false,
/// If a block has a `return`, then we know with high certainty that the
has_return: bool = false,
}

impl<'a> Visitor<'a> for IdentFinder {
fn visit_ident(&mut self, ident: &Ident) {
for i in &self.idents {
if ident.name == i.name {
self.references_ident = true;
}
}
}
fn visit_expr(&mut self, node: &'a Expr) {
if let ExprKind::Ret(..) = node.kind {
self.has_return = true;
}
walk_expr(self, node);
}
}
24 changes: 24 additions & 0 deletions tests/ui/uninhabited/missing-if-let-or-let-else.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
fn a() {
let Some(x) = foo() { //~ ERROR expected one of
//~^ HELP you might have meant to use `if let`
let y = x;
}
}
fn b() {
let Some(x) = foo() { //~ ERROR expected one of
//~^ HELP you might have meant to use `let else`
return;
}
}
fn c() {
let Some(x) = foo() { //~ ERROR expected one of
//~^ HELP you might have meant to use `if let`
//~| HELP alternatively, you might have meant to use `let else`
// The parser check happens pre-macro-expansion, so we don't know for sure.
println!("{x}");
}
}
fn foo() -> Option<i32> {
Some(42)
}
fn main() {}
39 changes: 39 additions & 0 deletions tests/ui/uninhabited/missing-if-let-or-let-else.stderr
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
error: expected one of `.`, `;`, `?`, `else`, or an operator, found `{`
--> $DIR/missing-if-let-or-let-else.rs:2:25
|
LL | let Some(x) = foo() {
| ^ expected one of `.`, `;`, `?`, `else`, or an operator
|
help: you might have meant to use `if let`
|
LL | if let Some(x) = foo() {
| ++

error: expected one of `.`, `;`, `?`, `else`, or an operator, found `{`
--> $DIR/missing-if-let-or-let-else.rs:8:25
|
LL | let Some(x) = foo() {
| ^ expected one of `.`, `;`, `?`, `else`, or an operator
|
help: you might have meant to use `let else`
|
LL | let Some(x) = foo() else {
Copy link
Member

Choose a reason for hiding this comment

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

if we suggest add else here, a following error missing ; will comes after applying the fix.

Copy link
Member

Choose a reason for hiding this comment

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

so the correct code maybe:

 let Some(x) = foo() else { 
        return;
 };  // need `;` here

i'm not sure whether we should also suggest the ; at the same time, maybe goes too far.

Copy link
Member

Choose a reason for hiding this comment

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

emm, this code is also valid 😀

fn b() {
    if let Some(x) = foo() {
        return;
    };
}

Copy link
Contributor Author

@estebank estebank Aug 30, 2025

Choose a reason for hiding this comment

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

The last example is valid, but unless x is used, then the intent seems muddled. We can of course just suggest if-let in all cases.

Keep in mind that a person is still in the loop. If they didn't mean let-else, then it would give them enough information to realize that the code they wrote wasn't if-let.

Copy link
Member

Choose a reason for hiding this comment

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

yeah, make sense.

| ++++

error: expected one of `.`, `;`, `?`, `else`, or an operator, found `{`
--> $DIR/missing-if-let-or-let-else.rs:14:25
|
LL | let Some(x) = foo() {
| ^ expected one of `.`, `;`, `?`, `else`, or an operator
|
help: you might have meant to use `if let`
|
LL | if let Some(x) = foo() {
| ++
help: alternatively, you might have meant to use `let else`
|
LL | let Some(x) = foo() else {
| ++++

error: aborting due to 3 previous errors

Loading