Skip to content

Commit d0a2564

Browse files
committed
Make behavior change detection generic
Automatically detect when shouldMarkAsObjC() will behave differently based on the objcImpl early adopter flag and diagnose it. This works in either direction, although there isn’t anything yet that will emit diag:: objc_implementation_will_become_nonobjc. NFC except for some minor changes to the wording of notes.
1 parent fdc5aa8 commit d0a2564

File tree

5 files changed

+95
-62
lines changed

5 files changed

+95
-62
lines changed

include/swift/AST/DiagnosticsSema.def

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1814,11 +1814,16 @@ ERROR(attr_implementation_category_goes_on_objc_attr,none,
18141814
"'@implementation'",
18151815
())
18161816

1817-
WARNING(objc_implementation_lock_down_non_member_impl_behavior,none,
1817+
WARNING(objc_implementation_will_become_objc,none,
18181818
"%kind0 will become implicitly '@objc' after adopting '@objc "
18191819
"@implementation'; add '%select{@nonobjc|final}1' to keep the current "
18201820
"behavior",
1821-
(const ValueDecl *, bool))
1821+
(const ValueDecl *, /*suggestFinal=*/bool))
1822+
WARNING(objc_implementation_will_become_nonobjc,none,
1823+
"%kind0 will no longer be implicitly '@objc' after adopting '@objc "
1824+
"@implementation'; add '@objc' to keep the current "
1825+
"behavior",
1826+
(const ValueDecl *))
18221827

