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
100 changes: 65 additions & 35 deletions compiler/rustc_parse/src/parser/diagnostics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
Expand All @@ -1289,6 +1265,50 @@ 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, 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>) {
Expand Down Expand Up @@ -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,
);
}
33 changes: 31 additions & 2 deletions compiler/rustc_parse/src/parser/expr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::{
Expand Down Expand Up @@ -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<Box<Expr>>> {
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.
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 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, Many<(), i32, S, S>, i32, i32>::new());
//~^ ERROR expected expression
}
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 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, Many<(), i32, S, S>, i32, i32>::new());
//~^ ERROR expected expression
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
error: expected expression, found `,`
--> $DIR/suggest-turbofish-parsed-as-comparisons.rs:23:46
|
LL | let _ = bar(Many<i32, Many<(), i32, S, S>, i32, i32>::new());
| ^ expected expression
|
help: use `::<...>` instead of `<...>` to specify lifetime, type, or const arguments
|
LL | let _ = bar(Many::<i32, Many<(), i32, S, S>, 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, Many<(), i32, S, S>, 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, Many<(), i32, S, S>, i32, i32>::new());
| ++

error: aborting due to 2 previous errors

10 changes: 10 additions & 0 deletions tests/ui/suggestions/suggest-turbofish-unrelated-parse-error.rs
Original file line number Diff line number Diff line change
@@ -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 `@`
}
Original file line number Diff line number Diff line change
@@ -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

Loading