Skip to content

Commit 47413ca

Browse files
committed
Add suggestions for expressions in patterns
1 parent 3cc4839 commit 47413ca

15 files changed

+1134
-72
lines changed

compiler/rustc_errors/src/lib.rs

+13
Original file line numberDiff line numberDiff line change
@@ -546,6 +546,8 @@ pub enum StashKey {
546546
/// Query cycle detected, stashing in favor of a better error.
547547
Cycle,
548548
UndeterminedMacroResolution,
549+
/// Used by `Parser::maybe_recover_trailing_expr`
550+
ExprInPat,
549551
}
550552

551553
fn default_track_diagnostic<R>(diag: DiagInner, f: &mut dyn FnMut(DiagInner) -> R) -> R {
@@ -1292,6 +1294,17 @@ impl<'a> DiagCtxtHandle<'a> {
12921294
self.create_err(err).emit()
12931295
}
12941296

1297+
/// See [`DiagCtxtHandle::stash_diagnostic`] for details.
1298+
#[track_caller]
1299+
pub fn stash_err(
1300+
&'a self,
1301+
span: Span,
1302+
key: StashKey,
1303+
err: impl Diagnostic<'a>,
1304+
) -> ErrorGuaranteed {
1305+
self.create_err(err).stash(span, key).unwrap()
1306+
}
1307+
12951308
/// Ensures that an error is printed. See `Level::DelayedBug`.
12961309
//
12971310
// 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
@@ -801,6 +801,14 @@ parse_unexpected_expr_in_pat =
801801
802802
.label = arbitrary expressions are not allowed in patterns
803803
804+
parse_unexpected_expr_in_pat_const_sugg = consider extracting the expression into a `const`
805+
806+
parse_unexpected_expr_in_pat_create_guard_sugg = consider moving the expression to a match arm guard
807+
808+
parse_unexpected_expr_in_pat_inline_const_sugg = consider wrapping the expression in an inline `const` (requires `{"#"}![feature(inline_const_pat)]`)
809+
810+
parse_unexpected_expr_in_pat_update_guard_sugg = consider moving the expression to the match arm guard
811+
804812
parse_unexpected_if_with_if = unexpected `if` in the condition expression
805813
.suggestion = remove the `if`
806814

compiler/rustc_parse/src/errors.rs

+77
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
// ignore-tidy-filelength
2+
13
use std::borrow::Cow;
24

35
use rustc_ast::token::Token;
@@ -2592,11 +2594,86 @@ pub(crate) struct ExpectedCommaAfterPatternField {
25922594
#[derive(Diagnostic)]
25932595
#[diag(parse_unexpected_expr_in_pat)]
25942596
pub(crate) struct UnexpectedExpressionInPattern {
2597+
/// The unexpected expr's span.
25952598
#[primary_span]
25962599
#[label]
25972600
pub span: Span,
25982601
/// Was a `RangePatternBound` expected?
25992602
pub is_bound: bool,
2603+
/// The unexpected expr's precedence (used in match arm guard suggestions).
2604+
pub expr_precedence: i8,
2605+
}
2606+
2607+
#[derive(Subdiagnostic)]
2608+
pub(crate) enum UnexpectedExpressionInPatternSugg {
2609+
#[multipart_suggestion(
2610+
parse_unexpected_expr_in_pat_create_guard_sugg,
2611+
applicability = "maybe-incorrect"
2612+
)]
2613+
CreateGuard {
2614+
/// Where to put the suggested identifier.
2615+
#[suggestion_part(code = "{ident}")]
2616+
ident_span: Span,
2617+
/// Where to put the match arm.
2618+
#[suggestion_part(code = " if {ident} == {expr}")]
2619+
pat_hi: Span,
2620+
/// The suggested identifier.
2621+
ident: String,
2622+
/// The unexpected expression.
2623+
expr: String,
2624+
},
2625+
2626+
#[multipart_suggestion(
2627+
parse_unexpected_expr_in_pat_update_guard_sugg,
2628+
applicability = "maybe-incorrect"
2629+
)]
2630+
UpdateGuard {
2631+
/// Where to put the suggested identifier.
2632+
#[suggestion_part(code = "{ident}")]
2633+
ident_span: Span,
2634+
/// The beginning of the match arm guard's expression (insert a `(` if `Some`).
2635+
#[suggestion_part(code = "(")]
2636+
guard_lo: Option<Span>,
2637+
/// The end of the match arm guard's expression.
2638+
#[suggestion_part(code = "{guard_hi_paren} && {ident} == {expr}")]
2639+
guard_hi: Span,
2640+
/// Either `")"` or `""`.
2641+
guard_hi_paren: &'static str,
2642+
/// The suggested identifier.
2643+
ident: String,
2644+
/// The unexpected expression.
2645+
expr: String,
2646+
},
2647+
2648+
#[multipart_suggestion(
2649+
parse_unexpected_expr_in_pat_const_sugg,
2650+
applicability = "has-placeholders"
2651+
)]
2652+
Const {
2653+
/// Where to put the extracted constant declaration.
2654+
#[suggestion_part(code = "{indentation}const {ident}: /* Type */ = {expr};\n")]
2655+
stmt_lo: Span,
2656+
/// Where to put the suggested identifier.
2657+
#[suggestion_part(code = "{ident}")]
2658+
ident_span: Span,
2659+
/// The suggested identifier.
2660+
ident: String,
2661+
/// The unexpected expression.
2662+
expr: String,
2663+
/// The statement's block's indentation.
2664+
indentation: String,
2665+
},
2666+
2667+
#[multipart_suggestion(
2668+
parse_unexpected_expr_in_pat_inline_const_sugg,
2669+
applicability = "maybe-incorrect"
2670+
)]
2671+
InlineConst {
2672+
#[suggestion_part(code = "const {{ ")]
2673+
start_span: Span,
2674+
#[suggestion_part(code = " }}")]
2675+
end_span: Span,
2676+
},
26002677
}
26012678

