Skip to content

Commit 388b48f

Browse files
committed
Disallow async IBAction Methods
The IBAction signature does not allow completion handlers, so making IBAction methods async doesn't make sense. Disallowing it statically so it doesn't just break people when they try. This adds a note with a possible fix + fix-it for the issue, recommending removing the `async` and wrapping the body of the function in a task.
1 parent 11e3d00 commit 388b48f

File tree

6 files changed

+110
-5
lines changed

6 files changed

+110
-5
lines changed

include/swift/AST/DiagnosticsSema.def

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1454,6 +1454,8 @@ NOTE(fixit_rename_in_swift,none,
14541454
"change Swift name to %0", (DeclName))
14551455
NOTE(fixit_rename_in_objc,none,
14561456
"change Objective-C selector to %0", (ObjCSelector))
1457+
NOTE(remove_async_add_task,none,
1458+
"remove 'async' and wrap in 'Task' to use concurrency in %0", (DeclName))
14571459
ERROR(no_objc_tagged_pointer_not_class_protocol,none,
14581460
"@unsafe_no_objc_tagged_pointer can only be applied to class protocols",
14591461
())
@@ -1467,11 +1469,12 @@ ERROR(cdecl_empty_name,none,
14671469
"@_cdecl symbol name cannot be empty", ())
14681470
ERROR(cdecl_throws,none,
14691471
"raising errors from @_cdecl functions is not supported", ())
1470-
ERROR(cdecl_async,none,
1471-
"@_cdecl functions cannot be asynchronous", ())
14721472

