Skip to content

[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

Merged

Conversation

owenv
Copy link
Contributor

@owenv owenv commented May 31, 2019

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:

let x: [Int]? = nil
if let sumOfSquares = x?.map{$0*$0}.reduce(0, +) {
  print(sumOfSquares)
}

Used to result in these errors:

error: anonymous closure argument not contained in a closure
if let sumOfSquares = x?.map{$0*$0}.reduce(0, +) {
                             ^
error: anonymous closure argument not contained in a closure
if let sumOfSquares = x?.map{$0*$0}.reduce(0, +) {
                                ^
error: consecutive statements on a line must be separated by ';'
if let sumOfSquares = x?.map{$0*$0}.reduce(0, +) {
                                   ^
                                   ;
error: expression type '(((Int) throws -> _) throws -> [_])?' is ambiguous without more context
if let sumOfSquares = x?.map{$0*$0}.reduce(0, +) {
                      ~~~^~~
error: use of unresolved identifier 'sumOfSquares'
  print(sumOfSquares)
        ^~~~~~~~~~~~

but now can be diagnosed as follows:

error: trailing closure requires parentheses for disambiguation in this context
if let sumOfSquares = x?.map{$0*$0}.reduce(0, +) {
                            ^
                            (      )

Resolves SR8736

@jrose-apple jrose-apple requested a review from rintaro May 31, 2019 17:14
Copy link
Member

@rintaro rintaro left a 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.

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)) {
Copy link
Member

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 };
}
Copy link
Member

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.

Copy link
Contributor Author

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.

Copy link
Member

@rintaro rintaro Jun 7, 2019

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).

Copy link
Contributor

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.

@owenv owenv force-pushed the improve_trailing_closure_in_condition_diag branch 2 times, most recently from 8fe5950 to c8f59ae Compare June 5, 2019 04:47
@owenv
Copy link
Contributor Author

owenv commented Jun 5, 2019

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.

@owenv
Copy link
Contributor Author

owenv commented Jun 5, 2019

Actually, this isn't quite ready yet, sorry, found one more test failure I missed earlier I need to take a look at

@owenv owenv force-pushed the improve_trailing_closure_in_condition_diag branch 2 times, most recently from cf27a17 to 4dc7e4a Compare June 7, 2019 02:15
@owenv
Copy link
Contributor Author

owenv commented Jun 7, 2019

@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?

// 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};
Copy link
Member

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 };

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)) {
Copy link
Member

@rintaro rintaro Jun 7, 2019

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;
}

@owenv owenv force-pushed the improve_trailing_closure_in_condition_diag branch 2 times, most recently from 28ad977 to 9189c9b Compare June 8, 2019 18:18
@owenv
Copy link
Contributor Author

owenv commented Jun 8, 2019

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.

@rintaro
Copy link
Member

rintaro commented Jun 10, 2019

@swift-ci Please Test Source Compatibility

@rintaro
Copy link
Member

rintaro commented Jun 10, 2019

@swift-ci Please smoke test

@rintaro
Copy link
Member

rintaro commented Jun 10, 2019

Thank you! I think this is good to go.
@jrose-apple Do you have any concern?

@owenv
Copy link
Contributor Author

owenv commented Jun 11, 2019

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};
Copy link
Contributor

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?

Copy link
Contributor Author

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.


if (id { true }) { return } // OK

if true { }
Copy link
Contributor

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?

Copy link
Contributor Author

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
@owenv owenv force-pushed the improve_trailing_closure_in_condition_diag branch from 9189c9b to 7785b9d Compare June 11, 2019 03:37
@jrose-apple
Copy link
Contributor

@swift-ci Please smoke test

@owenv
Copy link
Contributor Author

owenv commented Jun 27, 2019

Just wanted to bump this, lost track of the PR for awhile, sorry about that! Looks like it should still merge cleanly

@jrose-apple
Copy link
Contributor

@swift-ci Please smoke test

@jrose-apple
Copy link
Contributor

Whoops, guess we lost track of it too. Restarted tests just in case.

@jrose-apple jrose-apple self-assigned this Jun 27, 2019
@jrose-apple jrose-apple merged commit 4da49fc into swiftlang:master Jun 28, 2019
@jrose-apple
Copy link
Contributor

Thanks, @owenv!

@jrose-apple
Copy link
Contributor

jrose-apple commented Jun 28, 2019

*sigh* We found a case where this is source-breaking, so I'm going to have to revert it for now:

let array = [1, 2, 3]
if true != array.contains { $0 > 0 } {}

jrose-apple added a commit that referenced this pull request Jun 28, 2019
@owenv
Copy link
Contributor Author

owenv commented Jun 28, 2019

Oh, interesting. Thanks for catching this! I’ll take a look at this soon and see if it’s fixable

@jrose-apple
Copy link
Contributor

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants