Skip to content

Commit db09345

Browse files
committed
Add suggestions for expressions in patterns
1 parent c204721 commit db09345

15 files changed

+1122
-72
lines changed

compiler/rustc_errors/src/lib.rs

+2
Original file line numberDiff line numberDiff line change
@@ -571,6 +571,8 @@ pub enum StashKey {
571571
/// Query cycle detected, stashing in favor of a better error.
572572
Cycle,
573573
UndeterminedMacroResolution,
574+
/// Used by `Parser::maybe_recover_trailing_expr`
575+
ExprInPat,
574576
}
575577

576578
fn default_track_diagnostic<R>(diag: DiagInner, f: &mut dyn FnMut(DiagInner) -> R) -> R {

compiler/rustc_parse/messages.ftl

+8
Original file line numberDiff line numberDiff line change
@@ -807,6 +807,14 @@ parse_unexpected_expr_in_pat =
807807
808808
.label = arbitrary expressions are not allowed in patterns
809809
810+
parse_unexpected_expr_in_pat_const_sugg = consider extracting the expression into a `const`
811+
812+
parse_unexpected_expr_in_pat_create_guard_sugg = consider moving the expression to a match arm guard
813+
814+
parse_unexpected_expr_in_pat_inline_const_sugg = consider wrapping the expression in an inline `const` (requires `{"#"}![feature(inline_const_pat)]`)
815+
816+
parse_unexpected_expr_in_pat_update_guard_sugg = consider moving the expression to the match arm guard
817+
810818
parse_unexpected_if_with_if = unexpected `if` in the condition expression
811819
.suggestion = remove the `if`
812820

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

+218-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, IdentIsRaw, 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,219 @@ 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()
457+
.create_err(UnexpectedExpressionInPattern {
458+
span,
459+
is_bound,
460+
expr_precedence: expr.precedence().order(),
461+
})
462+
.stash(span, StashKey::ExprInPat)
463+
.unwrap(),
464+
span,
454465
))
455466
}
456467

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

0 commit comments

Comments
 (0)