Skip to content

Commit a34ced9

Browse files
committed
Add suggestions for expressions in patterns
1 parent 49f43c6 commit a34ced9

22 files changed

+1406
-306
lines changed

compiler/rustc_errors/src/lib.rs

+30
Original file line numberDiff line numberDiff line change
@@ -522,6 +522,8 @@ pub enum StashKey {
522522
/// Query cycle detected, stashing in favor of a better error.
523523
Cycle,
524524
UndeterminedMacroResolution,
525+
/// Used by `Parser::maybe_recover_trailing_expr`
526+
ExprInPat,
525527
}
526528

527529
fn default_track_diagnostic<R>(diag: DiagInner, f: &mut dyn FnMut(DiagInner) -> R) -> R {
@@ -806,6 +808,23 @@ impl DiagCtxt {
806808
Some(Diag::new_diagnostic(self, diag))
807809
}
808810

811+
/// Steals a previously stashed error with the given `Span` and
812+
/// [`StashKey`] as the key, and cancels it if found.
813+
/// Panics if the found diagnostic's level isn't `Level::Error`.
814+
pub fn steal_err(&self, span: Span, key: StashKey, _: ErrorGuaranteed) -> bool {
815+
let key = (span.with_parent(None), key);
816+
// FIXME(#120456) - is `swap_remove` correct?
817+
self.inner
818+
.borrow_mut()
819+
.stashed_diagnostics
820+
.swap_remove(&key)
821+
.inspect(|(diag, guar)| {
822+
assert_eq!(diag.level, Error);
823+
assert!(guar.is_some())
824+
})
825+
.is_some()
826+
}
827+
809828
/// Steals a previously stashed error with the given `Span` and
810829
/// [`StashKey`] as the key, modifies it, and emits it. Returns `None` if
811830
/// no matching diagnostic is found. Panics if the found diagnostic's level
@@ -1250,6 +1269,17 @@ impl DiagCtxt {
12501269
self.create_err(err).emit()
12511270
}
12521271

1272+
/// See [`DiagCtxt::stash_diagnostic`] for details.
1273+
#[track_caller]
1274+
pub fn stash_err<'a>(
1275+
&'a self,
1276+
span: Span,
1277+
key: StashKey,
1278+
err: impl Diagnostic<'a>,
1279+
) -> ErrorGuaranteed {
1280+
self.create_err(err).stash(span, key).unwrap()
1281+
}
1282+
12531283
/// Ensures that an error is printed. See `Level::DelayedBug`.
12541284
//
12551285
// No `#[rustc_lint_diagnostics]` and no `impl Into<DiagMessage>` because bug messages aren't

compiler/rustc_parse/messages.ftl

+8
Original file line numberDiff line numberDiff line change
@@ -780,6 +780,14 @@ parse_unexpected_expr_in_pat =
780780
781781
.label = arbitrary expressions are not allowed in patterns
782782
783+
parse_unexpected_expr_in_pat_const_sugg = extract the expression into a `const` and refer to it
784+
785+
parse_unexpected_expr_in_pat_create_guard_sugg = check the value in an arm guard
786+
787+
parse_unexpected_expr_in_pat_inline_const_sugg = wrap the expression in a inline const (requires `{"#"}![feature(inline_const)]`)
788+
789+
parse_unexpected_expr_in_pat_update_guard_sugg = check the value in the arm guard
790+
783791
parse_unexpected_if_with_if = unexpected `if` in the condition expression
784792
.suggestion = remove the `if`
785793

compiler/rustc_parse/src/errors.rs

+68
Original file line numberDiff line numberDiff line change
@@ -2441,6 +2441,74 @@ pub(crate) struct UnexpectedExpressionInPattern {
24412441
pub is_bound: bool,
24422442
}
24432443

