Skip to content

Commit cd8d3ad

Browse files
committed
Mimic old objcImpl behavior for early adopters
Before the update to support the new syntax, the decl checker treated private and fileprivate members of objcImpl extensions as non-@objc by default. But SE-0436 specified that private and fileprivate members should be implicitly @objc unless opted out, so when support for the final syntax was added, this behavior was changed retroactively. Unfortunately, we’ve found that some early adopters depended on the old behavior. Restore some logic deleted by swiftlang#73309 for early adopter syntax *only*, with a warning telling the developer to put `final` or `@nonobjc` on the declaration if they want to preserve the behavior after updating it. Also tweaks the ObjCImplementationChecker to ensure this logic will actually be run in time for it to suppress the “upgrade to @objc @implementation” warning, and corrects a couple of regressions arising from that change. Fixes rdar://135747897.
1 parent 07357b9 commit cd8d3ad

File tree

5 files changed

+61
-7
lines changed

5 files changed

+61
-7
lines changed

include/swift/AST/DiagnosticsSema.def

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

1817+
WARNING(objc_implementation_lock_down_non_member_impl_behavior,none,
1818+
"%kind0 will become implicitly '@objc' after adopting '@objc "
1819+
"@implementation'; add '%select{@nonobjc|final}1' to keep the current "
1820+
"behavior",
1821+
(const ValueDecl *, bool))
1822+
18171823
ERROR(member_of_objc_implementation_not_objc_or_final,none,
18181824
"%kind0 does not match any %kindonly0 declared in the headers for %1; "
18191825
"did you use the %kindonly0's Swift name?",

lib/Sema/TypeCheckDeclObjC.cpp

Lines changed: 50 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1493,6 +1493,19 @@ static std::optional<ObjCReason> shouldMarkAsObjC(const ValueDecl *VD,
14931493
return ObjCReason(ObjCReason::MemberOfObjCProtocol);
14941494
}
14951495

1496+
// Emulating old logic:
1497+
// 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+
}
1508+
14961509
// A @nonobjc is not @objc, even if it is an override of an @objc, so check
14971510
// for @nonobjc first.
14981511
if (VD->getAttrs().hasAttribute<NonObjCAttr>() ||
@@ -1501,6 +1514,10 @@ static std::optional<ObjCReason> shouldMarkAsObjC(const ValueDecl *VD,
15011514
.hasAttribute<NonObjCAttr>()))
15021515
return std::nullopt;
15031516

1517+
// Set to non-null to indicate a difference between early-adopter and final
1518+
// objcImpl behavior.
1519+
ObjCImplementationAttr *attrToWarnOfObjCImplBehaviorChange = nullptr;
1520+
15041521
if (isMemberOfObjCImplementationExtension(VD)) {
15051522
// A `final` member of an @objc @implementation extension is not @objc.
15061523
if (VD->isFinal())
@@ -1510,7 +1527,14 @@ static std::optional<ObjCReason> shouldMarkAsObjC(const ValueDecl *VD,
15101527
auto ext = VD->getDeclContext()->getAsDecl();
15111528
auto attr = ext->getAttrs()
15121529
.getAttribute<ObjCImplementationAttr>(/*AllowInvalid=*/true);
1513-
return ObjCReason(ObjCReason::MemberOfObjCImplementationExtension, attr);
1530+
1531+
if (!attr->isEarlyAdopter())
1532+
return ObjCReason(ObjCReason::MemberOfObjCImplementationExtension,attr);
1533+
1534+
// If we got here, that means the new syntax might behave differently from
1535+
// the old one, unless one of the remaining `return ObjCReason`s below
1536+
// would have made it isObjC() anyway.
1537+
attrToWarnOfObjCImplBehaviorChange = attr;
15141538
}
15151539
}
15161540
else if (isMemberOfObjCClassExtension(VD) &&
@@ -1532,6 +1556,19 @@ static std::optional<ObjCReason> shouldMarkAsObjC(const ValueDecl *VD,
15321556
return ObjCReason::witnessToObjC(requirements.front());
15331557
}
15341558

1559+
if (attrToWarnOfObjCImplBehaviorChange) {
1560+
bool suggestFinal = !isa<ConstructorDecl>(VD);
1561+
VD->diagnose(diag::objc_implementation_lock_down_non_member_impl_behavior,
1562+
VD, suggestFinal)
1563+
.fixItInsert(VD->getAttributeInsertionLoc(suggestFinal),
1564+
suggestFinal ? "final " : "@nonobjc ");
1565+
1566+
VD->diagnose(diag::make_decl_objc, VD->getDescriptiveKind())
1567+
.fixItInsert(VD->getAttributeInsertionLoc(/*modifier=*/false), "@objc ");
1568+
1569+
attrToWarnOfObjCImplBehaviorChange->setHasInvalidImplicitLangAttrs();
1570+
}
1571+
15351572
return std::nullopt;
15361573
}
15371574

