Skip to content

Sema: Don't diagnose implied conformance of imported type to Sendable #81576

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 2 commits into from
May 22, 2025
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
26 changes: 17 additions & 9 deletions lib/Sema/TypeCheckConcurrency.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -6816,6 +6816,9 @@ static bool checkSendableInstanceStorage(

bool swift::checkSendableConformance(
ProtocolConformance *conformance, SendableCheck check) {
ASSERT(conformance->getProtocol()->isSpecificProtocol(
KnownProtocolKind::Sendable));

auto conformanceDC = conformance->getDeclContext();
auto nominal = conformance->getType()->getAnyNominal();
if (!nominal)
Expand Down Expand Up @@ -6905,13 +6908,16 @@ bool swift::checkSendableConformance(
return false;
}

// An implied conformance is generated when you state a conformance to
// a protocol P that inherits from Sendable.
bool wasImplied = (conformance->getSourceKind() ==
ConformanceEntryKind::Implied);

// Sendable can only be used in the same source file.
auto conformanceDecl = conformanceDC->getAsDecl();
SendableCheckContext checkContext(conformanceDC, check);
DiagnosticBehavior behavior = checkContext.defaultDiagnosticBehavior();
if (conformance->getSourceKind() == ConformanceEntryKind::Implied &&
conformance->getProtocol()->isSpecificProtocol(
KnownProtocolKind::Sendable)) {
if (wasImplied) {
if (auto optBehavior = checkContext.preconcurrencyBehavior(
nominal, /*ignoreExplicitConformance=*/true))
behavior = *optBehavior;
Expand All @@ -6920,12 +6926,14 @@ bool swift::checkSendableConformance(
if (conformanceDC->getOutermostParentSourceFile() &&
conformanceDC->getOutermostParentSourceFile() !=
nominal->getOutermostParentSourceFile()) {
conformanceDecl->diagnose(diag::concurrent_value_outside_source_file,
nominal)
.limitBehaviorUntilSwiftVersion(behavior, 6);
if (!(nominal->hasClangNode() && wasImplied)) {
conformanceDecl->diagnose(diag::concurrent_value_outside_source_file,
nominal)
.limitBehaviorUntilSwiftVersion(behavior, 6);

if (behavior == DiagnosticBehavior::Unspecified)
return true;
if (behavior == DiagnosticBehavior::Unspecified)
return true;
}
}

if (classDecl && classDecl->getParentSourceFile()) {
Expand Down Expand Up @@ -6963,7 +6971,7 @@ bool swift::checkSendableConformance(
// a Sendable conformance. The implied conformance is unconditional, so check
// the storage for sendability as if the conformance was declared on the nominal,
// and not some (possibly constrained) extension.
if (conformance->getSourceKind() == ConformanceEntryKind::Implied)
if (wasImplied)
conformanceDC = nominal;
return checkSendableInstanceStorage(nominal, conformanceDC, check);
}
Expand Down
158 changes: 86 additions & 72 deletions lib/Sema/TypeCheckDeclPrimary.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1823,106 +1823,120 @@ static void diagnoseRetroactiveConformances(
return;
}

Type extendedType = ext->getExtendedType();
NominalTypeDecl *extendedNominalDecl = ext->getExtendedNominal();
if (!extendedNominalDecl) {
if (!extendedNominalDecl || isa<BuiltinTupleDecl>(extendedNominalDecl))
return;
}

ModuleDecl *extTypeModule = extendedNominalDecl->getParentModule();

// If the type comes from the __ObjC clang header module, don't warn.
if (extTypeModule->getName().is(CLANG_HEADER_MODULE_NAME)) {
if (extTypeModule->getName().is(CLANG_HEADER_MODULE_NAME))
return;
}

// At this point, we know we're extending a type declared outside this module.
// We better only be conforming it to protocols declared within this module.
llvm::SmallSetVector<ProtocolDecl *, 8> externalProtocols;
llvm::SmallMapVector<ProtocolDecl *, bool, 8> protocols;
llvm::SmallSet<ProtocolDecl *, 8> protocolsWithRetroactiveAttr;
for (const InheritedEntry &entry : ext->getInherited().getEntries()) {
if (entry.getType().isNull() || !entry.getTypeRepr()) {
continue;

for (auto *conformance : ext->getLocalConformances()) {
auto *proto = conformance->getProtocol();
bool inserted = protocols.insert(std::make_pair(
proto, conformance->isRetroactive())).second;
ASSERT(inserted);

if (proto->isSpecificProtocol(KnownProtocolKind::SendableMetatype)) {
protocolsWithRetroactiveAttr.insert(proto);
}

auto proto =
dyn_cast_or_null<ProtocolDecl>(entry.getType()->getAnyNominal());
if (!proto) {
continue;
// Implied conformance to Sendable is a special case that should not be
// diagnosed. Pretend it's always @retroactive.
if (conformance->getSourceKind() == ConformanceEntryKind::Implied &&
proto->isSpecificProtocol(KnownProtocolKind::Sendable) &&
extendedNominalDecl->hasClangNode()) {
protocolsWithRetroactiveAttr.insert(proto);
}
// As a fallback, to support previous language versions, also allow
// this through if the protocol has been explicitly module-qualified.
TypeRepr *repr = unwrapAttributedRepr(entry.getTypeRepr());
if (isModuleQualified(repr, proto->getParentModule())) {
}

for (const InheritedEntry &entry : ext->getInherited().getEntries()) {
auto inheritedTy = entry.getType();
if (inheritedTy.isNull() || !entry.getTypeRepr()) {
continue;
}

proto->walkInheritedProtocols([&](ProtocolDecl *decl) {

// Get the original conformance of the extended type to this protocol.
auto conformanceRef = lookupConformance(extendedType, decl);
if (!conformanceRef.isConcrete()) {
return TypeWalker::Action::Continue;
}
auto conformance = conformanceRef.getConcrete();
SmallVector<ProtocolDecl *, 2> protos;
if (auto *protoTy = inheritedTy->getAs<ProtocolType>()) {
auto *proto = protoTy->getDecl();

// If that conformance came from this extension, then we warn. Otherwise
// we will have diagnosed it on the extension that actually declares this
// specific conformance.
if (conformance->getDeclContext() != ext) {
return TypeWalker::Action::Continue;
// As a fallback, to support previous language versions, also allow
// this through if the protocol has been explicitly module-qualified.
TypeRepr *repr = unwrapAttributedRepr(entry.getTypeRepr());
if (isModuleQualified(repr, proto->getParentModule())) {
protocolsWithRetroactiveAttr.insert(proto);
continue;
}

// If this isn't a retroactive conformance, skip it.
if (!conformance->isRetroactive()) {
// However, if this is the protocol in the inherited type entry,
// check to make sure it's not erroneously marked @retroactive when it's
// not actually retroactive.
if (decl == proto && entry.isRetroactive()) {
auto loc =
entry.getTypeRepr()->findAttrLoc(TypeAttrKind::Retroactive);

bool typeInSamePackage = extTypeModule->inSamePackage(module);
bool typeIsSameModule =
extTypeModule->isSameModuleLookingThroughOverlays(module);

auto declForDiag = (typeIsSameModule || typeInSamePackage)
? extendedNominalDecl
: proto;
bool isSameModule = declForDiag->getParentModule()
->isSameModuleLookingThroughOverlays(module);

diags
.diagnose(loc, diag::retroactive_attr_does_not_apply, declForDiag,
isSameModule)
.warnUntilSwiftVersion(6)
.fixItRemove(SourceRange(loc, loc.getAdvancedLoc(1)));
return TypeWalker::Action::Stop;
protos.push_back(proto);
} else if (auto *compositionTy = inheritedTy->getAs<ProtocolCompositionType>()) {
for (auto memberTy : compositionTy->getMembers()) {
if (auto *protoTy = memberTy->getAs<ProtocolType>()) {
protos.push_back(protoTy->getDecl());
}
return TypeWalker::Action::Continue;
}
} else {
continue;
}

// If it's marked @retroactive, no need to warn.
if (entry.isRetroactive()) {
// Note that we encountered this protocol through a conformance marked
// @retroactive in case multiple clauses cause the protocol to be
// inherited.
protocolsWithRetroactiveAttr.insert(decl);
return TypeWalker::Action::Continue;
}
for (auto *proto : protos) {
proto->walkInheritedProtocols([&](ProtocolDecl *decl) {
// If this isn't a retroactive conformance, skip it.
auto found = protocols.find(proto);
if (found != protocols.end() && !found->second) {
// However, if this is the protocol in the inherited type entry,
// check to make sure it's not erroneously marked @retroactive when it's
// not actually retroactive.
if (decl == proto && entry.isRetroactive()) {
auto loc =
entry.getTypeRepr()->findAttrLoc(TypeAttrKind::Retroactive);

bool typeInSamePackage = extTypeModule->inSamePackage(module);
bool typeIsSameModule =
extTypeModule->isSameModuleLookingThroughOverlays(module);

auto declForDiag = (typeIsSameModule || typeInSamePackage)
? extendedNominalDecl
: proto;
bool isSameModule = declForDiag->getParentModule()
->isSameModuleLookingThroughOverlays(module);

diags
.diagnose(loc, diag::retroactive_attr_does_not_apply, declForDiag,
isSameModule)
.warnUntilSwiftVersion(6)
.fixItRemove(SourceRange(loc, loc.getAdvancedLoc(1)));
return TypeWalker::Action::Stop;
}
return TypeWalker::Action::Continue;
}

// If we've come this far, we know this extension is the first declaration
// of the conformance of the extended type to this protocol.
externalProtocols.insert(decl);
// If it's marked @retroactive, no need to warn.
if (entry.isRetroactive()) {
// Note that we encountered this protocol through a conformance marked
// @retroactive in case multiple clauses cause the protocol to be
// inherited.
protocolsWithRetroactiveAttr.insert(decl);
return TypeWalker::Action::Continue;
}

return TypeWalker::Action::Continue;
});
return TypeWalker::Action::Continue;
});
}
}

// Remove protocols that are reachable through a @retroactive conformance.
for (auto *proto : protocolsWithRetroactiveAttr) {
externalProtocols.remove(proto);
SmallSetVector<ProtocolDecl *, 4> externalProtocols;
for (auto pair : protocols) {
if (pair.second && !protocolsWithRetroactiveAttr.count(pair.first))
externalProtocols.insert(pair.first);
}

// If we didn't find any violations, we're done.
Expand Down
15 changes: 15 additions & 0 deletions test/Concurrency/implied_sendable_conformance_objc.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// RUN: %target-swift-frontend(mock-sdk: %clang-importer-sdk) -emit-sil -o /dev/null -I %S/Inputs/custom-modules %s -verify -parse-as-library -swift-version 6

// REQUIRES: objc_interop
// REQUIRES: concurrency

import Foundation

extension CGRect: Sendable {}
// expected-warning@-1 {{extension declares a conformance of imported type 'CGRect' to imported protocol 'Sendable'; this will not behave correctly if the owners of 'CoreGraphics' introduce this conformance in the future}}
// expected-note@-2 {{add '@retroactive' to silence this warning}}
// expected-error@-3 {{conformance to 'Sendable' must occur in the same source file as struct 'CGRect'; use '@unchecked Sendable' for retroactive conformance}}
protocol P: Sendable {}

extension CGPoint: P {}

Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
protocol _CFObject: Hashable {}

#if CGFLOAT_IN_COREFOUNDATION
public struct CGFloat {
public struct CGFloat: @unchecked Sendable {
#if _pointerBitWidth(_32)
public typealias UnderlyingType = Float
#elseif _pointerBitWidth(_64)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ public func == (lhs: CGPoint, rhs: CGPoint) -> Bool {
}

#if !CGFLOAT_IN_COREFOUNDATION
public struct CGFloat {
public struct CGFloat: Sendable {
#if _pointerBitWidth(_32)
public typealias UnderlyingType = Float
#elseif _pointerBitWidth(_64)
Expand Down
15 changes: 12 additions & 3 deletions test/Sema/extension_retroactive_conformances.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ public struct Sample3 {}
public struct Sample4 {}
public struct Sample5 {}
public struct Sample6 {}
public struct Sample7 {}
public struct Sample8 {}

public struct SampleAlreadyConforms: SampleProtocol1 {}

Expand Down Expand Up @@ -83,9 +85,9 @@ protocol ClientProtocol {}
// ok, conforming a type from another module to a protocol within this module is totally fine
extension Sample1: ClientProtocol {}

struct Sample7: @retroactive SampleProtocol1 {} // expected-error {{'@retroactive' only applies in inheritance clauses in extensions}}{{17-30=}}
struct MySample7: @retroactive SampleProtocol1 {} // expected-error {{'@retroactive' only applies in inheritance clauses in extensions}}{{19-32=}}

extension Sample7: @retroactive ClientProtocol {} // expected-warning {{'retroactive' attribute does not apply; 'Sample7' is declared in this module}}{{20-33=}}
extension MySample7: @retroactive ClientProtocol {} // expected-warning {{'retroactive' attribute does not apply; 'MySample7' is declared in this module}}{{22-35=}}

extension Int: @retroactive ClientProtocol {} // expected-warning {{'retroactive' attribute does not apply; 'ClientProtocol' is declared in this module}}{{16-29=}}

Expand All @@ -109,4 +111,11 @@ extension GenericSample1: SampleProtocol1 where T: SampleProtocol1 {}
// expected-warning@-1 {{extension declares a conformance of imported type 'GenericSample1' to imported protocol 'SampleProtocol1'; this will not behave correctly if the owners of 'Library' introduce this conformance in the future}}
// expected-note@-2 {{add '@retroactive' to silence this warning}}

#endif
// Don't forget about protocol compositions
extension Sample7: SampleProtocol1 & SampleProtocol2 {}
// expected-warning@-1 {{extension declares a conformance of imported type 'Sample7' to imported protocols 'SampleProtocol1', 'SampleProtocol2'; this will not behave correctly if the owners of 'Library' introduce this conformance in the future}}
// expected-note@-2 {{add '@retroactive' to silence this warning}}

extension Sample8: @retroactive SampleProtocol1 & SampleProtocol2 {} // ok

#endif