Skip to content

Commit 0270b50

Browse files
Recover unclosed char literal being parsed as lifetime
1 parent cde693c commit 0270b50

11 files changed

+209
-23
lines changed

compiler/rustc_errors/src/lib.rs

+3
Original file line numberDiff line numberDiff line change
@@ -461,6 +461,9 @@ pub enum StashKey {
461461
UnderscoreForArrayLengths,
462462
EarlySyntaxWarning,
463463
CallIntoMethod,
464+
/// When an invalid lifetime e.g. `'2` should be reinterpreted
465+
/// as a char literal in the parser
466+
LifetimeIsChar,
464467
}
465468

466469
fn default_track_diagnostic(_: &Diagnostic) {}

compiler/rustc_parse/src/lexer/mod.rs

+7-2
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@ use rustc_ast::ast::{self, AttrStyle};
33
use rustc_ast::token::{self, CommentKind, Delimiter, Token, TokenKind};
44
use rustc_ast::tokenstream::TokenStream;
55
use rustc_ast::util::unicode::contains_text_flow_control_chars;
6-
use rustc_errors::{error_code, Applicability, DiagnosticBuilder, ErrorGuaranteed, PResult};
6+
use rustc_errors::{
7+
error_code, Applicability, DiagnosticBuilder, ErrorGuaranteed, PResult, StashKey,
8+
};
79
use rustc_lexer::unescape::{self, Mode};
810
use rustc_lexer::Cursor;
911
use rustc_lexer::{Base, DocStyle, RawStrError};
@@ -203,7 +205,10 @@ impl<'a> StringReader<'a> {
203205
// this is necessary.
204206
let lifetime_name = self.str_from(start);
205207
if starts_with_number {
206-
self.err_span_(start, self.pos, "lifetimes cannot start with a number");
208+
let span = self.mk_sp(start, self.pos);
209+
let mut diag = self.sess.struct_err("lifetimes cannot start with a number");
210+
diag.set_span(span);
211+
diag.stash(span, StashKey::LifetimeIsChar);
207212
}
208213
let ident = Symbol::intern(lifetime_name);
209214
token::Lifetime(ident)

compiler/rustc_parse/src/parser/expr.rs

+67-9
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,10 @@ use rustc_ast::{AnonConst, BinOp, BinOpKind, FnDecl, FnRetTy, MacCall, Param, Ty
4242
use rustc_ast::{Arm, Async, BlockCheckMode, Expr, ExprKind, Label, Movability, RangeLimits};
4343
use rustc_ast::{ClosureBinder, StmtKind};
4444
use rustc_ast_pretty::pprust;
45-
use rustc_errors::IntoDiagnostic;
46-
use rustc_errors::{Applicability, Diagnostic, PResult};
45+
use rustc_errors::{
46+
Applicability, Diagnostic, DiagnosticBuilder, ErrorGuaranteed, IntoDiagnostic, PResult,
47+
StashKey,
48+
};
4749
use rustc_session::errors::ExprParenthesesNeeded;
4850
use rustc_session::lint::builtin::BREAK_WITH_LABEL_AND_LOOP;
4951
use rustc_session::lint::BuiltinLintDiagnostics;
@@ -1513,11 +1515,11 @@ impl<'a> Parser<'a> {
15131515
/// Parse `'label: $expr`. The label is already parsed.
15141516
fn parse_labeled_expr(
15151517
&mut self,
1516-
label: Label,
1518+
label_: Label,
15171519
mut consume_colon: bool,
15181520
) -> PResult<'a, P<Expr>> {
1519-
let lo = label.ident.span;
1520-
let label = Some(label);
1521+
let lo = label_.ident.span;
1522+
let label = Some(label_);
15211523
let ate_colon = self.eat(&token::Colon);
15221524
let expr = if self.eat_keyword(kw::While) {
15231525
self.parse_while_expr(label, lo)
@@ -1529,6 +1531,19 @@ impl<'a> Parser<'a> {
15291531
|| self.token.is_whole_block()
15301532
{
15311533
self.parse_block_expr(label, lo, BlockCheckMode::Default)
1534+
} else if !ate_colon
1535+
&& (matches!(self.token.kind, token::CloseDelim(_) | token::Comma)
1536+
|| self.token.is_op())
1537+
{
1538+
let lit = self.recover_unclosed_char(label_.ident, |self_| {
1539+
self_.sess.create_err(UnexpectedTokenAfterLabel {
1540+
span: self_.token.span,
1541+
remove_label: None,
1542+
enclose_in_block: None,
1543+
})
1544+
});
1545+
consume_colon = false;
1546+
Ok(self.mk_expr(lo, ExprKind::Lit(lit)))
15321547
} else if !ate_colon
15331548
&& (self.check_noexpect(&TokenKind::Comma) || self.check_noexpect(&TokenKind::Gt))
15341549
{
@@ -1603,6 +1618,39 @@ impl<'a> Parser<'a> {
16031618
Ok(expr)
16041619
}
16051620

1621+
/// Emit an error when a char is parsed as a lifetime because of a missing quote
1622+
pub(super) fn recover_unclosed_char(
1623+
&mut self,
1624+
lifetime: Ident,
1625+
err: impl FnOnce(&mut Self) -> DiagnosticBuilder<'a, ErrorGuaranteed>,
1626+
) -> ast::Lit {
1627+
if let Some(mut diag) =
1628+
self.sess.span_diagnostic.steal_diagnostic(lifetime.span, StashKey::LifetimeIsChar)
1629+
{
1630+
diag.span_suggestion_verbose(
1631+
lifetime.span.shrink_to_hi(),
1632+
"add `'` to close the char literal",
1633+
"'",
1634+
Applicability::MaybeIncorrect,
1635+
)
1636+
.emit();
1637+
} else {
1638+
err(self)
1639+
.span_suggestion_verbose(
1640+
lifetime.span.shrink_to_hi(),
1641+
"add `'` to close the char literal",
1642+
"'",
1643+
Applicability::MaybeIncorrect,
1644+
)
1645+
.emit();
1646+
}
1647+
ast::Lit {
1648+
token_lit: token::Lit::new(token::LitKind::Char, lifetime.name, None),
1649+
kind: ast::LitKind::Char(lifetime.name.as_str().chars().next().unwrap_or('_')),
1650+
span: lifetime.span,
1651+
}
1652+
}
1653+
16061654
/// Recover on the syntax `do catch { ... }` suggesting `try { ... }` instead.
16071655
fn recover_do_catch(&mut self) -> PResult<'a, P<Expr>> {
16081656
let lo = self.token.span;
@@ -1728,7 +1776,7 @@ impl<'a> Parser<'a> {
17281776
}
17291777

17301778
pub(super) fn parse_lit(&mut self) -> PResult<'a, Lit> {
1731-
self.parse_opt_lit().ok_or_else(|| {
1779+
self.parse_opt_lit().ok_or(()).or_else(|()| {
17321780
if let token::Interpolated(inner) = &self.token.kind {
17331781
let expr = match inner.as_ref() {
17341782
token::NtExpr(expr) => Some(expr),
@@ -1740,12 +1788,22 @@ impl<'a> Parser<'a> {
17401788
let mut err = InvalidInterpolatedExpression { span: self.token.span }
17411789
.into_diagnostic(&self.sess.span_diagnostic);
17421790
err.downgrade_to_delayed_bug();
1743-
return err;
1791+
return Err(err);
17441792
}
17451793
}
17461794
}
1747-
let msg = format!("unexpected token: {}", super::token_descr(&self.token));
1748-
self.struct_span_err(self.token.span, &msg)
1795+
let token = self.token.clone();
1796+
let err = |self_: &mut Self| {
1797+
let msg = format!("unexpected token: {}", super::token_descr(&token));
1798+
self_.struct_span_err(token.span, &msg)
1799+
};
1800+
// On an error path, eagerly consider a lifetime to be an unclosed character lit
1801+
if self.token.is_lifetime() {
1802+
let lt = self.expect_lifetime();
1803+
Ok(self.recover_unclosed_char(lt.ident, err))
1804+
} else {
1805+
Err(err(self))
1806+
}
17491807
})
17501808
}
17511809

compiler/rustc_parse/src/parser/pat.rs

+20
Original file line numberDiff line numberDiff line change
@@ -402,6 +402,25 @@ impl<'a> Parser<'a> {
402402
} else {
403403
PatKind::Path(qself, path)
404404
}
405+
} else if matches!(self.token.kind, token::Lifetime(_))
406+
// In pattern position, we're totally fine with using "next token isn't colon"
407+
// as a heuristic. We could probably just always try to recover if it's a lifetime,
408+
// because we never have `'a: label {}` in a pattern position anyways, but it does
409+
// keep us from suggesting something like `let 'a: Ty = ..` => `let 'a': Ty = ..`
410+
&& !self.look_ahead(1, |token| matches!(token.kind, token::Colon))
411+
{
412+
// Recover a `'a` as a `'a'` literal
413+
let lt = self.expect_lifetime();
414+
let lit = self.recover_unclosed_char(lt.ident, |self_| {
415+
let expected = expected.unwrap_or("pattern");
416+
let msg =
417+
format!("expected {}, found {}", expected, super::token_descr(&self_.token));
418+
419+
let mut err = self_.struct_span_err(self_.token.span, &msg);
420+
err.span_label(self_.token.span, format!("expected {}", expected));
421+
err
422+
});
423+
PatKind::Lit(self.mk_expr(lo, ExprKind::Lit(lit)))
405424
} else {
406425
// Try to parse everything else as literal with optional minus
407426
match self.parse_literal_maybe_minus() {
@@ -799,6 +818,7 @@ impl<'a> Parser<'a> {
799818
|| t.kind == token::Dot // e.g. `.5` for recovery;
800819
|| t.can_begin_literal_maybe_minus() // e.g. `42`.
801820
|| t.is_whole_expr()
821+
|| t.is_lifetime() // recover `'a` instead of `'a'`
802822
})
803823
}
804824

src/test/ui/parser/issues/issue-93282.rs

+1
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,5 @@ fn foo() {
1212
let x = 1;
1313
bar('y, x);
1414
//~^ ERROR expected
15+
//~| ERROR mismatched types
1516
}

src/test/ui/parser/issues/issue-93282.stderr

+26-1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,11 @@ error: expected `while`, `for`, `loop` or `{` after a label
33
|
44
LL | f<'a,>
55
| ^ expected `while`, `for`, `loop` or `{` after a label
6+
|
7+
help: add `'` to close the char literal
8+
|
9+
LL | f<'a',>
10+
| +
611

712
error: expected one of `.`, `:`, `;`, `?`, `for`, `loop`, `while`, `}`, or an operator, found `,`
813
--> $DIR/issue-93282.rs:2:9
@@ -20,6 +25,26 @@ error: expected `while`, `for`, `loop` or `{` after a label
2025
|
2126
LL | bar('y, x);
2227
| ^ expected `while`, `for`, `loop` or `{` after a label
28+
|
29+
help: add `'` to close the char literal
30+
|
31+
LL | bar('y', x);
32+
| +
33+
34+
error[E0308]: mismatched types
35+
--> $DIR/issue-93282.rs:13:9
36+
|
37+
LL | bar('y, x);
38+
| --- ^^ expected `usize`, found `char`
39+
| |
40+
| arguments to this function are incorrect
41+
|
42+
note: function defined here
43+
--> $DIR/issue-93282.rs:7:4
44+
|
45+
LL | fn bar(a: usize, b: usize) -> usize {
46+
| ^^^ --------
2347

24-
error: aborting due to 3 previous errors
48+
error: aborting due to 4 previous errors
2549

50+
For more information about this error, try `rustc --explain E0308`.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
fn main() {
2+
let c = 'a;
3+
//~^ ERROR expected `while`, `for`, `loop` or `{` after a label
4+
//~| HELP add `'` to close the char literal
5+
match c {
6+
'a'..='b => {}
7+
//~^ ERROR unexpected token: `'b`
8+
//~| HELP add `'` to close the char literal
9+
_ => {}
10+
}
11+
let x = ['a, 'b];
12+
//~^ ERROR expected `while`, `for`, `loop` or `{` after a label
13+
//~| ERROR expected `while`, `for`, `loop` or `{` after a label
14+
//~| HELP add `'` to close the char literal
15+
//~| HELP add `'` to close the char literal
16+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
error: expected `while`, `for`, `loop` or `{` after a label
2+
--> $DIR/label-is-actually-char.rs:2:15
3+
|
4+
LL | let c = 'a;
5+
| ^ expected `while`, `for`, `loop` or `{` after a label
6+
|
7+
help: add `'` to close the char literal
8+
|
9+
LL | let c = 'a';
10+
| +
11+
12+
error: unexpected token: `'b`
13+
--> $DIR/label-is-actually-char.rs:6:15
14+
|
15+
LL | 'a'..='b => {}
16+
| ^^
17+
|
18+
help: add `'` to close the char literal
19+
|
20+
LL | 'a'..='b' => {}
21+
| +
22+
23+
error: expected `while`, `for`, `loop` or `{` after a label
24+
--> $DIR/label-is-actually-char.rs:11:16
25+
|
26+
LL | let x = ['a, 'b];
27+
| ^ expected `while`, `for`, `loop` or `{` after a label
28+
|
29+
help: add `'` to close the char literal
30+
|
31+
LL | let x = ['a', 'b];
32+
| +
33+
34+
error: expected `while`, `for`, `loop` or `{` after a label
35+
--> $DIR/label-is-actually-char.rs:11:20
36+
|
37+
LL | let x = ['a, 'b];
38+
| ^ expected `while`, `for`, `loop` or `{` after a label
39+
|
40+
help: add `'` to close the char literal
41+
|
42+
LL | let x = ['a, 'b'];
43+
| +
44+
45+
error: aborting due to 4 previous errors
46+
+8-8
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,11 @@
1+
error[E0308]: mismatched types
2+
--> $DIR/numeric-lifetime.rs:6:20
3+
|
4+
LL | let x: usize = "";
5+
| ----- ^^ expected `usize`, found `&str`
6+
| |
7+
| expected due to this
8+
19
error: lifetimes cannot start with a number
210
--> $DIR/numeric-lifetime.rs:1:10
311
|
@@ -10,14 +18,6 @@ error: lifetimes cannot start with a number
1018
LL | struct S<'1> { s: &'1 usize }
1119
| ^^
1220

13-
error[E0308]: mismatched types
14-
--> $DIR/numeric-lifetime.rs:6:20
15-
|
16-
LL | let x: usize = "";
17-
| ----- ^^ expected `usize`, found `&str`
18-
| |
19-
| expected due to this
20-
2121
error: aborting due to 3 previous errors
2222

2323
For more information about this error, try `rustc --explain E0308`.

src/test/ui/parser/require-parens-for-chained-comparison.rs

+2
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,13 @@ fn main() {
2323
//~^ ERROR expected one of
2424
//~| HELP use `::<...>` instead of `<...>` to specify lifetime, type, or const arguments
2525
//~| ERROR expected
26+
//~| HELP add `'` to close the char literal
2627

2728
f<'_>();
2829
//~^ comparison operators cannot be chained
2930
//~| HELP use `::<...>` instead of `<...>` to specify lifetime, type, or const arguments
3031
//~| ERROR expected
32+
//~| HELP add `'` to close the char literal
3133

3234
let _ = f<u8>;
3335
//~^ ERROR comparison operators cannot be chained

src/test/ui/parser/require-parens-for-chained-comparison.stderr

+13-3
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,11 @@ error: expected `while`, `for`, `loop` or `{` after a label
5858
|
5959
LL | let _ = f<'_, i8>();
6060
| ^ expected `while`, `for`, `loop` or `{` after a label
61+
|
62+
help: add `'` to close the char literal
63+
|
64+
LL | let _ = f<'_', i8>();
65+
| +
6166

6267
error: expected one of `.`, `:`, `;`, `?`, `else`, `for`, `loop`, `while`, or an operator, found `,`
6368
--> $DIR/require-parens-for-chained-comparison.rs:22:17
@@ -71,13 +76,18 @@ LL | let _ = f::<'_, i8>();
7176
| ++
7277

7378
error: expected `while`, `for`, `loop` or `{` after a label
74-
--> $DIR/require-parens-for-chained-comparison.rs:27:9
79+
--> $DIR/require-parens-for-chained-comparison.rs:28:9
7580
|
7681
LL | f<'_>();
7782
| ^ expected `while`, `for`, `loop` or `{` after a label
83+
|
84+
help: add `'` to close the char literal
85+
|
86+
LL | f<'_'>();
87+
| +
7888

7989
error: comparison operators cannot be chained
80-
--> $DIR/require-parens-for-chained-comparison.rs:27:6
90+
--> $DIR/require-parens-for-chained-comparison.rs:28:6
8191
|
8292
LL | f<'_>();
8393
| ^ ^
@@ -88,7 +98,7 @@ LL | f::<'_>();
8898
| ++
8999

90100
error: comparison operators cannot be chained
91-
--> $DIR/require-parens-for-chained-comparison.rs:32:14
101+
--> $DIR/require-parens-for-chained-comparison.rs:34:14
92102
|
93103
LL | let _ = f<u8>;
94104
| ^ ^

0 commit comments

Comments
 (0)