Skip to content

[AST] Choose an implied conformance source next to the type, if possible. #17832

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
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
13 changes: 13 additions & 0 deletions lib/AST/ConformanceLookupTable.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -672,6 +672,19 @@ ConformanceLookupTable::Ordering ConformanceLookupTable::compareConformances(
: Ordering::After;
}

// If one of the conformances comes from the same file as the type
// declaration, pick that one; this is so that conformance synthesis works if
// there's any implied conformance in the same file as the type.
auto NTD =
lhs->getDeclContext()->getAsNominalTypeOrNominalTypeExtensionContext();
auto typeSF = NTD->getParentSourceFile();
if (typeSF) {
if (typeSF == lhsSF)
return Ordering::Before;
if (typeSF == rhsSF)
return Ordering::After;
}

// Otherwise, pick the earlier file unit.
auto lhsFileUnit
= dyn_cast<FileUnit>(lhs->getDeclContext()->getModuleScopeContext());
Expand Down
6 changes: 6 additions & 0 deletions test/Sema/Inputs/enum_conformance_synthesis_other.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,9 @@ enum GenericOtherFileNonconforming<T> {
// expected-note@-1 {{type declared here}}
case A(T)
}

protocol ImplierOther: Equatable {}
extension ImpliedMain: ImplierMain {}
enum ImpliedOther: ImplierOther {
case a(Int)
}
4 changes: 4 additions & 0 deletions test/Sema/Inputs/struct_equatable_hashable_other.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,7 @@ struct GenericOtherFileNonconforming<T> {
// expected-note@-1{{type declared here}}
let v: T
}

protocol ImplierOther: Equatable {}
extension ImpliedMain: ImplierOther {}
struct ImpliedOther: ImplierOther {}
138 changes: 87 additions & 51 deletions test/Sema/enum_conformance_synthesis.swift
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
// RUN: %empty-directory(%t)
// RUN: cp %s %t/main.swift
// RUN: %target-swift-frontend -typecheck -verify -primary-file %t/main.swift %S/Inputs/enum_conformance_synthesis_other.swift -verify-ignore-unknown -swift-version 4
// RUN: %target-swift-frontend -typecheck -verify -primary-file %s %S/Inputs/enum_conformance_synthesis_other.swift -verify-ignore-unknown -swift-version 4

var hasher = Hasher()

enum Foo: CaseIterable {
case A, B
}

if Foo.A == .B { }
var aHash: Int = Foo.A.hashValue
Foo.A.hash(into: &hasher)
_ = Foo.allCases
func foo() {
if Foo.A == .B { }
var _: Int = Foo.A.hashValue
Foo.A.hash(into: &hasher)
_ = Foo.allCases

Foo.A == Foo.B // expected-warning {{result of operator '==' is unused}}
Foo.A == Foo.B // expected-warning {{result of operator '==' is unused}}
}

enum Generic<T>: CaseIterable {
case A, B
Expand All @@ -25,10 +25,12 @@ enum Generic<T>: CaseIterable {
}
}

if Generic<Foo>.A == .B { }
var gaHash: Int = Generic<Foo>.A.hashValue
Generic<Foo>.A.hash(into: &hasher)
_ = Generic<Foo>.allCases
func generic() {
if Generic<Foo>.A == .B { }
var _: Int = Generic<Foo>.A.hashValue
Generic<Foo>.A.hash(into: &hasher)
_ = Generic<Foo>.allCases
}

