| // Copyright 2017, The Go Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style |
| // license that can be found in the LICENSE file. |
| |
| package cmpopts |
| |
| import ( |
| "fmt" |
| "reflect" |
| "unicode" |
| "unicode/utf8" |
| |
| "github.com/google/go-cmp/cmp" |
| "github.com/google/go-cmp/cmp/internal/function" |
| ) |
| |
| // IgnoreFields returns an [cmp.Option] that ignores fields of the |
| // given names on a single struct type. It respects the names of exported fields |
| // that are forwarded due to struct embedding. |
| // The struct type is specified by passing in a value of that type. |
| // |
| // The name may be a dot-delimited string (e.g., "Foo.Bar") to ignore a |
| // specific sub-field that is embedded or nested within the parent struct. |
| func IgnoreFields(typ interface{}, names ...string) cmp.Option { |
| sf := newStructFilter(typ, names...) |
| return cmp.FilterPath(sf.filter, cmp.Ignore()) |
| } |
| |
| // IgnoreTypes returns an [cmp.Option] that ignores all values assignable to |
| // certain types, which are specified by passing in a value of each type. |
| func IgnoreTypes(typs ...interface{}) cmp.Option { |
| tf := newTypeFilter(typs...) |
| return cmp.FilterPath(tf.filter, cmp.Ignore()) |
| } |
| |
| type typeFilter []reflect.Type |
| |
| func newTypeFilter(typs ...interface{}) (tf typeFilter) { |
| for _, typ := range typs { |
| t := reflect.TypeOf(typ) |
| if t == nil { |
| // This occurs if someone tries to pass in sync.Locker(nil) |
| panic("cannot determine type; consider using IgnoreInterfaces") |
| } |
| tf = append(tf, t) |
| } |
| return tf |
| } |
| func (tf typeFilter) filter(p cmp.Path) bool { |
| if len(p) < 1 { |
| return false |
| } |
| t := p.Last().Type() |
| for _, ti := range tf { |
| if t.AssignableTo(ti) { |
| return true |
| } |
| } |
| return false |
| } |
| |
| // IgnoreInterfaces returns an [cmp.Option] that ignores all values or references of |
| // values assignable to certain interface types. These interfaces are specified |
| // by passing in an anonymous struct with the interface types embedded in it. |
| // For example, to ignore [sync.Locker], pass in struct{sync.Locker}{}. |
| func IgnoreInterfaces(ifaces interface{}) cmp.Option { |
| tf := newIfaceFilter(ifaces) |
| return cmp.FilterPath(tf.filter, cmp.Ignore()) |
| } |
| |
| type ifaceFilter []reflect.Type |
| |
| func newIfaceFilter(ifaces interface{}) (tf ifaceFilter) { |
| t := reflect.TypeOf(ifaces) |
| if ifaces == nil || t.Name() != "" || t.Kind() != reflect.Struct { |
| panic("input must be an anonymous struct") |
| } |
| for i := 0; i < t.NumField(); i++ { |
| fi := t.Field(i) |
| switch { |
| case !fi.Anonymous: |
| panic("struct cannot have named fields") |
| case fi.Type.Kind() != reflect.Interface: |
| panic("embedded field must be an interface type") |
| case fi.Type.NumMethod() == 0: |
| // This matches everything; why would you ever want this? |
| panic("cannot ignore empty interface") |
| default: |
| tf = append(tf, fi.Type) |
| } |
| } |
| return tf |
| } |
| func (tf ifaceFilter) filter(p cmp.Path) bool { |
| if len(p) < 1 { |
| return false |
| } |
| t := p.Last().Type() |
| for _, ti := range tf { |
| if t.AssignableTo(ti) { |
| return true |
| } |
| if t.Kind() != reflect.Ptr && reflect.PtrTo(t).AssignableTo(ti) { |
| return true |
| } |
| } |
| return false |
| } |
| |
| // IgnoreUnexported returns an [cmp.Option] that only ignores the immediate unexported |
| // fields of a struct, including anonymous fields of unexported types. |
| // In particular, unexported fields within the struct's exported fields |
| // of struct types, including anonymous fields, will not be ignored unless the |
| // type of the field itself is also passed to IgnoreUnexported. |
| // |
| // Avoid ignoring unexported fields of a type which you do not control (i.e. a |
| // type from another repository), as changes to the implementation of such types |
| // may change how the comparison behaves. Prefer a custom [cmp.Comparer] instead. |
| func IgnoreUnexported(typs ...interface{}) cmp.Option { |
| ux := newUnexportedFilter(typs...) |
| return cmp.FilterPath(ux.filter, cmp.Ignore()) |
| } |
| |
| type unexportedFilter struct{ m map[reflect.Type]bool } |
| |
| func newUnexportedFilter(typs ...interface{}) unexportedFilter { |
| ux := unexportedFilter{m: make(map[reflect.Type]bool)} |
| for _, typ := range typs { |
| t := reflect.TypeOf(typ) |
| if t == nil || t.Kind() != reflect.Struct { |
| panic(fmt.Sprintf("%T must be a non-pointer struct", typ)) |
| } |
| ux.m[t] = true |
| } |
| return ux |
| } |
| func (xf unexportedFilter) filter(p cmp.Path) bool { |
| sf, ok := p.Index(-1).(cmp.StructField) |
| if !ok { |
| return false |
| } |
| return xf.m[p.Index(-2).Type()] && !isExported(sf.Name()) |
| } |
| |
| // isExported reports whether the identifier is exported. |
| func isExported(id string) bool { |
| r, _ := utf8.DecodeRuneInString(id) |
| return unicode.IsUpper(r) |
| } |
| |
| // IgnoreSliceElements returns an [cmp.Option] that ignores elements of []V. |
| // The discard function must be of the form "func(T) bool" which is used to |
| // ignore slice elements of type V, where V is assignable to T. |
| // Elements are ignored if the function reports true. |
| func IgnoreSliceElements(discardFunc interface{}) cmp.Option { |
| vf := reflect.ValueOf(discardFunc) |
| if !function.IsType(vf.Type(), function.ValuePredicate) || vf.IsNil() { |
| panic(fmt.Sprintf("invalid discard function: %T", discardFunc)) |
| } |
| return cmp.FilterPath(func(p cmp.Path) bool { |
| si, ok := p.Index(-1).(cmp.SliceIndex) |
| if !ok { |
| return false |
| } |
| if !si.Type().AssignableTo(vf.Type().In(0)) { |
| return false |
| } |
| vx, vy := si.Values() |
| if vx.IsValid() && vf.Call([]reflect.Value{vx})[0].Bool() { |
| return true |
| } |
| if vy.IsValid() && vf.Call([]reflect.Value{vy})[0].Bool() { |
| return true |
| } |
| return false |
| }, cmp.Ignore()) |
| } |
| |
| // IgnoreMapEntries returns an [cmp.Option] that ignores entries of map[K]V. |
| // The discard function must be of the form "func(T, R) bool" which is used to |
| // ignore map entries of type K and V, where K and V are assignable to T and R. |
| // Entries are ignored if the function reports true. |
| func IgnoreMapEntries(discardFunc interface{}) cmp.Option { |
| vf := reflect.ValueOf(discardFunc) |
| if !function.IsType(vf.Type(), function.KeyValuePredicate) || vf.IsNil() { |
| panic(fmt.Sprintf("invalid discard function: %T", discardFunc)) |
| } |
| return cmp.FilterPath(func(p cmp.Path) bool { |
| mi, ok := p.Index(-1).(cmp.MapIndex) |
| if !ok { |
| return false |
| } |
| if !mi.Key().Type().AssignableTo(vf.Type().In(0)) || !mi.Type().AssignableTo(vf.Type().In(1)) { |
| return false |
| } |
| k := mi.Key() |
| vx, vy := mi.Values() |
| if vx.IsValid() && vf.Call([]reflect.Value{k, vx})[0].Bool() { |
| return true |
| } |
| if vy.IsValid() && vf.Call([]reflect.Value{k, vy})[0].Bool() { |
| return true |
| } |
| return false |
| }, cmp.Ignore()) |
| } |