Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions compiler/rustc_parse/src/diagnostics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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> {
Expand Down
124 changes: 81 additions & 43 deletions compiler/rustc_parse/src/parser/diagnostics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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();
}
Expand All @@ -1289,6 +1272,61 @@ impl<'a> Parser<'a> {
Err(e)
}

/// Parses the `, T, U>` tail of a `Foo<T, U>` 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<Box<Expr>> {
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>) {
Expand Down
50 changes: 48 additions & 2 deletions compiler/rustc_parse/src/parser/expr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Box<Expr>>> {
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.
Expand Down
34 changes: 34 additions & 0 deletions tests/ui/suggestions/suggest-turbofish-parsed-as-comparisons.fixed
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
//@ run-rustfix
#![allow(dead_code)]

struct S;

struct Many<A, B, C, D> {
a: A,
b: B,
c: C,
d: D,
}
impl<A, B, C, D> Many<A, B, C, D> {
fn new() -> Self {
todo!()
}
}
fn bar<A, B, C, D>(_: Many<A, B, C, D>) {}

fn take_two(_: bool, _: bool) {}
fn take_three(_: bool, _: bool, _: Many<i32, Many<(), i32, S, S>, i32, i32>) {}

fn main() {
let _ = bar(Many::<i32, Many<(), i32, S, S>, 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, Many<(), i32, S, S>, i32, i32>::new());
//~^ ERROR generic args in this position require the turbofish syntax
}
34 changes: 34 additions & 0 deletions tests/ui/suggestions/suggest-turbofish-parsed-as-comparisons.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
//@ run-rustfix
#![allow(dead_code)]

struct S;

struct Many<A, B, C, D> {

@chenyukang chenyukang Aug 7, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we use run-rustfix for this test?

View changes since the review

a: A,
b: B,
c: C,
d: D,
}
impl<A, B, C, D> Many<A, B, C, D> {
fn new() -> Self {
todo!()
}
}
fn bar<A, B, C, D>(_: Many<A, B, C, D>) {}

fn take_two(_: bool, _: bool) {}
fn take_three(_: bool, _: bool, _: Many<i32, Many<(), i32, S, S>, i32, i32>) {}

fn main() {
let _ = bar(Many<i32, Many<(), i32, S, S>, 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, Many<(), i32, S, S>, i32, i32>::new());
//~^ ERROR generic args in this position require the turbofish syntax
}
Original file line number Diff line number Diff line change
@@ -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, Many<(), i32, S, S>, i32, i32>::new());
| ^^^^^^^^
|
help: use `::<...>` instead of `<...>` to specify lifetime, type, or const arguments
|
LL | let _ = bar(Many::<i32, Many<(), i32, S, S>, 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, Many<(), i32, S, S>, i32, i32>::new());
| ^^^^^^^^
|
help: use `::<...>` instead of `<...>` to specify lifetime, type, or const arguments
|
LL | take_three(a < b, c > (d), Many::<i32, Many<(), i32, S, S>, i32, i32>::new());
| ++

error: aborting due to 2 previous errors

Loading