1// Copyright 2017, The Go Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style
3// license that can be found in the LICENSE file.
4
5package cmpopts
6
7import (
8	"fmt"
9	"reflect"
10	"unicode"
11	"unicode/utf8"
12
13	"github.com/google/go-cmp/cmp"
14	"github.com/google/go-cmp/cmp/internal/function"
15)
16
17// IgnoreFields returns an Option that ignores fields of the
18// given names on a single struct type. It respects the names of exported fields
19// that are forwarded due to struct embedding.
20// The struct type is specified by passing in a value of that type.
21//
22// The name may be a dot-delimited string (e.g., "Foo.Bar") to ignore a
23// specific sub-field that is embedded or nested within the parent struct.
24func IgnoreFields(typ interface{}, names ...string) cmp.Option {
25	sf := newStructFilter(typ, names...)
26	return cmp.FilterPath(sf.filter, cmp.Ignore())
27}
28
29// IgnoreTypes returns an Option that ignores all values assignable to
30// certain types, which are specified by passing in a value of each type.
31func IgnoreTypes(typs ...interface{}) cmp.Option {
32	tf := newTypeFilter(typs...)
33	return cmp.FilterPath(tf.filter, cmp.Ignore())
34}
35
36type typeFilter []reflect.Type
37
38func newTypeFilter(typs ...interface{}) (tf typeFilter) {
39	for _, typ := range typs {
40		t := reflect.TypeOf(typ)
41		if t == nil {
42			// This occurs if someone tries to pass in sync.Locker(nil)
43			panic("cannot determine type; consider using IgnoreInterfaces")
44		}
45		tf = append(tf, t)
46	}
47	return tf
48}
49func (tf typeFilter) filter(p cmp.Path) bool {
50	if len(p) < 1 {
51		return false
52	}
53	t := p.Last().Type()
54	for _, ti := range tf {
55		if t.AssignableTo(ti) {
56			return true
57		}
58	}
59	return false
60}
61
62// IgnoreInterfaces returns an Option that ignores all values or references of
63// values assignable to certain interface types. These interfaces are specified
64// by passing in an anonymous struct with the interface types embedded in it.
65// For example, to ignore sync.Locker, pass in struct{sync.Locker}{}.
66func IgnoreInterfaces(ifaces interface{}) cmp.Option {
67	tf := newIfaceFilter(ifaces)
68	return cmp.FilterPath(tf.filter, cmp.Ignore())
69}
70
71type ifaceFilter []reflect.Type
72
73func newIfaceFilter(ifaces interface{}) (tf ifaceFilter) {
74	t := reflect.TypeOf(ifaces)
75	if ifaces == nil || t.Name() != "" || t.Kind() != reflect.Struct {
76		panic("input must be an anonymous struct")
77	}
78	for i := 0; i < t.NumField(); i++ {
79		fi := t.Field(i)
80		switch {
81		case !fi.Anonymous:
82			panic("struct cannot have named fields")
83		case fi.Type.Kind() != reflect.Interface:
84			panic("embedded field must be an interface type")
85		case fi.Type.NumMethod() == 0:
86			// This matches everything; why would you ever want this?
87			panic("cannot ignore empty interface")
88		default:
89			tf = append(tf, fi.Type)
90		}
91	}
92	return tf
93}
94func (tf ifaceFilter) filter(p cmp.Path) bool {
95	if len(p) < 1 {
96		return false
97	}
98	t := p.Last().Type()
99	for _, ti := range tf {
100		if t.AssignableTo(ti) {
101			return true
102		}
103		if t.Kind() != reflect.Ptr && reflect.PtrTo(t).AssignableTo(ti) {
104			return true
105		}
106	}
107	return false
108}
109
110// IgnoreUnexported returns an Option that only ignores the immediate unexported
111// fields of a struct, including anonymous fields of unexported types.
112// In particular, unexported fields within the struct's exported fields
113// of struct types, including anonymous fields, will not be ignored unless the
114// type of the field itself is also passed to IgnoreUnexported.
115//
116// Avoid ignoring unexported fields of a type which you do not control (i.e. a
117// type from another repository), as changes to the implementation of such types
118// may change how the comparison behaves. Prefer a custom Comparer instead.
119func IgnoreUnexported(typs ...interface{}) cmp.Option {
120	ux := newUnexportedFilter(typs...)
121	return cmp.FilterPath(ux.filter, cmp.Ignore())
122}
123
124type unexportedFilter struct{ m map[reflect.Type]bool }
125
126func newUnexportedFilter(typs ...interface{}) unexportedFilter {
127	ux := unexportedFilter{m: make(map[reflect.Type]bool)}
128	for _, typ := range typs {
129		t := reflect.TypeOf(typ)
130		if t == nil || t.Kind() != reflect.Struct {
131			panic(fmt.Sprintf("%T must be a non-pointer struct", typ))
132		}
133		ux.m[t] = true
134	}
135	return ux
136}
137func (xf unexportedFilter) filter(p cmp.Path) bool {
138	sf, ok := p.Index(-1).(cmp.StructField)
139	if !ok {
140		return false
141	}
142	return xf.m[p.Index(-2).Type()] && !isExported(sf.Name())
143}
144
145// isExported reports whether the identifier is exported.
146func isExported(id string) bool {
147	r, _ := utf8.DecodeRuneInString(id)
148	return unicode.IsUpper(r)
149}
150
151// IgnoreSliceElements returns an Option that ignores elements of []V.
152// The discard function must be of the form "func(T) bool" which is used to
153// ignore slice elements of type V, where V is assignable to T.
154// Elements are ignored if the function reports true.
155func IgnoreSliceElements(discardFunc interface{}) cmp.Option {
156	vf := reflect.ValueOf(discardFunc)
157	if !function.IsType(vf.Type(), function.ValuePredicate) || vf.IsNil() {
158		panic(fmt.Sprintf("invalid discard function: %T", discardFunc))
159	}
160	return cmp.FilterPath(func(p cmp.Path) bool {
161		si, ok := p.Index(-1).(cmp.SliceIndex)
162		if !ok {
163			return false
164		}
165		if !si.Type().AssignableTo(vf.Type().In(0)) {
166			return false
167		}
168		vx, vy := si.Values()
169		if vx.IsValid() && vf.Call([]reflect.Value{vx})[0].Bool() {
170			return true
171		}
172		if vy.IsValid() && vf.Call([]reflect.Value{vy})[0].Bool() {
173			return true
174		}
175		return false
176	}, cmp.Ignore())
177}
178
179// IgnoreMapEntries returns an Option that ignores entries of map[K]V.
180// The discard function must be of the form "func(T, R) bool" which is used to
181// ignore map entries of type K and V, where K and V are assignable to T and R.
182// Entries are ignored if the function reports true.
183func IgnoreMapEntries(discardFunc interface{}) cmp.Option {
184	vf := reflect.ValueOf(discardFunc)
185	if !function.IsType(vf.Type(), function.KeyValuePredicate) || vf.IsNil() {
186		panic(fmt.Sprintf("invalid discard function: %T", discardFunc))
187	}
188	return cmp.FilterPath(func(p cmp.Path) bool {
189		mi, ok := p.Index(-1).(cmp.MapIndex)
190		if !ok {
191			return false
192		}
193		if !mi.Key().Type().AssignableTo(vf.Type().In(0)) || !mi.Type().AssignableTo(vf.Type().In(1)) {
194			return false
195		}
196		k := mi.Key()
197		vx, vy := mi.Values()
198		if vx.IsValid() && vf.Call([]reflect.Value{k, vx})[0].Bool() {
199			return true
200		}
201		if vy.IsValid() && vf.Call([]reflect.Value{k, vy})[0].Bool() {
202			return true
203		}
204		return false
205	}, cmp.Ignore())
206}
207