Skip to content

Commit c3ad843

Browse files
authored
Add cmpopts.EquateComparable (#340)
This helper function makes it easier to specify that comparable types are safe to directly compare with the == operator in Go. The API does not use generics as it follows existing options like cmp.AllowUnexported, cmpopts.IgnoreUnexported, or cmpopts.IgnoreTypes. While generics provides type safety, the user experience is not as nice. Our current API allows multiple types to be specified: cmpopts.EquateComparable(netip.Addr{}, netip.Prefix{}) While generics would not allow variadic arguments: cmpopts.EquateComparable[netip.Addr]() cmpopts.EquateComparable[netip.Prefix]() Bump mininimum supported Go to 1.18 for net/netip type. Start testing on Go 1.21. Fixes #339
1 parent e250a55 commit c3ad843

File tree

4 files changed

+64
-2
lines changed

4 files changed

+64
-2
lines changed

.github/workflows/test.yml

+2-2
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ jobs:
66
test:
77
strategy:
88
matrix:
9-
go-version: [1.13.x, 1.14.x, 1.15.x, 1.16.x, 1.17.x, 1.18.x, 1.19.x, 1.20.x]
9+
go-version: [1.18.x, 1.19.x, 1.20.x, 1.21.x]
1010
os: [ubuntu-latest, macos-latest]
1111
runs-on: ${{ matrix.os }}
1212
steps:
@@ -19,5 +19,5 @@ jobs:
1919
- name: Test
2020
run: go test -v -race ./...
2121
- name: Format
22-
if: matrix.go-version == '1.20.x'
22+
if: matrix.go-version == '1.21.x'
2323
run: diff -u <(echo -n) <(gofmt -d .)

cmp/cmpopts/equate.go

+29
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ package cmpopts
77

88
import (
99
"errors"
10+
"fmt"
1011
"math"
1112
"reflect"
1213
"time"
@@ -154,3 +155,31 @@ func compareErrors(x, y interface{}) bool {
154155
ye := y.(error)
155156
return errors.Is(xe, ye) || errors.Is(ye, xe)
156157
}
158+
159+
// EquateComparable returns a [cmp.Option] that determines equality
160+
// of comparable types by directly comparing them using the == operator in Go.
161+
// The types to compare are specified by passing a value of that type.
162+
// This option should only be used on types that are documented as being
163+
// safe for direct == comparison. For example, [net/netip.Addr] is documented
164+
// as being semantically safe to use with ==, while [time.Time] is documented
165+
// to discourage the use of == on time values.
166+
func EquateComparable(typs ...interface{}) cmp.Option {
167+
types := make(typesFilter)
168+
for _, typ := range typs {
169+
switch t := reflect.TypeOf(typ); {
170+
case !t.Comparable():
171+
panic(fmt.Sprintf("%T is not a comparable Go type", typ))
172+
case types[t]:
173+
panic(fmt.Sprintf("%T is already specified", typ))
174+
default:
175+
types[t] = true
176+
}
177+
}
178+
return cmp.FilterPath(types.filter, cmp.Comparer(equateAny))
179+
}
180+
181+
type typesFilter map[reflect.Type]bool
182+
183+
func (tf typesFilter) filter(p cmp.Path) bool { return tf[p.Last().Type()] }
184+
185+
func equateAny(x, y interface{}) bool { return x == y }

cmp/cmpopts/util_test.go

+31
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"fmt"
1111
"io"
1212
"math"
13+
"net/netip"
1314
"reflect"
1415
"strings"
1516
"sync"
@@ -676,6 +677,36 @@ func TestOptions(t *testing.T) {
676677
opts: []cmp.Option{EquateErrors()},
677678
wantEqual: false,
678679
reason: "AnyError is not equal to nil value",
680+
}, {
681+
label: "EquateComparable",
682+
x: []struct{ P netip.Addr }{
683+
{netip.AddrFrom4([4]byte{1, 2, 3, 4})},
684+
{netip.AddrFrom4([4]byte{1, 2, 3, 5})},
685+
{netip.AddrFrom4([4]byte{1, 2, 3, 6})},
686+
},
687+
y: []struct{ P netip.Addr }{
688+
{netip.AddrFrom4([4]byte{1, 2, 3, 4})},
689+
{netip.AddrFrom4([4]byte{1, 2, 3, 5})},
690+
{netip.AddrFrom4([4]byte{1, 2, 3, 6})},
691+
},
692+
opts: []cmp.Option{EquateComparable(netip.Addr{})},
693+
wantEqual: true,
694+
reason: "equal because all IP addresses are the same",
695+
}, {
696+
label: "EquateComparable",
697+
x: []struct{ P netip.Addr }{
698+
{netip.AddrFrom4([4]byte{1, 2, 3, 4})},
699+
{netip.AddrFrom4([4]byte{1, 2, 3, 5})},
700+
{netip.AddrFrom4([4]byte{1, 2, 3, 6})},
701+
},
702+
y: []struct{ P netip.Addr }{
703+
{netip.AddrFrom4([4]byte{1, 2, 3, 4})},
704+
{netip.AddrFrom4([4]byte{1, 2, 3, 7})},
705+
{netip.AddrFrom4([4]byte{1, 2, 3, 6})},
706+
},
707+
opts: []cmp.Option{EquateComparable(netip.Addr{})},
708+
wantEqual: false,
709+
reason: "not equal because second IP address is different",
679710
}, {
680711
label: "IgnoreFields",
681712
x: Bar1{Foo3{&Foo2{&Foo1{Alpha: 5}}}},

cmp/options.go

+2
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,8 @@ func (validator) apply(s *state, vx, vy reflect.Value) {
234234
name = fmt.Sprintf("%q.%v", t.PkgPath(), t.Name()) // e.g., "path/to/package".MyType
235235
if _, ok := reflect.New(t).Interface().(error); ok {
236236
help = "consider using cmpopts.EquateErrors to compare error values"
237+
} else if t.Comparable() {
238+
help = "consider using cmpopts.EquateComparable to compare comparable Go types"
237239
}
238240
} else {
239241
// Unnamed type with unexported fields. Derive PkgPath from field.

0 commit comments

Comments
 (0)