Skip to content

Commit dc003dd

Browse files
authored
Rollup merge of #88546 - scrabsha:scrabsha/closure-missing-braces, r=estebank
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. Closes #88065. r? `@estebank`
2 parents 358a018 + b21425d commit dc003dd

File tree

7 files changed

+234
-9
lines changed

7 files changed

+234
-9
lines changed

compiler/rustc_parse/src/parser/expr.rs

+26-4
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
use super::pat::{RecoverColon, RecoverComma, PARAM_EXPECTED};
22
use super::ty::{AllowPlus, RecoverQPath, RecoverReturnSign};
3-
use super::{AttrWrapper, BlockMode, ForceCollect, Parser, PathStyle, Restrictions, TokenType};
3+
use super::{
4+
AttrWrapper, BlockMode, ClosureSpans, ForceCollect, Parser, PathStyle, Restrictions, TokenType,
5+
};
46
use super::{SemiColonMode, SeqSep, TokenExpectType, TrailingToken};
57
use crate::maybe_recover_from_interpolated_ty_qpath;
68

9+
use ast::token::DelimToken;
710
use rustc_ast::ptr::P;
811
use rustc_ast::token::{self, Token, TokenKind};
912
use rustc_ast::tokenstream::Spacing;
@@ -91,6 +94,8 @@ impl<'a> Parser<'a> {
9194
/// Parses an expression.
9295
#[inline]
9396
pub fn parse_expr(&mut self) -> PResult<'a, P<Expr>> {
97+
self.current_closure.take();
98+
9499
self.parse_expr_res(Restrictions::empty(), None)
95100
}
96101

