Skip to content

Commit d6be703

Browse files
committed
Warn about derived Hashable implementation if there's a custom Equatable
1 parent 0807812 commit d6be703

5 files changed

+136
-7
lines changed

include/swift/AST/DiagnosticsSema.def

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

6129+
WARNING(synthesized_hashable_may_not_match_custom_equatable, none,
6130+
"automatically generated 'Hashable' implementation for type %0 "
6131+
"may not match the behavior of custom '==' operator", (Type))
6132+
NOTE(add_custom_hashable, none, "add a custom 'hash(into:)' method", ())
6133+
61296134
WARNING(hashvalue_implementation,Deprecation,
61306135
"'Hashable.hashValue' is deprecated as a protocol requirement; "
61316136
"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);
@@ -1006,14 +1006,14 @@ ValueDecl *DerivedConformance::deriveHashable(ValueDecl *requirement) {
10061006
if (hashValueDecl->isImplicit()) {
10071007
// Neither hashValue nor hash(into:) is explicitly defined; we need to do
10081008
// a full Hashable derivation.
1009-
1009+
10101010
// Refuse to synthesize Hashable if type isn't a struct or enum, or if it
10111011
// has non-Hashable stored properties/associated values.
10121012
auto hashableProto = C.getProtocol(KnownProtocolKind::Hashable);
1013+
auto nominalTy = Nominal->getDeclaredType();
10131014
if (!canDeriveConformance(getConformanceContext(), Nominal,
10141015
hashableProto)) {
1015-
ConformanceDecl->diagnose(diag::type_does_not_conform,
1016-
Nominal->getDeclaredType(),
1016+
ConformanceDecl->diagnose(diag::type_does_not_conform, nominalTy,
10171017
hashableProto->getDeclaredInterfaceType());
10181018
// Ideally, this would be diagnosed in
10191019
// ConformanceChecker::resolveWitnessViaLookup. That doesn't work for
@@ -1028,6 +1028,27 @@ ValueDecl *DerivedConformance::deriveHashable(ValueDecl *requirement) {
10281028
if (checkAndDiagnoseDisallowedContext(requirement))
10291029
return nullptr;
10301030

1031+
// Warn the user that having a custom == almost surely require a
1032+
// custom Hashable conformance even if, for source stability reasons,
1033+
// it will be synthesized
1034+
auto equatableProto = C.getProtocol(KnownProtocolKind::Equatable);
1035+
auto eqConformance = TypeChecker::conformsToProtocol(nominalTy,
1036+
equatableProto, getParentModule());
1037+
if (eqConformance) {
1038+
DeclName equalsName(C, DeclBaseName(C.Id_EqualsOperator),
1039+
{Identifier(), Identifier()});
1040+
ConcreteDeclRef witness = eqConformance.getWitnessByName(
1041+
nominalTy->getRValueType(), equalsName);
1042+
1043+
auto equalsDeclaration = witness.getDecl();
1044+
if (equalsDeclaration && !equalsDeclaration->isImplicit()) {
1045+
equalsDeclaration->diagnose(
1046+
diag::synthesized_hashable_may_not_match_custom_equatable,
1047+
nominalTy);
1048+
equalsDeclaration->diagnose(diag::add_custom_hashable);
1049+
}
1050+
}
1051+
10311052
if (auto ED = dyn_cast<EnumDecl>(Nominal)) {
10321053
std::pair<BraceStmt *, bool> (*bodySynthesizer)(
10331054
AbstractFunctionDecl *, void *);
@@ -1043,7 +1064,7 @@ ValueDecl *DerivedConformance::deriveHashable(ValueDecl *requirement) {
10431064
&deriveBodyHashable_struct_hashInto);
10441065
else // This should've been caught by canDeriveHashable above.
10451066
llvm_unreachable("Attempt to derive Hashable for a type other "
1046-
"than a struct or enum");
1067+
"than a struct or enum");
10471068
} else {
10481069
// hashValue has an explicit implementation, but hash(into:) doesn't.
10491070
// 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)