[Async Refactoring] Split ambiguous (error + success) completion handler calls into success and error case

Previously, when a completion handler call had arguments for both the success and error parameters, we were always interpreting it as an error call. In case the error argument was an `Optional`, this could cause us to generate code that didn't compile, because we `throw`ed the `Error?` or passed it to `continuation.resume(throwing)`, both of which didn't work.

We now generate an `if let` statement that checks if the error is `nil`. If it is, the error is thrown. Otherwise, we interpret the call as a success call and return the result.

- Example 1 (convert to continuation)
-- Base Code
```swift
func test(completionHandler: (Int?, Error?) -> Void) {
  withoutAsyncAlternativeThrowing { (theValue, theError) in
    completionHandler(theValue, theError)
  }
}
```
-- Old Refactoring Result
```swift
func test() async throws -> Int {
  return try await withCheckedThrowingContinuation { continuation in
    withoutAsyncAlternativeThrowing { (theValue, theError) in
      continuation.resume(throwing: theError) // error: Argument type 'Error?' does not conform to expected type 'Error'
    }
  }
}
-- New Refactoring Result
```swift
func testThrowingContinuationRelayingErrorAndResult() async throws -> Int {
  return try await withCheckedThrowingContinuation { continuation in
    withoutAsyncAlternativeThrowing { (theValue, theError) in
      if let error = theError {
        continuation.resume(throwing: error)
      } else {
        guard let theValue = theValue else {
          fatalError("Expected non-nil success argument 'theValue' for nil error")
        }
        continuation.resume(returning: theValue)
      }
    }
  }
}
```

- Example 2 (convert to async/await)
-- Base Code
```swift
func test(completion: (String?, Error?) -> Void) {
  simpleErr() { (res, err) in
    completion(res, err)
  }
}
```
-- Old Refactoring Result
```swift
func test() async throws -> String {
  let res = try await simpleErr()
  throw <#err#>
}
```
-- New Refactoring Result
```swift
func test() async throws -> String {
  let res = try await simpleErr()
  return res
}
```
This commit is contained in:
Alex Hoppen
2021-07-06 18:16:16 +02:00
parent 03d3887bba
commit 6cbbb7cb1f
4 changed files with 830 additions and 35 deletions

View File

@@ -10,6 +10,9 @@ func simpleErr(arg: String, _ completion: (String?, Error?) -> Void) { }
func simpleRes(arg: String, _ completion: (Result<String, Error>) -> Void) { }
func run(block: () -> Bool) -> Bool { return false }
func makeOptionalError() -> Error? { return nil }
func makeOptionalString() -> String? { return nil }
// RUN: %refactor -convert-to-async -dump-text -source-filename %s -pos=%(line+1):1 | %FileCheck -check-prefix=NESTED %s
func nested() {
simple {
@@ -295,6 +298,27 @@ func testReturnHandling3(_ completion: (String?, Error?) -> Void) {
// RETURN-HANDLING3-NEXT: {{^}} return ""{{$}}
// RETURN-HANDLING3-NEXT: }
// RUN: %refactor -add-async-alternative -dump-text -source-filename %s -pos=%(line+1):1 | %FileCheck -check-prefix=RETURN-HANDLING4 %s
func testReturnHandling4(_ completion: (String?, Error?) -> Void) {
simpleErr(arg: "xxx") { str, err in
if str != nil {
completion(str, err)
return
}
print("some error stuff")
completion(str, err)
}
}
// RETURN-HANDLING4: func testReturnHandling4() async throws -> String {
// RETURN-HANDLING4-NEXT: do {
// RETURN-HANDLING4-NEXT: let str = try await simpleErr(arg: "xxx")
// RETURN-HANDLING4-NEXT: return str
// RETURN-HANDLING4-NEXT: } catch let err {
// RETURN-HANDLING4-NEXT: print("some error stuff")
// RETURN-HANDLING4-NEXT: throw err
// RETURN-HANDLING4-NEXT: }
// RETURN-HANDLING4-NEXT: }
// RUN: %refactor -convert-to-async -dump-text -source-filename %s -pos=%(line+1):1 | %FileCheck -check-prefix=RDAR78693050 %s
func rdar78693050(_ completion: () -> Void) {
simple { str in
@@ -342,4 +366,234 @@ func withImplicitReturn(completionHandler: (String) -> Void) {
// IMPLICIT-RETURN: func withImplicitReturn() async -> String {
// IMPLICIT-RETURN-NEXT: let val0 = await simple()
// IMPLICIT-RETURN-NEXT: return val0
// IMPLICIT-RETURN-NEXT: }
// IMPLICIT-RETURN-NEXT: }
// This code doesn't compile after refactoring because we can't return `nil` from the async function.
// But there's not much else we can do here.
// RUN: %refactor -convert-to-async -dump-text -source-filename %s -pos=%(line+1):1 | %FileCheck -check-prefix=NIL-RESULT-AND-NIL-ERROR %s
func nilResultAndNilError(completion: (String?, Error?) -> Void) {
completion(nil, nil)
}
// NIL-RESULT-AND-NIL-ERROR: func nilResultAndNilError() async throws -> String {
// NIL-RESULT-AND-NIL-ERROR-NEXT: return nil
// NIL-RESULT-AND-NIL-ERROR-NEXT: }
// RUN: %refactor -convert-to-async -dump-text -source-filename %s -pos=%(line+1):1 | %FileCheck -check-prefix=NIL-RESULT-AND-OPTIONAL-RELAYED-ERROR %s
func nilResultAndOptionalRelayedError(completion: (String?, Error?) -> Void) {
simpleErr(arg: "test") { (res, err) in
completion(nil, err)
}
}
// NIL-RESULT-AND-OPTIONAL-RELAYED-ERROR: func nilResultAndOptionalRelayedError() async throws -> String {
// NIL-RESULT-AND-OPTIONAL-RELAYED-ERROR-NEXT: let res = try await simpleErr(arg: "test")
// NIL-RESULT-AND-OPTIONAL-RELAYED-ERROR-EMPTY:
// NIL-RESULT-AND-OPTIONAL-RELAYED-ERROR-NEXT: }
// This code doesn't compile after refactoring because we can't throw an optional error returned from makeOptionalError().
// But it's not clear what the intended result should be either.
// RUN: %refactor -convert-to-async -dump-text -source-filename %s -pos=%(line+1):1 | %FileCheck -check-prefix=NIL-RESULT-AND-OPTIONAL-COMPLEX-ERROR %s
func nilResultAndOptionalComplexError(completion: (String?, Error?) -> Void) {
completion(nil, makeOptionalError())
}
// NIL-RESULT-AND-OPTIONAL-COMPLEX-ERROR: func nilResultAndOptionalComplexError() async throws -> String {
// NIL-RESULT-AND-OPTIONAL-COMPLEX-ERROR-NEXT: throw makeOptionalError()
// NIL-RESULT-AND-OPTIONAL-COMPLEX-ERROR-NEXT: }
// RUN: %refactor -convert-to-async -dump-text -source-filename %s -pos=%(line+1):1 | %FileCheck -check-prefix=NIL-RESULT-AND-NON-OPTIONAL-ERROR %s
func nilResultAndNonOptionalError(completion: (String?, Error?) -> Void) {
completion(nil, CustomError.Bad)
}
// NIL-RESULT-AND-NON-OPTIONAL-ERROR: func nilResultAndNonOptionalError() async throws -> String {
// NIL-RESULT-AND-NON-OPTIONAL-ERROR-NEXT: throw CustomError.Bad
// NIL-RESULT-AND-NON-OPTIONAL-ERROR-NEXT: }
// In this case, we are previously ignoring the error returned from simpleErr but are rethrowing it in the refactored case.
// That's probably fine although it changes semantics.
// RUN: %refactor -convert-to-async -dump-text -source-filename %s -pos=%(line+1):1 | %FileCheck -check-prefix=OPTIONAL-RELAYED-RESULT-AND-NIL-ERROR %s
func optionalRelayedResultAndNilError(completion: (String?, Error?) -> Void) {
simpleErr(arg: "test") { (res, err) in
completion(res, nil)
}
}
// OPTIONAL-RELAYED-RESULT-AND-NIL-ERROR: func optionalRelayedResultAndNilError() async throws -> String {
// OPTIONAL-RELAYED-RESULT-AND-NIL-ERROR-NEXT: let res = try await simpleErr(arg: "test")
// OPTIONAL-RELAYED-RESULT-AND-NIL-ERROR-NEXT: return res
// OPTIONAL-RELAYED-RESULT-AND-NIL-ERROR-NEXT: }
// RUN: %refactor -convert-to-async -dump-text -source-filename %s -pos=%(line+1):1 | %FileCheck -check-prefix=OPTIONAL-RELAYED-RESULT-AND-OPTIONAL-RELAYED-ERROR %s
func optionalRelayedResultAndOptionalRelayedError(completion: (String?, Error?) -> Void) {
simpleErr(arg: "test") { (res, err) in
completion(res, err)
}
}
// OPTIONAL-RELAYED-RESULT-AND-OPTIONAL-RELAYED-ERROR: func optionalRelayedResultAndOptionalRelayedError() async throws -> String {
// OPTIONAL-RELAYED-RESULT-AND-OPTIONAL-RELAYED-ERROR-NEXT: let res = try await simpleErr(arg: "test")
// OPTIONAL-RELAYED-RESULT-AND-OPTIONAL-RELAYED-ERROR-NEXT: return res
// OPTIONAL-RELAYED-RESULT-AND-OPTIONAL-RELAYED-ERROR-NEXT: }
// RUN: %refactor -convert-to-async -dump-text -source-filename %s -pos=%(line+1):1 | %FileCheck -check-prefix=OPTIONAL-RELAYED-RESULT-AND-OPTIONAL-COMPLEX-ERROR %s
func optionalRelayedResultAndOptionalComplexError(completion: (String?, Error?) -> Void) {
simpleErr(arg: "test") { (res, err) in
completion(res, makeOptionalError())
}
}
// OPTIONAL-RELAYED-RESULT-AND-OPTIONAL-COMPLEX-ERROR: func optionalRelayedResultAndOptionalComplexError() async throws -> String {
// OPTIONAL-RELAYED-RESULT-AND-OPTIONAL-COMPLEX-ERROR-NEXT: let res = try await simpleErr(arg: "test")
// OPTIONAL-RELAYED-RESULT-AND-OPTIONAL-COMPLEX-ERROR-NEXT: if let error = makeOptionalError() {
// OPTIONAL-RELAYED-RESULT-AND-OPTIONAL-COMPLEX-ERROR-NEXT: throw error
// OPTIONAL-RELAYED-RESULT-AND-OPTIONAL-COMPLEX-ERROR-NEXT: } else {
// OPTIONAL-RELAYED-RESULT-AND-OPTIONAL-COMPLEX-ERROR-NEXT: return res
// OPTIONAL-RELAYED-RESULT-AND-OPTIONAL-COMPLEX-ERROR-NEXT: }
// OPTIONAL-RELAYED-RESULT-AND-OPTIONAL-COMPLEX-ERROR-NEXT: }
// RUN: %refactor -convert-to-async -dump-text -source-filename %s -pos=%(line+1):1 | %FileCheck -check-prefix=OPTIONAL-RELAYED-RESULT-AND-NON-OPTIONAL-ERROR %s
func optionalRelayedResultAndNonOptionalError(completion: (String?, Error?) -> Void) {
simpleErr(arg: "test") { (res, err) in
completion(res, CustomError.Bad)
}
}
// OPTIONAL-RELAYED-RESULT-AND-NON-OPTIONAL-ERROR: func optionalRelayedResultAndNonOptionalError() async throws -> String {
// OPTIONAL-RELAYED-RESULT-AND-NON-OPTIONAL-ERROR-NEXT: let res = try await simpleErr(arg: "test")
// OPTIONAL-RELAYED-RESULT-AND-NON-OPTIONAL-ERROR-NEXT: throw CustomError.Bad
// OPTIONAL-RELAYED-RESULT-AND-NON-OPTIONAL-ERROR-NEXT: }
// RUN: %refactor -convert-to-async -dump-text -source-filename %s -pos=%(line+1):1 | %FileCheck -check-prefix=NON-OPTIONAL-RELAYED-RESULT-AND-NIL-ERROR %s
func nonOptionalRelayedResultAndNilError(completion: (String?, Error?) -> Void) {
simple { res in
completion(res, nil)
}
}
// NON-OPTIONAL-RELAYED-RESULT-AND-NIL-ERROR: func nonOptionalRelayedResultAndNilError() async throws -> String {
// NON-OPTIONAL-RELAYED-RESULT-AND-NIL-ERROR-NEXT: let res = await simple()
// NON-OPTIONAL-RELAYED-RESULT-AND-NIL-ERROR-NEXT: return res
// NON-OPTIONAL-RELAYED-RESULT-AND-NIL-ERROR-NEXT: }
// RUN: %refactor -convert-to-async -dump-text -source-filename %s -pos=%(line+1):1 | %FileCheck -check-prefix=NON-OPTIONAL-RELAYED-RESULT-AND-OPTIONAL-COMPLEX-ERROR %s
func nonOptionalRelayedResultAndOptionalComplexError(completion: (String?, Error?) -> Void) {
simple { res in
completion(res, makeOptionalError())
}
}
// NON-OPTIONAL-RELAYED-RESULT-AND-OPTIONAL-COMPLEX-ERROR: func nonOptionalRelayedResultAndOptionalComplexError() async throws -> String {
// NON-OPTIONAL-RELAYED-RESULT-AND-OPTIONAL-COMPLEX-ERROR-NEXT: let res = await simple()
// NON-OPTIONAL-RELAYED-RESULT-AND-OPTIONAL-COMPLEX-ERROR-NEXT: if let error = makeOptionalError() {
// NON-OPTIONAL-RELAYED-RESULT-AND-OPTIONAL-COMPLEX-ERROR-NEXT: throw error
// NON-OPTIONAL-RELAYED-RESULT-AND-OPTIONAL-COMPLEX-ERROR-NEXT: } else {
// NON-OPTIONAL-RELAYED-RESULT-AND-OPTIONAL-COMPLEX-ERROR-NEXT: return res
// NON-OPTIONAL-RELAYED-RESULT-AND-OPTIONAL-COMPLEX-ERROR-NEXT: }
// NON-OPTIONAL-RELAYED-RESULT-AND-OPTIONAL-COMPLEX-ERROR-NEXT: }
// RUN: %refactor -convert-to-async -dump-text -source-filename %s -pos=%(line+1):1 | %FileCheck -check-prefix=NON-OPTIONAL-RELAYED-RESULT-AND-NON-OPTIONAL-ERROR %s
func nonOptionalRelayedResultAndNonOptionalError(completion: (String?, Error?) -> Void) {
simple { res in
completion(res, CustomError.Bad)
}
}
// NON-OPTIONAL-RELAYED-RESULT-AND-NON-OPTIONAL-ERROR: func nonOptionalRelayedResultAndNonOptionalError() async throws -> String {
// NON-OPTIONAL-RELAYED-RESULT-AND-NON-OPTIONAL-ERROR-NEXT: let res = await simple()
// NON-OPTIONAL-RELAYED-RESULT-AND-NON-OPTIONAL-ERROR-NEXT: throw CustomError.Bad
// NON-OPTIONAL-RELAYED-RESULT-AND-NON-OPTIONAL-ERROR-NEXT: }
// The refactored code doesn't compile because we can't return an optional String from the async function.
// But it's not clear what the intended result should be either, because `error` is always `nil`.
// RUN: %refactor -convert-to-async -dump-text -source-filename %s -pos=%(line+1):1 | %FileCheck -check-prefix=OPTIONAL-COMPLEX-RESULT-AND-NIL-ERROR %s
func optionalComplexResultAndNilError(completion: (String?, Error?) -> Void) {
completion(makeOptionalString(), nil)
}
// OPTIONAL-COMPLEX-RESULT-AND-NIL-ERROR: func optionalComplexResultAndNilError() async throws -> String {
// OPTIONAL-COMPLEX-RESULT-AND-NIL-ERROR-NEXT: return makeOptionalString()
// OPTIONAL-COMPLEX-RESULT-AND-NIL-ERROR-NEXT: }
// RUN: %refactor -convert-to-async -dump-text -source-filename %s -pos=%(line+1):1 | %FileCheck -check-prefix=OPTIONAL-COMPLEX-RESULT-AND-OPTIONAL-RELAYED-ERROR %s
func optionalComplexResultAndOptionalRelayedError(completion: (String?, Error?) -> Void) {
simpleErr(arg: "test") { (res, err) in
completion(makeOptionalString(), err)
}
}
// OPTIONAL-COMPLEX-RESULT-AND-OPTIONAL-RELAYED-ERROR: func optionalComplexResultAndOptionalRelayedError() async throws -> String {
// OPTIONAL-COMPLEX-RESULT-AND-OPTIONAL-RELAYED-ERROR-NEXT: let res = try await simpleErr(arg: "test")
// OPTIONAL-COMPLEX-RESULT-AND-OPTIONAL-RELAYED-ERROR-NEXT: return makeOptionalString()
// OPTIONAL-COMPLEX-RESULT-AND-OPTIONAL-RELAYED-ERROR-NEXT: }
// RUN: %refactor -convert-to-async -dump-text -source-filename %s -pos=%(line+1):1 | %FileCheck -check-prefix=OPTIONAL-COMPLEX-RESULT-AND-OPTIONAL-COMPLEX-ERROR %s
func optionalComplexResultAndOptionalComplexError(completion: (String?, Error?) -> Void) {
completion(makeOptionalString(), makeOptionalError())
}
// OPTIONAL-COMPLEX-RESULT-AND-OPTIONAL-COMPLEX-ERROR: func optionalComplexResultAndOptionalComplexError() async throws -> String {
// OPTIONAL-COMPLEX-RESULT-AND-OPTIONAL-COMPLEX-ERROR-NEXT: if let error = makeOptionalError() {
// OPTIONAL-COMPLEX-RESULT-AND-OPTIONAL-COMPLEX-ERROR-NEXT: throw error
// OPTIONAL-COMPLEX-RESULT-AND-OPTIONAL-COMPLEX-ERROR-NEXT: } else {
// OPTIONAL-COMPLEX-RESULT-AND-OPTIONAL-COMPLEX-ERROR-NEXT: return makeOptionalString()
// OPTIONAL-COMPLEX-RESULT-AND-OPTIONAL-COMPLEX-ERROR-NEXT: }
// OPTIONAL-COMPLEX-RESULT-AND-OPTIONAL-COMPLEX-ERROR-NEXT: }
// RUN: %refactor -convert-to-async -dump-text -source-filename %s -pos=%(line+1):1 | %FileCheck -check-prefix=OPTIONAL-COMPLEX-RESULT-AND-NON-OPTIONAL-ERROR %s
func optionalComplexResultAndNonOptionalError(completion: (String?, Error?) -> Void) {
completion(makeOptionalString(), CustomError.Bad)
}
// OPTIONAL-COMPLEX-RESULT-AND-NON-OPTIONAL-ERROR: func optionalComplexResultAndNonOptionalError() async throws -> String {
// OPTIONAL-COMPLEX-RESULT-AND-NON-OPTIONAL-ERROR-NEXT: throw CustomError.Bad
// OPTIONAL-COMPLEX-RESULT-AND-NON-OPTIONAL-ERROR-NEXT: }
// RUN: %refactor -convert-to-async -dump-text -source-filename %s -pos=%(line+1):1 | %FileCheck -check-prefix=NON-OPTIONAL-RESULT-AND-NIL-ERROR %s
func nonOptionalResultAndNilError(completion: (String?, Error?) -> Void) {
completion("abc", nil)
}
// NON-OPTIONAL-RESULT-AND-NIL-ERROR: func nonOptionalResultAndNilError() async throws -> String {
// NON-OPTIONAL-RESULT-AND-NIL-ERROR-NEXT: return "abc"
// NON-OPTIONAL-RESULT-AND-NIL-ERROR-NEXT: }
// RUN: %refactor -convert-to-async -dump-text -source-filename %s -pos=%(line+1):1 | %FileCheck -check-prefix=NON-OPTIONAL-RESULT-AND-OPTIONAL-RELAYED-ERROR %s
func nonOptionalResultAndOptionalRelayedError(completion: (String?, Error?) -> Void) {
simpleErr(arg: "test") { (res, err) in
completion("abc", err)
}
}
// NON-OPTIONAL-RESULT-AND-OPTIONAL-RELAYED-ERROR: func nonOptionalResultAndOptionalRelayedError() async throws -> String {
// NON-OPTIONAL-RESULT-AND-OPTIONAL-RELAYED-ERROR-NEXT: let res = try await simpleErr(arg: "test")
// NON-OPTIONAL-RESULT-AND-OPTIONAL-RELAYED-ERROR-NEXT: return "abc"
// NON-OPTIONAL-RESULT-AND-OPTIONAL-RELAYED-ERROR-NEXT: }
// RUN: %refactor -convert-to-async -dump-text -source-filename %s -pos=%(line+1):1 | %FileCheck -check-prefix=NON-OPTIONAL-RESULT-AND-OPTIONAL-COMPLEX-ERROR %s
func nonOptionalResultAndOptionalComplexError(completion: (String?, Error?) -> Void) {
completion("abc", makeOptionalError())
}
// NON-OPTIONAL-RESULT-AND-OPTIONAL-COMPLEX-ERROR: func nonOptionalResultAndOptionalComplexError() async throws -> String {
// NON-OPTIONAL-RESULT-AND-OPTIONAL-COMPLEX-ERROR-NEXT: if let error = makeOptionalError() {
// NON-OPTIONAL-RESULT-AND-OPTIONAL-COMPLEX-ERROR-NEXT: throw error
// NON-OPTIONAL-RESULT-AND-OPTIONAL-COMPLEX-ERROR-NEXT: } else {
// NON-OPTIONAL-RESULT-AND-OPTIONAL-COMPLEX-ERROR-NEXT: return "abc"
// NON-OPTIONAL-RESULT-AND-OPTIONAL-COMPLEX-ERROR-NEXT: }
// NON-OPTIONAL-RESULT-AND-OPTIONAL-COMPLEX-ERROR-NEXT: }
// RUN: %refactor -convert-to-async -dump-text -source-filename %s -pos=%(line+1):1 | %FileCheck -check-prefix=NON-OPTIONAL-RESULT-AND-NON-OPTIONAL-ERROR %s
func nonOptionalResultAndNonOptionalError(completion: (String?, Error?) -> Void) {
completion("abc", CustomError.Bad)
}
// NON-OPTIONAL-RESULT-AND-NON-OPTIONAL-ERROR: func nonOptionalResultAndNonOptionalError() async throws -> String {
// NON-OPTIONAL-RESULT-AND-NON-OPTIONAL-ERROR-NEXT: throw CustomError.Bad
// NON-OPTIONAL-RESULT-AND-NON-OPTIONAL-ERROR-NEXT: }
// RUN: %refactor -convert-to-async -dump-text -source-filename %s -pos=%(line+1):1 | %FileCheck -check-prefix=WRAP-COMPLETION-CALL-IN-PARENS %s
func wrapCompletionCallInParenthesis(completion: (String?, Error?) -> Void) {
simpleErr(arg: "test") { (res, err) in
(completion(res, err))
}
}
// WRAP-COMPLETION-CALL-IN-PARENS: func wrapCompletionCallInParenthesis() async throws -> String {
// WRAP-COMPLETION-CALL-IN-PARENS-NEXT: let res = try await simpleErr(arg: "test")
// WRAP-COMPLETION-CALL-IN-PARENS-NEXT: return res
// WRAP-COMPLETION-CALL-IN-PARENS-NEXT: }
// RUN: %refactor -convert-to-async -dump-text -source-filename %s -pos=%(line+1):1 | %FileCheck -check-prefix=TWO-COMPLETION-HANDLER-CALLS %s
func twoCompletionHandlerCalls(completion: (String?, Error?) -> Void) {
simpleErr(arg: "test") { (res, err) in
completion(res, err)
completion(res, err)
}
}
// TWO-COMPLETION-HANDLER-CALLS: func twoCompletionHandlerCalls() async throws -> String {
// TWO-COMPLETION-HANDLER-CALLS-NEXT: let res = try await simpleErr(arg: "test")
// TWO-COMPLETION-HANDLER-CALLS-NEXT: return res
// TWO-COMPLETION-HANDLER-CALLS-NEXT: return res
// TWO-COMPLETION-HANDLER-CALLS-NEXT: }