Skip to content

Commit 215e313

Browse files
committed
[Refactoring] Improve Void handling for async conversion
When converting a function with a completion handler that has a Void success parameter, e.g `(Void?, Error?) -> Void`, or more likely a `Result<Void, Error>` parameter, make sure to omit the `-> Void` from the resulting async function conversion. In addition, strip any Void bindings from an async function call, and any explicit Void return values from inside the async function. Resolves rdar://75189289
1 parent b08d3bb commit 215e313

File tree

4 files changed

+200
-50
lines changed

4 files changed

+200
-50
lines changed

lib/IDE/Refactoring.cpp

Lines changed: 98 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -4150,6 +4150,14 @@ struct AsyncHandlerDesc {
41504150
return Ty->getParams();
41514151
}
41524152

4153+
/// Retrieve the parameters relevant to a successful return from the
4154+
/// completion handler. This drops the Error parameter if present.
4155+
ArrayRef<AnyFunctionType::Param> getSuccessParams() const {
4156+
if (HasError && Type == HandlerType::PARAMS)
4157+
return params().drop_back();
4158+
return params();
4159+
}
4160+
41534161
/// The `CallExpr` if the given node is a call to the `Handler`
41544162
CallExpr *getAsHandlerCall(ASTNode Node) const {
41554163
if (!isValid())
@@ -4171,13 +4179,16 @@ struct AsyncHandlerDesc {
41714179
auto Args = ArgList.ref();
41724180

41734181
if (Type == HandlerType::PARAMS) {
4174-
if (!HasError)
4175-
return HandlerResult(Args);
4176-
4177-
if (!isa<NilLiteralExpr>(Args.back()))
4182+
// If there's an error parameter and the user isn't passing nil to it,
4183+
// assume this is the error path.
4184+
if (HasError && !isa<NilLiteralExpr>(Args.back()))
41784185
return HandlerResult(Args.back(), true);
41794186

4180-
return HandlerResult(Args.drop_back());
4187+
// We can drop the args altogether if they're just Void.
4188+
if (willAsyncReturnVoid())
4189+
return HandlerResult();
4190+
4191+
return HandlerResult(HasError ? Args.drop_back() : Args);
41814192
} else if (Type == HandlerType::RESULT) {
41824193
if (Args.size() != 1)
41834194
return HandlerResult(Args);
@@ -4196,12 +4207,62 @@ struct AsyncHandlerDesc {
41964207
return HandlerResult(Args);
41974208

41984209
auto ResultArgList = callArgs(ResultCE);
4199-
return HandlerResult(ResultArgList.ref()[0],
4200-
D->getNameStr() == StringRef("failure"));
4210+
auto isFailure = D->getNameStr() == StringRef("failure");
4211+
4212+
// We can drop the arg altogether if it's just Void.
4213+
if (!isFailure && willAsyncReturnVoid())
4214+
return HandlerResult();
4215+
4216+
// Otherwise the arg gets the .success() or .failure() call dropped.
4217+
return HandlerResult(ResultArgList.ref()[0], isFailure);
42014218
}
42024219

42034220
llvm_unreachable("Unhandled result type");
42044221
}
4222+
4223+
// Convert the type of a success parameter in the completion handler function
4224+
// to a return type suitable for an async function. If there is an error
4225+
// parameter present e.g (T?, Error?) -> Void, this unwraps a level of
4226+
// optionality from T?. If this is a Result<T, U> type, returns the success
4227+
// type T.
4228+
swift::Type getSuccessParamAsyncReturnType(swift::Type Ty) const {
4229+
switch (Type) {
4230+
case HandlerType::PARAMS: {
4231+
// If there's an Error parameter in the handler, the success branch can
4232+
// be unwrapped.
4233+
if (HasError)
4234+
Ty = Ty->lookThroughSingleOptionalType();
4235+
4236+
return Ty;
4237+
}
4238+
case HandlerType::RESULT: {
4239+
// Result<T, U> maps to T.
4240+
return Ty->castTo<BoundGenericType>()->getGenericArgs()[0];
4241+
}
4242+
case HandlerType::INVALID:
4243+
llvm_unreachable("Invalid handler type");
4244+
}
4245+
}
4246+
4247+
/// Gets the return value types for the async equivalent of this handler.
4248+
ArrayRef<swift::Type>
4249+
getAsyncReturnTypes(SmallVectorImpl<swift::Type> &Scratch) const {
4250+
for (auto &Param : getSuccessParams()) {
4251+
auto Ty = Param.getParameterType();
4252+
Scratch.push_back(getSuccessParamAsyncReturnType(Ty));
4253+
}
4254+
return Scratch;
4255+
}
4256+
4257+
/// Whether the async equivalent of this handler returns Void.
4258+
bool willAsyncReturnVoid() const {
4259+
// If all of the success params will be converted to Void return types,
4260+
// this will be a Void async function.
4261+
return llvm::all_of(getSuccessParams(), [&](auto &param) {
4262+
auto Ty = param.getParameterType();
4263+
return getSuccessParamAsyncReturnType(Ty)->isVoid();
4264+
});
4265+
}
42054266
};
42064267

42074268
enum class ConditionType { INVALID, NIL, NOT_NIL };
@@ -4919,42 +4980,24 @@ class AsyncConverter : private SourceEntityWalker {
49194980
return;
49204981
}
49214982

4922-
auto HandlerParams = TopHandler.params();
4923-
if (TopHandler.Type == HandlerType::PARAMS && TopHandler.HasError) {
4924-
HandlerParams = HandlerParams.drop_back();
4925-
}
4926-
4927-
if (HandlerParams.empty()) {
4983+
SmallVector<Type, 2> Scratch;
4984+
auto ReturnTypes = TopHandler.getAsyncReturnTypes(Scratch);
4985+
if (ReturnTypes.empty()) {
49284986
OS << " ";
49294987
return;
49304988
}
49314989

4932-
OS << " -> ";
4990+
// Print the function result type, making sure to omit a '-> Void' return.
4991+
if (!TopHandler.willAsyncReturnVoid()) {
4992+
OS << " -> ";
4993+
if (ReturnTypes.size() > 1)
4994+
OS << "(";
49334995

4934-
if (HandlerParams.size() > 1) {
4935-
OS << "(";
4936-
}
4937-
for (size_t I = 0, E = HandlerParams.size(); I < E; ++I) {
4938-
if (I > 0) {
4939-
OS << ", ";
4940-
}
4996+
llvm::interleave(
4997+
ReturnTypes, [&](Type Ty) { Ty->print(OS); }, [&]() { OS << ", "; });
49414998

4942-
auto &Param = HandlerParams[I];
4943-
if (TopHandler.Type == HandlerType::PARAMS) {
4944-
Type ToPrint = Param.getPlainType();
4945-
if (TopHandler.HasError)
4946-
ToPrint = ToPrint->lookThroughSingleOptionalType();
4947-
ToPrint->print(OS);
4948-
} else if (TopHandler.Type == HandlerType::RESULT) {
4949-
auto ResultTy = Param.getPlainType()->getAs<BoundGenericType>();
4950-
assert(ResultTy && "Result must have generic type");
4951-
ResultTy->getGenericArgs()[0]->print(OS);
4952-
} else {
4953-
llvm_unreachable("Unhandled handler type");
4954-
}
4955-
}
4956-
if (HandlerParams.size() > 1) {
4957-
OS << ")";
4999+
if (ReturnTypes.size() > 1)
5000+
OS << ")";
49585001
}
49595002

49605003
if (FD->hasBody())
@@ -5081,8 +5124,7 @@ class AsyncConverter : private SourceEntityWalker {
50815124
addFallbackVars(CallbackParams->getArray(), Blocks);
50825125
addDo();
50835126
addAwaitCall(CE, ArgList.ref(), Blocks.SuccessBlock, SuccessParams,
5084-
/*HasError=*/HandlerDesc.HasError,
5085-
/*AddDeclarations=*/!HandlerDesc.HasError);
5127+
HandlerDesc, /*AddDeclarations=*/!HandlerDesc.HasError);
50865128
addFallbackCatch(ErrParam);
50875129
OS << "\n";
50885130
convertNodes(CallbackBody);
@@ -5115,10 +5157,9 @@ class AsyncConverter : private SourceEntityWalker {
51155157

51165158
setNames(Blocks.SuccessBlock, SuccessParams);
51175159
addAwaitCall(CE, ArgList.ref(), Blocks.SuccessBlock, SuccessParams,
5118-
/*HasError=*/HandlerDesc.HasError,
5119-
/*AddDeclarations=*/true);
5160+
HandlerDesc, /*AddDeclarations=*/true);
51205161

5121-
prepareNamesForBody(HandlerDesc.Type, SuccessParams, ErrParams);
5162+
prepareNamesForBody(HandlerDesc, SuccessParams, ErrParams);
51225163
convertNodes(Blocks.SuccessBlock.nodes());
51235164

51245165
if (RequireDo) {
@@ -5128,7 +5169,7 @@ class AsyncConverter : private SourceEntityWalker {
51285169
HandlerDesc.Type != HandlerType::RESULT);
51295170
addCatch(ErrParam);
51305171

5131-
prepareNamesForBody(HandlerDesc.Type, ErrParams, SuccessParams);
5172+
prepareNamesForBody(HandlerDesc, ErrParams, SuccessParams);
51325173
addCatchBody(ErrParam, Blocks.ErrorBlock);
51335174
}
51345175

@@ -5137,9 +5178,11 @@ class AsyncConverter : private SourceEntityWalker {
51375178

51385179
void addAwaitCall(const CallExpr *CE, ArrayRef<Expr *> Args,
51395180
const ClassifiedBlock &SuccessBlock,
5140-
ArrayRef<const ParamDecl *> SuccessParams, bool HasError,
5141-
bool AddDeclarations) {
5142-
if (!SuccessParams.empty()) {
5181+
ArrayRef<const ParamDecl *> SuccessParams,
5182+
const AsyncHandlerDesc &HandlerDesc, bool AddDeclarations) {
5183+
// Print the bindings to match the completion handler success parameters,
5184+
// making sure to omit in the case of a Void return.
5185+
if (!SuccessParams.empty() && !HandlerDesc.willAsyncReturnVoid()) {
51435186
if (AddDeclarations) {
51445187
if (SuccessBlock.allLet()) {
51455188
OS << tok::kw_let;
@@ -5161,7 +5204,7 @@ class AsyncConverter : private SourceEntityWalker {
51615204
OS << " " << tok::equal << " ";
51625205
}
51635206

5164-
if (HasError) {
5207+
if (HandlerDesc.HasError) {
51655208
OS << tok::kw_try << " ";
51665209
}
51675210
OS << "await ";
@@ -5207,16 +5250,21 @@ class AsyncConverter : private SourceEntityWalker {
52075250
OS << "\n" << tok::r_brace;
52085251
}
52095252

5210-
void prepareNamesForBody(HandlerType ResultType,
5253+
void prepareNamesForBody(const AsyncHandlerDesc &HandlerDesc,
52115254
ArrayRef<const ParamDecl *> CurrentParams,
52125255
ArrayRef<const ParamDecl *> OtherParams) {
5213-
switch (ResultType) {
5256+
switch (HandlerDesc.Type) {
52145257
case HandlerType::PARAMS:
52155258
for (auto *Param : CurrentParams) {
5216-
if (Param->getType()->getOptionalObjectType()) {
5259+
auto Ty = Param->getType();
5260+
if (Ty->getOptionalObjectType()) {
52175261
Unwraps.insert(Param);
52185262
Placeholders.insert(Param);
52195263
}
5264+
// Void parameters get omitted where possible, so turn any reference
5265+
// into a placeholder, as its usage is unlikely what the user wants.
5266+
if (HandlerDesc.getSuccessParamAsyncReturnType(Ty)->isVoid())
5267+
Placeholders.insert(Param);
52205268
}
52215269
// Use of the other params is invalid within the current body
52225270
Placeholders.insert(OtherParams.begin(), OtherParams.end());

test/refactoring/ConvertAsync/basic.swift

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,18 @@ func blockConvention(completion: @convention(block) () -> Void) { }
190190
func cConvention(completion: @convention(c) () -> Void) { }
191191
// C-CONVENTION: func cConvention() async { }
192192

193+
// RUN: %refactor -add-async-alternative -dump-text -source-filename %s -pos=%(line+1):1 | %FileCheck -check-prefix VOID-HANDLER %s
194+
func voidCompletion(completion: (Void) -> Void) {}
195+
// VOID-HANDLER: func voidCompletion() async {}
196+
197+
// RUN: %refactor -add-async-alternative -dump-text -source-filename %s -pos=%(line+1):1 | %FileCheck -check-prefix VOID-AND-ERROR-HANDLER %s
198+
func voidAndErrorCompletion(completion: (Void?, Error?) -> Void) {}
199+
// VOID-AND-ERROR-HANDLER: func voidAndErrorCompletion() async throws {}
200+
201+
// RUN: %refactor -add-async-alternative -dump-text -source-filename %s -pos=%(line+1):1 | %FileCheck -check-prefix TOO-MUCH-VOID-AND-ERROR-HANDLER %s
202+
func tooMuchVoidAndErrorCompletion(completion: (Void?, Void?, Error?) -> Void) {}
203+
// TOO-MUCH-VOID-AND-ERROR-HANDLER: func tooMuchVoidAndErrorCompletion() async throws {}
204+
193205
// 2. Check that the various ways to call a function (and the positions the
194206
// refactoring is called from) are handled correctly
195207

@@ -348,5 +360,26 @@ func testCalls() {
348360
}
349361
// C-CONVENTION-CALL: await cConvention(){{$}}
350362
// C-CONVENTION-CALL-NEXT: {{^}}print("cConvention")
363+
364+
// RUN: %refactor -convert-call-to-async-alternative -dump-text -source-filename %s -pos=%(line+1):3 | %FileCheck -check-prefix=VOID-AND-ERROR-CALL %s
365+
voidAndErrorCompletion { v, err in
366+
print("void and error completion \(v)")
367+
}
368+
// VOID-AND-ERROR-CALL: {{^}}try await voidAndErrorCompletion(){{$}}
369+
// VOID-AND-ERROR-CALL: {{^}}print("void and error completion \(<#v#>)"){{$}}
370+
371+
// RUN: %refactor -convert-call-to-async-alternative -dump-text -source-filename %s -pos=%(line+1):3 | %FileCheck -check-prefix=VOID-AND-ERROR-CALL2 %s
372+
voidAndErrorCompletion { _, err in
373+
print("void and error completion 2")
374+
}
375+
// VOID-AND-ERROR-CALL2: {{^}}try await voidAndErrorCompletion(){{$}}
376+
// VOID-AND-ERROR-CALL2: {{^}}print("void and error completion 2"){{$}}
377+
378+
// RUN: %refactor -convert-call-to-async-alternative -dump-text -source-filename %s -pos=%(line+1):3 | %FileCheck -check-prefix=VOID-AND-ERROR-CALL3 %s
379+
tooMuchVoidAndErrorCompletion { v, v1, err in
380+
print("void and error completion 3")
381+
}
382+
// VOID-AND-ERROR-CALL3: {{^}}try await tooMuchVoidAndErrorCompletion(){{$}}
383+
// VOID-AND-ERROR-CALL3: {{^}}print("void and error completion 3"){{$}}
351384
}
352385
// CONVERT-FUNC: {{^}}}

test/refactoring/ConvertAsync/convert_function.swift

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,3 +148,51 @@ func asyncUnhandledCompletion(_ completion: (String) -> Void) {
148148
// ASYNC-UNHANDLED-NEXT: return "bad"
149149
// ASYNC-UNHANDLED-NEXT: }
150150
// ASYNC-UNHANDLED-NEXT: }
151+
152+
// RUN: %refactor -add-async-alternative -dump-text -source-filename %s -pos=%(line+1):1 | %FileCheck -check-prefix VOID-AND-ERROR-HANDLER %s
153+
func voidAndErrorCompletion(completion: (Void?, Error?) -> Void) {
154+
if .random() {
155+
completion((), nil) // Make sure we drop the ()
156+
} else {
157+
completion(nil, CustomError.Bad)
158+
}
159+
}
160+
// VOID-AND-ERROR-HANDLER: func voidAndErrorCompletion() async throws {
161+
// VOID-AND-ERROR-HANDLER-NEXT: if .random() {
162+
// VOID-AND-ERROR-HANDLER-NEXT: return // Make sure we drop the ()
163+
// VOID-AND-ERROR-HANDLER-NEXT: } else {
164+
// VOID-AND-ERROR-HANDLER-NEXT: throw CustomError.Bad
165+
// VOID-AND-ERROR-HANDLER-NEXT: }
166+
// VOID-AND-ERROR-HANDLER-NEXT: }
167+
168+
// RUN: %refactor -add-async-alternative -dump-text -source-filename %s -pos=%(line+1):1 | %FileCheck -check-prefix TOO-MUCH-VOID-AND-ERROR-HANDLER %s
169+
func tooMuchVoidAndErrorCompletion(completion: (Void?, Void?, Error?) -> Void) {
170+
if .random() {
171+
completion((), (), nil) // Make sure we drop the ()s
172+
} else {
173+
completion(nil, nil, CustomError.Bad)
174+
}
175+
}
176+
// TOO-MUCH-VOID-AND-ERROR-HANDLER: func tooMuchVoidAndErrorCompletion() async throws {
177+
// TOO-MUCH-VOID-AND-ERROR-HANDLER-NEXT: if .random() {
178+
// TOO-MUCH-VOID-AND-ERROR-HANDLER-NEXT: return // Make sure we drop the ()s
179+
// TOO-MUCH-VOID-AND-ERROR-HANDLER-NEXT: } else {
180+
// TOO-MUCH-VOID-AND-ERROR-HANDLER-NEXT: throw CustomError.Bad
181+
// TOO-MUCH-VOID-AND-ERROR-HANDLER-NEXT: }
182+
// TOO-MUCH-VOID-AND-ERROR-HANDLER-NEXT: }
183+
184+
// RUN: %refactor -add-async-alternative -dump-text -source-filename %s -pos=%(line+1):1 | %FileCheck -check-prefix VOID-RESULT-HANDLER %s
185+
func voidResultCompletion(completion: (Result<Void, Error>) -> Void) {
186+
if .random() {
187+
completion(.success(())) // Make sure we drop the .success(())
188+
} else {
189+
completion(.failure(CustomError.Bad))
190+
}
191+
}
192+
// VOID-RESULT-HANDLER: func voidResultCompletion() async throws {
193+
// VOID-RESULT-HANDLER-NEXT: if .random() {
194+
// VOID-RESULT-HANDLER-NEXT: return // Make sure we drop the .success(())
195+
// VOID-RESULT-HANDLER-NEXT: } else {
196+
// VOID-RESULT-HANDLER-NEXT: throw CustomError.Bad
197+
// VOID-RESULT-HANDLER-NEXT: }
198+
// VOID-RESULT-HANDLER-NEXT: }

test/refactoring/ConvertAsync/convert_result.swift

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,14 @@ func simple(_ completion: (Result<String, Error>) -> Void) { }
22
func noError(_ completion: (Result<String, Never>) -> Void) { }
33
func test(_ str: String) -> Bool { return false }
44

5+
// RUN: %refactor -add-async-alternative -dump-text -source-filename %s -pos=%(line+1):1 | %FileCheck -check-prefix VOID-RESULT %s
6+
func voidResult(completion: (Result<Void, Never>) -> Void) {}
7+
// VOID-RESULT: func voidResult() async {}
8+
9+
// RUN: %refactor -add-async-alternative -dump-text -source-filename %s -pos=%(line+1):1 | %FileCheck -check-prefix VOID-AND-ERROR-RESULT %s
10+
func voidAndErrorResult(completion: (Result<Void, Error>) -> Void) {}
11+
// VOID-AND-ERROR-RESULT: func voidAndErrorResult() async throws {}
12+
513
// RUN: %refactor -convert-call-to-async-alternative -dump-text -source-filename %s -pos=%(line+1):1 | %FileCheck -check-prefix=SIMPLE %s
614
simple { res in
715
print("result \(res)")
@@ -301,3 +309,16 @@ simple { res in
301309
// NESTEDBREAK-NEXT: print("after")
302310
// NESTEDBREAK-NOT: }
303311

312+
// RUN: %refactor -convert-call-to-async-alternative -dump-text -source-filename %s -pos=%(line+1):1 | %FileCheck -check-prefix=VOID-RESULT-CALL %s
313+
voidResult { res in
314+
print(res)
315+
}
316+
// VOID-RESULT-CALL: {{^}}await voidResult()
317+
// VOID-RESULT-CALL: {{^}}print(<#res#>)
318+
319+
// RUN: %refactor -convert-call-to-async-alternative -dump-text -source-filename %s -pos=%(line+1):1 | %FileCheck -check-prefix=VOID-AND-ERROR-RESULT-CALL %s
320+
voidAndErrorResult { res in
321+
print(res)
322+
}
323+
// VOID-AND-ERROR-RESULT-CALL: {{^}}try await voidAndErrorResult()
324+
// VOID-AND-ERROR-RESULT-CALL: {{^}}print(<#res#>)

0 commit comments

Comments
 (0)