2444+
#[derive(Subdiagnostic)]
2445+
pub(crate) enum UnexpectedExpressionInPatternArmSugg {
2446+
#[multipart_suggestion(
2447+
parse_unexpected_expr_in_pat_create_guard_sugg,
2448+
applicability = "maybe-incorrect"
2449+
)]
2450+
CreateGuard {
2451+
/// The span of the `PatKind:Err` to be transformed into a `PatKind::Ident`.
2452+
#[suggestion_part(code = "{ident}")]
2453+
ident_span: Span,
2454+
/// The end of the match arm's pattern.
2455+
#[suggestion_part(code = " if {ident} == {expr}")]
2456+
pat_hi: Span,
2457+
/// The suggested identifier.
2458+
ident: String,
2459+
/// `ident_span`'s snippet.
2460+
expr: String,
2461+
},
2462+
#[multipart_suggestion(
2463+
parse_unexpected_expr_in_pat_update_guard_sugg,
2464+
applicability = "maybe-incorrect"
2465+
)]
2466+
UpdateGuard {
2467+
/// The span of the `PatKind:Err` to be transformed into a `PatKind::Ident`.
2468+
#[suggestion_part(code = "{ident}")]
2469+
ident_span: Span,
2470+
/// The beginning of the match arm guard's expression.
2471+
#[suggestion_part(code = "(")]
2472+
guard_lo: Span,
2473+
/// The end of the match arm guard's expression.
2474+
#[suggestion_part(code = ") && {ident} == {expr}")]
2475+
guard_hi: Span,
2476+
/// The suggested identifier.
2477+
ident: String,
2478+
/// `ident_span`'s snippet.
2479+
expr: String,
2480+
},
2481+
}
2482+
2483+
#[derive(Subdiagnostic)]
2484+
#[multipart_suggestion(parse_unexpected_expr_in_pat_const_sugg, applicability = "has-placeholders")]
2485+
pub(crate) struct UnexpectedExpressionInPatternConstSugg {
2486+
/// The beginning of statement's line.
2487+
#[suggestion_part(code = "{indentation}const {ident}: _ = {expr};\n")]
2488+
pub stmt_lo: Span,
2489+
/// The span of the `PatKind:Err` to be transformed into a `PatKind::Ident`.
2490+
#[suggestion_part(code = "{ident}")]
2491+
pub ident_span: Span,
2492+
/// The suggested identifier.
2493+
pub ident: String,
2494+
/// `ident_span`'s snippet.
2495+
pub expr: String,
2496+
/// The statement's block's indentation.
2497+
pub indentation: String,
2498+
}
2499+
2500+
#[derive(Subdiagnostic)]
2501+
#[multipart_suggestion(
2502+
parse_unexpected_expr_in_pat_inline_const_sugg,
2503+
applicability = "maybe-incorrect"
2504+
)]
2505+
pub(crate) struct UnexpectedExpressionInPatternInlineConstSugg {
2506+
#[suggestion_part(code = "const {{ ")]
2507+
pub start_span: Span,
2508+
#[suggestion_part(code = " }}")]
2509+
pub end_span: Span,
2510+
}
2511+
24442512
#[derive(Diagnostic)]
24452513
#[diag(parse_unexpected_paren_in_range_pat)]
24462514
pub(crate) struct UnexpectedParenInRangePat {

compiler/rustc_parse/src/parser/item.rs

+167-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@ use super::{
44
AttrWrapper, FollowedByType, ForceCollect, Parser, PathStyle, Recovered, Trailing,
55
TrailingToken,
66
};
7-
use crate::errors::{self, MacroExpandsToAdtField};
7+
use crate::errors::{
8+
self, MacroExpandsToAdtField, UnexpectedExpressionInPatternArmSugg,
9+
UnexpectedExpressionInPatternConstSugg, UnexpectedExpressionInPatternInlineConstSugg,
10+
};
811
use crate::fluent_generated as fluent;
912
use crate::maybe_whole;
1013
use ast::token::IdentIsRaw;
@@ -13,6 +16,7 @@ use rustc_ast::ptr::P;
1316
use rustc_ast::token::{self, Delimiter, TokenKind};
1417
use rustc_ast::tokenstream::{DelimSpan, TokenStream, TokenTree};
1518
use rustc_ast::util::case::Case;
19+
use rustc_ast::visit::{walk_arm, walk_pat, walk_pat_field, walk_stmt, Visitor};
1620
use rustc_ast::{self as ast};
1721
use rustc_ast_pretty::pprust;
1822
use rustc_errors::{codes::*, struct_span_code_err, Applicability, PResult, StashKey};
@@ -2362,6 +2366,168 @@ impl<'a> Parser<'a> {
23622366
}
23632367
(AttrVec::new(), None)
23642368
};
2369+
2370+
if let Some(body) = &body
2371+
&& self.dcx().err_count() > 0
2372+
{
2373+
// WIP: once a fn body has been parsed, we walk through all its patterns,
2374+
// and emit now what errors `maybe_recover_trailing_expr()` stashed,
2375+
// with suggestions depending on which statement the pattern is.
2376+
2377+
struct PatVisitor<'a> {
2378+
/// `self`
2379+
parser: &'a Parser<'a>,
2380+
/// The current statement.
2381+
stmt: Option<&'a Stmt>,
2382+
/// The current match arm.
2383+
arm: Option<&'a Arm>,
2384+
/// The current struct field.
2385+
field: Option<&'a PatField>,
2386+
}
2387+
2388+
impl<'a> Visitor<'a> for PatVisitor<'a> {
2389+
fn visit_stmt(&mut self, s: &'a Stmt) -> Self::Result {
2390+
self.stmt = Some(s);
2391+
2392+
walk_stmt(self, s)
2393+
}
2394+
2395+
fn visit_arm(&mut self, a: &'a Arm) -> Self::Result {
2396+
self.arm = Some(a);
2397+
walk_arm(self, a);
2398+
self.arm = None;
2399+
}
2400+
2401+
fn visit_pat_field(&mut self, fp: &'a PatField) -> Self::Result {
2402+
self.field = Some(fp);
2403+
walk_pat_field(self, fp);
2404+
self.field = None;
2405+
}
2406+
2407+
fn visit_pat(&mut self, p: &'a Pat) -> Self::Result {
2408+
// Looks for stashed `ExprInPat` errors in `stash_span`, and emit them with suggestions.
2409+
// `stash_span` is contained in `expr_span`, the latter being larger in borrow patterns;
2410+
// ```txt
2411+
// &mut x.y
2412+
// -----^^^ `stash_span`
2413+
// |
2414+
// `expr_span`
2415+
// ```
2416+
let emit_now = |that: &Self,
2417+
stash_span: Span,
2418+
expr_span: Span|
2419+
-> Self::Result {
2420+
that.parser.dcx().try_steal_modify_and_emit_err(
2421+
stash_span,
2422+
StashKey::ExprInPat,
2423+
|err| {
2424+
let sm = that.parser.psess.source_map();
2425+
let stmt = that.stmt.unwrap();
2426+
let line_lo = sm.span_extend_to_line(stmt.span).shrink_to_lo();
2427+
let indentation =
2428+
sm.indentation_before(stmt.span).unwrap_or_default();
2429+
let expr = that.parser.span_to_snippet(expr_span).unwrap();
2430+
2431+
err.span.replace(stash_span, expr_span);
2432+
2433+
if let StmtKind::Let(local) = &stmt.kind {
2434+
// If we have an `ExprInPat`, the user tried to assign a value to another value,
2435+
// which doesn't makes much sense.
2436+
match &local.kind {
2437+
LocalKind::Decl => {}
2438+
LocalKind::Init(_) => {}
2439+
LocalKind::InitElse(_, _) => {}
2440+
}
2441+
}
2442+
else {
2443+
// help: use an arm guard `if val == expr`
2444+
if let Some(arm) = &self.arm {
2445+
let (ident, ident_span) = match self.field {
2446+
Some(field) => (field.ident.to_string(), field.ident.span.to(expr_span)),
2447+
None => ("val".to_owned(), expr_span),
2448+
};
2449+
2450+
match &arm.guard {
2451+
None => {
2452+
err.subdiagnostic(&that.parser.dcx(), UnexpectedExpressionInPatternArmSugg::CreateGuard {
2453+
ident_span,
2454+
pat_hi: arm.pat.span.shrink_to_hi(),
2455+
ident,
2456+
expr: expr.clone(),
2457+
});
2458+
}
2459+
Some(guard) => {
2460+
err.subdiagnostic(&that.parser.dcx(), UnexpectedExpressionInPatternArmSugg::UpdateGuard {
2461+
ident_span,
2462+
guard_lo: guard.span.shrink_to_lo(),
2463+
guard_hi: guard.span.shrink_to_hi(),
2464+
ident,
2465+
expr: expr.clone(),
2466+
});
2467+
}
2468+
}
2469+
}
2470+
2471+
// help: extract the expr into a `const VAL: _ = expr`
2472+
let ident = match self.field {
2473+
Some(field) => field.ident.as_str().to_uppercase(),
2474+
None => "VAL".to_owned(),
2475+
};
2476+
err.subdiagnostic(
2477+
&that.parser.dcx(),
2478+
UnexpectedExpressionInPatternConstSugg {
2479+
stmt_lo: line_lo,
2480+
ident_span: expr_span,
2481+
expr,
2482+
ident,
2483+
indentation,
2484+
},
2485+
);
2486+
2487+
// help: wrap the expr in a `const { expr }`
2488+
// FIXME(inline_const): once stabilized, remove this check and remove the `(requires #[feature(inline_const])` note from the message
2489+
if that.parser.psess.unstable_features.is_nightly_build() {
2490+
err.subdiagnostic(
2491+
&that.parser.dcx(),
2492+
UnexpectedExpressionInPatternInlineConstSugg {
2493+
start_span: expr_span.shrink_to_lo(),
2494+
end_span: expr_span.shrink_to_hi(),
2495+
},
2496+
);
2497+
}
2498+
}
2499+
},
2500+
);
2501+
}; // end of `emit_now` closure, we're back in `visit_pat`
2502+
2503+
match &p.kind {
2504+
// Base expression
2505+
PatKind::Err(_) => emit_now(self, p.span, p.span),
2506+
// Sub-patterns
2507+
PatKind::Box(subpat) | PatKind::Ref(subpat, _)
2508+
if matches!(subpat.kind, PatKind::Err(_)) =>
2509+
{
2510+
emit_now(self, subpat.span, p.span)
2511+
}
2512+
// Sub-expressions
2513+
PatKind::Range(start, end, _) => {
2514+
if let Some(start) = start {
2515+
emit_now(self, start.span, start.span);
2516+
}
2517+
2518+
if let Some(end) = end {
2519+
emit_now(self, end.span, end.span);
2520+
}
2521+
}
2522+
// Walk continuation
2523+
_ => walk_pat(self, p),
2524+
}
2525+
}
2526+
} // end of `PatVisitor` impl, we're back in `parse_fn_body`
2527+
2528+
PatVisitor { parser: self, stmt: None, arm: None, field: None }.visit_block(body);
2529+
}
2530+
23652531
attrs.extend(inner_attrs);
23662532
Ok(body)
23672533
}