@@ -2412,6 +2449,11 @@ bool swift::diagnoseUnintendedObjCMethodOverrides(SourceFile &sf) {
24122449
continue;
24132450
}
24142451

2452+
// Skip declarations that are member implementations;
2453+
// ObjCImplmentationChecker will ensure they match a redeclared method.
2454+
if (method->isObjCMemberImplementation())
2455+
continue;
2456+
24152457
auto classDecl = method->getDeclContext()->getSelfClassDecl();
24162458
if (!classDecl)
24172459
continue; // error-recovery path, only
@@ -3263,6 +3305,9 @@ class ObjCImplementationChecker {
32633305
continue;
32643306

32653307
if (auto VD = dyn_cast<ValueDecl>(member)) {
3308+
// Force isObjC() to make sure hasInvalidImplicitLangAttrs() is set.
3309+
(void)VD->isObjC();
3310+
32663311
// Skip non-member implementations.
32673312
// FIXME: Should we consider them if only rejected for access level?
32683313
if (!VD->isObjCMemberImplementation()) {
@@ -3283,7 +3328,8 @@ class ObjCImplementationChecker {
32833328
return ObjCSelector(VD->getASTContext(), 0, { ident });
32843329
}
32853330
if (auto objcAttr = VD->getAttrs().getAttribute<ObjCAttr>())
3286-
return objcAttr->getName().value_or(ObjCSelector());
3331+
if (!objcAttr->isNameImplicit())
3332+
return objcAttr->getName().value_or(ObjCSelector());
32873333
return ObjCSelector();
32883334
}
32893335

@@ -3870,7 +3916,8 @@ class ObjCImplementationChecker {
38703916
void diagnoseEarlyAdopterDeprecation() {
38713917
// Only encourage use of @implementation for early adopters, and only when
38723918
// there are no mismatches that they might be working around with it.
3873-
if (hasDiagnosed || !getAttr()->isEarlyAdopter())
3919+
if (hasDiagnosed || !getAttr()->isEarlyAdopter()
3920+
|| getAttr()->hasInvalidImplicitLangAttrs())
38743921
return;
38753922

38763923
// Only encourage adoption if the corresponding language feature is enabled.

test/SILOptimizer/let_properties_opts_objc.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ public func testObjcInterface(_ x: ObjcInterface) -> Int {
1818

1919
// Test that private @objc constants aren't optimized, but instead continue to pass through ObjC message dispatch.
2020
@_objcImplementation extension ObjcInterfaceConstInit {
21-
private let constant: Int = 0
21+
@objc private let constant: Int = 0
2222

2323
// CHECK-LABEL: sil hidden @$sSo22ObjcInterfaceConstInitC4testE0E15PrivateConstantSiyF : $@convention(method) (@guaranteed ObjcInterfaceConstInit) -> Int {
2424
// CHECK: objc_method %0 : $ObjcInterfaceConstInit, #ObjcInterfaceConstInit.constant!getter.foreign

test/Serialization/objc_implementation.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@ import objc_implementation
1818
@_objcImplementation extension ObjCImpl {
1919
// CHECK-DAG: func cannotBeObjCMethod(_ value: Int?)
2020
private func cannotBeObjCMethod(_ value: Int?) {}
21-
// expected-warning@-1 {{method cannot be in an @objc @implementation extension of a class (without final or @nonobjc) because the type of the parameter cannot be represented in Objective-C}}
21+
// expected-warning@-1 {{instance method 'cannotBeObjCMethod' will become implicitly '@objc' after adopting '@objc @implementation'; add 'final' to keep the current behavior}} {{3-3=final }}
22+
// expected-note@-2 {{add '@objc' to expose this instance method to Objective-C}} {{3-3=@objc }}
2223

2324
// CHECK-DAG: @objc func goodMethod()
2425
@objc public func goodMethod() {}

test/decl/ext/objc_implementation_early_adopter.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -443,8 +443,8 @@ protocol EmptySwiftProto {}
443443
}
444444

445445
private func privateNonObjCMethod(_: EmptySwiftProto) {
446-
// expected-warning@-1 {{method cannot be in an @objc @implementation extension of a class (without final or @nonobjc) because the type of the parameter cannot be represented in Objective-C}}
447-
// expected-note@-2 {{protocol-constrained type containing protocol 'EmptySwiftProto' cannot be represented in Objective-C}}
446+
// expected-warning@-1 {{instance method 'privateNonObjCMethod' will become implicitly '@objc' after adopting '@objc @implementation'; add 'final' to keep the current behavior}} {{3-3=final }}
447+
// expected-note@-2 {{add '@objc' to expose this instance method to Objective-C}} {{3-3=@objc }}
448448
}
449449
}
450450

0 commit comments

Comments
 (0)