Skip to content

Commit e08bd95

Browse files
authored
Merge pull request #75135 from hborla/unavailable-superclass-conformance
[ConformanceLookup] Don't allow skipping inherited unavailable conformances in favor of explicit available ones.
2 parents 8be6696 + e2ddc6c commit e08bd95

9 files changed

+141
-48
lines changed

include/swift/AST/DiagnosticsSema.def

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3147,6 +3147,9 @@ WARNING(witness_deprecated,none,
31473147
"protocol %1%select{|: %2}2",
31483148
(const ValueDecl *, Identifier, StringRef))
31493149

3150+
WARNING(unavailable_conformance,none,
3151+
"conformance of %0 to protocol %1 is already unavailable",
3152+
(Type, Identifier))
31503153
ERROR(redundant_conformance,none,
31513154
"redundant conformance of %0 to protocol %1", (Type, Identifier))
31523155
ERROR(redundant_conformance_conditional,none,

lib/AST/ConformanceLookupTable.cpp

Lines changed: 26 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -69,9 +69,18 @@ void ConformanceLookupTable::ConformanceEntry::markSupersededBy(
6969
SupersededBy = entry;
7070

7171
if (diagnose) {
72+
// If an unavailable Sendable conformance is superseded by a
73+
// retroactive one in the client, we need to record this error
74+
// at the client decl context.
75+
auto *dc = getDeclContext();
76+
if (getProtocol()->isMarkerProtocol() && isFixed() &&
77+
!entry->isFixed()) {
78+
dc = entry->getDeclContext();
79+
}
80+
7281
// Record the problem in the conformance table. We'll
7382
// diagnose these in semantic analysis.
74-
table.AllSupersededDiagnostics[getDeclContext()].push_back(this);
83+
table.AllSupersededDiagnostics[dc].push_back(this);
7584
}
7685
}
7786

@@ -259,14 +268,6 @@ void ConformanceLookupTable::inheritConformances(ClassDecl *classDecl,
259268
auto addInheritedConformance = [&](ConformanceEntry *entry) {
260269
auto protocol = entry->getProtocol();
261270

262-
// Don't add unavailable conformances.
263-
if (auto dc = entry->Source.getDeclContext()) {
264-
if (auto ext = dyn_cast<ExtensionDecl>(dc)) {
265-
if (AvailableAttr::isUnavailable(ext))
266-
return;
267-
}
268-
}
269-
270271
// Don't add redundant conformances here. This is merely an
271272
// optimization; resolveConformances() would zap the duplicates
272273
// anyway.
@@ -614,30 +615,23 @@ ConformanceLookupTable::Ordering ConformanceLookupTable::compareConformances(
614615
// same conformance, this silently takes the class and drops the extension.
615616
if (lhs->getDeclContext()->isAlwaysAvailableConformanceContext() !=
616617
rhs->getDeclContext()->isAlwaysAvailableConformanceContext()) {
618+
// Diagnose conflicting marker protocol conformances that differ in
619+
// un-availability.
620+
diagnoseSuperseded = lhs->getProtocol()->isMarkerProtocol();
621+
617622
return (lhs->getDeclContext()->isAlwaysAvailableConformanceContext()
618623
? Ordering::Before
619624
: Ordering::After);
620625
}
621626

622627
// If one entry is fixed and the other is not, we have our answer.
623628
if (lhs->isFixed() != rhs->isFixed()) {
624-
auto isReplaceableOrMarker = [](ConformanceEntry *entry) -> bool {
625-
ConformanceEntryKind kind = entry->getRankingKind();
626-
if (isReplaceable(kind))
627-
return true;
628-
629-
// Allow replacement of an explicit conformance to a marker protocol.
630-
// (This permits redundant explicit declarations of `Sendable`.)
631-
return (kind == ConformanceEntryKind::Explicit
632-
&& entry->getProtocol()->isMarkerProtocol());
633-
};
634-
635629
// If the non-fixed conformance is not replaceable, we have a failure to
636630
// diagnose.
637631
// FIXME: We should probably diagnose if they have different constraints.
638-
diagnoseSuperseded = (lhs->isFixed() && !isReplaceableOrMarker(rhs)) ||
639-
(rhs->isFixed() && !isReplaceableOrMarker(lhs));
640-
632+
diagnoseSuperseded = (lhs->isFixed() && !isReplaceable(rhs->getRankingKind())) ||
633+
(rhs->isFixed() && !isReplaceable(lhs->getRankingKind()));
634+
641635
return lhs->isFixed() ? Ordering::Before : Ordering::After;
642636
}
643637

@@ -879,8 +873,6 @@ DeclContext *ConformanceLookupTable::getConformingContext(
879873
return nullptr;
880874
auto inheritedConformance = ModuleDecl::lookupConformance(
881875
superclassTy, protocol, /*allowMissing=*/false);
882-
if (inheritedConformance.hasUnavailableConformance())
883-
inheritedConformance = ProtocolConformanceRef::forInvalid();
884876
if (inheritedConformance)
885877
return superclassDecl;
886878
} while ((superclassDecl = superclassDecl->getSuperclassDecl()));
@@ -1145,9 +1137,17 @@ void ConformanceLookupTable::lookupConformances(
11451137
if (diagnostics) {
11461138
auto knownDiags = AllSupersededDiagnostics.find(dc);
11471139
if (knownDiags != AllSupersededDiagnostics.end()) {
1148-
for (const auto *entry : knownDiags->second) {
1140+
for (auto *entry : knownDiags->second) {
11491141
ConformanceEntry *supersededBy = entry->getSupersededBy();
11501142

1143+
// Diagnose the client conformance as superseded.
1144+
auto *definingModule = nominal->getParentModule();
1145+
if (entry->getDeclContext()->getParentModule() == definingModule &&
1146+
supersededBy->getDeclContext()->getParentModule() != definingModule) {
1147+
supersededBy = entry;
1148+
entry = entry->getSupersededBy();
1149+
}
1150+
11511151
diagnostics->push_back({entry->getProtocol(),
11521152
entry->getDeclaredLoc(),
11531153
entry->getKind(),

lib/AST/ProtocolConformanceRef.cpp

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -288,7 +288,8 @@ bool ProtocolConformanceRef::hasUnavailableConformance() const {
288288

289289
// Check whether this conformance is on an unavailable extension.
290290
auto concrete = getConcrete();
291-
auto ext = dyn_cast<ExtensionDecl>(concrete->getDeclContext());
291+
auto *dc = concrete->getRootConformance()->getDeclContext();
292+
auto ext = dyn_cast<ExtensionDecl>(dc);
292293
if (ext && AvailableAttr::isUnavailable(ext))
293294
return true;
294295

lib/Sema/TypeCheckConcurrency.cpp

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6413,8 +6413,16 @@ ProtocolConformance *swift::deriveImplicitSendableConformance(
64136413

64146414
// Local function to form the implicit conformance.
64156415
auto formConformance = [&](const DeclAttribute *attrMakingUnavailable)
6416-
-> NormalProtocolConformance * {
6416+
-> ProtocolConformance * {
64176417
DeclContext *conformanceDC = nominal;
6418+
6419+
// FIXME: @_nonSendable should be a builtin extension macro. This behavior
6420+
// of explanding the unavailable conformance during implicit Sendable
6421+
// derivation means that clients can unknowingly ignore unavailable Sendable
6422+
// Sendable conformances from the original module added via @_nonSendable
6423+
// because they are not expanded if an explicit conformance is found via
6424+
// conformance lookup. So, if a retroactive, unchecked Sendable conformance
6425+
// is written, no redundant conformance warning is emitted.
64186426
if (attrMakingUnavailable) {
64196427
// Conformance availability is currently tied to the declaring extension.
64206428
// FIXME: This is a hack--we should give conformances real availability.
@@ -6442,6 +6450,18 @@ ProtocolConformance *swift::deriveImplicitSendableConformance(
64426450
file->getOrCreateSynthesizedFile().addTopLevelDecl(extension);
64436451

64446452
conformanceDC = extension;
6453+
6454+
// Let the conformance lookup table register the conformance
6455+
// from the extension. Otherwise, we'll end up with redundant
6456+
// conformances between the explicit conformance from the extension
6457+
// and the conformance synthesized below.
6458+
SmallVector<ProtocolConformance *, 2> conformances;
6459+
nominal->lookupConformance(proto, conformances);
6460+
for (auto conformance : conformances) {
6461+
if (conformance->getDeclContext() == conformanceDC) {
6462+
return conformance;
6463+
}
6464+
}
64456465
}
64466466

64476467
auto conformance = ctx.getNormalConformance(
@@ -6463,9 +6483,6 @@ ProtocolConformance *swift::deriveImplicitSendableConformance(
64636483
auto inheritedConformance = ModuleDecl::checkConformance(
64646484
classDecl->mapTypeIntoContext(superclass),
64656485
proto, /*allowMissing=*/false);
6466-
if (inheritedConformance.hasUnavailableConformance())
6467-
inheritedConformance = ProtocolConformanceRef::forInvalid();
6468-
64696486
if (inheritedConformance) {
64706487
inheritedConformance = inheritedConformance
64716488
.mapConformanceOutOfContext();

lib/Sema/TypeCheckProtocol.cpp

Lines changed: 48 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6256,20 +6256,56 @@ void TypeChecker::checkConformancesInContext(IterableDeclContext *idc) {
62566256
// protocol, just warn; we'll pick up the original conformance.
62576257
auto existingModule = diag.ExistingDC->getParentModule();
62586258
auto extendedNominal = diag.ExistingDC->getSelfNominalTypeDecl();
6259-
if (existingModule != dc->getParentModule() &&
6260-
(existingModule->getName() ==
6261-
extendedNominal->getParentModule()->getName() ||
6259+
auto definingModule = extendedNominal->getParentModule()->getName();
6260+
bool conformanceInOrigModule =
6261+
(existingModule->getName() == definingModule ||
62626262
existingModule == diag.Protocol->getParentModule() ||
6263-
existingModule->getName().is("CoreGraphics"))) {
6263+
existingModule->getName().is("CoreGraphics"));
6264+
6265+
// Redundant Sendable conformances are always warnings.
6266+
auto knownProtocol = diag.Protocol->getKnownProtocolKind();
6267+
bool isSendable = knownProtocol == KnownProtocolKind::Sendable;
6268+
// Try to find an inherited Sendable conformance if there is one.
6269+
if (isSendable && !SendableConformance) {
6270+
SmallVector<ProtocolConformance *, 2> conformances;
6271+
nominal->lookupConformance(diag.Protocol, conformances);
6272+
for (auto conformance : conformances) {
6273+
if (isa<InheritedProtocolConformance>(conformance))
6274+
SendableConformance = conformance;
6275+
}
6276+
}
6277+
6278+
if ((existingModule != dc->getParentModule() && conformanceInOrigModule) ||
6279+
isSendable) {
62646280
// Warn about the conformance.
6265-
auto diagID = differentlyConditional
6266-
? diag::redundant_conformance_adhoc_conditional
6267-
: diag::redundant_conformance_adhoc;
6268-
Context.Diags.diagnose(diag.Loc, diagID, dc->getDeclaredInterfaceType(),
6269-
diag.Protocol->getName(),
6270-
existingModule->getName() ==
6271-
extendedNominal->getParentModule()->getName(),
6272-
existingModule->getName());
6281+
if (isSendable && SendableConformance &&
6282+
isa<InheritedProtocolConformance>(SendableConformance)) {
6283+
// Allow re-stated unchecked conformances to Sendable in subclasses
6284+
// as long as the inherited conformance isn't unavailable.
6285+
auto *conformance = SendableConformance->getRootConformance();
6286+
auto *decl = conformance->getDeclContext()->getAsDecl();
6287+
if (!AvailableAttr::isUnavailable(decl)) {
6288+
continue;
6289+
}
6290+
6291+
Context.Diags.diagnose(diag.Loc, diag::unavailable_conformance,
6292+
nominal->getDeclaredInterfaceType(),
6293+
diag.Protocol->getName());
6294+
} else if (existingModule == dc->getParentModule()) {
6295+
Context.Diags.diagnose(diag.Loc, diag::redundant_conformance,
6296+
nominal->getDeclaredInterfaceType(),
6297+
diag.Protocol->getName())
6298+
.limitBehavior(DiagnosticBehavior::Warning);
6299+
} else {
6300+
auto diagID = differentlyConditional
6301+
? diag::redundant_conformance_adhoc_conditional
6302+
: diag::redundant_conformance_adhoc;
6303+
Context.Diags.diagnose(diag.Loc, diagID, dc->getDeclaredInterfaceType(),
6304+
diag.Protocol->getName(),
6305+
existingModule->getName() ==
6306+
extendedNominal->getParentModule()->getName(),
6307+
existingModule->getName());
6308+
}
62736309

62746310
// Complain about any declarations in this extension whose names match
62756311
// a requirement in that protocol.
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
2+
public class NonSendableClass {}
3+
4+
@available(*, unavailable)
5+
extension NonSendableClass: @unchecked Sendable {}
6+
7+
public struct SendableStruct: Sendable {}
8+
9+
public struct AnotherSendableStruct: Sendable {}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
// RUN: %empty-directory(%t)
2+
// RUN: %target-swift-frontend -emit-module -emit-module-path %t/SendableConformances.swiftmodule -module-name SendableConformances %S/Inputs/SendableConformances.swift
3+
4+
// RUN: %target-swift-frontend -typecheck %s -verify -swift-version 6 -I %t
5+
6+
// REQUIRES: concurrency
7+
8+
import SendableConformances
9+
10+
typealias RequireSendable<T: Sendable> = T
11+
12+
extension NonSendableClass: @retroactive @unchecked Sendable {}
13+
// expected-warning@-1 {{conformance of 'NonSendableClass' to protocol 'Sendable' was already stated in the type's module 'SendableConformances'}}
14+
15+
typealias CheckNonSendableClass = RequireSendable<NonSendableClass>
16+
17+
extension SendableStruct: @retroactive @unchecked Sendable {}
18+
// expected-warning@-1 {{conformance of 'SendableStruct' to protocol 'Sendable' was already stated in the type's module 'SendableConformances'}}
19+
20+
@available(*, unavailable)
21+
extension AnotherSendableStruct: @retroactive @unchecked Sendable {}
22+
// expected-warning@-1 {{conformance of 'AnotherSendableStruct' to protocol 'Sendable' was already stated in the type's module 'SendableConformances'}}
23+
24+
typealias CheckAnotherSendableStruct = RequireSendable<AnotherSendableStruct>

test/Concurrency/sendable_checking.swift

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -102,21 +102,24 @@ public actor MyActor: MyProto {
102102
}
103103

104104
// Make sure the generic signature doesn't minimize away Sendable requirements.
105-
@_nonSendable class NSClass { }
105+
class NSClass { }
106+
107+
@available(*, unavailable)
108+
extension NSClass: @unchecked Sendable {} // expected-note {{conformance of 'NSClass' to 'Sendable' has been explicitly marked unavailable here}}
106109

107110
struct WrapClass<T: NSClass> {
108111
var t: T
109112
}
110113

111114
extension WrapClass: Sendable where T: Sendable { }
112115

113-
// Make sure we don't inherit the unavailable Sendable conformance from
114-
// our superclass.
116+
// expected-warning@+2 {{conformance of 'SendableSubclass' to protocol 'Sendable' is already unavailable}}
117+
// expected-note@+1 {{'SendableSubclass' inherits conformance to protocol 'Sendable' from superclass here}}
115118
class SendableSubclass: NSClass, @unchecked Sendable { }
116119

117120
@available(SwiftStdlib 5.1, *)
118121
func testSubclassing(obj: SendableSubclass) async {
119-
acceptCV(obj) // okay!
122+
acceptCV(obj) // expected-warning {{conformance of 'NSClass' to 'Sendable' is unavailable; this is an error in the Swift 6 language mode}}
120123
}
121124

122125

test/Concurrency/sendable_conformance_checking.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,7 @@ extension SendableExtSub: @unchecked Sendable {}
180180

181181
// Still want to know about same-class redundancy
182182
class MultiConformance: @unchecked Sendable {} // expected-note {{'MultiConformance' declares conformance to protocol 'Sendable' here}}
183-
extension MultiConformance: @unchecked Sendable {} // expected-error {{redundant conformance of 'MultiConformance' to protocol 'Sendable'}}
183+
extension MultiConformance: @unchecked Sendable {} // expected-warning {{redundant conformance of 'MultiConformance' to protocol 'Sendable'}}
184184

185185
@available(SwiftStdlib 5.1, *)
186186
actor MyActor {

0 commit comments

Comments
 (0)