-
Notifications
You must be signed in to change notification settings - Fork 10.5k
[Refactoring] Support refactoring calls to async if a variable or function is used as completion handler #37314
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
[Refactoring] Support refactoring calls to async if a variable or function is used as completion handler #37314
Conversation
This will later allow us to reuse parts of `LegacyAlternativeBodyCreator` from `AsyncConverter` when refactoring calls to an async alternative if they pass a variable as the completion handler.
… possible The code moved from `LegacyAlternativeBodyCreator` was using constant strings a lot. Use `tok::` instead to match the style of `AsyncConverter`.
@swift-ci Please test |
@@ -656,3 +656,209 @@ func testCalls() { | |||
// VOID-AND-ERROR-CALL4-NEXT: {{^}}print("void and error completion \(<#v#>)"){{$}} | |||
} | |||
// CONVERT-FUNC: {{^}}} | |||
|
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.
Maybe we should have a file with all the declarations of to-be-async functions and then split basic.swift into multiple files that then compile with that file? It's getting... quite large. Can do in another PR though I guess.
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 can split it in a follow-up PR once your and my changes are merged.
func testSimpleWithVariableCompletionHandler(completionHandler: (String) -> Void) { | ||
// RUN: %refactor -convert-call-to-async-alternative -dump-text -source-filename %s -pos=%(line+1):3 | %FileCheck -check-prefix=SIMPLE-WITH-VARIABLE-COMPLETION-HANDLER %s | ||
simple(completion: completionHandler) | ||
// SIMPLE-WITH-VARIABLE-COMPLETION-HANDLER: let result = await simple() | ||
// SIMPLE-WITH-VARIABLE-COMPLETION-HANDLER: completionHandler(result) | ||
} |
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.
IMO I'd name completionHandler
something else for these tests, since otherwise they look like completion handler functions, but we're using convert call instead of convert function. maybe runAfter
or something?
func asyncThen(runAfter: (String) -> Void) async {
let result = await simple()
runAfter(result)
}
The function as-is looks like it should just be converted to:
func someCompletionHandler() async -> String {
return simple()
}
The member test cases look good 👍
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.
Also, if these were treated as completion handler functions, what happens when converting/adding async? eg.
func simple(_ completionHandler: () -> Void) {}
// Convert/add on `delay`
func delay(_ function: () -> Void) {
simple(function)
}
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.
IMO I'd name completionHandler something else for these tests, since otherwise they look like completion handler functions, but we're using convert call instead of convert function. maybe runAfter or something?
testSimpleWithVariableCompletionHandler
is essentially just a wrapper around simple
. The user might want to do some logging here, convert arguments etc, so I think completionHandler
is the correct name. I
deliberately didn’t use completion
to make sure we are not picking up the variable name from the simple
function declaration.
The function as-is looks like it should just be converted to
I don’t think I follow. Is this the same point as the one below?
Also, if these were treated as completion handler functions, what happens when converting/adding async? eg.
Good point. With this change, we rewrite
// RUN: %refactor -convert-to-async -dump-rewritten -source-filename %s -pos=%(line+1):1
func testSimpleWithVariableCompletionHandler(completionHandler: (String) -> Void) {
simple(completion: completionHandler)
}
to
func testSimpleWithVariableCompletionHandler() async -> String {
let result = await simple()
completionHandler(result)
}
which is definitely not correct.
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.
Yeah, basically the same point. I'm not really sure what this should be converted to though.
…ction is used as completion handler Previously, we only supported refactoring a function to call the async alternative if a closure was used for the callback parameter. With this change, we also support calling a arbitrary function (or variable with function type) that is passed to the completion handler argument. The implementation basically re-uses the code we already have to create the legacy function’s body (which calls the newly created async version and then forwards the arguments to the legacy completion handler). To describe the completion handler that the result is being forwarded to, I’m also using `AsyncHandlerDesc`, but since the completion handler may be a variable, it doesn’t necessarily have an `Index` within a function decl that declares it. Because of this, I split the `AsyncHandlerDesc` up into a context-free `AsyncHandlerDesc` (without an `Index`) and `AsyncHandlerParamDesc` (which includes the `Index`). It turns out that `AsyncHandlerDesc` is sufficient in most places. Resolves rdar://77460524
…eter is not optional If the parameter is not an `Optional`, add a placeholder instead that the user can fill with a sensible default value.
e225d9b
to
6884520
Compare
OK, so I’ve moved all my new test cases to
func foo(completion: (String) -> Void) {
bar(completion)
} now becomes func foo() async -> String {
return await bar()
}
|
@swift-ci Please test |
Build failed |
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.
Other than that one comment, looks good to me! You're probably going to beat me to merge, I realised there was another bug with the shadowing change - it wasn't renaming hoisted decls when the call was eg. within an if :(. So I'll probably be rebasing :)
// RUN: %refactor -convert-call-to-async-alternative -dump-text -source-filename %s -pos=%(line+2):3 | %FileCheck -check-prefix=NO-PARAMS-VARIABLE-COMPLETION-HANDLER %s | ||
func testNoParamsVariableCompletionHandler(completionHandler: () -> Void) { | ||
noParams(completion: completionHandler) | ||
} | ||
// NO-PARAMS-VARIABLE-COMPLETION-HANDLER-FUNC: func testNoParamsVariableCompletionHandler() async { | ||
// NO-PARAMS-VARIABLE-COMPLETION-HANDLER-FUNC-NOT: return | ||
// NO-PARAMS-VARIABLE-COMPLETION-HANDLER-FUNC-NEXT: await noParams |
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.
Should this be noParams()
?
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, it should.
When a function’s completion handler is being passed to another function and the function that the completion handler is being declared in is converted to async, replace the call to the completion handler by a `return` statement. For example: ```swift func foo(completion: (String) -> Void) { bar(completion) } ``` becomes ```swift func foo() async -> String { return await bar() } ``` Previously, we were calling the completion handler, which no longer exists in the newly created `async` function.
…ecks in `variable_as_callback.swift` These were a legacy from when the tests lived in `basic.swift`. They are no longer needed.
…_as_callback.swift`
0dfa4fd
to
f1455c2
Compare
@swift-ci Please test |
Previously, we only supported refactoring a function to call the async alternative if a closure was used for the callback parameter. With this change, we also support calling a arbitrary function (or variable with function type) that is passed to the completion handler argument.
The implementation basically re-uses the code we already have to create the legacy function’s body (which calls the newly created async version and then forwards the arguments to the legacy completion handler).
To describe the completion handler that the result is being forwarded to, I’m also using
AsyncHandlerDesc
, but since the completion handler may be a variable, it doesn’t necessarily have anIndex
within a function decl that declares it. Because of this, I split theAsyncHandlerDesc
up into a context-freeAsyncHandlerDesc
(without anIndex
) andAsyncHandlerParamDesc
(which includes theIndex
). It turns out thatAsyncHandlerDesc
is sufficient in most places.Resolves rdar://77460524 [SR-14573]
This PR consists of three commits, which might be easiest to review on their own. After each commit, the source should be in a buildable state that passes all tests
1. Merge
LegacyAlternativeBodyCreator
intoAsyncConverter
This will later allow us to reuse parts of
LegacyAlternativeBodyCreator
fromAsyncConverter
when refactoring calls to an async alternative if they pass a variable as the completion handler.2. Replace usage of constant strings with
tok::
wherever possibleThe code moved from
LegacyAlternativeBodyCreator
was using constant strings a lot. Usetok::
instead to match the style ofAsyncConverter
.3. Support refactoring calls to async if a variable or function is used as completion handler
The main change described above.