Skip to content

Commit d673d0a

Browse files
committed
Use heuristics to recover parsing of missing ;
- Detect `,` and `:` typos where `;` was intended. - When the next token could have been the start of a new statement, detect a missing semicolon.
1 parent 03a50ae commit d673d0a

12 files changed

+120
-100
lines changed

src/libsyntax/parse/parser/diagnostics.rs

Lines changed: 54 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ use crate::ast::{
66
self, Param, BinOpKind, BindingMode, BlockCheckMode, Expr, ExprKind, Ident, Item, ItemKind,
77
Mutability, Pat, PatKind, PathSegment, QSelf, Ty, TyKind,
88
};
9-
use crate::parse::token::{self, TokenKind};
9+
use crate::parse::token::{self, TokenKind, token_can_begin_expr};
1010
use crate::print::pprust;
1111
use crate::ptr::P;
1212
use crate::symbol::{kw, sym};
@@ -326,34 +326,8 @@ impl<'a> Parser<'a> {
326326
}
327327
}
328328

329-
let is_semi_suggestable = expected.iter().any(|t| match t {
330-
TokenType::Token(token::Semi) => true, // We expect a `;` here.
331-
_ => false,
332-
}) && ( // A `;` would be expected before the current keyword.
333-
self.token.is_keyword(kw::Break) ||
334-
self.token.is_keyword(kw::Continue) ||
335-
self.token.is_keyword(kw::For) ||
336-
self.token.is_keyword(kw::If) ||
337-
self.token.is_keyword(kw::Let) ||
338-
self.token.is_keyword(kw::Loop) ||
339-
self.token.is_keyword(kw::Match) ||
340-
self.token.is_keyword(kw::Return) ||
341-
self.token.is_keyword(kw::While)
342-
);
343329
let sm = self.sess.source_map();
344330
match (sm.lookup_line(self.token.span.lo()), sm.lookup_line(sp.lo())) {
345-
(Ok(ref a), Ok(ref b)) if a.line != b.line && is_semi_suggestable => {
346-
// The spans are in different lines, expected `;` and found `let` or `return`.
347-
// High likelihood that it is only a missing `;`.
348-
err.span_suggestion_short(
349-
label_sp,
350-
"a semicolon may be missing here",
351-
";".to_string(),
352-
Applicability::MaybeIncorrect,
353-
);
354-
err.emit();
355-
return Ok(true);
356-
}
357331
(Ok(ref a), Ok(ref b)) if a.line == b.line => {
358332
// When the spans are in the same line, it means that the only content between
359333
// them is whitespace, point at the found token in that case:
@@ -902,18 +876,61 @@ impl<'a> Parser<'a> {
902876
}
903877
}
904878
let sm = self.sess.source_map();
905-
match (sm.lookup_line(prev_sp.lo()), sm.lookup_line(sp.lo())) {
906-
(Ok(ref a), Ok(ref b)) if a.line == b.line => {
907-
// When the spans are in the same line, it means that the only content
908-
// between them is whitespace, point only at the found token.
909-
err.span_label(sp, label_exp);
879+
if !sm.is_multiline(prev_sp.until(sp)) {
880+
// When the spans are in the same line, it means that the only content
881+
// between them is whitespace, point only at the found token.
882+
err.span_label(sp, label_exp);
883+
} else {
884+
err.span_label(prev_sp, label_exp);
885+
err.span_label(sp, "unexpected token");
886+
}
887+
Err(err)
888+
}
889+
890+
pub(super) fn expect_semi(&mut self) -> PResult<'a, ()> {
891+
if self.eat(&token::Semi) {
892+
return Ok(());
893+
}
894+
let sm = self.sess.source_map();
895+
let msg = format!("expected `;`, found `{}`", self.this_token_descr());
896+
let appl = Applicability::MachineApplicable;
897+
if self.look_ahead(1, |t| t == &token::CloseDelim(token::Brace)
898+
|| token_can_begin_expr(t) && t.kind != token::Colon
899+
) && [token::Comma, token::Colon].contains(&self.token.kind) {
900+
// Likely typo: `,` → `;` or `:` → `;`. This is triggered if the current token is
901+
// either `,` or `:`, and the next token could either start a new statement or is a
902+
// block close. For example:
903+
//
904+
// let x = 32:
905+
// let y = 42;
906+
if sm.is_multiline(self.prev_span.until(self.token.span)) {
907+
self.bump();
908+
let sp = self.prev_span;
909+
self.struct_span_err(sp, &msg)
910+
.span_suggestion(sp, "change this to `;`", ";".to_string(), appl)
911+
.emit();
912+
return Ok(())
910913
}
911-
_ => {
912-
err.span_label(prev_sp, label_exp);
913-
err.span_label(sp, "unexpected token");
914+
} else if self.look_ahead(0, |t| t == &token::CloseDelim(token::Brace) || (
915+
token_can_begin_expr(t)
916+
&& t != &token::Semi
917+
&& t != &token::Pound // Avoid triggering with too many trailing `#` in raw string.
918+
)) {
919+
// Missing semicolon typo. This is triggered if the next token could either start a
920+
// new statement or is a block close. For example:
921+
//
922+
// let x = 32
923+
// let y = 42;
924+
if sm.is_multiline(self.prev_span.until(self.token.span)) {
925+
let sp = self.prev_span.shrink_to_hi();
926+
self.struct_span_err(sp, &msg)
927+
.span_label(self.token.span, "unexpected token")
928+
.span_suggestion_short(sp, "add `;` here", ";".to_string(), appl)
929+
.emit();
930+
return Ok(())
914931
}
915932
}
916-
Err(err)
933+
self.expect(&token::Semi).map(|_| ()) // Error unconditionally
917934
}
918935

919936
pub(super) fn parse_semi_or_incorrect_foreign_fn_body(
@@ -943,7 +960,7 @@ impl<'a> Parser<'a> {
943960
Err(mut err) => {
944961
err.cancel();
945962
mem::replace(self, parser_snapshot);
946-
self.expect(&token::Semi)?;
963+
self.expect_semi()?;
947964
}
948965
}
949966
} else {

src/libsyntax/parse/parser/item.rs

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ impl<'a> Parser<'a> {
9898
if self.eat_keyword(kw::Use) {
9999
// USE ITEM
100100
let item_ = ItemKind::Use(P(self.parse_use_tree()?));
101-
self.expect(&token::Semi)?;
101+
self.expect_semi()?;
102102

103103
let span = lo.to(self.prev_span);
104104
let item = self.mk_item(span, Ident::invalid(), item_, vis, attrs);
@@ -526,7 +526,7 @@ impl<'a> Parser<'a> {
526526
// eat a matched-delimiter token tree:
527527
let (delim, tts) = self.expect_delimited_token_tree()?;
528528
if delim != MacDelimiter::Brace {
529-
self.expect(&token::Semi)?;
529+
self.expect_semi()?;
530530
}
531531

532532
Ok(Some(Mac {
@@ -776,7 +776,7 @@ impl<'a> Parser<'a> {
776776
let typ = self.parse_ty()?;
777777
self.expect(&token::Eq)?;
778778
let expr = self.parse_expr()?;
779-
self.expect(&token::Semi)?;
779+
self.expect_semi()?;
780780
Ok((name, ImplItemKind::Const(typ, expr), Generics::default()))
781781
}
782782

@@ -813,7 +813,7 @@ impl<'a> Parser<'a> {
813813

814814
let bounds = self.parse_generic_bounds(None)?;
815815
tps.where_clause = self.parse_where_clause()?;
816-
self.expect(&token::Semi)?;
816+
self.expect_semi()?;
817817

818818
let whole_span = lo.to(self.prev_span);
819819
if is_auto == IsAuto::Yes {
@@ -927,7 +927,7 @@ impl<'a> Parser<'a> {
927927
} else {
928928
None
929929
};
930-
self.expect(&token::Semi)?;
930+
self.expect_semi()?;
931931
Ok((ident, TraitItemKind::Const(ty, default), Generics::default()))
932932
}
933933

@@ -951,7 +951,7 @@ impl<'a> Parser<'a> {
951951
} else {
952952
None
953953
};
954-
self.expect(&token::Semi)?;
954+
self.expect_semi()?;
955955

956956
Ok((ident, TraitItemKind::Type(bounds, default), generics))
957957
}
@@ -1054,7 +1054,7 @@ impl<'a> Parser<'a> {
10541054
} else {
10551055
(orig_name, None)
10561056
};
1057-
self.expect(&token::Semi)?;
1057+
self.expect_semi()?;
10581058

10591059
let span = lo.to(self.prev_span);
10601060
Ok(self.mk_item(span, item_name, ItemKind::ExternCrate(orig_name), visibility, attrs))
@@ -1217,7 +1217,7 @@ impl<'a> Parser<'a> {
12171217
self.expect(&token::Colon)?;
12181218
let ty = self.parse_ty()?;
12191219
let hi = self.token.span;
1220-
self.expect(&token::Semi)?;
1220+
self.expect_semi()?;
12211221
Ok(ForeignItem {
12221222
ident,
12231223
attrs,
@@ -1235,7 +1235,7 @@ impl<'a> Parser<'a> {
12351235

12361236
let ident = self.parse_ident()?;
12371237
let hi = self.token.span;
1238-
self.expect(&token::Semi)?;
1238+
self.expect_semi()?;
12391239
Ok(ast::ForeignItem {
12401240
ident,
12411241
attrs,
@@ -1282,7 +1282,7 @@ impl<'a> Parser<'a> {
12821282

12831283
self.expect(&token::Eq)?;
12841284
let e = self.parse_expr()?;
1285-
self.expect(&token::Semi)?;
1285+
self.expect_semi()?;
12861286
let item = match m {
12871287
Some(m) => ItemKind::Static(ty, m, e),
12881288
None => ItemKind::Const(ty, e),
@@ -1344,7 +1344,7 @@ impl<'a> Parser<'a> {
13441344
let ty = self.parse_ty()?;
13451345
AliasKind::Weak(ty)
13461346
};
1347-
self.expect(&token::Semi)?;
1347+
self.expect_semi()?;
13481348
Ok((ident, alias, tps))
13491349
}
13501350

@@ -1468,7 +1468,7 @@ impl<'a> Parser<'a> {
14681468
} else if self.token == token::OpenDelim(token::Paren) {
14691469
let body = VariantData::Tuple(self.parse_tuple_struct_body()?, DUMMY_NODE_ID);
14701470
generics.where_clause = self.parse_where_clause()?;
1471-
self.expect(&token::Semi)?;
1471+
self.expect_semi()?;
14721472
body
14731473
} else {
14741474
let token_str = self.this_token_descr();

src/libsyntax/parse/parser/stmt.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -432,6 +432,7 @@ impl<'a> Parser<'a> {
432432
None => return Ok(None),
433433
};
434434

435+
let mut eat_semi = true;
435436
match stmt.kind {
436437
StmtKind::Expr(ref expr) if self.token != token::Eof => {
437438
// expression without semicolon
@@ -453,13 +454,14 @@ impl<'a> Parser<'a> {
453454
if macro_legacy_warnings && self.token != token::Semi {
454455
self.warn_missing_semicolon();
455456
} else {
456-
self.expect_one_of(&[], &[token::Semi])?;
457+
self.expect_semi()?;
458+
eat_semi = false;
457459
}
458460
}
459461
_ => {}
460462
}
461463

462-
if self.eat(&token::Semi) {
464+
if eat_semi && self.eat(&token::Semi) {
463465
stmt = stmt.add_trailing_semicolon();
464466
}
465467
stmt.span = stmt.span.to(self.prev_span);

src/libsyntax/parse/token.rs

Lines changed: 26 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -143,34 +143,35 @@ impl Lit {
143143

144144
pub(crate) fn ident_can_begin_expr(name: ast::Name, span: Span, is_raw: bool) -> bool {
145145
let ident_token = Token::new(Ident(name, is_raw), span);
146+
token_can_begin_expr(&ident_token)
147+
}
146148

149+
pub(crate) fn token_can_begin_expr(ident_token: &Token) -> bool {
147150
!ident_token.is_reserved_ident() ||
148151
ident_token.is_path_segment_keyword() ||
149-
[
150-
kw::Async,
151-
152-
// FIXME: remove when `await!(..)` syntax is removed
153-
// https://github.com/rust-lang/rust/issues/60610
154-
kw::Await,
155-
156-
kw::Do,
157-
kw::Box,
158-
kw::Break,
159-
kw::Continue,
160-
kw::False,
161-
kw::For,
162-
kw::If,
163-
kw::Let,
164-
kw::Loop,
165-
kw::Match,
166-
kw::Move,
167-
kw::Return,
168-
kw::True,
169-
kw::Unsafe,
170-
kw::While,
171-
kw::Yield,
172-
kw::Static,
173-
].contains(&name)
152+
match ident_token.kind {
153+
TokenKind::Ident(ident, _) => [
154+
kw::Async,
155+
kw::Do,
156+
kw::Box,
157+
kw::Break,
158+
kw::Continue,
159+
kw::False,
160+
kw::For,
161+
kw::If,
162+
kw::Let,
163+
kw::Loop,
164+
kw::Match,
165+
kw::Move,
166+
kw::Return,
167+
kw::True,
168+
kw::Unsafe,
169+
kw::While,
170+
kw::Yield,
171+
kw::Static,
172+
].contains(&ident),
173+
_=> false,
174+
}
174175
}
175176

176177
fn ident_can_begin_type(name: ast::Name, span: Span, is_raw: bool) -> bool {

src/test/ui/parser/import-from-path.stderr

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ error: expected `;`, found `::`
22
--> $DIR/import-from-path.rs:2:15
33
|
44
LL | use foo::{bar}::baz
5-
| ^^ expected `;`
5+
| ^^ expected `;` here
66

77
error: aborting due to previous error
88

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
error: expected `;`, found keyword `as`
1+
error: expected `;`, found `as`
22
--> $DIR/import-from-rename.rs:3:16
33
|
44
LL | use foo::{bar} as baz;
5-
| ^^ expected `;`
5+
| ^^ expected `;` here
66

77
error: aborting due to previous error
88

src/test/ui/parser/import-glob-path.stderr

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ error: expected `;`, found `::`
22
--> $DIR/import-glob-path.rs:2:11
33
|
44
LL | use foo::*::bar
5-
| ^^ expected `;`
5+
| ^^ expected `;` here
66

77
error: aborting due to previous error
88

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
error: expected `;`, found keyword `as`
1+
error: expected `;`, found `as`
22
--> $DIR/import-glob-rename.rs:3:12
33
|
44
LL | use foo::* as baz;
5-
| ^^ expected `;`
5+
| ^^ expected `;` here
66

77
error: aborting due to previous error
88

src/test/ui/parser/issue-3036.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,5 @@
22

33
fn main()
44
{
5-
let x = 3
6-
} //~ ERROR: expected one of `.`, `;`, `?`, or an operator, found `}`
5+
let x = 3 //~ ERROR: expected `;`
6+
}

src/test/ui/parser/issue-3036.stderr

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
error: expected one of `.`, `;`, `?`, or an operator, found `}`
2-
--> $DIR/issue-3036.rs:6:1
1+
error: expected `;`, found ``}``
2+
--> $DIR/issue-3036.rs:5:14
33
|
44
LL | let x = 3
5-
| - expected one of `.`, `;`, `?`, or an operator here
5+
| ^ help: add `;` here
66
LL | }
7-
| ^ unexpected token
7+
| - unexpected token
88

99
error: aborting due to previous error
1010

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
fn main() {
22
let _: usize = ()
33
//~^ ERROR mismatched types
4+
//~| ERROR expected `;`
45
let _ = 3;
5-
//~^ ERROR expected one of
66
}
77

88
fn foo() -> usize {
99
let _: usize = ()
1010
//~^ ERROR mismatched types
11+
//~| ERROR expected `;`
1112
return 3;
12-
//~^ ERROR expected one of
1313
}

0 commit comments

Comments
 (0)