diff --git a/compiler/rustc_parse/src/parser/diagnostics.rs b/compiler/rustc_parse/src/parser/diagnostics.rs index 40dbda2466de4..33d6251136d29 100644 --- a/compiler/rustc_parse/src/parser/diagnostics.rs +++ b/compiler/rustc_parse/src/parser/diagnostics.rs @@ -2844,7 +2844,11 @@ impl<'a> Parser<'a> { { return false; } - let label = self.eat_label().expect("just checked if a label exists"); + let (label, err) = self.eat_label(); + if let Some(e) = err { + e.emit(); + } + let label = label.expect("just checked if a label exists"); self.bump(); // eat `:` let span = label.ident.span.to(self.prev_token.span); let mut diag = self diff --git a/compiler/rustc_parse/src/parser/expr.rs b/compiler/rustc_parse/src/parser/expr.rs index 8238a6518e41d..1b1a98dba5ff7 100644 --- a/compiler/rustc_parse/src/parser/expr.rs +++ b/compiler/rustc_parse/src/parser/expr.rs @@ -274,9 +274,70 @@ impl<'a> Parser<'a> { Fixity::Right => Bound::Included(prec), Fixity::Left | Fixity::None => Bound::Excluded(prec), }; - let rhs = self.with_res(restrictions - Restrictions::STMT_EXPR, |this| { - this.parse_expr_assoc(min_prec) - })?; + let mut rhs_restrictions = restrictions - Restrictions::STMT_EXPR; + if matches!(op, AssocOp::Binary(BinOpKind::Lt)) + && matches!(lhs.kind, ExprKind::Path(..)) + { + rhs_restrictions |= Restrictions::IS_RHS_OF_LT_AFTER_PATH; + } + let rhs = if op == AssocOp::Binary(BinOpKind::Lt) { + // Make the path ident available to `parse_expr_labeled` so the turbofish + // suggestion can name the actual path (e.g. "for `Struct`"). + if let ExprKind::Path(_, ref path) = lhs.kind { + self.expected_turbofish_context = + path.segments.last().map(|seg| (op_span, seg.ident)); + } + let res = self.with_res(rhs_restrictions, |this| this.parse_expr_assoc(min_prec)); + // Clear regardless of whether recovery fired; the context is only valid for + // this one `<` operator. + self.expected_turbofish_context = None; + res? + } else { + self.with_res(rhs_restrictions, |this| this.parse_expr_assoc(min_prec))? + }; + + // Recover `Struct<'_>` missing its turbofish `::`. + // + // `parse_expr_labeled` set `turbofish_missing_lifetime_recovery` and returned + // an `ExprKind::Err` sentinel after emitting the diagnostic. We now have all + // the information needed to rebuild a well-formed path with the lifetime grafted + // onto the last segment, and then resume normal postfix parsing. + if let Some((ident, span)) = self.turbofish_missing_lifetime_recovery.take() { + if let ExprKind::Err(_) = rhs.kind { + let mut lhs_expr = *lhs; + if let ExprKind::Path(qself, mut path) = lhs_expr.kind { + // Graft the lifetime onto the `lhs` path. + if let Some(last_segment) = path.segments.last_mut() { + let arg = ast::GenericArg::Lifetime(ast::Lifetime { + id: ast::DUMMY_NODE_ID, + ident, + }); + let args = ast::AngleBracketedArgs { + span: op_span.to(span), + args: thin_vec::thin_vec![ast::AngleBracketedArg::Arg(arg)], + }; + last_segment.args = + Some(Box::new(ast::GenericArgs::AngleBracketed(args))); + } + + // Resume parsing as a struct literal. + if self.token == token::OpenBrace { + if let Some(expr) = self.maybe_parse_struct_expr(&qself, &path) { + lhs = expr?; + continue; + } + } + + // Otherwise, resume parsing as a postfix expression (e.g. function call). + lhs_expr.kind = ExprKind::Path(qself, path); + let span = lhs_expr.span; + lhs = Box::new(lhs_expr); + lhs = self.parse_expr_dot_or_call_with(ast::AttrVec::new(), lhs, span)?; + continue; + } + lhs = Box::new(lhs_expr); + } + } let span = self.mk_expr_sp(&lhs, lhs_span, op_span, rhs.span); lhs = match op { @@ -680,7 +741,7 @@ impl<'a> Parser<'a> { segment.ident.span, ), }; - match self.parse_expr_labeled(label, false) { + match self.parse_expr_labeled(label, false, None) { Ok(expr) => { type_err.cancel(); self.dcx().emit_err(diagnostics::MalformedLoopLabel { @@ -874,6 +935,14 @@ impl<'a> Parser<'a> { mut e: Box, lo: Span, ) -> PResult<'a, Box> { + // When recovering a missing turbofish lifetime, `e` is an `ExprKind::Err` sentinel + // that `parse_expr_assoc_rest` will replace with the correctly-reconstructed node. + // Skip postfix parsing here so we don't attach method/call/index suffixes to the + // error node before the replacement happens. + if self.turbofish_missing_lifetime_recovery.is_some() { + return Ok(e); + } + let mut res = loop { let has_question = if self.prev_token == TokenKind::Ident(kw::Return, IdentIsRaw::No) { // We are using noexpect here because we don't expect a `?` directly after @@ -1518,8 +1587,8 @@ impl<'a> Parser<'a> { } } else if this.eat_keyword(exp!(While)) { this.parse_expr_while(None, lo) - } else if let Some(label) = this.eat_label() { - this.parse_expr_labeled(label, true) + } else if let (Some(label), err) = this.eat_label() { + this.parse_expr_labeled(label, true, err) } else if this.eat_keyword(exp!(Loop)) { this.parse_expr_loop(None, lo).map_err(|mut err| { err.span_label(lo, "while parsing this `loop` expression"); @@ -1714,10 +1783,57 @@ impl<'a> Parser<'a> { &mut self, label_: Label, mut consume_colon: bool, + mut label_err: Option>, ) -> PResult<'a, Box> { let lo = label_.ident.span; let label = Some(label_); let ate_colon = self.eat(exp!(Colon)); + + // Intercept `Struct<'_>` / `Struct<'a>` / `Struct<'abc>` missing their turbofish `::`. + // Fires when the lifetime is on the RHS of `<` after a path, has no following `:`, and + // the next token is `>` — i.e. this was never a real label, just a mistyped type arg. + // Reserved-keyword lifetimes (`'_`) already carry a diag; named ones need a fresh one. + if !ate_colon + && self.restrictions.contains(Restrictions::IS_RHS_OF_LT_AFTER_PATH) + && self.check_noexpect(&token::Gt) + { + let mut err = if let Some(e) = label_err.take() { + e + } else { + self.dcx().create_err(diagnostics::UnexpectedTokenAfterLabel { + span: self.token.span, + remove_label: None, + enclose_in_block: None, + }) + }; + let (op_span, ident) = self.expected_turbofish_context.take().expect( + "IS_RHS_OF_LT_AFTER_PATH is only ever set alongside expected_turbofish_context \ + in parse_expr_assoc_rest, so this must be Some here", + ); + err.span_suggestion( + op_span.shrink_to_lo(), + format!( + "use `::<...>` instead of `<...>` to specify lifetime arguments for `{ident}`" + ), + "::", + Applicability::MachineApplicable, + ); + err.emit(); + self.bump(); // consume `>` + + // Return an `Err` sentinel so `parse_expr_assoc_rest` can detect the recovery and + // graft the lifetime onto the lhs path. We cannot build the final node here because + // `lhs` (the path being parameterized) is owned by the caller above us. + self.turbofish_missing_lifetime_recovery = Some((label_.ident, label_.ident.span)); + return Ok( + self.mk_expr_err(lo, self.dcx().delayed_bug("turbofish_missing_lifetime_recovery")) + ); + } + + if let Some(e) = label_err.take() { + e.emit(); + } + let tok_sp = self.token.span; let expr = if self.eat_keyword(exp!(While)) { self.parse_expr_while(label, lo) @@ -1909,13 +2025,16 @@ impl<'a> Parser<'a> { /// with a labeled loop does not even get a warning because there is no ambiguity. fn parse_expr_break(&mut self) -> PResult<'a, Box> { let lo = self.prev_token.span; - let mut label = self.eat_label(); + let (mut label, err) = self.eat_label(); + if let Some(e) = err { + e.emit(); + } let kind = if self.token == token::Colon && let Some(label) = label.take() { // The value expression can be a labeled loop, see issue #86948, e.g.: // `loop { break 'label: loop { break 'label 42; }; }` - let lexpr = self.parse_expr_labeled(label, true)?; + let lexpr = self.parse_expr_labeled(label, true, None)?; self.dcx().emit_err(diagnostics::LabeledLoopInBreak { span: lexpr.span, sub: diagnostics::WrapInParentheses::Expression { @@ -1976,7 +2095,10 @@ impl<'a> Parser<'a> { /// Parse `"continue" label?`. fn parse_expr_continue(&mut self, lo: Span) -> PResult<'a, Box> { - let mut label = self.eat_label(); + let (mut label, err) = self.eat_label(); + if let Some(e) = err { + e.emit(); + } // Recover `continue label` -> `continue 'label` if self.may_recover() @@ -3162,17 +3284,18 @@ impl<'a> Parser<'a> { )) } - pub(crate) fn eat_label(&mut self) -> Option