diff options
| author | Lieselotte <52315535+she3py@users.noreply.github.com> | 2024-09-18 20:38:43 +0200 |
|---|---|---|
| committer | Lieselotte <52315535+she3py@users.noreply.github.com> | 2024-09-18 20:38:43 +0200 |
| commit | db09345ef6e7f39110704e6196bb239d8f10fc27 (patch) | |
| tree | a0374ff643b776d21bb32c07aa4cb88631c4d2b5 /compiler/rustc_parse | |
| parent | c2047219b59ac7830c4315653858f28cf0d57b9d (diff) | |
| download | rust-db09345ef6e7f39110704e6196bb239d8f10fc27.tar.gz rust-db09345ef6e7f39110704e6196bb239d8f10fc27.zip | |
Add suggestions for expressions in patterns
Diffstat (limited to 'compiler/rustc_parse')
| -rw-r--r-- | compiler/rustc_parse/messages.ftl | 8 | ||||
| -rw-r--r-- | compiler/rustc_parse/src/errors.rs | 77 | ||||
| -rw-r--r-- | compiler/rustc_parse/src/parser/pat.rs | 227 | ||||
| -rw-r--r-- | compiler/rustc_parse/src/parser/stmt.rs | 10 |
4 files changed, 311 insertions, 11 deletions
diff --git a/compiler/rustc_parse/messages.ftl b/compiler/rustc_parse/messages.ftl index 3d0ce77bc47..f5aa8984f51 100644 --- a/compiler/rustc_parse/messages.ftl +++ b/compiler/rustc_parse/messages.ftl @@ -807,6 +807,14 @@ parse_unexpected_expr_in_pat = .label = arbitrary expressions are not allowed in patterns +parse_unexpected_expr_in_pat_const_sugg = consider extracting the expression into a `const` + +parse_unexpected_expr_in_pat_create_guard_sugg = consider moving the expression to a match arm guard + +parse_unexpected_expr_in_pat_inline_const_sugg = consider wrapping the expression in an inline `const` (requires `{"#"}![feature(inline_const_pat)]`) + +parse_unexpected_expr_in_pat_update_guard_sugg = consider moving the expression to the match arm guard + parse_unexpected_if_with_if = unexpected `if` in the condition expression .suggestion = remove the `if` diff --git a/compiler/rustc_parse/src/errors.rs b/compiler/rustc_parse/src/errors.rs index a91aa1f5018..e3e7fcebaaa 100644 --- a/compiler/rustc_parse/src/errors.rs +++ b/compiler/rustc_parse/src/errors.rs @@ -1,3 +1,5 @@ +// ignore-tidy-filelength + use std::borrow::Cow; use rustc_ast::token::Token; @@ -2592,11 +2594,86 @@ pub(crate) struct ExpectedCommaAfterPatternField { #[derive(Diagnostic)] #[diag(parse_unexpected_expr_in_pat)] pub(crate) struct UnexpectedExpressionInPattern { + /// The unexpected expr's span. #[primary_span] #[label] pub span: Span, /// Was a `RangePatternBound` expected? pub is_bound: bool, + /// The unexpected expr's precedence (used in match arm guard suggestions). + pub expr_precedence: i8, +} + +#[derive(Subdiagnostic)] +pub(crate) enum UnexpectedExpressionInPatternSugg { + #[multipart_suggestion( + parse_unexpected_expr_in_pat_create_guard_sugg, + applicability = "maybe-incorrect" + )] + CreateGuard { + /// Where to put the suggested identifier. + #[suggestion_part(code = "{ident}")] + ident_span: Span, + /// Where to put the match arm. + #[suggestion_part(code = " if {ident} == {expr}")] + pat_hi: Span, + /// The suggested identifier. + ident: String, + /// The unexpected expression. + expr: String, + }, + + #[multipart_suggestion( + parse_unexpected_expr_in_pat_update_guard_sugg, + applicability = "maybe-incorrect" + )] + UpdateGuard { + /// Where to put the suggested identifier. + #[suggestion_part(code = "{ident}")] + ident_span: Span, + /// The beginning of the match arm guard's expression (insert a `(` if `Some`). + #[suggestion_part(code = "(")] + guard_lo: Option<Span>, + /// The end of the match arm guard's expression. + #[suggestion_part(code = "{guard_hi_paren} && {ident} == {expr}")] + guard_hi: Span, + /// Either `")"` or `""`. + guard_hi_paren: &'static str, + /// The suggested identifier. + ident: String, + /// The unexpected expression. + expr: String, + }, + + #[multipart_suggestion( + parse_unexpected_expr_in_pat_const_sugg, + applicability = "has-placeholders" + )] + Const { + /// Where to put the extracted constant declaration. + #[suggestion_part(code = "{indentation}const {ident}: /* Type */ = {expr};\n")] + stmt_lo: Span, + /// Where to put the suggested identifier. + #[suggestion_part(code = "{ident}")] + ident_span: Span, + /// The suggested identifier. + ident: String, + /// The unexpected expression. + expr: String, + /// The statement's block's indentation. + indentation: String, + }, + + #[multipart_suggestion( + parse_unexpected_expr_in_pat_inline_const_sugg, + applicability = "maybe-incorrect" + )] + InlineConst { + #[suggestion_part(code = "const {{ ")] + start_span: Span, + #[suggestion_part(code = " }}")] + end_span: Span, + }, } #[derive(Diagnostic)] diff --git a/compiler/rustc_parse/src/parser/pat.rs b/compiler/rustc_parse/src/parser/pat.rs index 94c01f7e0d4..647df25c82e 100644 --- a/compiler/rustc_parse/src/parser/pat.rs +++ b/compiler/rustc_parse/src/parser/pat.rs @@ -1,12 +1,14 @@ -use rustc_ast::mut_visit::{walk_pat, MutVisitor}; +use rustc_ast::mut_visit::{self, MutVisitor}; use rustc_ast::ptr::P; use rustc_ast::token::{self, BinOpToken, Delimiter, IdentIsRaw, Token}; +use rustc_ast::visit::{self, Visitor}; use rustc_ast::{ - self as ast, AttrVec, BindingMode, ByRef, Expr, ExprKind, MacCall, Mutability, Pat, PatField, - PatFieldsRest, PatKind, Path, QSelf, RangeEnd, RangeSyntax, + self as ast, Arm, AttrVec, BinOpKind, BindingMode, ByRef, Expr, ExprKind, ExprPrecedence, + LocalKind, MacCall, Mutability, Pat, PatField, PatFieldsRest, PatKind, Path, QSelf, RangeEnd, + RangeSyntax, Stmt, StmtKind, }; use rustc_ast_pretty::pprust; -use rustc_errors::{Applicability, Diag, PResult}; +use rustc_errors::{Applicability, Diag, DiagArgValue, PResult, StashKey}; use rustc_session::errors::ExprParenthesesNeeded; use rustc_span::source_map::{respan, Spanned}; use rustc_span::symbol::{kw, sym, Ident}; @@ -21,8 +23,8 @@ use crate::errors::{ InclusiveRangeExtraEquals, InclusiveRangeMatchArrow, InclusiveRangeNoEnd, InvalidMutInPattern, ParenRangeSuggestion, PatternOnWrongSideOfAt, RemoveLet, RepeatedMutInPattern, SwitchRefBoxOrder, TopLevelOrPatternNotAllowed, TopLevelOrPatternNotAllowedSugg, - TrailingVertNotAllowed, UnexpectedExpressionInPattern, UnexpectedLifetimeInPattern, - UnexpectedParenInRangePat, UnexpectedParenInRangePatSugg, + TrailingVertNotAllowed, UnexpectedExpressionInPattern, UnexpectedExpressionInPatternSugg, + UnexpectedLifetimeInPattern, UnexpectedParenInRangePat, UnexpectedParenInRangePatSugg, UnexpectedVertVertBeforeFunctionParam, UnexpectedVertVertInPattern, WrapInParens, }; use crate::parser::expr::{could_be_unclosed_char_literal, DestructuredFloat}; @@ -448,12 +450,219 @@ impl<'a> Parser<'a> { || self.token == token::CloseDelim(Delimiter::Parenthesis) && self.look_ahead(1, Token::is_range_separator); + let span = expr.span; + Some(( - self.dcx().emit_err(UnexpectedExpressionInPattern { span: expr.span, is_bound }), - expr.span, + self.dcx() + .create_err(UnexpectedExpressionInPattern { + span, + is_bound, + expr_precedence: expr.precedence().order(), + }) + .stash(span, StashKey::ExprInPat) + .unwrap(), + span, )) } + /// Called by [`Parser::parse_stmt_without_recovery`], used to add statement-aware subdiagnostics to the errors stashed + /// by [`Parser::maybe_recover_trailing_expr`]. + pub(super) fn maybe_augment_stashed_expr_in_pats_with_suggestions(&mut self, stmt: &Stmt) { + if self.dcx().has_errors().is_none() { + // No need to walk the statement if there's no stashed errors. + return; + } + + struct PatVisitor<'a> { + /// `self` + parser: &'a Parser<'a>, + /// The freshly-parsed statement. + stmt: &'a Stmt, + /// The current match arm (for arm guard suggestions). + arm: Option<&'a Arm>, + /// The current struct field (for variable name suggestions). + field: Option<&'a PatField>, + } + + impl<'a> PatVisitor<'a> { + /// Looks for stashed [`StashKey::ExprInPat`] errors in `stash_span`, and emit them with suggestions. + /// `stash_span` is contained in `expr_span`, the latter being larger in borrow patterns; + /// ```txt + /// &mut x.y + /// -----^^^ `stash_span` + /// | + /// `expr_span` + /// ``` + /// `is_range_bound` is used to exclude arm guard suggestions in range pattern bounds. + fn maybe_add_suggestions_then_emit( + &self, + stash_span: Span, + expr_span: Span, + is_range_bound: bool, + ) { + self.parser.dcx().try_steal_modify_and_emit_err( + stash_span, + StashKey::ExprInPat, + |err| { + // Includes pre-pats (e.g. `&mut <err>`) in the diagnostic. + err.span.replace(stash_span, expr_span); + + let sm = self.parser.psess.source_map(); + let stmt = self.stmt; + let line_lo = sm.span_extend_to_line(stmt.span).shrink_to_lo(); + let indentation = sm.indentation_before(stmt.span).unwrap_or_default(); + let Ok(expr) = self.parser.span_to_snippet(expr_span) else { + // FIXME: some suggestions don't actually need the snippet; see PR #123877's unresolved conversations. + return; + }; + + if let StmtKind::Let(local) = &stmt.kind { + match &local.kind { + LocalKind::Decl | LocalKind::Init(_) => { + // It's kinda hard to guess what the user intended, so don't make suggestions. + return; + } + + LocalKind::InitElse(_, _) => {} + } + } + + // help: use an arm guard `if val == expr` + // FIXME(guard_patterns): suggest this regardless of a match arm. + if let Some(arm) = &self.arm + && !is_range_bound + { + let (ident, ident_span) = match self.field { + Some(field) => { + (field.ident.to_string(), field.ident.span.to(expr_span)) + } + None => ("val".to_owned(), expr_span), + }; + + // Are parentheses required around `expr`? + // HACK: a neater way would be preferable. + let expr = match &err.args["expr_precedence"] { + DiagArgValue::Number(expr_precedence) => { + if *expr_precedence + <= ExprPrecedence::Binary(BinOpKind::Eq).order() as i32 + { + format!("({expr})") + } else { + format!("{expr}") + } + } + _ => unreachable!(), + }; + + match &arm.guard { + None => { + err.subdiagnostic( + UnexpectedExpressionInPatternSugg::CreateGuard { + ident_span, + pat_hi: arm.pat.span.shrink_to_hi(), + ident, + expr, + }, + ); + } + Some(guard) => { + // Are parentheses required around the old guard? + let wrap_guard = guard.precedence().order() + <= ExprPrecedence::Binary(BinOpKind::And).order(); + + err.subdiagnostic( + UnexpectedExpressionInPatternSugg::UpdateGuard { + ident_span, + guard_lo: if wrap_guard { + Some(guard.span.shrink_to_lo()) + } else { + None + }, + guard_hi: guard.span.shrink_to_hi(), + guard_hi_paren: if wrap_guard { ")" } else { "" }, + ident, + expr, + }, + ); + } + } + } + + // help: extract the expr into a `const VAL: _ = expr` + let ident = match self.field { + Some(field) => field.ident.as_str().to_uppercase(), + None => "VAL".to_owned(), + }; + err.subdiagnostic(UnexpectedExpressionInPatternSugg::Const { + stmt_lo: line_lo, + ident_span: expr_span, + expr, + ident, + indentation, + }); + + // help: wrap the expr in a `const { expr }` + // FIXME(inline_const_pat): once stabilized, remove this check and remove the `(requires #[feature(inline_const_pat)])` note from the message + if self.parser.psess.unstable_features.is_nightly_build() { + err.subdiagnostic(UnexpectedExpressionInPatternSugg::InlineConst { + start_span: expr_span.shrink_to_lo(), + end_span: expr_span.shrink_to_hi(), + }); + } + }, + ); + } + } + + impl<'a> Visitor<'a> for PatVisitor<'a> { + fn visit_arm(&mut self, a: &'a Arm) -> Self::Result { + self.arm = Some(a); + visit::walk_arm(self, a); + self.arm = None; + } + + fn visit_pat_field(&mut self, fp: &'a PatField) -> Self::Result { + self.field = Some(fp); + visit::walk_pat_field(self, fp); + self.field = None; + } + + fn visit_pat(&mut self, p: &'a Pat) -> Self::Result { + match &p.kind { + // Base expression + PatKind::Err(_) | PatKind::Lit(_) => { + self.maybe_add_suggestions_then_emit(p.span, p.span, false) + } + + // Sub-patterns + // FIXME: this doesn't work with recursive subpats (`&mut &mut <err>`) + PatKind::Box(subpat) | PatKind::Ref(subpat, _) + if matches!(subpat.kind, PatKind::Err(_) | PatKind::Lit(_)) => + { + self.maybe_add_suggestions_then_emit(subpat.span, p.span, false) + } + + // Sub-expressions + PatKind::Range(start, end, _) => { + if let Some(start) = start { + self.maybe_add_suggestions_then_emit(start.span, start.span, true); + } + + if let Some(end) = end { + self.maybe_add_suggestions_then_emit(end.span, end.span, true); + } + } + + // Walk continuation + _ => visit::walk_pat(self, p), + } + } + } + + // Starts the visit. + PatVisitor { parser: self, stmt, arm: None, field: None }.visit_stmt(stmt); + } + /// Parses a pattern, with a setting whether modern range patterns (e.g., `a..=b`, `a..b` are /// allowed). fn parse_pat_with_range_pat( @@ -845,7 +1054,7 @@ impl<'a> Parser<'a> { self.0 = true; *m = Mutability::Mut; } - walk_pat(self, pat); + mut_visit::walk_pat(self, pat); } } diff --git a/compiler/rustc_parse/src/parser/stmt.rs b/compiler/rustc_parse/src/parser/stmt.rs index 26ad39e06cd..92fba89d28a 100644 --- a/compiler/rustc_parse/src/parser/stmt.rs +++ b/compiler/rustc_parse/src/parser/stmt.rs @@ -29,6 +29,9 @@ use crate::{errors, maybe_whole}; impl<'a> Parser<'a> { /// Parses a statement. This stops just before trailing semicolons on everything but items. /// e.g., a `StmtKind::Semi` parses to a `StmtKind::Expr`, leaving the trailing `;` unconsumed. + /// + /// If `force_collect` is [`ForceCollect::Yes`], forces collection of tokens regardless of + /// whether or not we have attributes. // Public for rustfmt usage. pub(super) fn parse_stmt(&mut self, force_collect: ForceCollect) -> PResult<'a, Option<Stmt>> { Ok(self.parse_stmt_without_recovery(false, force_collect).unwrap_or_else(|e| { @@ -66,7 +69,7 @@ impl<'a> Parser<'a> { }); } - Ok(Some(if self.token.is_keyword(kw::Let) { + let stmt = if self.token.is_keyword(kw::Let) { self.collect_tokens(None, attrs, force_collect, |this, attrs| { this.expect_keyword(kw::Let)?; let local = this.parse_local(attrs)?; @@ -163,7 +166,10 @@ impl<'a> Parser<'a> { } else { self.error_outer_attrs(attrs); return Ok(None); - })) + }; + + self.maybe_augment_stashed_expr_in_pats_with_suggestions(&stmt); + Ok(Some(stmt)) } fn parse_stmt_path_start(&mut self, lo: Span, attrs: AttrWrapper) -> PResult<'a, Stmt> { |