18231828
ERROR(member_of_objc_implementation_not_objc_or_final,none,
18241829
"%kind0 does not match any %kindonly0 declared in the headers for %1; "
@@ -1828,12 +1833,10 @@ NOTE(fixit_add_private_for_objc_implementation,none,
18281833
"add 'private' or 'fileprivate' to define an Objective-C-compatible %0 "
18291834
"not declared in the header",
18301835
(DescriptiveDeclKind))
1831-
NOTE(fixit_add_final_for_objc_implementation,none,
1832-
"add 'final' to define a Swift %0 that cannot be overridden",
1833-
(DescriptiveDeclKind))
1834-
NOTE(fixit_add_nonobjc_for_objc_implementation,none,
1835-
"add '@nonobjc' to define a Swift-only %0",
1836-
(DescriptiveDeclKind))
1836+
NOTE(fixit_add_nonobjc_or_final_for_objc_implementation,none,
1837+
"add '%select{@nonobjc|final}1' to define a Swift-only %0"
1838+
"%select{| that cannot be overridden}1",
1839+
(DescriptiveDeclKind, /*suggestFinal=*/bool))
18371840

18381841
ERROR(objc_implementation_wrong_category,none,
18391842
"%kind0 should be implemented in extension for "

lib/Sema/TypeCheckDeclObjC.cpp

Lines changed: 66 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1390,8 +1390,9 @@ static std::optional<ObjCReason> shouldMarkClassAsObjC(const ClassDecl *CD) {
13901390
}
13911391

13921392
/// Figure out if a declaration should be exported to Objective-C.
1393-
static std::optional<ObjCReason> shouldMarkAsObjC(const ValueDecl *VD,
1394-
bool allowImplicit) {
1393+
static std::optional<ObjCReason>
1394+
shouldMarkAsObjC(const ValueDecl *VD, bool allowImplicit,
1395+
ObjCImplementationAttr *implAttr, bool isImplEarlyAdopter) {
13951396
// If Objective-C interoperability is disabled, nothing gets marked as @objc.
13961397
if (!VD->getASTContext().LangOpts.EnableObjCInterop)
13971398
return std::nullopt;
@@ -1495,16 +1496,10 @@ static std::optional<ObjCReason> shouldMarkAsObjC(const ValueDecl *VD,
14951496

14961497
// Emulating old logic:
14971498
// A member implementation of an @objcImplementation extension is @objc...
1498-
if (VD->isObjCMemberImplementation()) {
1499-
auto ext = VD->getDeclContext()->getAsDecl();
1500-
auto attr = ext->getAttrs()
1501-
.getAttribute<ObjCImplementationAttr>(/*AllowInvalid=*/true);
1502-
1503-
// ...for early adopters only. (There's new logic for non-early adopters
1504-
// below.)
1505-
if (attr->isEarlyAdopter())
1506-
return ObjCReason(ObjCReason::MemberOfObjCImplementationExtension, attr);
1507-
}
1499+
// for early adopters only. (There's new logic for non-early adopters below.)
1500+
if (isImplEarlyAdopter && VD->isObjCMemberImplementation())
1501+
return ObjCReason(ObjCReason::MemberOfObjCImplementationExtension,
1502+
implAttr);
15081503

15091504
// A @nonobjc is not @objc, even if it is an override of an @objc, so check
15101505
// for @nonobjc first.
@@ -1514,25 +1509,13 @@ static std::optional<ObjCReason> shouldMarkAsObjC(const ValueDecl *VD,
15141509
.hasAttribute<NonObjCAttr>()))
15151510
return std::nullopt;
15161511

1517-
// Set to non-null to indicate a difference between early-adopter and final
1518-
// objcImpl behavior.
1519-
ObjCImplementationAttr *attrToWarnOfObjCImplBehaviorChange = nullptr;
1520-
15211512
if (isMemberOfObjCImplementationExtension(VD)) {
15221513
// A non-`final` member of an @objc @implementation extension is @objc
15231514
// with a special reason.
15241515
if (!VD->isFinal() && canInferImplicitObjC(/*allowAnyAccess*/true)) {
1525-
auto ext = VD->getDeclContext()->getAsDecl();
1526-
auto attr = ext->getAttrs()
1527-
.getAttribute<ObjCImplementationAttr>(/*AllowInvalid=*/true);
1528-
1529-
if (!attr->isEarlyAdopter())
1530-
return ObjCReason(ObjCReason::MemberOfObjCImplementationExtension,attr);
1531-
1532-
// If we got here, that means the new syntax might behave differently from
1533-
// the old one, unless one of the remaining `return ObjCReason`s below
1534-
// would have made it isObjC() anyway.
1535-
attrToWarnOfObjCImplBehaviorChange = attr;
1516+
if (!isImplEarlyAdopter)
1517+
return ObjCReason(ObjCReason::MemberOfObjCImplementationExtension,
1518+
implAttr);
15361519
}
15371520
}
15381521
else if (isMemberOfObjCClassExtension(VD) &&
@@ -1554,20 +1537,64 @@ static std::optional<ObjCReason> shouldMarkAsObjC(const ValueDecl *VD,
15541537
return ObjCReason::witnessToObjC(requirements.front());
15551538
}
15561539

1557-
if (attrToWarnOfObjCImplBehaviorChange) {
1558-
bool suggestFinal = !isa<ConstructorDecl>(VD);
1559-
VD->diagnose(diag::objc_implementation_lock_down_non_member_impl_behavior,
1560-
VD, suggestFinal)
1540+
return std::nullopt;
1541+
}
1542+
1543+
static std::optional<ObjCReason> shouldMarkAsObjC(const ValueDecl *VD,
1544+
bool allowImplicit) {
1545+
// The real logic is in the four-argument version above. This wrapper just
1546+
// detects future `@objc @implementation` behavior changes and warns about
1547+
// them.
1548+
1549+
ObjCImplementationAttr *implAttr = nullptr;
1550+
if (auto ctxDecl = VD->getDeclContext()->getAsDecl())
1551+
implAttr = ctxDecl->getAttrs().getAttribute<ObjCImplementationAttr>(
1552+
/*AllowInvalid=*/true);
1553+
1554+
bool isEarlyAdopter = implAttr ? implAttr->isEarlyAdopter() : false;
1555+
1556+
auto currentlyObjC = shouldMarkAsObjC(VD, allowImplicit,
1557+
implAttr, isEarlyAdopter);
1558+
1559+
// If we weren't an early adopter, there's no differences to detect.
1560+
if (!isEarlyAdopter)
1561+
return currentlyObjC;
1562+
1563+
// Re-run the logic, but with the early adopter flag forced to `false`.
1564+
auto eventuallyObjC = shouldMarkAsObjC(VD, allowImplicit,
1565+
implAttr, /*isEarlyAdopter=*/false);
1566+
1567+
// Would that have behaved differently? If so, warn about it so the developer
1568+
// knows they should update their code.
1569+
if (currentlyObjC.has_value() == eventuallyObjC.has_value())
1570+
return currentlyObjC;
1571+
1572+
bool suggestFinal = !isa<ConstructorDecl>(VD);
1573+
1574+
if (eventuallyObjC.has_value()) {
1575+
ASSERT(!currentlyObjC.has_value());
1576+
1577+
VD->diagnose(diag::objc_implementation_will_become_objc, VD, suggestFinal)
15611578
.fixItInsert(VD->getAttributeInsertionLoc(suggestFinal),
15621579
suggestFinal ? "final " : "@nonobjc ");
15631580

15641581
VD->diagnose(diag::make_decl_objc, VD->getDescriptiveKind())
1565-
.fixItInsert(VD->getAttributeInsertionLoc(/*modifier=*/false), "@objc ");
1582+
.fixItInsert(VD->getAttributeInsertionLoc(false), "@objc ");
1583+
} else {
1584+
ASSERT(currentlyObjC.has_value());
1585+
1586+
VD->diagnose(diag::objc_implementation_will_become_nonobjc, VD)
1587+
.fixItInsert(VD->getAttributeInsertionLoc(false), "@objc ");
15661588

1567-
attrToWarnOfObjCImplBehaviorChange->setHasInvalidImplicitLangAttrs();
1589+
VD->diagnose(diag::fixit_add_nonobjc_or_final_for_objc_implementation,
1590+
VD->getDescriptiveKind(), suggestFinal)
1591+
.fixItInsert(VD->getAttributeInsertionLoc(suggestFinal),
1592+
suggestFinal ? "final " : "@nonobjc ");
15681593
}
15691594

1570-
return std::nullopt;
1595+
implAttr->setHasInvalidImplicitLangAttrs();
1596+
1597+
return currentlyObjC;
15711598
}
15721599

15731600
/// Determine whether the given type is a C integer type.
@@ -3900,14 +3927,11 @@ class ObjCImplementationChecker {
39003927
}
39013928

39023929
// Initializers can't be 'final', but they can be '@nonobjc'
3903-
if (isa<ConstructorDecl>(cand))
3904-
diagnose(cand, diag::fixit_add_nonobjc_for_objc_implementation,
3905-
cand->getDescriptiveKind())
3906-
.fixItInsert(cand->getAttributeInsertionLoc(false), "@nonobjc ");
3907-
else
3908-
diagnose(cand, diag::fixit_add_final_for_objc_implementation,
3909-
cand->getDescriptiveKind())
3910-
.fixItInsert(cand->getAttributeInsertionLoc(true), "final ");
3930+
bool suggestFinal = !isa<ConstructorDecl>(cand);
3931+
diagnose(cand, diag::fixit_add_nonobjc_or_final_for_objc_implementation,
3932+
cand->getDescriptiveKind(), suggestFinal)
3933+
.fixItInsert(cand->getAttributeInsertionLoc(suggestFinal),
3934+
suggestFinal ? "final " : "@nonobjc ");
39113935
}
39123936
}
39133937

test/decl/ext/Inputs/objc_implementation.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
- (void)superclassMethod:(int)param;
1414
@property (assign) int superclassProperty;
1515

16+
+ (void)superclassClassMethod;
17+
1618
@end
1719

1820
@protocol ObjCProto
@@ -214,6 +216,10 @@
214216
@protocol EmptyObjCProto
215217
@end
216218

219+
@interface ObjCClassWithWeirdSwiftAttributeCombo : ObjCBaseClass
220+
221+
@end
222+
217223
#endif
218224

219225
void CImplFunc1(int param);

test/decl/ext/objc_implementation.swift

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ protocol EmptySwiftProto {}
2626
// FIXME: should emit expected-DISABLED-error@-1 {{instance method 'categoryMethod(fromHeader3:)' should be implemented in extension for category 'PresentAdditions', not main class interface}}
2727
// FIXME: expected-error@-2 {{instance method 'categoryMethod(fromHeader3:)' does not match any instance method declared in the headers for 'ObjCClass'; did you use the instance method's Swift name?}}
2828
// FIXME: expected-note@-3 {{add 'private' or 'fileprivate' to define an Objective-C-compatible instance method not declared in the header}} {{3-3=private }}
29-
// FIXME: expected-note@-4 {{add 'final' to define a Swift instance method that cannot be overridden}} {{3-3=final }}
29+
// FIXME: expected-note@-4 {{add 'final' to define a Swift-only instance method that cannot be overridden}} {{3-3=final }}
3030
}
3131

3232
@objc fileprivate func methodNot(fromHeader1: CInt) {
@@ -40,7 +40,7 @@ protocol EmptySwiftProto {}
4040
func methodNot(fromHeader3: CInt) {
4141
// expected-error@-1 {{instance method 'methodNot(fromHeader3:)' does not match any instance method declared in the headers for 'ObjCClass'; did you use the instance method's Swift name?}}
4242
// expected-note@-2 {{add 'private' or 'fileprivate' to define an Objective-C-compatible instance method not declared in the header}} {{3-3=private }}
43-
// expected-note@-3 {{add 'final' to define a Swift instance method that cannot be overridden}} {{3-3=final }}
43+
// expected-note@-3 {{add 'final' to define a Swift-only instance method that cannot be overridden}} {{3-3=final }}
4444
}
4545

4646
var methodFromHeader5: CInt
@@ -116,7 +116,7 @@ protocol EmptySwiftProto {}
116116
internal var propertyNotFromHeader1: CInt
117117
// expected-error@-1 {{property 'propertyNotFromHeader1' does not match any property declared in the headers for 'ObjCClass'; did you use the property's Swift name?}}
118118
// expected-note@-2 {{add 'private' or 'fileprivate' to define an Objective-C-compatible property not declared in the header}} {{3-11=private}}
119-
// expected-note@-3 {{add 'final' to define a Swift property that cannot be overridden}} {{3-3=final }}
119+
// expected-note@-3 {{add 'final' to define a Swift-only property that cannot be overridden}} {{3-3=final }}
120120

121121
@objc private var propertyNotFromHeader2: CInt
122122
// OK, provides a nonpublic but ObjC-compatible stored property
@@ -240,14 +240,14 @@ protocol EmptySwiftProto {}
240240
// FIXME: should emit expected-DISABLED-error@-1 {{instance method 'method(fromHeader3:)' should be implemented in extension for main class interface, not category 'PresentAdditions'}}
241241
// FIXME: expected-error@-2 {{instance method 'method(fromHeader3:)' does not match any instance method declared in the headers for 'ObjCClass'; did you use the instance method's Swift name?}}
242242
// FIXME: expected-note@-3 {{add 'private' or 'fileprivate' to define an Objective-C-compatible instance method not declared in the header}} {{3-3=private }}
243-
// FIXME: expected-note@-4 {{add 'final' to define a Swift instance method that cannot be overridden}} {{3-3=final }}
243+
// FIXME: expected-note@-4 {{add 'final' to define a Swift-only instance method that cannot be overridden}} {{3-3=final }}
244244
}
245245

246246
var propertyFromHeader7: CInt {
247247
// FIXME: should emit expected-DISABLED-error@-1 {{property 'propertyFromHeader7' should be implemented in extension for main class interface, not category 'PresentAdditions'}}
248248
// FIXME: expected-error@-2 {{property 'propertyFromHeader7' does not match any property declared in the headers for 'ObjCClass'; did you use the property's Swift name?}}
249249
// FIXME: expected-note@-3 {{add 'private' or 'fileprivate' to define an Objective-C-compatible property not declared in the header}} {{3-3=private }}
250-
// FIXME: expected-note@-4 {{add 'final' to define a Swift property that cannot be overridden}} {{3-3=final }}
250+
// FIXME: expected-note@-4 {{add 'final' to define a Swift-only property that cannot be overridden}} {{3-3=final }}
251251
get { return 1 }
252252
}
253253

@@ -270,7 +270,7 @@ protocol EmptySwiftProto {}
270270
func categoryMethodNot(fromHeader3: CInt) {
271271
// expected-error@-1 {{instance method 'categoryMethodNot(fromHeader3:)' does not match any instance method declared in the headers for 'ObjCClass'; did you use the instance method's Swift name?}}
272272
// expected-note@-2 {{add 'private' or 'fileprivate' to define an Objective-C-compatible instance method not declared in the header}} {{3-3=private }}
273-
// expected-note@-3 {{add 'final' to define a Swift instance method that cannot be overridden}} {{3-3=final }}
273+
// expected-note@-3 {{add 'final' to define a Swift-only instance method that cannot be overridden}} {{3-3=final }}
274274
}
275275

276276
var categoryPropertyFromHeader1: CInt

test/decl/ext/objc_implementation_early_adopter.swift

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ protocol EmptySwiftProto {}
2626
// FIXME: should emit expected-DISABLED-error@-1 {{instance method 'categoryMethod(fromHeader3:)' should be implemented in extension for category 'PresentAdditions', not main class interface}}
2727
// FIXME: expected-warning@-2 {{instance method 'categoryMethod(fromHeader3:)' does not match any instance method declared in the headers for 'ObjCClass'; did you use the instance method's Swift name?; this will become an error after adopting '@implementation'}}
2828
// FIXME: expected-note@-3 {{add 'private' or 'fileprivate' to define an Objective-C-compatible instance method not declared in the header}} {{3-3=private }}
29-
// FIXME: expected-note@-4 {{add 'final' to define a Swift instance method that cannot be overridden}} {{3-3=final }}
29+
// FIXME: expected-note@-4 {{add 'final' to define a Swift-only instance method that cannot be overridden}} {{3-3=final }}
3030
}
3131

3232
@objc fileprivate func methodNot(fromHeader1: CInt) {
@@ -40,7 +40,7 @@ protocol EmptySwiftProto {}
4040
func methodNot(fromHeader3: CInt) {
4141
// expected-warning@-1 {{instance method 'methodNot(fromHeader3:)' does not match any instance method declared in the headers for 'ObjCClass'; did you use the instance method's Swift name?; this will become an error after adopting '@implementation'}}
4242
// expected-note@-2 {{add 'private' or 'fileprivate' to define an Objective-C-compatible instance method not declared in the header}} {{3-3=private }}
43-
// expected-note@-3 {{add 'final' to define a Swift instance method that cannot be overridden}} {{3-3=final }}
43+
// expected-note@-3 {{add 'final' to define a Swift-only instance method that cannot be overridden}} {{3-3=final }}
4444
}
4545

4646
var methodFromHeader5: CInt
@@ -116,7 +116,7 @@ protocol EmptySwiftProto {}
116116
internal var propertyNotFromHeader1: CInt
117117
// expected-warning@-1 {{property 'propertyNotFromHeader1' does not match any property declared in the headers for 'ObjCClass'; did you use the property's Swift name?; this will become an error after adopting '@implementation'}}
118118
// expected-note@-2 {{add 'private' or 'fileprivate' to define an Objective-C-compatible property not declared in the header}} {{3-11=private}}
119-
// expected-note@-3 {{add 'final' to define a Swift property that cannot be overridden}} {{3-3=final }}
119+
// expected-note@-3 {{add 'final' to define a Swift-only property that cannot be overridden}} {{3-3=final }}
120120

121121
@objc private var propertyNotFromHeader2: CInt
122122
// OK, provides a nonpublic but ObjC-compatible stored property
@@ -240,14 +240,14 @@ protocol EmptySwiftProto {}
240240
// FIXME: should emit expected-DISABLED-error@-1 {{instance method 'method(fromHeader3:)' should be implemented in extension for main class interface, not category 'PresentAdditions'}}
241241
// FIXME: expected-warning@-2 {{instance method 'method(fromHeader3:)' does not match any instance method declared in the headers for 'ObjCClass'; did you use the instance method's Swift name?; this will become an error after adopting '@implementation'}}
242242
// FIXME: expected-note@-3 {{add 'private' or 'fileprivate' to define an Objective-C-compatible instance method not declared in the header}} {{3-3=private }}
243-
// FIXME: expected-note@-4 {{add 'final' to define a Swift instance method that cannot be overridden}} {{3-3=final }}
243+
// FIXME: expected-note@-4 {{add 'final' to define a Swift-only instance method that cannot be overridden}} {{3-3=final }}
244244
}
245245

246246
var propertyFromHeader7: CInt {
247247
// FIXME: should emit expected-DISABLED-error@-1 {{property 'propertyFromHeader7' should be implemented in extension for main class interface, not category 'PresentAdditions'}}
248248
// FIXME: expected-warning@-2 {{property 'propertyFromHeader7' does not match any property declared in the headers for 'ObjCClass'; did you use the property's Swift name?; this will become an error after adopting '@implementation'}}
249249
// FIXME: expected-note@-3 {{add 'private' or 'fileprivate' to define an Objective-C-compatible property not declared in the header}} {{3-3=private }}
250-
// FIXME: expected-note@-4 {{add 'final' to define a Swift property that cannot be overridden}} {{3-3=final }}
250+
// FIXME: expected-note@-4 {{add 'final' to define a Swift-only property that cannot be overridden}} {{3-3=final }}
251251
get { return 1 }
252252
}
253253

@@ -270,7 +270,7 @@ protocol EmptySwiftProto {}
270270
func categoryMethodNot(fromHeader3: CInt) {
271271
// expected-warning@-1 {{instance method 'categoryMethodNot(fromHeader3:)' does not match any instance method declared in the headers for 'ObjCClass'; did you use the instance method's Swift name?; this will become an error after adopting '@implementation'}}
272272
// expected-note@-2 {{add 'private' or 'fileprivate' to define an Objective-C-compatible instance method not declared in the header}} {{3-3=private }}
273-
// expected-note@-3 {{add 'final' to define a Swift instance method that cannot be overridden}} {{3-3=final }}
273+
// expected-note@-3 {{add 'final' to define a Swift-only instance method that cannot be overridden}} {{3-3=final }}
274274
}
275275

276276
var categoryPropertyFromHeader1: CInt

0 commit comments

Comments
 (0)