Skip to content

Mimic old objcImpl behavior for early adopters #76462

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Sep 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 15 additions & 6 deletions include/swift/AST/DiagnosticsSema.def
Original file line number Diff line number Diff line change
Expand Up @@ -1814,6 +1814,17 @@ ERROR(attr_implementation_category_goes_on_objc_attr,none,
"'@implementation'",
())

WARNING(objc_implementation_will_become_objc,none,
"%kind0 will become implicitly '@objc' after adopting '@objc "
"@implementation'; add '%select{@nonobjc|final}1' to keep the current "
"behavior",
(const ValueDecl *, /*suggestFinal=*/bool))
WARNING(objc_implementation_will_become_nonobjc,none,
"%kind0 will no longer be implicitly '@objc' after adopting '@objc "
"@implementation'; add '@objc' to keep the current "
"behavior",
(const ValueDecl *))

ERROR(member_of_objc_implementation_not_objc_or_final,none,
"%kind0 does not match any %kindonly0 declared in the headers for %1; "
"did you use the %kindonly0's Swift name?",
Expand All @@ -1822,12 +1833,10 @@ NOTE(fixit_add_private_for_objc_implementation,none,
"add 'private' or 'fileprivate' to define an Objective-C-compatible %0 "
"not declared in the header",
(DescriptiveDeclKind))
NOTE(fixit_add_final_for_objc_implementation,none,
"add 'final' to define a Swift %0 that cannot be overridden",
(DescriptiveDeclKind))
NOTE(fixit_add_nonobjc_for_objc_implementation,none,
"add '@nonobjc' to define a Swift-only %0",
(DescriptiveDeclKind))
NOTE(fixit_add_nonobjc_or_final_for_objc_implementation,none,
"add '%select{@nonobjc|final}1' to define a Swift-only %0"
"%select{| that cannot be overridden}1",
(DescriptiveDeclKind, /*suggestFinal=*/bool))

ERROR(objc_implementation_wrong_category,none,
"%kind0 should be implemented in extension for "
Expand Down
167 changes: 134 additions & 33 deletions lib/Sema/TypeCheckDeclObjC.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1262,17 +1262,39 @@ bool swift::canBeRepresentedInObjC(const ValueDecl *decl) {

#pragma mark "@objc declaration handling"

/// Whether this declaration is a member of a class extension marked @objc.
static bool isMemberOfObjCClassExtension(const ValueDecl *VD) {
enum class ObjCExtensionKind : uint8_t {
/// Not an @objc extension of any kind.
None,
/// This should be treated as an @objc @implementation extension.
ImplementationExtension,
/// This should be treated as an ordinary @objc extension.
NormalExtension,
};

static ObjCExtensionKind
classifyObjCExtensionContext(const ValueDecl *VD, bool isEarlyAdopter) {
auto ext = dyn_cast<ExtensionDecl>(VD->getDeclContext());
if (!ext) return false;
if (!ext || !ext->getSelfClassDecl())
return ObjCExtensionKind::None;

return ext->getSelfClassDecl() && ext->getAttrs().hasAttribute<ObjCAttr>();
}
auto objCAttr = ext->getAttrs().getAttribute<ObjCAttr>();
if (!objCAttr)
return ObjCExtensionKind::None;

if (ext->isObjCImplementation()) {
// Only apply objcImpl-specific logic at this point in the algorithm if the
// user wrote the new syntax.
if (!isEarlyAdopter)
return ObjCExtensionKind::ImplementationExtension;

// Here's the tricky case: If the `@objc` attribute is implicit, then it was
// added by the parser when it saw the early adopter syntax, so we should
// pretend it isn't there!
if (objCAttr->isImplicit())
return ObjCExtensionKind::None;
}

static bool isMemberOfObjCImplementationExtension(const ValueDecl *VD) {
return isMemberOfObjCClassExtension(VD) &&
cast<ExtensionDecl>(VD->getDeclContext())->isObjCImplementation();
return ObjCExtensionKind::NormalExtension;
}

/// Whether this declaration is a member of a class with the `@objcMembers`
Expand Down Expand Up @@ -1390,8 +1412,9 @@ static std::optional<ObjCReason> shouldMarkClassAsObjC(const ClassDecl *CD) {
}

/// Figure out if a declaration should be exported to Objective-C.
static std::optional<ObjCReason> shouldMarkAsObjC(const ValueDecl *VD,
bool allowImplicit) {
static std::optional<ObjCReason>
shouldMarkAsObjC(const ValueDecl *VD, bool allowImplicit,
ObjCImplementationAttr *implAttr, bool isImplEarlyAdopter) {
// If Objective-C interoperability is disabled, nothing gets marked as @objc.
if (!VD->getASTContext().LangOpts.EnableObjCInterop)
return std::nullopt;
Expand Down Expand Up @@ -1493,6 +1516,13 @@ static std::optional<ObjCReason> shouldMarkAsObjC(const ValueDecl *VD,
return ObjCReason(ObjCReason::MemberOfObjCProtocol);
}

// Emulating old logic:
// A member implementation of an @objcImplementation extension is @objc...
// for early adopters only. (There's new logic for non-early adopters below.)
if (isImplEarlyAdopter && VD->isObjCMemberImplementation())
return ObjCReason(ObjCReason::MemberOfObjCImplementationExtension,
implAttr);

// A @nonobjc is not @objc, even if it is an override of an @objc, so check
// for @nonobjc first.
if (VD->getAttrs().hasAttribute<NonObjCAttr>() ||
Expand All @@ -1501,21 +1531,28 @@ static std::optional<ObjCReason> shouldMarkAsObjC(const ValueDecl *VD,
.hasAttribute<NonObjCAttr>()))
return std::nullopt;

if (isMemberOfObjCImplementationExtension(VD)) {
// A `final` member of an @objc @implementation extension is not @objc.
if (VD->isFinal())
return std::nullopt;
// Other members get a special reason.
if (canInferImplicitObjC(/*allowAnyAccess*/true)) {
auto ext = VD->getDeclContext()->getAsDecl();
auto attr = ext->getAttrs()
.getAttribute<ObjCImplementationAttr>(/*AllowInvalid=*/true);
return ObjCReason(ObjCReason::MemberOfObjCImplementationExtension, attr);
// Handle @objc extensions of various sorts.
// The `ImplementationExtension` case is only taken if we are *not* an early
// adopter, though which of the other two it will take instead is complicated.
switch (classifyObjCExtensionContext(VD, isImplEarlyAdopter)) {
case ObjCExtensionKind::None:
break;

case ObjCExtensionKind::ImplementationExtension:
// A non-`final` member of an @objc @implementation extension is @objc
// with a special reason.
if (!VD->isFinal() && canInferImplicitObjC(/*allowAnyAccess*/true)) {
if (!isImplEarlyAdopter)
return ObjCReason(ObjCReason::MemberOfObjCImplementationExtension,
implAttr);
}
break;

case ObjCExtensionKind::NormalExtension:
if (canInferImplicitObjC(/*allowAnyAccess*/true))
return ObjCReason(ObjCReason::MemberOfObjCExtension);
break;
}
else if (isMemberOfObjCClassExtension(VD) &&
canInferImplicitObjC(/*allowAnyAccess*/true))
return ObjCReason(ObjCReason::MemberOfObjCExtension);

if (isMemberOfObjCMembersClass(VD) &&
canInferImplicitObjC(/*allowAnyAccess*/false))
Expand All @@ -1535,6 +1572,63 @@ static std::optional<ObjCReason> shouldMarkAsObjC(const ValueDecl *VD,
return std::nullopt;
}

static std::optional<ObjCReason> shouldMarkAsObjC(const ValueDecl *VD,
bool allowImplicit) {
// The real logic is in the four-argument version above. This wrapper just
// detects future `@objc @implementation` behavior changes and warns about
// them.

ObjCImplementationAttr *implAttr = nullptr;
if (auto ctxDecl = VD->getDeclContext()->getAsDecl())
implAttr = ctxDecl->getAttrs().getAttribute<ObjCImplementationAttr>(
/*AllowInvalid=*/true);

bool isEarlyAdopter = implAttr ? implAttr->isEarlyAdopter() : false;

auto currentlyObjC = shouldMarkAsObjC(VD, allowImplicit,
implAttr, isEarlyAdopter);

// If we weren't an early adopter, there's no differences to detect.
if (!isEarlyAdopter)
return currentlyObjC;

// Re-run the logic, but with the early adopter flag forced to `false`.
auto eventuallyObjC = shouldMarkAsObjC(VD, allowImplicit,
implAttr, /*isEarlyAdopter=*/false);

// Would that have behaved differently? If so, warn about it so the developer
// knows they should update their code.
if (currentlyObjC.has_value() == eventuallyObjC.has_value())
return currentlyObjC;

bool suggestFinal = !isa<ConstructorDecl>(VD);

if (eventuallyObjC.has_value()) {
ASSERT(!currentlyObjC.has_value());

VD->diagnose(diag::objc_implementation_will_become_objc, VD, suggestFinal)
.fixItInsert(VD->getAttributeInsertionLoc(suggestFinal),
suggestFinal ? "final " : "@nonobjc ");

VD->diagnose(diag::make_decl_objc, VD->getDescriptiveKind())
.fixItInsert(VD->getAttributeInsertionLoc(false), "@objc ");
} else {
ASSERT(currentlyObjC.has_value());

VD->diagnose(diag::objc_implementation_will_become_nonobjc, VD)
.fixItInsert(VD->getAttributeInsertionLoc(false), "@objc ");

VD->diagnose(diag::fixit_add_nonobjc_or_final_for_objc_implementation,
VD->getDescriptiveKind(), suggestFinal)
.fixItInsert(VD->getAttributeInsertionLoc(suggestFinal),
suggestFinal ? "final " : "@nonobjc ");
}

implAttr->setHasInvalidImplicitLangAttrs();

return currentlyObjC;
}

/// Determine whether the given type is a C integer type.
static bool isCIntegerType(Type type) {
auto nominal = type->getAnyNominal();
Expand Down Expand Up @@ -2412,6 +2506,11 @@ bool swift::diagnoseUnintendedObjCMethodOverrides(SourceFile &sf) {
continue;
}

// Skip declarations that are member implementations;
// ObjCImplmentationChecker will ensure they match a redeclared method.
if (method->isObjCMemberImplementation())
continue;

auto classDecl = method->getDeclContext()->getSelfClassDecl();
if (!classDecl)
continue; // error-recovery path, only
Expand Down Expand Up @@ -3263,6 +3362,9 @@ class ObjCImplementationChecker {
continue;

if (auto VD = dyn_cast<ValueDecl>(member)) {
// Force isObjC() to make sure hasInvalidImplicitLangAttrs() is set.
(void)VD->isObjC();

// Skip non-member implementations.
// FIXME: Should we consider them if only rejected for access level?
if (!VD->isObjCMemberImplementation()) {
Expand All @@ -3283,7 +3385,8 @@ class ObjCImplementationChecker {
return ObjCSelector(VD->getASTContext(), 0, { ident });
}
if (auto objcAttr = VD->getAttrs().getAttribute<ObjCAttr>())
return objcAttr->getName().value_or(ObjCSelector());
if (!objcAttr->isNameImplicit())
return objcAttr->getName().value_or(ObjCSelector());
return ObjCSelector();
}

Expand Down Expand Up @@ -3856,21 +3959,19 @@ class ObjCImplementationChecker {
}

// Initializers can't be 'final', but they can be '@nonobjc'
if (isa<ConstructorDecl>(cand))
diagnose(cand, diag::fixit_add_nonobjc_for_objc_implementation,
cand->getDescriptiveKind())
.fixItInsert(cand->getAttributeInsertionLoc(false), "@nonobjc ");
else
diagnose(cand, diag::fixit_add_final_for_objc_implementation,
cand->getDescriptiveKind())
.fixItInsert(cand->getAttributeInsertionLoc(true), "final ");
bool suggestFinal = !isa<ConstructorDecl>(cand);
diagnose(cand, diag::fixit_add_nonobjc_or_final_for_objc_implementation,
cand->getDescriptiveKind(), suggestFinal)
.fixItInsert(cand->getAttributeInsertionLoc(suggestFinal),
suggestFinal ? "final " : "@nonobjc ");
}
}

void diagnoseEarlyAdopterDeprecation() {
// Only encourage use of @implementation for early adopters, and only when
// there are no mismatches that they might be working around with it.
if (hasDiagnosed || !getAttr()->isEarlyAdopter())
if (hasDiagnosed || !getAttr()->isEarlyAdopter()
|| getAttr()->hasInvalidImplicitLangAttrs())
return;

// Only encourage adoption if the corresponding language feature is enabled.
Expand Down
2 changes: 1 addition & 1 deletion test/SILOptimizer/let_properties_opts_objc.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ public func testObjcInterface(_ x: ObjcInterface) -> Int {

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

// CHECK-LABEL: sil hidden @$sSo22ObjcInterfaceConstInitC4testE0E15PrivateConstantSiyF : $@convention(method) (@guaranteed ObjcInterfaceConstInit) -> Int {
// CHECK: objc_method %0 : $ObjcInterfaceConstInit, #ObjcInterfaceConstInit.constant!getter.foreign
Expand Down
3 changes: 2 additions & 1 deletion test/Serialization/objc_implementation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ import objc_implementation
@_objcImplementation extension ObjCImpl {
// CHECK-DAG: func cannotBeObjCMethod(_ value: Int?)
private func cannotBeObjCMethod(_ value: Int?) {}
// 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}}
// expected-warning@-1 {{instance method 'cannotBeObjCMethod' will become implicitly '@objc' after adopting '@objc @implementation'; add 'final' to keep the current behavior}} {{3-3=final }}
// expected-note@-2 {{add '@objc' to expose this instance method to Objective-C}} {{3-3=@objc }}

// CHECK-DAG: @objc func goodMethod()
@objc public func goodMethod() {}
Expand Down
6 changes: 6 additions & 0 deletions test/decl/ext/Inputs/objc_implementation.h
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
- (void)superclassMethod:(int)param;
@property (assign) int superclassProperty;

+ (void)superclassClassMethod;

@end

@protocol ObjCProto
Expand Down Expand Up @@ -214,6 +216,10 @@
@protocol EmptyObjCProto
@end

@interface ObjCClassWithWeirdSwiftAttributeCombo : ObjCBaseClass

@end

#endif

void CImplFunc1(int param);
Expand Down
23 changes: 17 additions & 6 deletions test/decl/ext/objc_implementation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ protocol EmptySwiftProto {}
// FIXME: should emit expected-DISABLED-error@-1 {{instance method 'categoryMethod(fromHeader3:)' should be implemented in extension for category 'PresentAdditions', not main class interface}}
// 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?}}
// FIXME: expected-note@-3 {{add 'private' or 'fileprivate' to define an Objective-C-compatible instance method not declared in the header}} {{3-3=private }}
// FIXME: expected-note@-4 {{add 'final' to define a Swift instance method that cannot be overridden}} {{3-3=final }}
// FIXME: expected-note@-4 {{add 'final' to define a Swift-only instance method that cannot be overridden}} {{3-3=final }}
}

@objc fileprivate func methodNot(fromHeader1: CInt) {
Expand All @@ -40,7 +40,7 @@ protocol EmptySwiftProto {}
func methodNot(fromHeader3: CInt) {
// 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?}}
// expected-note@-2 {{add 'private' or 'fileprivate' to define an Objective-C-compatible instance method not declared in the header}} {{3-3=private }}
// expected-note@-3 {{add 'final' to define a Swift instance method that cannot be overridden}} {{3-3=final }}
// expected-note@-3 {{add 'final' to define a Swift-only instance method that cannot be overridden}} {{3-3=final }}
}

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

@objc private var propertyNotFromHeader2: CInt
// OK, provides a nonpublic but ObjC-compatible stored property
Expand Down Expand Up @@ -240,14 +240,14 @@ protocol EmptySwiftProto {}
// FIXME: should emit expected-DISABLED-error@-1 {{instance method 'method(fromHeader3:)' should be implemented in extension for main class interface, not category 'PresentAdditions'}}
// 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?}}
// FIXME: expected-note@-3 {{add 'private' or 'fileprivate' to define an Objective-C-compatible instance method not declared in the header}} {{3-3=private }}
// FIXME: expected-note@-4 {{add 'final' to define a Swift instance method that cannot be overridden}} {{3-3=final }}
// FIXME: expected-note@-4 {{add 'final' to define a Swift-only instance method that cannot be overridden}} {{3-3=final }}
}

var propertyFromHeader7: CInt {
// FIXME: should emit expected-DISABLED-error@-1 {{property 'propertyFromHeader7' should be implemented in extension for main class interface, not category 'PresentAdditions'}}
// 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?}}
// FIXME: expected-note@-3 {{add 'private' or 'fileprivate' to define an Objective-C-compatible property not declared in the header}} {{3-3=private }}
// FIXME: expected-note@-4 {{add 'final' to define a Swift property that cannot be overridden}} {{3-3=final }}
// FIXME: expected-note@-4 {{add 'final' to define a Swift-only property that cannot be overridden}} {{3-3=final }}
get { return 1 }
}

Expand All @@ -270,7 +270,7 @@ protocol EmptySwiftProto {}
func categoryMethodNot(fromHeader3: CInt) {
// 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?}}
// expected-note@-2 {{add 'private' or 'fileprivate' to define an Objective-C-compatible instance method not declared in the header}} {{3-3=private }}
// expected-note@-3 {{add 'final' to define a Swift instance method that cannot be overridden}} {{3-3=final }}
// expected-note@-3 {{add 'final' to define a Swift-only instance method that cannot be overridden}} {{3-3=final }}
}

var categoryPropertyFromHeader1: CInt
Expand Down Expand Up @@ -446,6 +446,17 @@ protocol EmptySwiftProto {}
// expected-error@-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}}
// expected-note@-2 {{protocol-constrained type containing protocol 'EmptySwiftProto' cannot be represented in Objective-C}}
}

override public static func superclassClassMethod() {
// rdar://136113393: `static override` could make a non-`@objc` override
// of an `@objc` member when using new syntax.
}
}

@objc @implementation extension ObjCClassWithWeirdSwiftAttributeCombo {
static func staticFnPreviouslyTreatedAsAtObjcExtensionMember() {
// OK
}
}

// Intentionally using `@_objcImplementation` for this test; do not upgrade!
Expand Down
Loading