26022679
#[derive(Diagnostic)]

compiler/rustc_parse/src/parser/pat.rs

+219-9
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
1-
use rustc_ast::mut_visit::{walk_pat, MutVisitor};
1+
use rustc_ast::mut_visit::{self, MutVisitor};
22
use rustc_ast::ptr::P;
33
use rustc_ast::token::{self, BinOpToken, Delimiter, Token};
4+
use rustc_ast::visit::{self, Visitor};
45
use rustc_ast::{
5-
self as ast, AttrVec, BindingMode, ByRef, Expr, ExprKind, MacCall, Mutability, Pat, PatField,
6-
PatFieldsRest, PatKind, Path, QSelf, RangeEnd, RangeSyntax,
6+
self as ast, Arm, AttrVec, BinOpKind, BindingMode, ByRef, Expr, ExprKind, ExprPrecedence,
7+
LocalKind, MacCall, Mutability, Pat, PatField, PatFieldsRest, PatKind, Path, QSelf, RangeEnd,
8+
RangeSyntax, Stmt, StmtKind,
79
};
810
use rustc_ast_pretty::pprust;
9-
use rustc_errors::{Applicability, Diag, PResult};
11+
use rustc_errors::{Applicability, Diag, DiagArgValue, PResult, StashKey};
1012
use rustc_session::errors::ExprParenthesesNeeded;
1113
use rustc_span::source_map::{respan, Spanned};
1214
use rustc_span::symbol::{kw, sym, Ident};
@@ -21,8 +23,8 @@ use crate::errors::{
2123
InclusiveRangeExtraEquals, InclusiveRangeMatchArrow, InclusiveRangeNoEnd, InvalidMutInPattern,
2224
ParenRangeSuggestion, PatternOnWrongSideOfAt, RemoveLet, RepeatedMutInPattern,
2325
SwitchRefBoxOrder, TopLevelOrPatternNotAllowed, TopLevelOrPatternNotAllowedSugg,
24-
TrailingVertNotAllowed, UnexpectedExpressionInPattern, UnexpectedLifetimeInPattern,
25-
UnexpectedParenInRangePat, UnexpectedParenInRangePatSugg,
26+
TrailingVertNotAllowed, UnexpectedExpressionInPattern, UnexpectedExpressionInPatternSugg,
27+
UnexpectedLifetimeInPattern, UnexpectedParenInRangePat, UnexpectedParenInRangePatSugg,
2628
UnexpectedVertVertBeforeFunctionParam, UnexpectedVertVertInPattern, WrapInParens,
2729
};
2830
use crate::parser::expr::{could_be_unclosed_char_literal, DestructuredFloat};
@@ -448,12 +450,220 @@ impl<'a> Parser<'a> {
448450
|| self.token == token::CloseDelim(Delimiter::Parenthesis)
449451
&& self.look_ahead(1, Token::is_range_separator);
450452

453+
let span = expr.span;
454+
451455
Some((
452-
self.dcx().emit_err(UnexpectedExpressionInPattern { span: expr.span, is_bound }),
453-
expr.span,
456+
self.dcx().stash_err(
457+
span,
458+
StashKey::ExprInPat,
459+
UnexpectedExpressionInPattern {
460+
span,
461+
is_bound,
462+
expr_precedence: expr.precedence().order(),
463+
},
464+
),
465+
span,
454466
))
455467
}
456468

