diff --git a/compiler/rustc_parse/src/parser/diagnostics.rs b/compiler/rustc_parse/src/parser/diagnostics.rs index 0b91828639d36..c996b8f281f54 100644 --- a/compiler/rustc_parse/src/parser/diagnostics.rs +++ b/compiler/rustc_parse/src/parser/diagnostics.rs @@ -1244,43 +1244,19 @@ impl<'a> Parser<'a> { ) -> PResult<'a, ErrorGuaranteed> { if let ExprKind::Binary(binop, _, _) = &expr.kind && let ast::BinOpKind::Lt = binop.node - && self.eat(exp!(Comma)) + && self.parse_mistyped_turbofish_generic_args() { - let x = self.parse_seq_to_before_end( - exp!(Gt), - SeqSep::trailing_allowed(exp!(Comma)), - |p| match p.parse_generic_arg(None)? { - Some(arg) => Ok(arg), - // If we didn't eat a generic arg, then we should error. - None => p.unexpected_any(), - }, - ); - match x { - Ok((_, _, Recovered::No)) => { - if self.eat(exp!(Gt)) { - // We made sense of it. Improve the error message. - e.span_suggestion_verbose( - binop.span.shrink_to_lo(), - msg!("use `::<...>` instead of `<...>` to specify lifetime, type, or const arguments"), - "::", - Applicability::MaybeIncorrect, - ); - match self.parse_expr() { - Ok(_) => { - // The subsequent expression is valid. Mark - // `expr` as erroneous and emit `e` now, but - // return `Ok` so parsing can continue. - let guar = e.emit(); - *expr = self.mk_expr_err(expr.span.to(self.prev_token.span), guar); - return Ok(guar); - } - Err(err) => { - err.cancel(); - } - } - } + // We made sense of it. Improve the error message. + sugg_missing_turbofish(&mut e, binop.span); + match self.parse_expr() { + Ok(_) => { + // The subsequent expression is valid. Mark + // `expr` as erroneous and emit `e` now, but + // return `Ok` so parsing can continue. + let guar = e.emit(); + *expr = self.mk_expr_err(expr.span.to(self.prev_token.span), guar); + return Ok(guar); } - Ok((_, _, Recovered::Yes(_))) => {} Err(err) => { err.cancel(); } @@ -1289,6 +1265,50 @@ impl<'a> Parser<'a> { Err(e) } + /// Parses the `, T, U>` tail of a `Foo` whose turbofish `::` is missing, so it parsed + /// as a comparison. On failure the parser is left mid-way, so callers must snapshot first. + fn parse_mistyped_turbofish_generic_args(&mut self) -> bool { + if !self.eat(exp!(Comma)) { + return false; + } + match self.parse_seq_to_before_end(exp!(Gt), SeqSep::trailing_allowed(exp!(Comma)), |p| { + match p.parse_generic_arg(None)? { + Some(arg) => Ok(arg), + None => p.unexpected_any(), + } + }) { + Ok((_, _, Recovered::No)) => self.eat(exp!(Gt)), + Ok((_, _, Recovered::Yes(_))) => false, + Err(err) => { + err.cancel(); + false + } + } + } + + /// Check whether a call argument that parsed as a `<` comparison is really a path missing its + /// turbofish, i.e. the generic args are followed by `::` or a call. Leaves the parser after + /// what it managed to read, so callers must snapshot first. + pub(super) fn probe_missing_turbofish(&mut self) -> bool { + self.with_recovery(super::Recovery::Forbidden, |this| { + this.parse_mistyped_turbofish_generic_args() + && match this.token.kind { + token::PathSep => { + this.bump(); + match this.parse_expr() { + Ok(_) => true, + Err(err) => { + err.cancel(); + false + } + } + } + token::OpenParen => this.consume_fn_args().is_ok(), + _ => false, + } + }) + } + /// Suggest add the missing `let` before the identifier in stmt /// `a: Ty = 1` -> `let a: Ty = 1` pub(super) fn suggest_add_missing_let_for_stmt(&mut self, err: &mut Diag<'a>) { @@ -3188,3 +3208,13 @@ impl<'a> Parser<'a> { }) } } + +/// Suggest inserting the `::` of a turbofish before the `<` that parsed as a comparison. +pub(super) fn sugg_missing_turbofish(err: &mut Diag<'_>, binop_span: Span) { + err.span_suggestion_verbose( + binop_span.shrink_to_lo(), + msg!("use `::<...>` instead of `<...>` to specify lifetime, type, or const arguments"), + "::", + Applicability::MaybeIncorrect, + ); +} diff --git a/compiler/rustc_parse/src/parser/expr.rs b/compiler/rustc_parse/src/parser/expr.rs index 7227c814ce9d6..95cac2f3bdd86 100644 --- a/compiler/rustc_parse/src/parser/expr.rs +++ b/compiler/rustc_parse/src/parser/expr.rs @@ -27,7 +27,7 @@ use rustc_span::{BytePos, ErrorGuaranteed, Ident, Pos, Span, Spanned, Symbol, kw use thin_vec::{ThinVec, thin_vec}; use tracing::instrument; -use super::diagnostics::SnapshotParser; +use super::diagnostics::{SnapshotParser, sugg_missing_turbofish}; use super::pat::{CommaRecoveryMode, Expected, RecoverColon, RecoverComma}; use super::ty::{AllowPlus, RecoverQPath, RecoverReturnSign}; use super::{ @@ -87,7 +87,36 @@ impl<'a> Parser<'a> { /// Parses a sequence of expressions delimited by parentheses. fn parse_expr_paren_seq(&mut self) -> PResult<'a, ThinVec>> { - self.parse_paren_comma_seq(Self::parse_expr).map(|(r, _)| r) + let mut candidates = Vec::new(); + + self.parse_paren_comma_seq(|p| match p.parse_expr() { + Ok(expr) => { + if p.may_recover() + && let ExprKind::Binary(binop, _, _) = &expr.kind + && binop.node == BinOpKind::Lt + { + candidates.push((p.create_snapshot_for_diagnostic(), binop.span)); + } + Ok(expr) + } + Err(mut err) => { + if candidates.is_empty() { + return Err(err); + } + let failed = p.create_snapshot_for_diagnostic(); + let failed_pos = p.approx_token_stream_pos(); + while let Some((snapshot, binop_span)) = candidates.pop() { + p.restore_snapshot(snapshot); + if p.probe_missing_turbofish() && p.approx_token_stream_pos() > failed_pos { + sugg_missing_turbofish(&mut err, binop_span); + break; + } + } + p.restore_snapshot(failed); + Err(err) + } + }) + .map(|(r, _)| r) } /// Parses an expression, subject to the given restrictions. diff --git a/tests/ui/suggestions/suggest-turbofish-parsed-as-comparisons.fixed b/tests/ui/suggestions/suggest-turbofish-parsed-as-comparisons.fixed new file mode 100644 index 0000000000000..aedcf41a270a7 --- /dev/null +++ b/tests/ui/suggestions/suggest-turbofish-parsed-as-comparisons.fixed @@ -0,0 +1,34 @@ +//@ run-rustfix +#![allow(dead_code)] + +struct S; + +struct Many { + a: A, + b: B, + c: C, + d: D, +} +impl Many { + fn new() -> Self { + todo!() + } +} +fn bar(_: Many) {} + +fn take_two(_: bool, _: bool) {} +fn take_three(_: bool, _: bool, _: Many, i32, i32>) {} + +fn main() { + let _ = bar(Many::, i32, i32>::new()); + //~^ ERROR expected expression + + // These are unambiguously comparisons and must keep compiling. + let (a, b, c, d) = (1, 2, 3, 4); + take_two(a < b, c > (d)); + take_two(a < b, c > ::std::primitive::i32::MAX); + + // An argument preceded by genuine comparisons. + take_three(a < b, c > (d), Many::, i32, i32>::new()); + //~^ ERROR expected expression +} diff --git a/tests/ui/suggestions/suggest-turbofish-parsed-as-comparisons.rs b/tests/ui/suggestions/suggest-turbofish-parsed-as-comparisons.rs new file mode 100644 index 0000000000000..279008e8b743f --- /dev/null +++ b/tests/ui/suggestions/suggest-turbofish-parsed-as-comparisons.rs @@ -0,0 +1,34 @@ +//@ run-rustfix +#![allow(dead_code)] + +struct S; + +struct Many { + a: A, + b: B, + c: C, + d: D, +} +impl Many { + fn new() -> Self { + todo!() + } +} +fn bar(_: Many) {} + +fn take_two(_: bool, _: bool) {} +fn take_three(_: bool, _: bool, _: Many, i32, i32>) {} + +fn main() { + let _ = bar(Many, i32, i32>::new()); + //~^ ERROR expected expression + + // These are unambiguously comparisons and must keep compiling. + let (a, b, c, d) = (1, 2, 3, 4); + take_two(a < b, c > (d)); + take_two(a < b, c > ::std::primitive::i32::MAX); + + // An argument preceded by genuine comparisons. + take_three(a < b, c > (d), Many, i32, i32>::new()); + //~^ ERROR expected expression +} diff --git a/tests/ui/suggestions/suggest-turbofish-parsed-as-comparisons.stderr b/tests/ui/suggestions/suggest-turbofish-parsed-as-comparisons.stderr new file mode 100644 index 0000000000000..db529099f2e72 --- /dev/null +++ b/tests/ui/suggestions/suggest-turbofish-parsed-as-comparisons.stderr @@ -0,0 +1,24 @@ +error: expected expression, found `,` + --> $DIR/suggest-turbofish-parsed-as-comparisons.rs:23:46 + | +LL | let _ = bar(Many, i32, i32>::new()); + | ^ expected expression + | +help: use `::<...>` instead of `<...>` to specify lifetime, type, or const arguments + | +LL | let _ = bar(Many::, i32, i32>::new()); + | ++ + +error: expected expression, found `,` + --> $DIR/suggest-turbofish-parsed-as-comparisons.rs:32:61 + | +LL | take_three(a < b, c > (d), Many, i32, i32>::new()); + | ^ expected expression + | +help: use `::<...>` instead of `<...>` to specify lifetime, type, or const arguments + | +LL | take_three(a < b, c > (d), Many::, i32, i32>::new()); + | ++ + +error: aborting due to 2 previous errors + diff --git a/tests/ui/suggestions/suggest-turbofish-unrelated-parse-error.rs b/tests/ui/suggestions/suggest-turbofish-unrelated-parse-error.rs new file mode 100644 index 0000000000000..c8872a7a82925 --- /dev/null +++ b/tests/ui/suggestions/suggest-turbofish-unrelated-parse-error.rs @@ -0,0 +1,10 @@ +// An argument that fails to parse for an unrelated reason must not make us blame an earlier +// comparison for a missing turbofish. + +fn take_three(_: bool, _: bool, _: ()) {} + +fn main() { + let (a, b, c, d) = (1, 2, 3, 4); + take_three(a < b, c > (d), @); + //~^ ERROR expected expression, found `@` +} diff --git a/tests/ui/suggestions/suggest-turbofish-unrelated-parse-error.stderr b/tests/ui/suggestions/suggest-turbofish-unrelated-parse-error.stderr new file mode 100644 index 0000000000000..bc2a2846b0572 --- /dev/null +++ b/tests/ui/suggestions/suggest-turbofish-unrelated-parse-error.stderr @@ -0,0 +1,8 @@ +error: expected expression, found `@` + --> $DIR/suggest-turbofish-unrelated-parse-error.rs:8:32 + | +LL | take_three(a < b, c > (d), @); + | ^ expected expression + +error: aborting due to 1 previous error +