func localEnum() -> Bool {
enum Local {
Expand All @@ -47,9 +49,11 @@ func ==(x: CustomHashable, y: CustomHashable) -> Bool { // expected-note 5 {{non
return true
}

if CustomHashable.A == .B { }
var custHash: Int = CustomHashable.A.hashValue
CustomHashable.A.hash(into: &hasher)
func customHashable() {
if CustomHashable.A == .B { }
var _: Int = CustomHashable.A.hashValue
CustomHashable.A.hash(into: &hasher)
}

// We still synthesize conforming overloads of '==' and 'hashValue' if
// explicit definitions don't satisfy the protocol requirements. Probably
Expand All @@ -62,11 +66,14 @@ enum InvalidCustomHashable {
func ==(x: InvalidCustomHashable, y: InvalidCustomHashable) -> String { // expected-note 5 {{non-matching type}}
return ""
}
if InvalidCustomHashable.A == .B { }
var s: String = InvalidCustomHashable.A == .B
s = InvalidCustomHashable.A.hashValue
var i: Int = InvalidCustomHashable.A.hashValue
InvalidCustomHashable.A.hash(into: &hasher)
func invalidCustomHashable() {
if InvalidCustomHashable.A == .B { }
var s: String = InvalidCustomHashable.A == .B
s = InvalidCustomHashable.A.hashValue
_ = s
var _: Int = InvalidCustomHashable.A.hashValue
InvalidCustomHashable.A.hash(into: &hasher)
}

// Check use of an enum's synthesized members before the enum is actually declared.
struct UseEnumBeforeDeclaration {
Expand All @@ -77,25 +84,30 @@ enum EnumToUseBeforeDeclaration {
case A
}

// Check enums from another file in the same module.
if FromOtherFile.A == .A {}
let _: Int = FromOtherFile.A.hashValue

func getFromOtherFile() -> AlsoFromOtherFile { return .A }
if .A == getFromOtherFile() {}

func overloadFromOtherFile() -> YetAnotherFromOtherFile { return .A }
func overloadFromOtherFile() -> Bool { return false }
if .A == overloadFromOtherFile() {}

func useEnumBeforeDeclaration() {
// Check enums from another file in the same module.
if FromOtherFile.A == .A {}
let _: Int = FromOtherFile.A.hashValue

if .A == getFromOtherFile() {}

if .A == overloadFromOtherFile() {}
}

// Complex enums are not automatically Equatable, Hashable, or CaseIterable.
enum Complex {
case A(Int)
case B
}

if Complex.A(1) == .B { } // expected-error{{binary operator '==' cannot be applied to operands of type 'Complex' and '_'}}
// expected-note @-1 {{overloads for '==' exist with these partially matching parameter lists: }}
func complex() {
if Complex.A(1) == .B { } // expected-error{{binary operator '==' cannot be applied to operands of type 'Complex' and '_'}}
// expected-note @-1 {{overloads for '==' exist with these partially matching parameter lists: }}
}

// Enums with equatable payloads are equatable if they explicitly conform.
enum EnumWithEquatablePayload: Equatable {
Expand All @@ -104,9 +116,11 @@ enum EnumWithEquatablePayload: Equatable {
case C
}

if EnumWithEquatablePayload.A(1) == .B("x", 1) { }
if EnumWithEquatablePayload.A(1) == .C { }
if EnumWithEquatablePayload.B("x", 1) == .C { }
func enumWithEquatablePayload() {
if EnumWithEquatablePayload.A(1) == .B("x", 1) { }
if EnumWithEquatablePayload.A(1) == .C { }
if EnumWithEquatablePayload.B("x", 1) == .C { }
}

// Enums with hashable payloads are hashable if they explicitly conform.
enum EnumWithHashablePayload: Hashable {
Expand All @@ -115,18 +129,20 @@ enum EnumWithHashablePayload: Hashable {
case C
}

_ = EnumWithHashablePayload.A(1).hashValue
_ = EnumWithHashablePayload.B("x", 1).hashValue
_ = EnumWithHashablePayload.C.hashValue
func enumWithHashablePayload() {
_ = EnumWithHashablePayload.A(1).hashValue
_ = EnumWithHashablePayload.B("x", 1).hashValue
_ = EnumWithHashablePayload.C.hashValue

EnumWithHashablePayload.A(1).hash(into: &hasher)
EnumWithHashablePayload.B("x", 1).hash(into: &hasher)
EnumWithHashablePayload.C.hash(into: &hasher)
EnumWithHashablePayload.A(1).hash(into: &hasher)
EnumWithHashablePayload.B("x", 1).hash(into: &hasher)
EnumWithHashablePayload.C.hash(into: &hasher)

// ...and they should also inherit equatability from Hashable.
if EnumWithHashablePayload.A(1) == .B("x", 1) { }
if EnumWithHashablePayload.A(1) == .C { }
if EnumWithHashablePayload.B("x", 1) == .C { }
// ...and they should also inherit equatability from Hashable.
if EnumWithHashablePayload.A(1) == .B("x", 1) { }
if EnumWithHashablePayload.A(1) == .C { }
if EnumWithHashablePayload.B("x", 1) == .C { }
}

// Enums with non-hashable payloads don't derive conformance.
struct NotHashable {}
Expand All @@ -140,18 +156,22 @@ enum GenericHashable<T: Hashable>: Hashable {
case A(T)
case B
}
if GenericHashable<String>.A("a") == .B { }
var genericHashableHash: Int = GenericHashable<String>.A("a").hashValue
func genericHashable() {
if GenericHashable<String>.A("a") == .B { }
var _: Int = GenericHashable<String>.A("a").hashValue
}

// But it should be an error if the generic argument doesn't have the necessary
// constraints to satisfy the conditions for derivation.
enum GenericNotHashable<T: Equatable>: Hashable { // expected-error 2 {{does not conform to protocol 'Hashable'}}
case A(T)
case B
}
if GenericNotHashable<String>.A("a") == .B { }
let _: Int = GenericNotHashable<String>.A("a").hashValue // No error. hashValue is always synthesized, even if Hashable derivation fails
GenericNotHashable<String>.A("a").hash(into: &hasher) // expected-error {{value of type 'GenericNotHashable<String>' has no member 'hash'}}
func genericNotHashable() {
if GenericNotHashable<String>.A("a") == .B { }
let _: Int = GenericNotHashable<String>.A("a").hashValue // No error. hashValue is always synthesized, even if Hashable derivation fails
GenericNotHashable<String>.A("a").hash(into: &hasher) // expected-error {{value of type 'GenericNotHashable<String>' has no member 'hash'}}
}

// An enum with no cases should not derive conformance.
enum NoCases: Hashable {} // expected-error 2 {{does not conform}}
Expand Down Expand Up @@ -193,13 +213,17 @@ public enum Medicine {

extension Medicine : Equatable {}

public func ==(lhs: Medicine, rhs: Medicine) -> Bool { // expected-note 4 {{non-matching type}}
public func ==(lhs: Medicine, rhs: Medicine) -> Bool { // expected-note 5 {{non-matching type}}
return true
}

// No explicit conformance; but it can be derived, for the same-file cases.
extension Complex : Hashable {}
extension Complex : CaseIterable {} // expected-error {{type 'Complex' does not conform to protocol 'CaseIterable'}}
enum Complex2 {
case A(Int)
case B
}
extension Complex2 : Hashable {}
extension Complex2 : CaseIterable {} // expected-error {{type 'Complex2' does not conform to protocol 'CaseIterable'}}
extension FromOtherFile: CaseIterable {} // expected-error {{cannot be automatically synthesized in an extension in a different file to the type}} expected-error {{does not conform to protocol 'CaseIterable'}}

// No explicit conformance and it cannot be derived.
Expand All @@ -212,7 +236,7 @@ extension NotExplicitlyHashableAndCannotDerive : CaseIterable {} // expected-err
// Verify that conformance (albeit manually implemented) can still be added to
// a type in a different file.
extension OtherFileNonconforming: Hashable {
static func ==(lhs: OtherFileNonconforming, rhs: OtherFileNonconforming) -> Bool { // expected-note 4 {{non-matching type}}
static func ==(lhs: OtherFileNonconforming, rhs: OtherFileNonconforming) -> Bool { // expected-note 5 {{non-matching type}}
return true
}
var hashValue: Int { return 0 }
Expand Down Expand Up @@ -290,6 +314,18 @@ extension UnusedGenericDeriveExtension: Hashable {}
extension GenericOtherFileNonconforming: Equatable where T: Equatable {}
// expected-error@-1{{implementation of 'Equatable' cannot be automatically synthesized in an extension in a different file to the type}}

// rdar://problem/41852654

// There is a conformance to Equatable (or at least, one that implies Equatable)
// in the same file as the type, so the synthesis is okay. Both orderings are
// tested, to catch choosing extensions based on the order of the files, etc.
protocol ImplierMain: Equatable {}
enum ImpliedMain: ImplierMain {
case a(Int)
}
extension ImpliedOther: ImplierMain {}


// FIXME: Remove -verify-ignore-unknown.
// <unknown>:0: error: unexpected error produced: invalid redeclaration of 'hashValue'
// <unknown>:0: error: unexpected note produced: candidate has non-matching type '(Foo, Foo) -> Bool'
Expand Down
Loading