-
Notifications
You must be signed in to change notification settings - Fork 10.5k
[Diagnostic] Improve diagnostic for trailing closures in statement conditions #25165
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[Diagnostic] Improve diagnostic for trailing closures in statement conditions #25165
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Welcome to the project! And thank you for working on this!
The overall strategy is good, but there're several more cases we should handle:
func id<T>(fn: () -> T) -> T { return fn() }
func any<T>(fn: () -> T) -> Any { return fn() }
func test() {
if !id { true } { return } // prefix
if id { true } == true { return } // binary LHS
if true == id { true } { return } // binary RHS
if id { true } ? true : false { return } // ternary condition
if true ? id { true } : false { return } // ternary then
if true ? true : id { false } { return } // ternary else
if id { [false,true] }[0] { return } // subscript
if id { { true } } () { return } // call
if any { true } as! Bool { return } // 'as!' casting
if let _ = any { "test" } as? Int { return } // 'as?' casting
if any { "test" } is Int { return } // 'is' casting
if let _ = id { [] as [Int]? }?.first { return } // optional chain
if id { true as Bool? }! { return } // force unwrapping
if case id { 1 } = 1 { return } // pattern
}
These all should be diagnosed as single error: trailing closure requires parentheses for disambiguation
.
lib/Parse/ParseExpr.cpp
Outdated
Parser::BacktrackingScope backtrack(P); | ||
P.consumeToken(tok::l_brace); | ||
P.skipUntil(tok::r_brace); | ||
SourceLoc endLoc; | ||
if (!P.consumeIf(tok::r_brace, endLoc) || | ||
P.Tok.isNot(tok::l_brace, tok::kw_where, tok::comma)) { | ||
P.Tok.isNot(tok::l_brace, tok::kw_where, tok::comma, tok::period)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This isn't enough. For example, the .
in foo { $0 + 1 } .bar()
is tok::period_prefix
.
return { true, E }; | ||
} else if (isa<DotSyntaxCallExpr>(E)) { | ||
return { true, E }; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To handle binary expressions et al., I think we should maintain "not walk into" list, instead of walk into specific expression kinds.
I mean, currently we walk into only CallExpr
(and DotSyntaxCallExpr
). But I think we should walk into except ParenExpr
, TupleExpr
, ArrayExpr
, DictionaryExpr
, etc.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I ended up adopting a solution like this, my one concern is that a future new expression kind might cause a false positive to be diagnosed if it isn't included in the 'skipped' list. This should be mitigated by the fact that isValidTrailingClosure() still whitelists the individual tokens which may follow a closing brace for something to be considered a possibly valid trailing closure. It doesn't completely eliminate the possibility of future breakage though.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it's OK. False positive is better than false negative in this case. The user can easily workaround it by not using trailing closure (or enclose whole expression into parentheses).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
…as long as we don't break source compat.
8fe5950
to
c8f59ae
Compare
Thanks for the feedback @rintaro , I think I've addressed most of the issues with this PR. It's working correctly in all of the cases I've tested so far, but I think it would be a good idea to run this through the source compatibility suite as well since the scope has expanded quite a bit. The new changes do slightly hurt recovery in the case where a user incorrectly writes an array using c-style postfix brackets enclosing an expression with a trailing closure (see the test case I modified in recovery.swift). The error message in that case was already pretty poor though so I think it's probably a worthwhile tradeoff. |
Actually, this isn't quite ready yet, sorry, found one more test failure I missed earlier I need to take a look at |
cf27a17
to
4dc7e4a
Compare
@rintaro I think I've fixed the remaining regressions in this PR, it's passing all the tests for me locally now. Do you mind taking a look/running the test suite? |
lib/Sema/MiscDiagnostics.cpp
Outdated
// expression, don't diagnose it as there is no ambiguity | ||
static const std::set<ExprKind> skippedExpressionKinds{ | ||
ExprKind::Paren, ExprKind::Tuple, ExprKind::Array, | ||
ExprKind::Dictionary, ExprKind::InterpolatedStringLiteral}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Instead of using std::set
, please use switch
for this.
switch (E->getKind()) {
case ExprKind::Paren:
case ExprKind::Tuple:
case ExprKind::Array:
case ExprKind::Dictionary:
case ExprKind::InterpolatedStringLiteral:
// If a trailing closure appears as a child of one of these types of
// expression, don't diagnose it as there is no ambiguity.
return { E->isImplicit(), E };
case ExprKind::Call
diagnoseIt(CE)
break;
default;
break;
}
return { true, E };
lib/Parse/ParseExpr.cpp
Outdated
tok::kw_as, tok::question_postfix, tok::question_infix, | ||
tok::exclaim_postfix, tok::colon, tok::equal, | ||
tok::oper_postfix, tok::oper_binary_spaced, | ||
tok::oper_binary_unspaced)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you insert isAstStartOfLine()
check for newly added token kinds?
For example, the following example doesn't look like subscript calling.
if foo { something() }
[1, 2, 3].foo()
Like:
switch (Tok.getKind()) {
case tok::l_brace:
case tok::kw_where:
case tok::comma:
return true;
case tok::l_brace:
case tok::l_square:
case ...:
return !Tok.isAtStartOfLine();
default:
return false;
}
28ad977
to
9189c9b
Compare
Thanks again for walking me through this @rintaro , I definitely underestimated the complexity of implementing some of these improvements! I pushed another update with some fixes and added tests, and will add more cases if I think of any. |
@swift-ci Please Test Source Compatibility |
@swift-ci Please smoke test |
Thank you! I think this is good to go. |
hmm, not really sure what to make of the source compatibility failures, but it looks the exact same set of failures is present in a few other runs (like https://ci.swift.org/job/swift-PR-source-compat-suite-debug/1283/console) as well, so I don't think this PR causes any new failures. |
default: | ||
break; | ||
} | ||
return {true, E}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This part is a change in behavior; is that intentional?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes. The intent is to always walk into an expression, unless it's a non-implicit parent/tuple/etc. expression.
test/expr/closure/trailing.swift
Outdated
|
||
if (id { true }) { return } // OK | ||
|
||
if true { } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since true
is a keyword, I'm not sure this is testing exactly what you want to test. Maybe do a let myTrue = true
above?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oops, yeah that's a copy-paste error, 🤦♂. I updated the test
…nditions This improves the diagnostic for trailing closures in statement conditions to catch cases where one or more trailing closures are used in a chain composed of multiple calls
9189c9b
to
7785b9d
Compare
@swift-ci Please smoke test |
Just wanted to bump this, lost track of the PR for awhile, sorry about that! Looks like it should still merge cleanly |
@swift-ci Please smoke test |
Whoops, guess we lost track of it too. Restarted tests just in case. |
Thanks, @owenv! |
*sigh* We found a case where this is source-breaking, so I'm going to have to revert it for now:
|
Oh, interesting. Thanks for catching this! I’ll take a look at this soon and see if it’s fixable |
It Would Be Nice™ to still issue the diagnostic, but as a warning rather than an error in cases where it might have previously been accepted. One way to do that is to always issue it as a warning rather than an error, since anything that was previously an error will still be diagnosed and anything that previously slipped through inconsistently will only get a warning. |
… in statement conditions (swiftlang#25165)"" This reverts commit e3a6b67.
This improves the diagnostic for trailing closures in statement conditions to catch cases where one or more trailing closures are used within a chain composed of multiple calls (this is currently only diagnosed if the trailing closure appears at the very end)
For example:
Used to result in these errors:
but now can be diagnosed as follows:
Resolves SR8736