Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
96 changes: 96 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,100 @@ 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
});

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 it was
/// meant to be let-else.
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);
}
}

// 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 +1072,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
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