469+
/// Called by [`Parser::parse_stmt_without_recovery`], used to add statement-aware subdiagnostics to the errors stashed
470+
/// by [`Parser::maybe_recover_trailing_expr`].
471+
pub(super) fn maybe_augment_stashed_expr_in_pats_with_suggestions(&mut self, stmt: &Stmt) {
472+
if self.dcx().has_errors().is_none() {
473+
// No need to walk the statement if there's no stashed errors.
474+
return;
475+
}
476+
477+
struct PatVisitor<'a> {
478+
/// `self`
479+
parser: &'a Parser<'a>,
480+
/// The freshly-parsed statement.
481+
stmt: &'a Stmt,
482+
/// The current match arm (for arm guard suggestions).
483+
arm: Option<&'a Arm>,
484+
/// The current struct field (for variable name suggestions).
485+
field: Option<&'a PatField>,
486+
}
487+
488+
impl<'a> PatVisitor<'a> {
489+
/// Looks for stashed [`StashKey::ExprInPat`] errors in `stash_span`, and emit them with suggestions.
490+
/// `stash_span` is contained in `expr_span`, the latter being larger in borrow patterns;
491+
/// ```txt
492+
/// &mut x.y
493+
/// -----^^^ `stash_span`
494+
/// |
495+
/// `expr_span`
496+
/// ```
497+
/// `is_range_bound` is used to exclude arm guard suggestions in range pattern bounds.
498+
fn maybe_add_suggestions_then_emit(
499+
&self,
500+
stash_span: Span,
501+
expr_span: Span,
502+
is_range_bound: bool,
503+
) {
504+
self.parser.dcx().try_steal_modify_and_emit_err(
505+
stash_span,
506+
StashKey::ExprInPat,
507+
|err| {
508+
// Includes pre-pats (e.g. `&mut <err>`) in the diagnostic.
509+
err.span.replace(stash_span, expr_span);
510+
511+
let sm = self.parser.psess.source_map();
512+
let stmt = self.stmt;
513+
let line_lo = sm.span_extend_to_line(stmt.span).shrink_to_lo();
514+
let indentation = sm.indentation_before(stmt.span).unwrap_or_default();
515+
let Ok(expr) = self.parser.span_to_snippet(expr_span) else {
516+
// FIXME: some suggestions don't actually need the snippet; see PR #123877's unresolved conversations.
517+
return;
518+
};
519+
520+
if let StmtKind::Let(local) = &stmt.kind {
521+
match &local.kind {
522+
LocalKind::Decl | LocalKind::Init(_) => {
523+
// It's kinda hard to guess what the user intended, so don't make suggestions.
524+
return;
525+
}
526+
527+
LocalKind::InitElse(_, _) => {}
528+
}
529+
}
530+
531+
// help: use an arm guard `if val == expr`
532+
// FIXME(guard_patterns): suggest this regardless of a match arm.
533+
if let Some(arm) = &self.arm
534+
&& !is_range_bound
535+
{
536+
let (ident, ident_span) = match self.field {
537+
Some(field) => {
538+
(field.ident.to_string(), field.ident.span.to(expr_span))
539+
}
540+
None => ("val".to_owned(), expr_span),
541+
};
542+
543+
// Are parentheses required around `expr`?
544+
// HACK: a neater way would be preferable.
545+
let expr = match &err.args["expr_precedence"] {
546+
DiagArgValue::Number(expr_precedence) => {
547+
if *expr_precedence
548+
<= ExprPrecedence::Binary(BinOpKind::Eq).order() as i32
549+
{
550+
format!("({expr})")
551+
} else {
552+
format!("{expr}")
553+
}
554+
}
555+
_ => unreachable!(),
556+
};
557+
558+
match &arm.guard {
559+
None => {
560+
err.subdiagnostic(
561+
UnexpectedExpressionInPatternSugg::CreateGuard {
562+
ident_span,
563+
pat_hi: arm.pat.span.shrink_to_hi(),
564+
ident,
565+
expr,
566+
},
567+
);
568+
}
569+
Some(guard) => {
570+
// Are parentheses required around the old guard?
571+
let wrap_guard = guard.precedence().order()
572+
<= ExprPrecedence::Binary(BinOpKind::And).order();
573+
574+
err.subdiagnostic(
575+
UnexpectedExpressionInPatternSugg::UpdateGuard {
576+
ident_span,
577+
guard_lo: if wrap_guard {
578+
Some(guard.span.shrink_to_lo())
579+
} else {
580+
None
581+
},
582+
guard_hi: guard.span.shrink_to_hi(),
583+
guard_hi_paren: if wrap_guard { ")" } else { "" },
584+
ident,
585+
expr,
586+
},
587+
);
588+
}
589+
}
590+
}
591+
592+
// help: extract the expr into a `const VAL: _ = expr`
593+
let ident = match self.field {
594+
Some(field) => field.ident.as_str().to_uppercase(),
595+
None => "VAL".to_owned(),
596+
};
597+
err.subdiagnostic(UnexpectedExpressionInPatternSugg::Const {
598+
stmt_lo: line_lo,
599+
ident_span: expr_span,
600+
expr,
601+
ident,
602+
indentation,
603+
});
604+
605+
// help: wrap the expr in a `const { expr }`
606+
// FIXME(inline_const_pat): once stabilized, remove this check and remove the `(requires #[feature(inline_const_pat)])` note from the message
607+
if self.parser.psess.unstable_features.is_nightly_build() {
608+
err.subdiagnostic(UnexpectedExpressionInPatternSugg::InlineConst {
609+
start_span: expr_span.shrink_to_lo(),
610+
end_span: expr_span.shrink_to_hi(),
611+
});
612+
}
613+
},
614+
);
615+
}
616+
}
617+
618+
impl<'a> Visitor<'a> for PatVisitor<'a> {
619+
fn visit_arm(&mut self, a: &'a Arm) -> Self::Result {
620+
self.arm = Some(a);
621+
visit::walk_arm(self, a);
622+
self.arm = None;
623+
}
624+
625+
fn visit_pat_field(&mut self, fp: &'a PatField) -> Self::Result {
626+
self.field = Some(fp);
627+
visit::walk_pat_field(self, fp);
628+
self.field = None;
629+
}
630+
631+
fn visit_pat(&mut self, p: &'a Pat) -> Self::Result {
632+
match &p.kind {
633+
// Base expression
634+
PatKind::Err(_) | PatKind::Lit(_) => {
635+
self.maybe_add_suggestions_then_emit(p.span, p.span, false)
636+
}
637+
638+
// Sub-patterns
639+
// FIXME: this doesn't work with recursive subpats (`&mut &mut <err>`)
640+
PatKind::Box(subpat) | PatKind::Ref(subpat, _)
641+
if matches!(subpat.kind, PatKind::Err(_) | PatKind::Lit(_)) =>
642+
{
643+
self.maybe_add_suggestions_then_emit(subpat.span, p.span, false)
644+
}
645+
646+
// Sub-expressions
647+
PatKind::Range(start, end, _) => {
648+
if let Some(start) = start {
649+
self.maybe_add_suggestions_then_emit(start.span, start.span, true);
650+
}
651+
652+
if let Some(end) = end {
653+
self.maybe_add_suggestions_then_emit(end.span, end.span, true);
654+
}
655+
}
656+
657+
// Walk continuation
658+
_ => visit::walk_pat(self, p),
659+
}
660+
}
661+
}
662+
663+
// Starts the visit.
664+
PatVisitor { parser: self, stmt, arm: None, field: None }.visit_stmt(stmt);
665+
}
666+
457667
/// Parses a pattern, with a setting whether modern range patterns (e.g., `a..=b`, `a..b` are
458668
/// allowed).
459669
fn parse_pat_with_range_pat(
@@ -845,7 +1055,7 @@ impl<'a> Parser<'a> {
8451055
self.0 = true;
8461056
*m = Mutability::Mut;
8471057
}
848-
walk_pat(self, pat);
1058+
mut_visit::walk_pat(self, pat);
8491059
}
8501060
}
8511061

0 commit comments

Comments
 (0)