Skip to content

Commit c98ddb9

Browse files
committed
Sema: Warn about non-final classes conforming to protocols with a same-type requirement on 'Self'
This is unsound because it breaks covariance of protocol conformances on classes. See the test case and changelog entry for details.
1 parent be587c3 commit c98ddb9

File tree

6 files changed

+83
-5
lines changed

6 files changed

+83
-5
lines changed

CHANGELOG.md

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,48 @@ _**Note:** This is in reverse chronological order, so newer entries are added to
55

66
## Swift 5.7
77

8+
* The compiler now emits a warning when a non-final class conforms to a protocol that imposes a same-type requirement between `Self` and an associated type. This is because such a requirement makes the conformance unsound for subclasses.
9+
10+
For example, Swift 5.6 would allow the following code, which at runtime would construct an instanec of `C` and not `SubC` as expected:
11+
12+
```swift
13+
protocol P {
14+
associatedtype A : Q where Self == Self.A.B
15+
}
16+
17+
protocol Q {
18+
associatedtype B
19+
20+
static func getB() -> B
21+
}
22+
23+
class C : P {
24+
typealias A = D
25+
}
26+
27+
class D : Q {
28+
typealias B = C
29+
30+
static func getB() -> C { return C() }
31+
}
32+
33+
extension P {
34+
static func getAB() -> Self {
35+
// This is well-typed, because `Self.A.getB()` returns
36+
// `Self.A.B`, which is equivalent to `Self`.
37+
return Self.A.getB()
38+
}
39+
}
40+
41+
class SubC : C {}
42+
43+
// P.getAB() declares a return type of `Self`, so it should
44+
// return `SubC`, but it actually returns a `C`.
45+
print(SubC.getAB())
46+
```
47+
48+
To make the above example correct, either the class `C` needs to become `final` (in which case `SubC` cannot be declared) or protocol `P` needs to be re-designed to not include the same-type requirement `Self == Self.A.B`.
49+
850
* [SE-0341][]:
951

1052
Opaque types can now be used in the parameters of functions and subscripts, wher they provide a shorthand syntax for the introduction of a generic parameter. For example, the following:

include/swift/AST/DiagnosticsSema.def

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1942,6 +1942,11 @@ ERROR(type_does_not_conform,none,
19421942
WARNING(type_does_not_conform_swiftui_warning,none,
19431943
"type %0 does not conform to protocol %1", (Type, Type))
19441944

1945+
ERROR(non_final_class_cannot_conform_to_self_same_type,none,
1946+
"non-final class %0 cannot safely conform to protocol %1, "
1947+
"which requires that 'Self' is exactly equal to %2",
1948+
(Type, Type, Type))
1949+
19451950
ERROR(cannot_use_nil_with_this_type,none,
19461951
"'nil' cannot be used in context expecting type %0", (Type))
19471952

lib/Sema/TypeCheckProtocol.cpp

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4853,18 +4853,42 @@ void ConformanceChecker::ensureRequirementsAreSatisfied() {
48534853

48544854
CheckedRequirementSignature = true;
48554855

4856+
auto &diags = proto->getASTContext().Diags;
4857+
48564858
auto DC = Conformance->getDeclContext();
48574859
auto substitutingType = DC->mapTypeIntoContext(Conformance->getType());
48584860
auto substitutions = SubstitutionMap::getProtocolSubstitutions(
48594861
proto, substitutingType, ProtocolConformanceRef(Conformance));
48604862

4863+
auto reqSig = proto->getRequirementSignature().getRequirements();
4864+
48614865
auto result = TypeChecker::checkGenericArguments(
48624866
DC->getParentModule(), Loc, Loc,
48634867
// FIXME: maybe this should be the conformance's type
48644868
proto->getDeclaredInterfaceType(),
48654869
{ proto->getSelfInterfaceType() },
4866-
proto->getRequirementSignature().getRequirements(),
4867-
QuerySubstitutionMap{substitutions});
4870+
reqSig, QuerySubstitutionMap{substitutions});
4871+
4872+
// Non-final classes should not be able to conform to protocols with a
4873+
// same-type requirement on 'Self', since such a conformance would no
4874+
// longer be covariant. For now, this is a warning. Once this becomes
4875+
// an error, we can handle it as part of the above checkGenericArguments()
4876+
// call by passing in a superclass-bound archetype for the 'self' type
4877+
// instead of the concrete class type itself.
4878+
if (auto *classDecl = Adoptee->getClassOrBoundGenericClass()) {
4879+
if (!classDecl->isSemanticallyFinal()) {
4880+
for (auto req : reqSig) {
4881+
if (req.getKind() == RequirementKind::SameType &&
4882+
req.getFirstType()->isEqual(proto->getSelfInterfaceType())) {
4883+
diags.diagnose(Loc, diag::non_final_class_cannot_conform_to_self_same_type,
4884+
Adoptee, proto->getDeclaredInterfaceType(),
4885+
req.getSecondType())
4886+
.warnUntilSwiftVersion(6);
4887+
break;
4888+
}
4889+
}
4890+
}
4891+
}
48684892

48694893
switch (result) {
48704894
case RequirementCheckResult::Success:
@@ -4879,9 +4903,9 @@ void ConformanceChecker::ensureRequirementsAreSatisfied() {
48794903
// Diagnose the failure generically.
48804904
// FIXME: Would be nice to give some more context here!
48814905
if (!Conformance->isInvalid()) {
4882-
proto->getASTContext().Diags.diagnose(Loc, diag::type_does_not_conform,
4883-
Adoptee,
4884-
Proto->getDeclaredInterfaceType());
4906+
diags.diagnose(Loc, diag::type_does_not_conform,
4907+
Adoptee,
4908+
Proto->getDeclaredInterfaceType());
48854909
Conformance->setInvalid();
48864910
}
48874911
return;

test/Generics/sr13884.swift renamed to test/Generics/non_final_class_conforms_same_type_requirement_on_self.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,11 @@ public protocol Q {
99
associatedtype B
1010
}
1111

12+
// This is rejected, because 'A.B == Self' means that 'Self' must
13+
// exactly equal C; since C is not final, this means the conformance
14+
// is not covariant.
1215
public class C : P {
16+
// expected-warning@-1 {{non-final class 'C' cannot safely conform to protocol 'P', which requires that 'Self' is exactly equal to 'Self.A.B'; this is an error in Swift 6}}
1317
public typealias A = D
1418
}
1519

test/Generics/rdar80503090.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ extension P where T : Q {
1919
}
2020

2121
class C : P {}
22+
// expected-warning@-1 {{non-final class 'C' cannot safely conform to protocol 'P', which requires that 'Self' is exactly equal to 'Self.T'; this is an error in Swift 6}}
2223

2324
extension P where T : C {
2425
// CHECK-LABEL: Generic signature: <Self where Self == C>

validation-test/compiler_crashers_2_fixed/0189-sr10033.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@ extension P2 {
1414
}
1515

1616
class C1 : P1 {
17+
// expected-warning@-1 {{non-final class 'C1' cannot safely conform to protocol 'P1', which requires that 'Self' is exactly equal to 'Self.A2.A1'; this is an error in Swift 6}}
1718
class A2 : P2 {
19+
// expected-warning@-1 {{non-final class 'C1.A2' cannot safely conform to protocol 'P2', which requires that 'Self' is exactly equal to 'Self.A1.A2'; this is an error in Swift 6}}
1820
typealias A1 = C1
1921
}
2022
}

0 commit comments

Comments
 (0)