14731473
ERROR(attr_methods_only,none,
14741474
"only methods can be declared %0", (DeclAttribute))
1475+
ERROR(attr_decl_async,none,
1476+
"@%0 %1 cannot be asynchronous", (StringRef, DescriptiveDeclKind))
1477+
14751478
ERROR(access_control_in_protocol,none,
14761479
"%0 modifier cannot be used in protocols", (DeclAttribute))
14771480
NOTE(access_control_in_protocol_detail,none,

lib/Sema/TypeCheckAttr.cpp

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -476,6 +476,75 @@ void AttributeChecker::visitDynamicAttr(DynamicAttr *attr) {
476476
diagnoseAndRemoveAttr(attr, diag::dynamic_with_transparent);
477477
}
478478

479+
/// Replaces asynchronous IBActionAttr/IBSegueActionAttr function declarations
480+
/// with a synchronous function. The body of the original function is moved
481+
/// inside of a task executed on the MainActor
482+
static void emitFixItIBActionRemoveAsync(ASTContext &ctx, const FuncDecl &FD) {
483+
// If we don't have an async loc for some reason, things will explode
484+
if (!FD.getAsyncLoc())
485+
return;
486+
487+
std::string replacement = "";
488+
489+
// attributes, function name and everything up to `async` (exclusive)
490+
replacement +=
491+
CharSourceRange(ctx.SourceMgr, FD.getSourceRangeIncludingAttrs().Start,
492+
FD.getAsyncLoc())
493+
.str();
494+
495+
CharSourceRange returnType = Lexer::getCharSourceRangeFromSourceRange(
496+
ctx.SourceMgr, FD.getResultTypeSourceRange());
497+
498+
// If we have a return type, include that here
499+
if (returnType.isValid()) {
500+
replacement +=
501+
(llvm::Twine("-> ") + Lexer::getCharSourceRangeFromSourceRange(
502+
ctx.SourceMgr, FD.getResultTypeSourceRange())
503+
.str())
504+
.str();
505+
}
506+
507+
if (!FD.hasBody()) {
508+
// If we don't have any body, the sourcelocs won't work and will result in
509+
// crashes, so just swap out what we can
510+
511+
SourceLoc endLoc =
512+
returnType.isValid() ? returnType.getEnd() : FD.getAsyncLoc();
513+
ctx.Diags
514+
.diagnose(FD.getAsyncLoc(), diag::remove_async_add_task, FD.getName())
515+
.fixItReplace(
516+
SourceRange(FD.getSourceRangeIncludingAttrs().Start, endLoc),
517+
replacement);
518+
return;
519+
}
520+
521+
if (returnType.isValid())
522+
replacement += " "; // insert space between type name and lbrace
523+
524+
replacement += "{\nTask { @MainActor in";
525+
526+
// If the body of the function is just "{}", there isn't anything to wrap.
527+
// stepping over the braces to grab just the body will result in the `Start`
528+
// location of the source range to come after the `End` of the range, and we
529+
// will overflow. Dance around this by just appending the end of the fix to
530+
// the replacement.
531+
if (FD.getBody()->getLBraceLoc() !=
532+
FD.getBody()->getRBraceLoc().getAdvancedLocOrInvalid(-1)) {
533+
// We actually have a body, so add that to the string
534+
CharSourceRange functionBody(
535+
ctx.SourceMgr, FD.getBody()->getLBraceLoc().getAdvancedLocOrInvalid(1),
536+
FD.getBody()->getRBraceLoc().getAdvancedLocOrInvalid(-1));
537+
replacement += functionBody.str();
538+
}
539+
replacement += " }\n}";
540+
541+
ctx.Diags
542+
.diagnose(FD.getAsyncLoc(), diag::remove_async_add_task, FD.getName())
543+
.fixItReplace(SourceRange(FD.getSourceRangeIncludingAttrs().Start,
544+
FD.getBody()->getRBraceLoc()),
545+
replacement);
546+
}
547+
479548
static bool
480549
validateIBActionSignature(ASTContext &ctx, DeclAttribute *attr,
481550
const FuncDecl *FD, unsigned minParameters,
@@ -502,6 +571,13 @@ validateIBActionSignature(ASTContext &ctx, DeclAttribute *attr,
502571
valid = false;
503572
}
504573

574+
if (FD->isAsyncContext()) {
575+
ctx.Diags.diagnose(FD->getAsyncLoc(), diag::attr_decl_async,
576+
attr->getAttrName(), FD->getDescriptiveKind());
577+
emitFixItIBActionRemoveAsync(ctx, *FD);
578+
valid = false;
579+
}
580+
505581
// We don't need to check here that parameter or return types are
506582
// ObjC-representable; IsObjCRequest will validate that.
507583

lib/Sema/TypeCheckDeclPrimary.cpp

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2985,8 +2985,9 @@ class DeclChecker : public DeclVisitor<DeclChecker> {
29852985
if (isRepresentableInObjC(FD, reason, asyncConvention, errorConvention)) {
29862986
if (FD->hasAsync()) {
29872987
FD->setForeignAsyncConvention(*asyncConvention);
2988-
getASTContext().Diags.diagnose(CDeclAttr->getLocation(),
2989-
diag::cdecl_async);
2988+
getASTContext().Diags.diagnose(
2989+
CDeclAttr->getLocation(), diag::attr_decl_async,
2990+
CDeclAttr->getAttrName(), FD->getDescriptiveKind());
29902991
} else if (FD->hasThrows()) {
29912992
FD->setForeignErrorConvention(*errorConvention);
29922993
getASTContext().Diags.diagnose(CDeclAttr->getLocation(),

test/attr/attr_cdecl_async.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,6 @@
22

33
// REQUIRES: concurrency
44

5-
@_cdecl("async") // expected-error{{@_cdecl functions cannot be asynchronous}}
5+
@_cdecl("async") // expected-error{{@_cdecl global function cannot be asynchronous}}
66
func asynchronous() async { }
77

test/attr/attr_ibaction.swift

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,26 @@ class IBActionWrapperTy {
4141
func moreMagic(_: AnyObject) -> () {} // no-warning
4242
@objc @IBAction
4343
func evenMoreMagic(_: AnyObject) -> () {} // no-warning
44+
45+
@available(SwiftStdlib 5.5, *)
46+
@IBAction
47+
func asyncIBActionNoSpace(_: AnyObject) async -> () {}
48+
// expected-error@-1 {{@IBAction instance method cannot be async}}
49+
// expected-note@-2 {{remove 'async' and wrap in 'Task' to use concurrency in 'asyncIBActionNoSpace'}}{{45:3-47:57=@available(SwiftStdlib 5.5, *)\n @IBAction\n func asyncIBActionNoSpace(_: AnyObject) -> () {\nTask { @MainActor in \}\n\}}}
50+
51+
@available(SwiftStdlib 5.5, *)
52+
@IBAction
53+
func asyncIBActionWithFullBody(_: AnyObject) async {
54+
print("Hello World")
55+
}
56+
// expected-error@-3 {{@IBAction instance method cannot be async}}
57+
// expected-note@-4 {{remove 'async' and wrap in 'Task' to use concurrency in 'asyncIBActionWithFullBody'}}{{51:3-55:4=@available(SwiftStdlib 5.5, *)\n @IBAction\n func asyncIBActionWithFullBody(_: AnyObject) {\nTask { @MainActor in\n print("Hello World")\n \}\n\}}}
58+
59+
@available(SwiftStdlib 5.5, *) @IBAction func asyncIBActionNoBody(_: AnyObject) async
60+
// expected-error@-1 {{expected '{' in body of function declaration}}
61+
// expected-error@-2 {{@IBAction instance method cannot be asynchronous}}
62+
// expected-note@-3 {{remove 'async' and wrap in 'Task' to use concurrency in 'asyncIBActionNoBody}}{{3-88=@available(SwiftStdlib 5.5, *) @IBAction func asyncIBActionNoBody(_: AnyObject)}}
63+
4464
}
4565

4666
struct S { }

test/attr/attr_ibsegueaction.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,11 @@ class IBActionWrapperTy {
4545
func moreMagic(_: AnyObject, _: AnyObject, _: AnyObject) -> AnyObject {} // no-warning
4646
@objc @IBSegueAction
4747
func evenMoreMagic(_: AnyObject, _: AnyObject, _: AnyObject) -> AnyObject {} // no-warning
48+
49+
@available(SwiftStdlib 5.5, *) @IBSegueAction
50+
func process(_: AnyObject, _: AnyObject, _: AnyObject) async -> AnyObject { }
51+
// expected-error@-1 {{@IBSegueAction instance method cannot be asynchronous}}
52+
// expected-note@-2 {{remove 'async' and wrap in 'Task' to use concurrency in 'process'}}{{49:3-50:80=@available(SwiftStdlib 5.5, *) @IBSegueAction\n func process(_: AnyObject, _: AnyObject, _: AnyObject) -> AnyObject {\nTask { @MainActor in \}\n\}}}
4853
}
4954

5055
struct S { }

0 commit comments

Comments
 (0)