Skip to content

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

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

Conversation

ahoppen
Copy link
Member

@ahoppen ahoppen commented Jul 8, 2021

  • Explanation: 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 throwed 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.
  • Scope: Async refactoring where completion handler call has an argument for both the success and the error parameter (e.g. completion(result, error) instead of completion(result, nil))
  • Risk: Low (the code we were generating previously was in most cases not what the user wanted)
  • Testing: Added several regression tests
  • Issue: rdar://80318664
  • Reviewer: @hamishknight (Hamish Knight) and @bnbarham (Ben Barham) on original PR [Async Refactoring] Split ambiguous (error + success) completion handler calls into success and error case #38227

Example 1 (convert to continuation)

Base Code

func test(completionHandler: (Int?, Error?) -> Void) {
  withoutAsyncAlternativeThrowing { (theValue, theError) in
    completionHandler(theValue, theError)
  }
}

Old Refactoring Result

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

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

func test(completion: (String?, Error?) -> Void) {
  simpleErr() { (res, err) in
    completion(res, err)
  }
}

Old Refactoring Result

func test() async throws -> String {
  let res = try await simpleErr()
  throw <#err#>
}

New Refactoring Result

func test() async throws -> String {
  let res = try await simpleErr()
  return res
}

ahoppen added 4 commits July 8, 2021 14:07
…ler 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
}
```
…wrapper

Since we aren’t checking that all result arguments are `nil` if there was an `error` in all the other refactorings, also remove the assertions in the async wrapper.
…atalError` messages

The 'success param/argument' is a terminology we use internally in the refactoring and not one we want to present to the user. Just name it 'result' instead.
…ents in continuation

In the previous `fatalError` message `Expected non-nil result 'result' for nil error`, it wasn’t exactly clear wht the `error` referred to. In some cases, when the error was ignored, there wasn’t even an `error` variable in the refactored code.

The new `Expected non-nil result '...' in the non-error case` is a little more generic and also fits if an error is ignored.
@ahoppen ahoppen requested a review from a team as a code owner July 8, 2021 12:20
@ahoppen
Copy link
Member Author

ahoppen commented Jul 8, 2021

@swift-ci Please test

@ahoppen ahoppen merged commit 27ec6c8 into swiftlang:release/5.5 Jul 8, 2021
@ahoppen ahoppen deleted the pr-5.5/ambiguous-completion-handler-calls branch July 8, 2021 19:07
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.

2 participants