Skip to content

Commit 30ec0f4

Browse files
committed
Fix <rdar://23036383> QoI: Invalid trailing closures in stmt-conditions produce lowsy diagnostics
It is a common problem that people use a call to a function with a trailing closure in a if condition, foreach loop, etc. These don't allow trailing closures, because they are ambiguous with the brace-enclosed body of the conditional statement. In an effort to improve QoI on this, perform lookahead to disambiguate the most common form of this, in a very narrow situation. This allows us to produce a nice error with fixit hints like: t.swift:26:25: error: trailing closure requires parentheses for disambiguation in this context for _ in numbers.filter {$0 > 4} { ^ instead of spewing out this garbage: t.swift:26:26: error: anonymous closure argument not contained in a closure for _ in numbers.filter {$0 > 4} { ^ t.swift:26:33: error: consecutive statements on a line must be separated by ';' for _ in numbers.filter {$0 > 4} { ^ ; t.swift:26:34: error: statement cannot begin with a closure expression for _ in numbers.filter {$0 > 4} { ^ t.swift:26:34: note: explicitly discard the result of the closure by assigning to '_' for _ in numbers.filter {$0 > 4} { ^ _ = t.swift:26:34: error: braced block of statements is an unused closure for _ in numbers.filter {$0 > 4} { ^ t.swift:26:18: error: type '(@NoEscape (Int) throws -> Bool) throws -> [Int]' does not conform to protocol 'Sequence' for _ in numbers.filter {$0 > 4} { ^ t.swift:26:34: error: expression resolves to an unused function for _ in numbers.filter {$0 > 4} { ^
1 parent 4a126d7 commit 30ec0f4

File tree

3 files changed

+76
-3
lines changed

3 files changed

+76
-3
lines changed

Diff for: include/swift/AST/DiagnosticsParse.def

+4
Original file line numberDiff line numberDiff line change
@@ -987,6 +987,10 @@ ERROR(unexpected_tokens_before_closure_in,none,
987987
ERROR(expected_closure_rbrace,none,
988988
"expected '}' at end of closure", ())
989989

990+
ERROR(trailing_closure_requires_parens,none,
991+
"trailing closure requires parentheses for disambiguation in this"
992+
" context", ())
993+
990994
WARNING(trailing_closure_excess_newlines,none,
991995
"trailing closure is separated from call site by multiple newlines", ())
992996
NOTE(trailing_closure_call_here,none,

Diff for: lib/Parse/ParseExpr.cpp

+61-3
Original file line numberDiff line numberDiff line change
@@ -746,9 +746,66 @@ static bool isStartOfGetSetAccessor(Parser &P) {
746746
P.Tok.isContextualKeyword("willSet");
747747
}
748748

749+
/// Disambiguate and diagnose invalid uses of trailing closures in a situation
750+
/// where the parser requires an expr-basic (which does not allow them). We
751+
/// handle this by doing some lookahead in common situations and emitting a
752+
/// diagnostic with a fixit to add wrapping parens.
753+
static bool isValidTrailingClosure(bool isExprBasic, Expr *baseExpr, Parser &P){
754+
assert(P.Tok.is(tok::l_brace) && "Couldn't be a trailing closure");
755+
756+
// If this is the start of a get/set accessor, then it isn't a trailing
757+
// closure.
758+
if (isStartOfGetSetAccessor(P))
759+
return false;
760+
761+
// If this is a normal expression (not an expr-basic) then trailing closures
762+
// are allowed, so this is obviously one.
763+
// TODO: We could handle try to diambiguate cases like:
764+
// let x = foo
765+
// {...}()
766+
// by looking ahead for the ()'s, but this has been replaced by do{}, so this
767+
// probably isn't worthwhile.
768+
//
769+
if (!isExprBasic)
770+
return true;
771+
772+
// If this is an expr-basic, then a trailing closure is not allowed. However,
773+
// it is very common for someone to write something like:
774+
//
775+
// for _ in numbers.filter {$0 > 4} {
776+
//
777+
// and we want to recover from this very well. We need to perform arbitrary
778+
// look-ahead to disambiguate this case, so we only do this in the case where
779+
// the token after the { is on the same line as the {.
780+
if (P.peekToken().isAtStartOfLine())
781+
return false;
782+
783+
784+
// Determine if the {} goes with the expression by eating it, and looking
785+
// to see if it is immediately followed by another {. If so, we consider it
786+
// to be part of the proceeding expression.
787+
Parser::BacktrackingScope backtrack(P);
788+
auto startLoc = P.consumeToken(tok::l_brace);
789+
P.skipUntil(tok::r_brace);
790+
SourceLoc endLoc;
791+
if (!P.consumeIf(tok::r_brace, endLoc) ||
792+
P.Tok.isNot(tok::l_brace))
793+
return false;
794+
795+
// Diagnose the bad case and return true so that the caller parses this as a
796+
// trailing closure.
797+
P.diagnose(startLoc, diag::trailing_closure_requires_parens)
798+
.fixItInsert(baseExpr->getStartLoc(), "(")
799+
.fixItInsertAfter(endLoc, ")");
800+
return true;
801+
}
802+
803+
804+
749805
/// Map magic literal tokens such as #file to their
750806
/// MagicIdentifierLiteralExpr kind.
751-
MagicIdentifierLiteralExpr::Kind getMagicIdentifierLiteralKind(tok Kind) {
807+
static MagicIdentifierLiteralExpr::Kind
808+
getMagicIdentifierLiteralKind(tok Kind) {
752809
switch (Kind) {
753810
case tok::kw___COLUMN__:
754811
case tok::pound_column:
@@ -771,6 +828,7 @@ MagicIdentifierLiteralExpr::Kind getMagicIdentifierLiteralKind(tok Kind) {
771828
}
772829
}
773830

831+
774832
/// parseExprPostfix
775833
///
776834
/// expr-literal:
@@ -1260,8 +1318,8 @@ ParserResult<Expr> Parser::parseExprPostfix(Diag<> ID, bool isExprBasic) {
12601318
}
12611319

12621320
// Check for a trailing closure, if allowed.
1263-
if (!isExprBasic && Tok.is(tok::l_brace) &&
1264-
!isStartOfGetSetAccessor(*this)) {
1321+
if (Tok.is(tok::l_brace) &&
1322+
isValidTrailingClosure(isExprBasic, Result.get(), *this)) {
12651323
SourceLoc braceLoc = Tok.getLoc();
12661324
// Parse the closure.
12671325
ParserResult<Expr> closure = parseExprClosure();

Diff for: test/Parse/recovery.swift

+11
Original file line numberDiff line numberDiff line change
@@ -673,3 +673,14 @@ func postfixDot(a : String) {
673673
_ = a. // expected-error {{expected member name following '.'}}
674674
a. // expected-error {{expected member name following '.'}}
675675
}
676+
677+
// <rdar://problem/23036383> QoI: Invalid trailing closures in stmt-conditions produce lowsy diagnostics
678+
func r23036383(arr : [Int]?) {
679+
if let _ = arr?.map {$0+1} { // expected-error {{trailing closure requires parentheses for disambiguation in this context}} {{14-14=(}} {{29-29=)}}
680+
}
681+
682+
let numbers = [1, 2]
683+
for _ in numbers.filter {$0 > 4} { // expected-error {{trailing closure requires parentheses for disambiguation in this context}} {{12-12=(}} {{35-35=)}}
684+
}
685+
}
686+

0 commit comments

Comments
 (0)