compiler/rustc_parse/src/parser/pat.rs

+33-6
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ use rustc_ast::{
2020
PatFieldsRest, PatKind, Path, QSelf, RangeEnd, RangeSyntax,
2121
};
2222
use rustc_ast_pretty::pprust;
23-
use rustc_errors::{Applicability, Diag, PResult};
23+
use rustc_errors::{Applicability, Diag, PResult, StashKey};
2424
use rustc_session::errors::ExprParenthesesNeeded;
2525
use rustc_span::source_map::{respan, Spanned};
2626
use rustc_span::symbol::{kw, sym, Ident};
@@ -423,10 +423,15 @@ impl<'a> Parser<'a> {
423423
|| self.token.kind == token::CloseDelim(Delimiter::Parenthesis)
424424
&& self.look_ahead(1, Token::is_range_separator);
425425

426+
let span = expr.span;
427+
426428
return Some((
427-
self.dcx()
428-
.emit_err(UnexpectedExpressionInPattern { span: expr.span, is_bound }),
429-
expr.span,
429+
self.dcx().stash_err(
430+
span,
431+
StashKey::ExprInPat,
432+
UnexpectedExpressionInPattern { span, is_bound },
433+
),
434+
span,
430435
));
431436
}
432437
}
@@ -584,7 +589,11 @@ impl<'a> Parser<'a> {
584589

585590
match self.parse_range_end() {
586591
Some(form) => self.parse_pat_range_begin_with(begin, form)?,
587-
None => PatKind::Lit(begin),
592+
None => match &begin.kind {
593+
// Avoid `PatKind::Lit(ExprKind::Err)`
594+
ExprKind::Err(guar) => PatKind::Err(*guar),
595+
_ => PatKind::Lit(begin),
596+
},
588597
}
589598
}
590599
Err(err) => return self.fatal_unexpected_non_pat(err, expected),
@@ -756,7 +765,25 @@ impl<'a> Parser<'a> {
756765

757766
Ok(match self.maybe_recover_trailing_expr(open_paren.to(self.prev_token.span), false) {
758767
None => pat,
759-
Some((guar, _)) => PatKind::Err(guar),
768+
Some((guar, _)) => {
769+
// We just recovered a bigger expression, so cancel its children
770+
// (e.g. `(1 + 2) * 3`, cancel “`1 + 2` is not a pattern”).
771+
match pat {
772+
PatKind::Paren(pat) => {
773+
self.dcx().steal_err(pat.span, StashKey::ExprInPat, guar);
774+
}
775+
776+
PatKind::Tuple(fields) => {
777+
for pat in fields {
778+
self.dcx().steal_err(pat.span, StashKey::ExprInPat, guar);
779+
}
780+
}
781+
782+
_ => unreachable!(),
783+
}
784+
785+
PatKind::Err(guar)
786+
}
760787
})
761788
}
762789

0 commit comments

Comments
 (0)