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
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index a85a606..e21ebfa 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -6,7 +6,7 @@
   test:
     strategy:
       matrix:
-        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]
+        go-version: [1.18.x, 1.19.x, 1.20.x, 1.21.x]
         os: [ubuntu-latest, macos-latest]
     runs-on: ${{ matrix.os }}
     steps:
@@ -19,5 +19,5 @@
     - name: Test
       run: go test -v -race ./...
     - name: Format
-      if: matrix.go-version == '1.20.x'
+      if: matrix.go-version == '1.21.x'
       run: diff -u <(echo -n) <(gofmt -d .)
diff --git a/cmp/cmpopts/equate.go b/cmp/cmpopts/equate.go
index 90974e6..3d8d0cd 100644
--- a/cmp/cmpopts/equate.go
+++ b/cmp/cmpopts/equate.go
@@ -7,6 +7,7 @@
 
 import (
 	"errors"
+	"fmt"
 	"math"
 	"reflect"
 	"time"
@@ -154,3 +155,31 @@
 	ye := y.(error)
 	return errors.Is(xe, ye) || errors.Is(ye, xe)
 }
+
+// EquateComparable returns a [cmp.Option] that determines equality
+// of comparable types by directly comparing them using the == operator in Go.
+// The types to compare are specified by passing a value of that type.
+// This option should only be used on types that are documented as being
+// safe for direct == comparison. For example, [net/netip.Addr] is documented
+// as being semantically safe to use with ==, while [time.Time] is documented
+// to discourage the use of == on time values.
+func EquateComparable(typs ...interface{}) cmp.Option {
+	types := make(typesFilter)
+	for _, typ := range typs {
+		switch t := reflect.TypeOf(typ); {
+		case !t.Comparable():
+			panic(fmt.Sprintf("%T is not a comparable Go type", typ))
+		case types[t]:
+			panic(fmt.Sprintf("%T is already specified", typ))
+		default:
+			types[t] = true
+		}
+	}
+	return cmp.FilterPath(types.filter, cmp.Comparer(equateAny))
+}
+
+type typesFilter map[reflect.Type]bool
+
+func (tf typesFilter) filter(p cmp.Path) bool { return tf[p.Last().Type()] }
+
+func equateAny(x, y interface{}) bool { return x == y }
diff --git a/cmp/cmpopts/util_test.go b/cmp/cmpopts/util_test.go
index 7adeb9b..6a7c300 100644
--- a/cmp/cmpopts/util_test.go
+++ b/cmp/cmpopts/util_test.go
@@ -10,6 +10,7 @@
 	"fmt"
 	"io"
 	"math"
+	"net/netip"
 	"reflect"
 	"strings"
 	"sync"
@@ -677,6 +678,36 @@
 		wantEqual: false,
 		reason:    "AnyError is not equal to nil value",
 	}, {
+		label: "EquateComparable",
+		x: []struct{ P netip.Addr }{
+			{netip.AddrFrom4([4]byte{1, 2, 3, 4})},
+			{netip.AddrFrom4([4]byte{1, 2, 3, 5})},
+			{netip.AddrFrom4([4]byte{1, 2, 3, 6})},
+		},
+		y: []struct{ P netip.Addr }{
+			{netip.AddrFrom4([4]byte{1, 2, 3, 4})},
+			{netip.AddrFrom4([4]byte{1, 2, 3, 5})},
+			{netip.AddrFrom4([4]byte{1, 2, 3, 6})},
+		},
+		opts:      []cmp.Option{EquateComparable(netip.Addr{})},
+		wantEqual: true,
+		reason:    "equal because all IP addresses are the same",
+	}, {
+		label: "EquateComparable",
+		x: []struct{ P netip.Addr }{
+			{netip.AddrFrom4([4]byte{1, 2, 3, 4})},
+			{netip.AddrFrom4([4]byte{1, 2, 3, 5})},
+			{netip.AddrFrom4([4]byte{1, 2, 3, 6})},
+		},
+		y: []struct{ P netip.Addr }{
+			{netip.AddrFrom4([4]byte{1, 2, 3, 4})},
+			{netip.AddrFrom4([4]byte{1, 2, 3, 7})},
+			{netip.AddrFrom4([4]byte{1, 2, 3, 6})},
+		},
+		opts:      []cmp.Option{EquateComparable(netip.Addr{})},
+		wantEqual: false,
+		reason:    "not equal because second IP address is different",
+	}, {
 		label:     "IgnoreFields",
 		x:         Bar1{Foo3{&Foo2{&Foo1{Alpha: 5}}}},
 		y:         Bar1{Foo3{&Foo2{&Foo1{Alpha: 6}}}},
diff --git a/cmp/options.go b/cmp/options.go
index 518b6ac..754496f 100644
--- a/cmp/options.go
+++ b/cmp/options.go
@@ -234,6 +234,8 @@
 			name = fmt.Sprintf("%q.%v", t.PkgPath(), t.Name()) // e.g., "path/to/package".MyType
 			if _, ok := reflect.New(t).Interface().(error); ok {
 				help = "consider using cmpopts.EquateErrors to compare error values"
+			} else if t.Comparable() {
+				help = "consider using cmpopts.EquateComparable to compare comparable Go types"
 			}
 		} else {
 			// Unnamed type with unexported fields. Derive PkgPath from field.