From e0bdad97d07661bc4067a9c4930721320cd4c1a1 Mon Sep 17 00:00:00 2001 From: Marton Moro Date: Thu, 13 Aug 2026 11:49:34 +0200 Subject: [PATCH] parser: suggest turbofish for multi-param generics in call args --- compiler/rustc_parse/src/diagnostics.rs | 14 ++ .../rustc_parse/src/parser/diagnostics.rs | 124 ++++++++++++------ compiler/rustc_parse/src/parser/expr.rs | 50 ++++++- ...gest-turbofish-parsed-as-comparisons.fixed | 34 +++++ ...suggest-turbofish-parsed-as-comparisons.rs | 34 +++++ ...est-turbofish-parsed-as-comparisons.stderr | 24 ++++ 6 files changed, 235 insertions(+), 45 deletions(-) create mode 100644 tests/ui/suggestions/suggest-turbofish-parsed-as-comparisons.fixed create mode 100644 tests/ui/suggestions/suggest-turbofish-parsed-as-comparisons.rs create mode 100644 tests/ui/suggestions/suggest-turbofish-parsed-as-comparisons.stderr diff --git a/compiler/rustc_parse/src/diagnostics.rs b/compiler/rustc_parse/src/diagnostics.rs index 7897239d248ea..54031738d18ad 100644 --- a/compiler/rustc_parse/src/diagnostics.rs +++ b/compiler/rustc_parse/src/diagnostics.rs @@ -4146,6 +4146,20 @@ pub(crate) struct GenericArgsInPatRequireTurbofishSyntax { pub suggest_turbofish: Span, } +#[derive(Diagnostic)] +#[diag("generic args in this position require the turbofish syntax")] +pub(crate) struct GenericArgsInExprRequireTurbofishSyntax { + #[primary_span] + pub span: Span, + #[suggestion( + "use `::<...>` instead of `<...>` to specify lifetime, type, or const arguments", + style = "verbose", + code = "::", + applicability = "maybe-incorrect" + )] + pub suggest_turbofish: Span, +} + #[derive(Diagnostic)] #[diag("`for<...>` expected after `{$kw}`, not before")] pub(crate) struct TransposeDynOrImpl<'a> { diff --git a/compiler/rustc_parse/src/parser/diagnostics.rs b/compiler/rustc_parse/src/parser/diagnostics.rs index 0b91828639d36..13ba7f1d1a1a6 100644 --- a/compiler/rustc_parse/src/parser/diagnostics.rs +++ b/compiler/rustc_parse/src/parser/diagnostics.rs @@ -31,15 +31,15 @@ use crate::diagnostics::{ ComparisonOperatorsCannotBeChained, ComparisonOperatorsCannotBeChainedSugg, DocCommentDoesNotDocumentAnything, DocCommentOnParamType, DoubleColonInBound, ExpectedIdentifier, ExpectedSemi, ExpectedSemiSugg, ExprParenthesesNeeded, FoundPathInGenerics, - GenericParamsWithoutAngleBrackets, GenericParamsWithoutAngleBracketsSugg, - HelpIdentifierStartsWithNumber, HelpUseLatestEdition, InInTypo, IncorrectAwait, - IncorrectSemicolon, IncorrectUseOfAwait, IncorrectUseOfUse, MisspelledKw, - PatternMethodParamWithoutBody, QuestionMarkInType, QuestionMarkInTypeSugg, SelfParamNotFirst, - StructLiteralBodyWithoutPath, StructLiteralBodyWithoutPathSugg, SuggAddMissingLetStmt, - SuggEscapeIdentifier, SuggRemoveComma, SuggestBindTypeParameter, SuggestIntroduceTypeParameter, - TernaryOperator, TernaryOperatorSuggestion, UnexpectedConstInGenericParam, - UnexpectedConstParamDeclaration, UnexpectedConstParamDeclarationSugg, UnmatchedAngleBrackets, - UseEqInstead, WrapType, + GenericArgsInExprRequireTurbofishSyntax, GenericParamsWithoutAngleBrackets, + GenericParamsWithoutAngleBracketsSugg, HelpIdentifierStartsWithNumber, HelpUseLatestEdition, + InInTypo, IncorrectAwait, IncorrectSemicolon, IncorrectUseOfAwait, IncorrectUseOfUse, + MisspelledKw, PatternMethodParamWithoutBody, QuestionMarkInType, QuestionMarkInTypeSugg, + SelfParamNotFirst, StructLiteralBodyWithoutPath, StructLiteralBodyWithoutPathSugg, + SuggAddMissingLetStmt, SuggEscapeIdentifier, SuggRemoveComma, SuggestBindTypeParameter, + SuggestIntroduceTypeParameter, TernaryOperator, TernaryOperatorSuggestion, + UnexpectedConstInGenericParam, UnexpectedConstParamDeclaration, + UnexpectedConstParamDeclarationSugg, UnmatchedAngleBrackets, UseEqInstead, WrapType, }; use crate::exp; use crate::parser::attr::InnerAttrPolicy; @@ -1244,43 +1244,26 @@ 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(), - }, + // 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 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(); - } - } - } + 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 +1272,61 @@ 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. + pub(super) fn try_recover_fn_call_arg_missing_turbofish( + &mut self, + expr_span: Span, + binop_span: Span, + ) -> Option> { + let recovered = 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, + } + }); + if !recovered { + return None; + } + let guar = self.dcx().emit_err(GenericArgsInExprRequireTurbofishSyntax { + span: expr_span, + suggest_turbofish: binop_span.shrink_to_lo(), + }); + Some(self.mk_expr_err(expr_span.to(self.prev_token.span), guar)) + } + /// 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>) { diff --git a/compiler/rustc_parse/src/parser/expr.rs b/compiler/rustc_parse/src/parser/expr.rs index f81727eda4fb6..e51390e43864f 100644 --- a/compiler/rustc_parse/src/parser/expr.rs +++ b/compiler/rustc_parse/src/parser/expr.rs @@ -107,8 +107,54 @@ 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(|p| p.parse_expr_catch_underscore(Restrictions::empty())) - .map(|(r, _)| r) + let mut candidates = Vec::new(); + let mut arg_count = 0; + let mut recovered_args = Vec::new(); + + let (mut args, _) = self.parse_paren_comma_seq(|p| { + match p.parse_expr_catch_underscore(Restrictions::empty()) { + Ok(expr) => { + if let ExprKind::Binary(binop, _, _) = &expr.kind + && binop.node == BinOpKind::Lt + { + candidates.push(( + p.create_snapshot_for_diagnostic(), + expr.span, + binop.span, + arg_count, + )); + } + arg_count += 1; + Ok(expr) + } + Err(err) => { + if candidates.is_empty() { + return Err(err); + } + let failed = p.create_snapshot_for_diagnostic(); + while let Some((candidate, expr_span, binop_span, candidate_index)) = + candidates.pop() + { + p.restore_snapshot(candidate); + if let Some(expr) = + p.try_recover_fn_call_arg_missing_turbofish(expr_span, binop_span) + { + err.cancel(); + recovered_args.push((candidate_index, arg_count)); + candidates.clear(); + arg_count += 1; + return Ok(expr); + } + } + p.restore_snapshot(failed); + Err(err) + } + } + })?; + for (start, end) in recovered_args.into_iter().rev() { + args.drain(start..end); + } + Ok(args) } /// 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..8bb6f9b1815db --- /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 generic args in this position require the turbofish syntax + + // 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); + + // A recoverable argument preceded by genuine comparisons. + take_three(a < b, c > (d), Many::, i32, i32>::new()); + //~^ ERROR generic args in this position require the turbofish syntax +} 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..1dbf92783f888 --- /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 generic args in this position require the turbofish syntax + + // 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); + + // A recoverable argument preceded by genuine comparisons. + take_three(a < b, c > (d), Many, i32, i32>::new()); + //~^ ERROR generic args in this position require the turbofish syntax +} 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..56127d7901ae6 --- /dev/null +++ b/tests/ui/suggestions/suggest-turbofish-parsed-as-comparisons.stderr @@ -0,0 +1,24 @@ +error: generic args in this position require the turbofish syntax + --> $DIR/suggest-turbofish-parsed-as-comparisons.rs:23:17 + | +LL | let _ = bar(Many, i32, i32>::new()); + | ^^^^^^^^ + | +help: use `::<...>` instead of `<...>` to specify lifetime, type, or const arguments + | +LL | let _ = bar(Many::, i32, i32>::new()); + | ++ + +error: generic args in this position require the turbofish syntax + --> $DIR/suggest-turbofish-parsed-as-comparisons.rs:32:32 + | +LL | take_three(a < b, c > (d), Many, i32, i32>::new()); + | ^^^^^^^^ + | +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 +