@@ -1736,7 +1741,7 @@ impl<'a> Parser<'a> {
17361741
let capture_clause = self.parse_capture_clause()?;
17371742
let decl = self.parse_fn_block_decl()?;
17381743
let decl_hi = self.prev_token.span;
1739-
let body = match decl.output {
1744+
let mut body = match decl.output {
17401745
FnRetTy::Default(_) => {
17411746
let restrictions = self.restrictions - Restrictions::STMT_EXPR;
17421747
self.parse_expr_res(restrictions, None)?
@@ -1753,11 +1758,28 @@ impl<'a> Parser<'a> {
17531758
self.sess.gated_spans.gate(sym::async_closure, span);
17541759
}
17551760

1756-
Ok(self.mk_expr(
1761+
if self.token.kind == TokenKind::Semi && self.token_cursor.frame.delim == DelimToken::Paren
1762+
{
1763+
// It is likely that the closure body is a block but where the
1764+
// braces have been removed. We will recover and eat the next
1765+
// statements later in the parsing process.
1766+
body = self.mk_expr_err(body.span);
1767+
}
1768+
1769+
let body_span = body.span;
1770+
1771+
let closure = self.mk_expr(
17571772
lo.to(body.span),
17581773
ExprKind::Closure(capture_clause, asyncness, movability, decl, body, lo.to(decl_hi)),
17591774
attrs,
1760-
))
1775+
);
1776+
1777+
// Disable recovery for closure body
1778+
let spans =
1779+
ClosureSpans { whole_closure: closure.span, closing_pipe: decl_hi, body: body_span };
1780+
self.current_closure = Some(spans);
1781+
1782+
Ok(closure)
17611783
}
17621784

17631785
/// Parses an optional `move` prefix to a closure-like construct.

compiler/rustc_parse/src/parser/mod.rs

+98-5
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,17 @@ 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+
/// This allows us to recover when the user forget to add braces around
146+
/// multiple statements in the closure body.
147+
pub current_closure: Option<ClosureSpans>,
148+
}
149+
150+
/// Stores span informations about a closure.
151+
#[derive(Clone)]
152+
pub struct ClosureSpans {
153+
pub whole_closure: Span,
154+
pub closing_pipe: Span,
155+
pub body: Span,
145156
}
146157

147158
/// Indicates a range of tokens that should be replaced by
@@ -440,6 +451,7 @@ impl<'a> Parser<'a> {
440451
replace_ranges: Vec::new(),
441452
inner_attr_ranges: Default::default(),
442453
},
454+
current_closure: None,
443455
};
444456

445457
// Make parser point to the first token.
@@ -761,19 +773,41 @@ impl<'a> Parser<'a> {
761773
first = false;
762774
} else {
763775
match self.expect(t) {
764-
Ok(false) => {}
776+
Ok(false) => {
777+
self.current_closure.take();
778+
}
765779
Ok(true) => {
780+
self.current_closure.take();
766781
recovered = true;
767782
break;
768783
}
769784
Err(mut expect_err) => {
770785
let sp = self.prev_token.span.shrink_to_hi();
771786
let token_str = pprust::token_kind_to_string(t);
772787

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();
788+
match self.current_closure.take() {
789+
Some(closure_spans) if self.token.kind == TokenKind::Semi => {
790+
// Finding a semicolon instead of a comma
791+
// after a closure body indicates that the
792+
// closure body may be a block but the user
793+
// forgot to put braces around its
794+
// statements.
795+
796+
self.recover_missing_braces_around_closure_body(
797+
closure_spans,
798+
expect_err,
799+
)?;
800+
801+
continue;
802+
}
803+
804+
_ => {
805+
// Attempt to keep parsing if it was a similar separator.
806+
if let Some(ref tokens) = t.similar_tokens() {
807+
if tokens.contains(&self.token.kind) && !unclosed_delims {
808+
self.bump();
809+
}
810+
}
777811
}
778812
}
779813

@@ -839,6 +873,65 @@ impl<'a> Parser<'a> {
839873
Ok((v, trailing, recovered))
840874
}
841875

876+
fn recover_missing_braces_around_closure_body(
877+
&mut self,
878+
closure_spans: ClosureSpans,
879+
mut expect_err: DiagnosticBuilder<'_>,
880+
) -> PResult<'a, ()> {
881+
let initial_semicolon = self.token.span;
882+
883+
while self.eat(&TokenKind::Semi) {
884+
let _ = self.parse_stmt(ForceCollect::Yes)?;
885+
}
886+
887+
expect_err.set_primary_message(
888+
"closure bodies that contain statements must be surrounded by braces",
889+
);
890+
891+
let preceding_pipe_span = closure_spans.closing_pipe;
892+
let following_token_span = self.token.span;
893+
894+
let mut first_note = MultiSpan::from(vec![initial_semicolon]);
895+
first_note.push_span_label(
896+
initial_semicolon,
897+
"this `;` turns the preceding closure into a statement".to_string(),
898+
);
899+
first_note.push_span_label(
900+
closure_spans.body,
901+
"this expression is a statement because of the trailing semicolon".to_string(),
902+
);
903+
expect_err.span_note(first_note, "statement found outside of a block");
904+
905+
let mut second_note = MultiSpan::from(vec![closure_spans.whole_closure]);
906+
second_note.push_span_label(
907+
closure_spans.whole_closure,
908+
"this is the parsed closure...".to_string(),
909+
);
910+
second_note.push_span_label(
911+
following_token_span,
912+
"...but likely you meant the closure to end here".to_string(),
913+
);
914+
expect_err.span_note(second_note, "the closure body may be incorrectly delimited");
915+
916+
expect_err.set_span(vec![preceding_pipe_span, following_token_span]);
917+
918+
let opening_suggestion_str = " {".to_string();
919+
let closing_suggestion_str = "}".to_string();
920+
921+
expect_err.multipart_suggestion(
922+
"try adding braces",
923+
vec![
924+
(preceding_pipe_span.shrink_to_hi(), opening_suggestion_str),
925+
(following_token_span.shrink_to_lo(), closing_suggestion_str),
926+
],
927+
Applicability::MaybeIncorrect,
928+
);
929+
930+
expect_err.emit();
931+
932+
Ok(())
933+
}
934+
842935
/// Parses a sequence, not including the closing delimiter. The function
843936
/// `f` must consume tokens until reaching the next separator or
844937
/// 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 bodies 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 bodies 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,38 @@
1+
error: closure bodies 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+
note: statement found outside of a block
11+
--> $DIR/missing_braces_around_block.rs:16:26
12+
|
13+
LL | println!("{}", a);
14+
| -----------------^ this `;` turns the preceding closure into a statement
15+
| |
16+
| this expression is a statement because of the trailing semicolon
17+
note: the closure body may be incorrectly delimited
18+
--> $DIR/missing_braces_around_block.rs:14:21
19+
|
20+
LL | (1..num).reduce(|a, b|
21+
| _____________________^
22+
LL | |
23+
LL | | println!("{}", a);
24+
| |_________________________^ this is the parsed closure...
25+
LL | a * b
26+
LL | ).unwrap();
27+
| - ...but likely you meant the closure to end here
28+
help: try adding braces
29+
|
30+
LL ~ (1..num).reduce(|a, b| {
31+
LL |
32+
LL | println!("{}", a);
33+
LL | a * b
34+
LL ~ }).unwrap();
35+
|
36+
37+
error: aborting due to previous error
38+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
// Part of issue #27300.
2+
// The problem here is that ruby-style closures are parsed as blocks whose
3+
// first statement is a closure. See the issue for more details:
4+
// https://github.com/rust-lang/rust/issues/27300
5+
6+
// Note: this test represents what the compiler currently emits. The error
7+
// message will be improved later.
8+
9+
fn main() {
10+
let p = Some(45).and_then({
11+
//~^ expected a `FnOnce<({integer},)>` closure, found `Option<_>`
12+
|x| println!("doubling {}", x);
13+
Some(x * 2)
14+
//~^ ERROR: cannot find value `x` in this scope
15+
});
16+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
error[E0425]: cannot find value `x` in this scope
2+
--> $DIR/ruby_style_closure.rs:13:14
3+
|
4+
LL | Some(x * 2)
5+
| ^ not found in this scope
6+
7+
error[E0277]: expected a `FnOnce<({integer},)>` closure, found `Option<_>`
8+
--> $DIR/ruby_style_closure.rs:10:22
9+
|
10+
LL | let p = Some(45).and_then({
11+
| ^^^^^^^^ expected an `FnOnce<({integer},)>` closure, found `Option<_>`
12+
|
13+
= help: the trait `FnOnce<({integer},)>` is not implemented for `Option<_>`
14+
15+
error: aborting due to 2 previous errors
16+
17+
Some errors have detailed explanations: E0277, E0425.
18+
For more information about an error, try `rustc --explain E0277`.

0 commit comments

Comments
 (0)