Update the trailing closure handling logic to
handle multiple trailing closures, and adjust the
refactoring output to surround the call in
parentheses rather than adding '.self'. This allows
the parser to deal with the multiple trailing
closures and also silences a warning that would
previously occur.
rdar://81230908
The async refactorings ignore whether a completion handler had
`@escaping` or not. In preparation of fixing this, fix up all functions
to have `@escaping` for their completion handler parameter.
Also some small miscellaneous fixes in order to reduce the number of
warnings output on test failures and also the addition of `REQUIRES:
concurrency` on all tests.
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.
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.
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
}
```
If we are requested to convert a function to async, but the call in the function’s body that eventually calls the completion handler doesn’t have an async alternative, we are currently copying the call as-is, replacing any calls to the completion handler by placeholders.
For example,
```swift
func testDispatch(completionHandler: @escaping (Int) -> Void) {
DispatchQueue.global.async {
completionHandler(longSyncFunc())
}
}
```
becomes
```swift
func testDispatch() async -> Int {
DispatchQueue.global.async {
<#completionHandler#>(longSyncFunc())
}
}
```
and
```swift
func testUrlSession(completionHandler: @escaping (Data) -> Void) {
let task = URLSession.shared.dataTask(with: request) { data, response, error in
completion(data!)
}
task.resume()
}
```
becomes
```swift
func testUrlSession() async -> Data {
let task = URLSession.shared.dataTask(with: request) { data, response, error in
<#completion#>(data!)
}
task.resume()
}
```
Both of these are better modelled using continuations. Thus, if we find an expression that contains a call to the completion handler and can’t be hoisted to an await statement, we are wrapping the rest of the current scope in a `withChecked(Throwing)Continuation`, producing the following results:
```swift
func testDispatch() async -> Int {
return await withCheckedContinuation { (continuation: CheckedContinuation<Int, Never>) in
DispatchQueue.global.async {
continuation.resume(returning: syncComputation())
}
}
}
```
and
```swift
func testDataTask() async -> Int?
return await withCheckedContinuation { (continuation: CheckedContinuation<Data, Never>) in
let task = URLSession.shared.dataTask { data, response, error in
continuation.resume(returning: data!)
}
task.resume()
}
}
```
I think both are much closer to what the developer is actually expecting.
Resolves rdar://79304583