Skip to content

Commit 4bf1c34

Browse files
authored
[Parse/Sema][SR-1672] Improve diagnostics for trailing closures in stmt-condition (#3184)
Fix-it suggests normal argument expression, instead of of enclosing whole expression with parens. * Moved diagnostic logic to Sema, because we have to add correct argument label for the closure. if arr.starts(with: IDs) { $0.id == $2 } { ... } ~~^ , isEquivalent: ) * We now accept trailing closures for each expressions and right before `where` clause, as well as closures right before the body. if let _ = maybeInt { 1 }, someOtherCondition { ... } ~^ ( ) for let x in arr.map { $0 * 4 } where x != 0 { ... } ~^ ( )
1 parent cccfbf4 commit 4bf1c34

File tree

6 files changed

+201
-29
lines changed

6 files changed

+201
-29
lines changed

include/swift/AST/DiagnosticsParse.def

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -998,10 +998,6 @@ ERROR(unexpected_tokens_before_closure_in,none,
998998
ERROR(expected_closure_rbrace,none,
999999
"expected '}' at end of closure", ())
10001000

1001-
ERROR(trailing_closure_requires_parens,none,
1002-
"trailing closure requires parentheses for disambiguation in this"
1003-
" context", ())
1004-
10051001
WARNING(trailing_closure_excess_newlines,none,
10061002
"trailing closure is separated from call site by multiple newlines", ())
10071003
NOTE(trailing_closure_call_here,none,

include/swift/AST/DiagnosticsSema.def

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2342,6 +2342,10 @@ NOTE(add_where_newline, none,
23422342
NOTE(duplicate_where, none,
23432343
"duplicate the 'where' on both patterns to check both patterns",())
23442344

2345+
ERROR(trailing_closure_requires_parens,none,
2346+
"trailing closure requires parentheses for disambiguation in this"
2347+
" context", ())
2348+
23452349
//------------------------------------------------------------------------------
23462350
// Type Check Patterns
23472351
//------------------------------------------------------------------------------

lib/Parse/ParseExpr.cpp

Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -919,10 +919,10 @@ static bool isStartOfGetSetAccessor(Parser &P) {
919919
P.Tok.isContextualKeyword("willSet");
920920
}
921921

922-
/// Disambiguate and diagnose invalid uses of trailing closures in a situation
922+
/// Recover invalid uses of trailing closures in a situation
923923
/// where the parser requires an expr-basic (which does not allow them). We
924-
/// handle this by doing some lookahead in common situations and emitting a
925-
/// diagnostic with a fixit to add wrapping parens.
924+
/// handle this by doing some lookahead in common situations. And later, Sema
925+
/// will emit a diagnostic with a fixit to add wrapping parens.
926926
static bool isValidTrailingClosure(bool isExprBasic, Expr *baseExpr, Parser &P){
927927
assert(P.Tok.is(tok::l_brace) && "Couldn't be a trailing closure");
928928

@@ -955,21 +955,19 @@ static bool isValidTrailingClosure(bool isExprBasic, Expr *baseExpr, Parser &P){
955955

956956

957957
// Determine if the {} goes with the expression by eating it, and looking
958-
// to see if it is immediately followed by another {. If so, we consider it
959-
// to be part of the proceeding expression.
958+
// to see if it is immediately followed by '{', 'where', or comma. If so,
959+
// we consider it to be part of the proceeding expression.
960960
Parser::BacktrackingScope backtrack(P);
961-
auto startLoc = P.consumeToken(tok::l_brace);
961+
P.consumeToken(tok::l_brace);
962962
P.skipUntil(tok::r_brace);
963963
SourceLoc endLoc;
964964
if (!P.consumeIf(tok::r_brace, endLoc) ||
965-
P.Tok.isNot(tok::l_brace))
965+
P.Tok.isNot(tok::l_brace, tok::kw_where, tok::comma)) {
966966
return false;
967-
968-
// Diagnose the bad case and return true so that the caller parses this as a
969-
// trailing closure.
970-
P.diagnose(startLoc, diag::trailing_closure_requires_parens)
971-
.fixItInsert(baseExpr->getStartLoc(), "(")
972-
.fixItInsertAfter(endLoc, ")");
967+
}
968+
969+
// Recoverable case. Just return true here and Sema will emit a diagnostic
970+
// later. see: Sema/MiscDiagnostics.cpp#checkStmtConditionTrailingClosure
973971
return true;
974972
}
975973

lib/Sema/MiscDiagnostics.cpp

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2784,6 +2784,120 @@ static void checkSwitch(TypeChecker &TC, const SwitchStmt *stmt) {
27842784
}
27852785
}
27862786

2787+
// Perform checkStmtConditionTrailingClosure for single expression.
2788+
static void checkStmtConditionTrailingClosure(TypeChecker &TC, const Expr *E) {
2789+
if (E == nullptr || isa<ErrorExpr>(E)) return;
2790+
2791+
// Shallow walker. just dig into implicit expression.
2792+
class DiagnoseWalker : public ASTWalker {
2793+
TypeChecker &TC;
2794+
2795+
void diagnoseIt(const CallExpr *E) {
2796+
auto argsExpr = E->getArg();
2797+
auto argsTy = argsExpr->getType();
2798+
2799+
// Ignore invalid argument type. Some diagnostics are already emitted.
2800+
if (!argsTy || argsTy->is<ErrorType>()) return;
2801+
2802+
if (auto TSE = dyn_cast<TupleShuffleExpr>(argsExpr))
2803+
argsExpr = TSE->getSubExpr();
2804+
2805+
SmallString<16> replacement;
2806+
SourceLoc lastLoc;
2807+
SourceRange closureRange;
2808+
if (auto PE = dyn_cast<ParenExpr>(argsExpr)) {
2809+
// Ignore non-trailing-closure.
2810+
if (!PE->hasTrailingClosure()) return;
2811+
2812+
closureRange = PE->getSubExpr()->getSourceRange();
2813+
lastLoc = PE->getLParenLoc();
2814+
if (lastLoc.isValid()) {
2815+
// Empty paren: e.g. if funcName() { 1 } { ... }
2816+
replacement = "";
2817+
} else {
2818+
// Bare trailing closure: e.g. if funcName { 1 } { ... }
2819+
replacement = "(";
2820+
lastLoc = E->getFn()->getEndLoc();
2821+
}
2822+
} else if (auto TE = dyn_cast<TupleExpr>(argsExpr)) {
2823+
// Ignore non-trailing-closure.
2824+
if (!TE->hasTrailingClosure()) return;
2825+
2826+
// Tuple + trailing closure: e.g. if funcName(x: 1) { 1 } { ... }
2827+
auto numElements = TE->getNumElements();
2828+
assert(numElements >= 2 && "Unexpected num of elements in TupleExpr");
2829+
closureRange = TE->getElement(numElements - 1)->getSourceRange();
2830+
lastLoc = TE->getElement(numElements - 2)->getEndLoc();
2831+
replacement = ", ";
2832+
} else {
2833+
// Can't be here.
2834+
return;
2835+
}
2836+
2837+
// Add argument label of the closure that is going to be enclosed in
2838+
// parens.
2839+
if (auto TT = argsTy->getAs<TupleType>()) {
2840+
assert(TT->getNumElements() != 0 && "Unexpected empty TupleType");
2841+
auto closureLabel = TT->getElement(TT->getNumElements() - 1).getName();
2842+
if(!closureLabel.empty()) {
2843+
replacement += closureLabel.str();
2844+
replacement += ": ";
2845+
}
2846+
}
2847+
2848+
// Emit diagnostics.
2849+
lastLoc = Lexer::getLocForEndOfToken(TC.Context.SourceMgr, lastLoc);
2850+
TC.diagnose(closureRange.Start, diag::trailing_closure_requires_parens)
2851+
.fixItReplaceChars(lastLoc, closureRange.Start, replacement)
2852+
.fixItInsertAfter(closureRange.End, ")");
2853+
}
2854+
2855+
public:
2856+
DiagnoseWalker(TypeChecker &tc) : TC(tc) { }
2857+
2858+
virtual std::pair<bool, Expr *> walkToExprPre(Expr *E) override {
2859+
// Dig into implict expression.
2860+
if (E->isImplicit()) return { true, E };
2861+
// Diagnose call expression.
2862+
if (auto CE = dyn_cast<CallExpr>(E))
2863+
diagnoseIt(CE);
2864+
// Don't dig any further.
2865+
return { false, E };
2866+
}
2867+
};
2868+
2869+
DiagnoseWalker Walker(TC);
2870+
const_cast<Expr *>(E)->walk(Walker);
2871+
}
2872+
2873+
/// \brief Diagnose trailing closure in statement-conditions.
2874+
///
2875+
/// Conditional statements, including 'for' or `switch` doesn't allow ambiguous
2876+
/// trailing closures in these conditions part. Even if the parser can recover
2877+
/// them, we force them to disambiguate.
2878+
//
2879+
/// E.g.:
2880+
/// if let _ = arr?.map {$0+1} { ... }
2881+
/// for _ in numbers.filter {$0 > 4} { ... }
2882+
static void checkStmtConditionTrailingClosure(TypeChecker &TC, const Stmt *S) {
2883+
if (auto LCS = dyn_cast<LabeledConditionalStmt>(S)) {
2884+
for (auto elt : LCS->getCond()) {
2885+
if (elt.getKind() == StmtConditionElement::CK_PatternBinding)
2886+
checkStmtConditionTrailingClosure(TC, elt.getInitializer());
2887+
else if (elt.getKind() == StmtConditionElement::CK_Boolean)
2888+
checkStmtConditionTrailingClosure(TC, elt.getBoolean());
2889+
// No trailing closure for CK_Availability: e.g. `if #available() {}`.
2890+
}
2891+
} else if (auto SS = dyn_cast<SwitchStmt>(S)) {
2892+
checkStmtConditionTrailingClosure(TC, SS->getSubjectExpr());
2893+
} else if (auto FES = dyn_cast<ForEachStmt>(S)) {
2894+
checkStmtConditionTrailingClosure(TC, FES->getSequence());
2895+
checkStmtConditionTrailingClosure(TC, FES->getWhere());
2896+
} else if (auto DCS = dyn_cast<DoCatchStmt>(S)) {
2897+
for (auto CS : DCS->getCatches())
2898+
checkStmtConditionTrailingClosure(TC, CS->getGuardExpr());
2899+
}
2900+
}
27872901

27882902
static Optional<ObjCSelector>
27892903
parseObjCSelector(ASTContext &ctx, StringRef string) {
@@ -3222,6 +3336,8 @@ void swift::performStmtDiagnostics(TypeChecker &TC, const Stmt *S) {
32223336

32233337
if (auto switchStmt = dyn_cast<SwitchStmt>(S))
32243338
checkSwitch(TC, switchStmt);
3339+
3340+
checkStmtConditionTrailingClosure(TC, S);
32253341
}
32263342

32273343
//===----------------------------------------------------------------------===//

test/Parse/recovery.swift

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -652,19 +652,7 @@ func postfixDot(a : String) {
652652
a. // expected-error {{expected member name following '.'}}
653653
}
654654

655-
// <rdar://problem/23036383> QoI: Invalid trailing closures in stmt-conditions produce lowsy diagnostics
656-
func r23036383(arr : [Int]?) {
657-
if let _ = arr?.map {$0+1} { // expected-error {{trailing closure requires parentheses for disambiguation in this context}} {{14-14=(}} {{29-29=)}}
658-
}
659-
660-
let numbers = [1, 2]
661-
for _ in numbers.filter {$0 > 4} { // expected-error {{trailing closure requires parentheses for disambiguation in this context}} {{12-12=(}} {{35-35=)}}
662-
}
663-
}
664-
665655
// <rdar://problem/22290244> QoI: "UIColor." gives two issues, should only give one
666656
func f() {
667657
_ = ClassWithStaticDecls. // expected-error {{expected member name following '.'}}
668658
}
669-
670-

test/expr/closure/trailing.swift

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,3 +110,73 @@ let someInt = 0
110110
let intArray = [someInt]
111111
limitXY(someInt, toGamut: intArray) {} // expected-error {{extra argument 'toGamut' in call}}
112112

113+
114+
// <rdar://problem/23036383> QoI: Invalid trailing closures in stmt-conditions produce lowsy diagnostics
115+
func retBool(x: () -> Int) -> Bool {}
116+
func maybeInt(_: () -> Int) -> Int? {}
117+
class Foo23036383 {
118+
init() {}
119+
func map(_: (Int) -> Int) -> Int? {}
120+
func meth1(x: Int, _: () -> Int) -> Bool {}
121+
func meth2(_: Int, y: () -> Int) -> Bool {}
122+
func filter(by: (Int) -> Bool) -> [Int] {}
123+
}
124+
enum MyErr : ErrorProtocol {
125+
case A
126+
}
127+
128+
func r23036383(foo: Foo23036383?, obj: Foo23036383) {
129+
130+
if retBool(x: { 1 }) { } // OK
131+
if (retBool { 1 }) { } // OK
132+
133+
if retBool{ 1 } { // expected-error {{trailing closure requires parentheses for disambiguation in this context}} {{13-13=(x: }} {{18-18=)}}
134+
}
135+
if retBool { 1 } { // expected-error {{trailing closure requires parentheses for disambiguation in this context}} {{13-14=(x: }} {{19-19=)}}
136+
}
137+
if retBool() { 1 } { // expected-error {{trailing closure requires parentheses for disambiguation in this context}} {{14-16=x: }} {{21-21=)}}
138+
} else if retBool( ) { 0 } { // expected-error {{trailing closure requires parentheses for disambiguation in this context}} {{21-24=x: }} {{29-29=)}}
139+
}
140+
141+
if let _ = maybeInt { 1 } { // expected-error {{trailing closure requires parentheses for disambiguation in this context}} {{22-23=(}} {{28-28=)}}
142+
}
143+
if let _ = maybeInt { 1 } , true { // expected-error {{trailing closure requires parentheses for disambiguation in this context}} {{22-23=(}} {{28-28=)}}
144+
}
145+
146+
if let _ = foo?.map {$0+1} { // expected-error {{trailing closure requires parentheses for disambiguation in this context}} {{22-23=(}} {{29-29=)}}
147+
}
148+
if let _ = foo?.map() {$0+1} { // expected-error {{trailing closure requires parentheses for disambiguation in this context}} {{23-25=}} {{31-31=)}}
149+
}
150+
if let _ = foo, retBool { 1 } { // expected-error {{trailing closure requires parentheses for disambiguation in this context}} {{26-27=(x: }} {{32-32=)}}
151+
}
152+
153+
if obj.meth1(x: 1) { 0 } { // expected-error {{trailing closure requires parentheses for disambiguation in this context}} {{20-22=, }} {{27-27=)}}
154+
}
155+
if obj.meth2(1) { 0 } { // expected-error {{trailing closure requires parentheses for disambiguation in this context}} {{17-19=, y: }} {{24-24=)}}
156+
}
157+
158+
for _ in obj.filter {$0 > 4} { // expected-error {{trailing closure requires parentheses for disambiguation in this context}} {{22-23=(by: }} {{31-31=)}}
159+
}
160+
for _ in obj.filter {$0 > 4} where true { // expected-error {{trailing closure requires parentheses for disambiguation in this context}} {{22-23=(by: }} {{31-31=)}}
161+
}
162+
for _ in [1,2] where retBool { 1 } { // expected-error {{trailing closure requires parentheses for disambiguation in this context}} {{31-32=(x: }} {{37-37=)}}
163+
}
164+
165+
while retBool { 1 } { // expected-error {{trailing closure requires parentheses for disambiguation in this context}} {{16-17=(x: }} {{22-22=)}}
166+
}
167+
while let _ = foo, retBool { 1 } { // expected-error {{trailing closure requires parentheses for disambiguation in this context}} {{29-30=(x: }} {{35-35=)}}
168+
}
169+
170+
switch retBool { return 1 } { // expected-error {{trailing closure requires parentheses for disambiguation in this context}} {{17-18=(x: }} {{30-30=)}}
171+
default: break
172+
}
173+
174+
do {
175+
throw MyErr.A;
176+
} catch MyErr.A where retBool { 1 } { // expected-error {{trailing closure requires parentheses for disambiguation in this context}} {{32-33=(x: }} {{38-38=)}}
177+
} catch { }
178+
179+
if let _ = maybeInt { 1 }, retBool { 1 } { }
180+
// expected-error@-1 {{trailing closure requires parentheses for disambiguation in this context}} {{22-23=(}} {{28-28=)}}
181+
// expected-error@-2 {{trailing closure requires parentheses for disambiguation in this context}} {{37-38=(x: }} {{43-43=)}}
182+
}

0 commit comments

Comments
 (0)