Skip to content

Commit ff54f3b

Browse files
committed
witnesses for an async ObjC protocol requirement cannot be sync
While a Swift protocol can have its async requirements satisfied by a sync witness, this is not the case for ObjC protocols because it is not just a calling convention difference. Because every async ObjC function requirement in the protocol also has a sibling that is sync, a sync witness might be trying to conform to the sync version that takes a completion handler. Without this change, we were seeing an issue where we would consider both the async and sync versions of an ObjC requirement, and accidentially choose the async version when the witness was sync, and then raise an error about the typechecker's mistake. Instead of raising an error, this change removes the ability for the typechecker to even consider the errornous conformance. resolves rdar://73326234
1 parent 948166d commit ff54f3b

File tree

7 files changed

+130
-13
lines changed

7 files changed

+130
-13
lines changed

include/swift/AST/DiagnosticsSema.def

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4165,8 +4165,8 @@ ERROR(actor_isolated_objc,none,
41654165
"actor-isolated %0 %1 cannot be @objc",
41664166
(DescriptiveDeclKind, DeclName))
41674167
NOTE(protocol_witness_async_conflict,none,
4168-
"candidate is %select{not |}0'async', but protocol requirement is%select{| not}0",
4169-
(bool))
4168+
"candidate is %select{not |}0'async', but%select{| @objc}1 protocol requirement is%select{| not}0",
4169+
(bool, bool))
41704170
ERROR(async_autoclosure_nonasync_function,none,
41714171
"'async' autoclosure parameter in a non-'async' function", ())
41724172

