Skip to content

Commit 1f5568d

Browse files
author
Sasha Pourcelot
committed
Emit proper errors when on missing closure braces
This commit focuses on emitting clean errors for the following syntax error: ``` Some(42).map(|a| dbg!(a); a ); ``` Previous implementation tried to recover after parsing the closure body (the `dbg` expression) by replacing the next `;` with a `,`, which made the next expression belong to the next function argument. As such, the following errors were emitted (among others): - the semicolon token was not expected, - a is not in scope, - Option::map is supposed to take one argument, not two. This commit allows us to gracefully handle this situation by adding giving the parser the ability to remember when it has just parsed a closure body inside a function call. When this happens, we can treat the unexpected `;` specifically and try to parse as much statements as possible in order to eat the whole block. When we can't parse statements anymore, we generate a clean error indicating that the braces are missing, and return an ExprKind::Err.
1 parent 0a84708 commit 1f5568d

File tree

5 files changed

+154
-5
lines changed

5 files changed

+154
-5
lines changed

compiler/rustc_parse/src/parser/expr.rs

+15
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ use super::{AttrWrapper, BlockMode, ForceCollect, Parser, PathStyle, Restriction
44
use super::{SemiColonMode, SeqSep, TokenExpectType, TrailingToken};
55
use crate::maybe_recover_from_interpolated_ty_qpath;
66

7+
use ast::token::DelimToken;
78
use rustc_ast::ptr::P;
89
use rustc_ast::token::{self, Token, TokenKind};
910
use rustc_ast::tokenstream::Spacing;
@@ -91,6 +92,8 @@ impl<'a> Parser<'a> {
9192
/// Parses an expression.
9293
#[inline]
9394
pub fn parse_expr(&mut self) -> PResult<'a, P<Expr>> {
95+
self.last_closure_body.take();
96+
9497
self.parse_expr_res(Restrictions::empty(), None)
9598
}
9699

@@ -1733,6 +1736,18 @@ impl<'a> Parser<'a> {
17331736
self.sess.gated_spans.gate(sym::async_closure, span);
17341737
}
17351738

1739+
// Disable recovery for closure body
1740+
self.last_closure_body = Some(decl_hi);
1741+
1742+
if self.token.kind == TokenKind::Semi && self.token_cursor.frame.delim == DelimToken::Paren
1743+
{
1744+
// It is likely that the closure body is a block but where the
1745+
// braces have been removed. We will recover and eat the next
1746+
// statements later in the parsing process.
1747+
1748+
return Ok(self.mk_expr_err(lo.to(body.span)));
1749+
}
1750+
17361751
Ok(self.mk_expr(
17371752
lo.to(body.span),
17381753
ExprKind::Closure(capture_clause, asyncness, movability, decl, body, lo.to(decl_hi)),

compiler/rustc_parse/src/parser/mod.rs

+76-5
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,10 @@ pub struct Parser<'a> {
142142
/// If present, this `Parser` is not parsing Rust code but rather a macro call.
143143
subparser_name: Option<&'static str>,
144144
capture_state: CaptureState,
145+
146+
/// This allows us to recover when the user forget to add braces around
147+
/// multiple statements in the closure body.
148+
pub last_closure_body: Option<Span /* The closing `|` of the closure declarator. */>,
145149
}
146150

147151
/// Indicates a range of tokens that should be replaced by
@@ -440,6 +444,7 @@ impl<'a> Parser<'a> {
440444
replace_ranges: Vec::new(),
441445
inner_attr_ranges: Default::default(),
442446
},
447+
last_closure_body: None,
443448
};
444449

445450
// Make parser point to the first token.
@@ -761,20 +766,44 @@ impl<'a> Parser<'a> {
761766
first = false;
762767
} else {
763768
match self.expect(t) {
764-
Ok(false) => {}
769+
Ok(false) => {
770+
self.last_closure_body.take();
771+
}
765772
Ok(true) => {
773+
self.last_closure_body.take();
766774
recovered = true;
767775
break;
768776
}
769777
Err(mut expect_err) => {
770778
let sp = self.prev_token.span.shrink_to_hi();
771779
let token_str = pprust::token_kind_to_string(t);
772780

773-
// Attempt to keep parsing if it was a similar separator.
774-
if let Some(ref tokens) = t.similar_tokens() {
775-
if tokens.contains(&self.token.kind) && !unclosed_delims {
776-
self.bump();
781+
match self.last_closure_body.take() {
782+
None => {
783+
// Attempt to keep parsing if it was a similar separator.
784+
if let Some(ref tokens) = t.similar_tokens() {
785+
if tokens.contains(&self.token.kind) && !unclosed_delims {
786+
self.bump();
787+
}
788+
}
789+
}
790+
791+
Some(right_pipe_span) if self.token.kind == TokenKind::Semi => {
792+
// Finding a semicolon instead of a comma
793+
// after a closure body indicates that the
794+
// closure body may be a block but the user
795+
// forgot to put braces around its
796+
// statements.
797+
798+
self.recover_missing_braces_around_closure_body(
799+
right_pipe_span,
800+
expect_err,
801+
)?;
802+
803+
continue;
777804
}
805+
806+
_ => {}
778807
}
779808

780809
// If this was a missing `@` in a binding pattern
@@ -839,6 +868,48 @@ impl<'a> Parser<'a> {
839868
Ok((v, trailing, recovered))
840869
}
841870

871+
fn recover_missing_braces_around_closure_body(
872+
&mut self,
873+
right_pipe_span: Span,
874+
mut expect_err: DiagnosticBuilder<'_>,
875+
) -> PResult<'a, ()> {
876+
let initial_semicolon = self.token.span;
877+
878+
expect_err.span_help(
879+
initial_semicolon,
880+
"This `;` turns the expression into a statement, which must be placed in a block",
881+
);
882+
883+
while self.eat(&TokenKind::Semi) {
884+
let _ = self.parse_stmt(ForceCollect::Yes)?;
885+
}
886+
887+
expect_err.set_primary_message(
888+
"closure body that contain statements must be surrounded by braces",
889+
);
890+
891+
let preceding_pipe_span = right_pipe_span;
892+
let following_token_span = self.token.span;
893+
894+
expect_err.set_span(vec![preceding_pipe_span, following_token_span]);
895+
896+
let opening_suggestion_str = " {".to_string();
897+
let closing_suggestion_str = "}".to_string();
898+
899+
expect_err.multipart_suggestion(
900+
"try adding braces",
901+
vec![
902+
(preceding_pipe_span.shrink_to_hi(), opening_suggestion_str),
903+
(following_token_span.shrink_to_lo(), closing_suggestion_str),
904+
],
905+
Applicability::MaybeIncorrect,
906+
);
907+
908+
expect_err.emit();
909+
910+
Ok(())
911+
}
912+
842913
/// Parses a sequence, not including the closing delimiter. The function
843914
/// `f` must consume tokens until reaching the next separator or
844915
/// closing bracket.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
// This snippet ensures that no attempt to recover on a semicolon instead of
2+
// comma is made next to a closure body.
3+
//
4+
// If this recovery happens, then plenty of errors are emitted. Here, we expect
5+
// only one error.
6+
//
7+
// This is part of issue #88065:
8+
// https://github.com/rust-lang/rust/issues/88065
9+
10+
// run-rustfix
11+
12+
fn main() {
13+
let num = 5;
14+
(1..num).reduce(|a, b| {
15+
//~^ ERROR: closure body that contain statements must be surrounded by braces
16+
println!("{}", a);
17+
a * b
18+
}).unwrap();
19+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
// This snippet ensures that no attempt to recover on a semicolon instead of
2+
// comma is made next to a closure body.
3+
//
4+
// If this recovery happens, then plenty of errors are emitted. Here, we expect
5+
// only one error.
6+
//
7+
// This is part of issue #88065:
8+
// https://github.com/rust-lang/rust/issues/88065
9+
10+
// run-rustfix
11+
12+
fn main() {
13+
let num = 5;
14+
(1..num).reduce(|a, b|
15+
//~^ ERROR: closure body that contain statements must be surrounded by braces
16+
println!("{}", a);
17+
a * b
18+
).unwrap();
19+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
error: closure body that contain statements must be surrounded by braces
2+
--> $DIR/missing_braces_around_block.rs:14:26
3+
|
4+
LL | (1..num).reduce(|a, b|
5+
| ^
6+
...
7+
LL | ).unwrap();
8+
| ^
9+
|
10+
help: This `;` turns the expression into a statement, which must be placed in a block
11+
--> $DIR/missing_braces_around_block.rs:16:26
12+
|
13+
LL | println!("{}", a);
14+
| ^
15+
help: try adding braces
16+
|
17+
LL ~ (1..num).reduce(|a, b| {
18+
LL |
19+
LL | println!("{}", a);
20+
LL | a * b
21+
LL ~ }).unwrap();
22+
|
23+
24+
error: aborting due to previous error
25+

0 commit comments

Comments
 (0)