Skip to content

Commit 95500bb

Browse files
committed
Warn about derived Hashable implementation if there's a custom Equatable
1 parent fc24be2 commit 95500bb

5 files changed

+136
-7
lines changed

include/swift/AST/DiagnosticsSema.def

+5
Original file line numberDiff line numberDiff line change
@@ -5964,6 +5964,11 @@ ERROR(override_nsobject_hash_error,none,
59645964
"'NSObject.hash(into:)' is not overridable; "
59655965
"did you mean to override 'NSObject.hash'?", ())
59665966

5967+
WARNING(synthesized_hashable_may_not_match_custom_equatable, none,
5968+
"automatically generated 'Hashable' implementation for type %0 "
5969+
"may not match the behavior of custom '==' operator", (Type))
5970+
NOTE(add_custom_hashable, none, "add a custom 'hash(into:)' method", ())
5971+
59675972
WARNING(hashvalue_implementation,Deprecation,
59685973
"'Hashable.hashValue' is deprecated as a protocol requirement; "
59695974
"conform type %0 to 'Hashable' by implementing 'hash(into:)' instead",

lib/Sema/DerivedConformanceEquatableHashable.cpp

+28-7
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
//===----------------------------------------------------------------------===//
1212
//
1313
// This file implements implicit derivation of the Equatable and Hashable
14-
// protocols.
14+
// protocols.
1515
//
1616
//===----------------------------------------------------------------------===//
1717

@@ -232,7 +232,7 @@ deriveBodyEquatable_enum_hasAssociatedValues_eq(AbstractFunctionDecl *eqDecl,
232232
auto rhsVar = rhsPayloadVars[varIdx];
233233
auto rhsExpr = new (C) DeclRefExpr(rhsVar, DeclNameLoc(),
234234
/*Implicit*/true);
235-
auto guardStmt = DerivedConformance::returnFalseIfNotEqualGuard(C,
235+
auto guardStmt = DerivedConformance::returnFalseIfNotEqualGuard(C,
236236
lhsExpr, rhsExpr);
237237
statementsInCase.emplace_back(guardStmt);
238238
}
@@ -502,7 +502,7 @@ static CallExpr *createHasherCombineCall(ASTContext &C,
502502
// hasher.combine(_:)
503503
auto *combineCall = UnresolvedDotExpr::createImplicit(
504504
C, hasherExpr, C.Id_combine, {Identifier()});
505-
505+
506506
// hasher.combine(hashable)
507507
auto *argList = ArgumentList::forImplicitUnlabeled(C, {hashable});
508508
return CallExpr::createImplicit(C, combineCall, argList);
@@ -1007,14 +1007,14 @@ ValueDecl *DerivedConformance::deriveHashable(ValueDecl *requirement) {
10071007
if (hashValueDecl->isImplicit()) {
10081008
// Neither hashValue nor hash(into:) is explicitly defined; we need to do
10091009
// a full Hashable derivation.
1010-
1010+
10111011
// Refuse to synthesize Hashable if type isn't a struct or enum, or if it
10121012
// has non-Hashable stored properties/associated values.
10131013
auto hashableProto = C.getProtocol(KnownProtocolKind::Hashable);
1014+
auto nominalTy = Nominal->getDeclaredType();
10141015
if (!canDeriveConformance(getConformanceContext(), Nominal,
10151016
hashableProto)) {
1016-
ConformanceDecl->diagnose(diag::type_does_not_conform,
1017-
Nominal->getDeclaredType(),
1017+
ConformanceDecl->diagnose(diag::type_does_not_conform, nominalTy,
10181018
hashableProto->getDeclaredInterfaceType());
10191019
// Ideally, this would be diagnosed in
10201020
// ConformanceChecker::resolveWitnessViaLookup. That doesn't work for
@@ -1029,6 +1029,27 @@ ValueDecl *DerivedConformance::deriveHashable(ValueDecl *requirement) {
10291029
if (checkAndDiagnoseDisallowedContext(requirement))
10301030
return nullptr;
10311031

1032+
// Warn the user that having a custom == almost surely require a
1033+
// custom Hashable conformance even if, for source stability reasons,
1034+
// it will be synthesized
1035+
auto equatableProto = C.getProtocol(KnownProtocolKind::Equatable);
1036+
auto eqConformance = TypeChecker::conformsToProtocol(nominalTy,
1037+
equatableProto, getParentModule());
1038+
if (eqConformance) {
1039+
DeclName equalsName(C, DeclBaseName(C.Id_EqualsOperator),
1040+
{Identifier(), Identifier()});
1041+
ConcreteDeclRef witness = eqConformance.getWitnessByName(
1042+
nominalTy->getRValueType(), equalsName);
1043+
1044+
auto equalsDeclaration = witness.getDecl();
1045+
if (equalsDeclaration && !equalsDeclaration->isImplicit()) {
1046+
equalsDeclaration->diagnose(
1047+
diag::synthesized_hashable_may_not_match_custom_equatable,
1048+
nominalTy);
1049+
equalsDeclaration->diagnose(diag::add_custom_hashable);
1050+
}
1051+
}
1052+
10321053
if (auto ED = dyn_cast<EnumDecl>(Nominal)) {
10331054
std::pair<BraceStmt *, bool> (*bodySynthesizer)(
10341055
AbstractFunctionDecl *, void *);
@@ -1044,7 +1065,7 @@ ValueDecl *DerivedConformance::deriveHashable(ValueDecl *requirement) {
10441065
&deriveBodyHashable_struct_hashInto);
10451066
else // This should've been caught by canDeriveHashable above.
10461067
llvm_unreachable("Attempt to derive Hashable for a type other "
1047-
"than a struct or enum");
1068+
"than a struct or enum");
10481069
} else {
10491070
// hashValue has an explicit implementation, but hash(into:) doesn't.
10501071
// Emit a deprecation warning, then derive hash(into:) in terms of
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
public struct ImplicitEquatable: ExpressibleByIntegerLiteral, Equatable {
2+
public init(integerLiteral: Int) {}
3+
}
4+
5+
public struct ExplicitEquatable: ExpressibleByIntegerLiteral, Equatable {
6+
public init(integerLiteral: Int) {}
7+
8+
public static func == (lhs: ExplicitEquatable, rhs: ExplicitEquatable) -> Bool {
9+
return true
10+
}
11+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
// RUN: %empty-directory(%t)
2+
// RUN: %target-swift-frontend -emit-module -o %t/NormalLibrary.swiftmodule %S/Inputs/external_equatable_conformance.swift
3+
4+
// RUN: %target-typecheck-verify-swift -I %t
5+
6+
import NormalLibrary
7+
8+
// All conformance synthesized no warning emitted
9+
struct NoCustomEquatable: Hashable {
10+
let a:Int
11+
}
12+
13+
// Equatable implemented and Hashable synthesized raise a warning
14+
enum CustomEquatable: Hashable {
15+
case a
16+
17+
static func == (lhs: CustomEquatable, rhs: CustomEquatable) -> Bool {
18+
// expected-warning@-1 {{automatically generated 'Hashable' implementation for type 'CustomEquatable' may not match the behavior of custom '==' operator}}
19+
// expected-note@-2 {{add a custom 'hash(into:)' method}}
20+
return true
21+
}
22+
}
23+
24+
// All conformance implemented no warning emitted
25+
enum CustomHashable: Hashable {
26+
case a
27+
28+
func hash(into hasher: inout Hasher) {}
29+
30+
static func == (lhs: CustomHashable, rhs: CustomHashable) -> Bool {
31+
return true
32+
}
33+
}
34+
35+
struct ExtendedConformance: Hashable {
36+
let a:Int
37+
}
38+
39+
extension ExtendedConformance {
40+
static func == (lhs: ExtendedConformance, rhs: ExtendedConformance) -> Bool {
41+
// expected-warning@-1 {{automatically generated 'Hashable' implementation for type 'ExtendedConformance' may not match the behavior of custom '==' operator}}
42+
// expected-note@-2 {{add a custom 'hash(into:)' method}}
43+
return true
44+
}
45+
}
46+
47+
enum ImportedConformance: ImplicitEquatable {
48+
case x = 1
49+
}
50+
51+
enum ImportedExplicitConformance: ExplicitEquatable {
52+
case x = 1
53+
}
54+
55+
protocol DefaultedImplementationProtocol: Equatable {}
56+
extension DefaultedImplementationProtocol {
57+
static func == (lhs: Self, rhs: Self) -> Bool { true }
58+
// expected-warning@-1 {{automatically generated 'Hashable' implementation for type 'DefaultedImplementation' may not match the behavior of custom '==' operator}}
59+
// expected-note@-2 {{add a custom 'hash(into:)' method}}
60+
}
61+
62+
struct DefaultedImplementation: DefaultedImplementationProtocol, Hashable {
63+
let a: Int
64+
}
65+
66+
protocol ConditionalImplementationProtocol {}
67+
extension ConditionalImplementationProtocol where Self: Equatable {
68+
static func == (lhs: Self, rhs: Self) -> Bool { true }
69+
// expected-warning@-1 {{automatically generated 'Hashable' implementation for type 'ConditionalImplementation' may not match the behavior of custom '==' operator}}
70+
// expected-note@-2 {{add a custom 'hash(into:)' method}}
71+
}
72+
73+
struct ConditionalImplementation: ConditionalImplementationProtocol, Hashable {
74+
let a: Int
75+
}
76+
77+
protocol ConditionalHashableImplementationProtocol {}
78+
extension ConditionalHashableImplementationProtocol where Self: Equatable {
79+
static func == (lhs: Self, rhs: Self) -> Bool { true }
80+
// expected-warning@-1 {{automatically generated 'Hashable' implementation for type 'ConditionalHashableImplementation' may not match the behavior of custom '==' operator}}
81+
// expected-note@-2 {{add a custom 'hash(into:)' method}}
82+
}
83+
84+
extension ConditionalHashableImplementationProtocol where Self: Hashable {
85+
func hash(into hasher: Hasher) {}
86+
}
87+
88+
struct ConditionalHashableImplementation: ConditionalHashableImplementationProtocol, Hashable {
89+
let a: Int
90+
}

test/Sema/enum_conformance_synthesis.swift

+2
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,8 @@ public enum Medicine {
220220
extension Medicine : Equatable {}
221221

222222
public func ==(lhs: Medicine, rhs: Medicine) -> Bool {
223+
// expected-warning@-1 {{automatically generated 'Hashable' implementation for type 'Medicine' may not match the behavior of custom '==' operator}}
224+
// expected-note@-2 {{add a custom 'hash(into:)' method}}
223225
return true
224226
}
225227

0 commit comments

Comments
 (0)