@@ -4214,9 +4214,6 @@ ERROR(objc_ambiguous_async_convention,none,
42144214
NOTE(objc_ambiguous_async_convention_candidate,none,
42154215
"%0 provides async here", (DeclName))
42164216

4217-
ERROR(satisfy_async_objc,none,
4218-
"satisfying an asychronous @objc %select{method|initializer}0 with "
4219-
"a synchronous %select{method|initializer}0 is not supported", (bool))
42204217
ERROR(async_objc_dynamic_self,none,
42214218
"asynchronous method returning 'Self' cannot be '@objc'", ())
42224219

lib/Sema/TypeCheckAttr.cpp

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1162,8 +1162,11 @@ void AttributeChecker::visitObjCAttr(ObjCAttr *attr) {
11621162
if (CD->isObjCZeroParameterWithLongSelector())
11631163
numParameters = 0; // Something like "init(foo: ())"
11641164

1165-
// A throwing method has an error parameter.
1166-
if (func->hasThrows())
1165+
// An async method, even if it is also 'throws', has
1166+
// one additional completion handler parameter in ObjC.
1167+
if (func->hasAsync())
1168+
++numParameters;
1169+
else if (func->hasThrows()) // A throwing method has an error parameter.
11671170
++numParameters;
11681171

11691172
unsigned numArgumentNames = objcName->getNumArgs();

lib/Sema/TypeCheckDeclObjC.cpp

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1795,11 +1795,8 @@ void markAsObjC(ValueDecl *D, ObjCReason reason,
17951795

17961796
// Attach the foreign async convention.
17971797
if (inheritedAsyncConvention) {
1798-
if (!method->hasAsync())
1799-
method->diagnose(diag::satisfy_async_objc,
1800-
isa<ConstructorDecl>(method));
1801-
else
1802-
method->setForeignAsyncConvention(*inheritedAsyncConvention);
1798+
assert(method->hasAsync() && "async objc req offered for sync witness?");
1799+
method->setForeignAsyncConvention(*inheritedAsyncConvention);
18031800

18041801
} else if (method->hasAsync()) {
18051802
assert(asyncConvention && "Missing async convention");

lib/Sema/TypeCheckProtocol.cpp

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -731,6 +731,12 @@ swift::matchWitness(
731731
return RequirementMatch(witness, MatchKind::AsyncConflict);
732732
}
733733

734+
// If witness is sync, the requirement cannot be @objc and 'async'
735+
if (!witnessFnType->getExtInfo().isAsync() &&
736+
(req->isObjC() && reqFnType->getExtInfo().isAsync())) {
737+
return RequirementMatch(witness, MatchKind::AsyncConflict);
738+
}
739+
734740
// If the witness is 'throws', the requirement must be.
735741
if (witnessFnType->getExtInfo().isThrowing() &&
736742
!reqFnType->getExtInfo().isThrowing()) {
@@ -2381,7 +2387,8 @@ diagnoseMatch(ModuleDecl *module, NormalProtocolConformance *conformance,
23812387

23822388
case MatchKind::AsyncConflict:
23832389
diags.diagnose(match.Witness, diag::protocol_witness_async_conflict,
2384-
cast<AbstractFunctionDecl>(match.Witness)->hasAsync());
2390+
cast<AbstractFunctionDecl>(match.Witness)->hasAsync(),
2391+
req->isObjC());
23852392
break;
23862393

23872394
case MatchKind::ThrowsConflict:

test/ClangImporter/objc_async_conformance.swift

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,57 @@ class C1: ConcurrentProtocol {
1111

1212
func askUser(toJumpThroughHoop hoop: String) async -> String { "hello" }
1313
}
14+
15+
// try to conform to an objc protocol that has both a sync and async requirement
16+
// that has the same name, and both requirements are optional.
17+
class C2 : NSObject, OptionalObserver {}
18+
extension C2 {
19+
func hello(_ session: NSObject) -> Bool { true }
20+
}
21+
22+
// a version of C2 that requires both sync and async methods (differing only by
23+
// completion handler) in ObjC, is not possible to conform to with 'async' in
24+
// a Swift protocol
25+
class C3 : NSObject, RequiredObserver {}
26+
extension C3 {
27+
func hello() -> Bool { true } // expected-note {{'hello()' previously declared here}}
28+
func hello() async -> Bool { true } // expected-error {{invalid redeclaration of 'hello()'}}
29+
}
30+
31+
// the only way to conform to 'RequiredObserver' in Swift is to not use 'async'
32+
class C4 : NSObject, RequiredObserver {}
33+
extension C4 {
34+
func hello() -> Bool { true }
35+
func hello(_ completion : @escaping (Bool) -> Void) -> Void { completion(true) }
36+
}
37+
38+
///////
39+
// selector conflicts
40+
41+
// attempting to satisfy the ObjC async requirement in two ways simultaenously
42+
// is problematic due to a clash in selector names on this ObjC-compatible type
43+
class SelectorConflict : NSObject, RequiredObserverOnlyCompletion {
44+
func hello() async -> Bool { true } // expected-note {{method 'hello()' declared here}}
45+
46+
// expected-error@+1 {{method 'hello' with Objective-C selector 'hello:' conflicts with method 'hello()' with the same Objective-C selector}}
47+
func hello(_ completion : @escaping (Bool) -> Void) -> Void { completion(true) }
48+
}
49+
50+
// making either one of the two methods nonobjc fixes it:
51+
class SelectorOK1 : NSObject, RequiredObserverOnlyCompletion {
52+
@nonobjc func hello() async -> Bool { true }
53+
func hello(_ completion : @escaping (Bool) -> Void) -> Void { completion(true) }
54+
}
55+
56+
class SelectorOK2 : NSObject, RequiredObserverOnlyCompletion {
57+
func hello() async -> Bool { true }
58+
@nonobjc func hello(_ completion : @escaping (Bool) -> Void) -> Void { completion(true) }
59+
}
60+
61+
// additional coverage for situation like C4, where the method names don't
62+
// clash on the ObjC side, but they do on Swift side, BUT their ObjC selectors
63+
// differ, so it's OK.
64+
class Rock : NSObject, Rollable {
65+
func roll(completionHandler: @escaping () -> Void) { completionHandler() }
66+
func roll() { roll(completionHandler: {}) }
67+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
// RUN: %target-typecheck-verify-swift -enable-experimental-concurrency
2+
// REQUIRES: objc_interop
3+
// REQUIRES: concurrency
4+
5+
import Foundation
6+
7+
// async objc requirement, sync witness
8+
@objc protocol Tracker {
9+
func track(event: String) async // expected-note {{protocol requires function 'track(event:)' with type '(String) async -> ()'; do you want to add a stub?}}
10+
}
11+
class Dog: NSObject, Tracker { // expected-error {{type 'Dog' does not conform to protocol 'Tracker'}}
12+
func track(event: String) {} // expected-note {{candidate is not 'async', but @objc protocol requirement is}}
13+
}
14+
15+
// sync objc requirement, async witness
16+
@objc protocol Runner {
17+
func run(event: String) // expected-note {{protocol requires function 'run(event:)' with type '(String) -> ()'; do you want to add a stub?}}
18+
}
19+
class Athlete: NSObject, Runner { // expected-error {{type 'Athlete' does not conform to protocol 'Runner'}}
20+
func run(event: String) async {} // expected-note {{candidate is 'async', but @objc protocol requirement is not}}
21+
}
22+
23+
24+
// async swift protocol, sync witness
25+
protocol Snacker {
26+
func snack(food: String) async
27+
}
28+
29+
class Foodie: Snacker {
30+
func snack(food: String) {}
31+
}
32+
33+
34+
// sync swift protocol, async witness
35+
protocol Backer {
36+
func back(stonk: String) // expected-note {{protocol requires function 'back(stonk:)' with type '(String) -> ()'; do you want to add a stub?}}
37+
}
38+
39+
class Investor: Backer { // expected-error {{type 'Investor' does not conform to protocol 'Backer'}}
40+
func back(stonk: String) async {} // expected-note {{candidate is 'async', but protocol requirement is not}}
41+
}

test/Inputs/clang-importer-sdk/usr/include/ObjCConcurrency.h

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,24 @@ typedef void (^CompletionHandler)(NSString * _Nullable, NSString * _Nullable_res
7777
-(void)missingAtAttributeMethod __attribute__((__swift_attr__("asyncHandler")));
7878
@end
7979

80+
@protocol OptionalObserver <NSObject>
81+
@optional
82+
- (void)hello:(NSObject *)session completion:(void (^)(BOOL answer))completion;
83+
- (BOOL)hello:(NSObject *)session;
84+
@end
85+
86+
@protocol RequiredObserverOnlyCompletion <NSObject>
87+
- (void)hello:(void (^)(BOOL answer))completion;
88+
@end
89+
90+
@protocol RequiredObserver <RequiredObserverOnlyCompletion>
91+
- (BOOL)hello;
92+
@end
93+
94+
@protocol Rollable <NSObject>
95+
- (void)rollWithCompletionHandler: (void (^)(void))completionHandler;
96+
@end
97+
8098
#define MAGIC_NUMBER 42
8199

82100
#pragma clang assume_nonnull end

0 commit comments

Comments
 (0)