Skip to content

Commit 8099a97

Browse files
authored
Refactor option evaluation logic (#32)
The previous implementation of options had a single "option" type that was used to represent either an Ignore, Comparer, or Transformer and all of the filters relevant to each of them. We refactor this logic by creating a new type to represent each of the fundamental options and filtering options. Construction of filtered options is now as simple as wrapping the input option with the appropriate filter type. Evaluation of filters now takes a top-down two-step approach where 1. We start with the set of all options, and recursively apply the filters to create the "applicable" set S. 2. We apply the set S. Both steps are represented as the filter and apply methods on each of the core options. This approach has the following advantages: * It is faster because the same filter that was applied to multiple options now only needs to execute once. * It more closely matches the documented algorithm in cmp.Equal. * It allows for easier extension of the API to add new fundamental option types.
1 parent 3fe0215 commit 8099a97

File tree

4 files changed

+287
-209
lines changed

4 files changed

+287
-209
lines changed

cmp/compare.go

Lines changed: 39 additions & 100 deletions
Original file line numberDiff line numberDiff line change
@@ -54,11 +54,10 @@ var nothing = reflect.Value{}
5454
// If at least one Ignore exists in S, then the comparison is ignored.
5555
// If the number of Transformer and Comparer options in S is greater than one,
5656
// then Equal panics because it is ambiguous which option to use.
57-
// If S contains a single Transformer, then apply that transformer on the
58-
// current values and recursively call Equal on the transformed output values.
59-
// If S contains a single Comparer, then use that Comparer to determine whether
60-
// the current values are equal or not.
61-
// Otherwise, S is empty and evaluation proceeds to the next rule.
57+
// If S contains a single Transformer, then use that to transform the current
58+
// values and recursively call Equal on the output values.
59+
// If S contains a single Comparer, then use that to compare the current values.
60+
// Otherwise, evaluation proceeds to the next rule.
6261
//
6362
// • If the values have an Equal method of the form "(T) Equal(T) bool" or
6463
// "(T) Equal(I) bool" where T is assignable to I, then use the result of
@@ -119,47 +118,39 @@ type state struct {
119118

120119
// These fields, once set by processOption, will not change.
121120
exporters map[reflect.Type]bool // Set of structs with unexported field visibility
122-
optsIgn []option // List of all ignore options without value filters
123-
opts []option // List of all other options
121+
opts Options // List of all fundamental and filter options
124122
}
125123

126124
func newState(opts []Option) *state {
127125
s := new(state)
128126
for _, opt := range opts {
129127
s.processOption(opt)
130128
}
131-
// Move Ignore options to the front so that they are evaluated first.
132-
for i, j := 0, 0; i < len(s.opts); i++ {
133-
if s.opts[i].op == nil {
134-
s.opts[i], s.opts[j] = s.opts[j], s.opts[i]
135-
j++
136-
}
137-
}
138129
return s
139130
}
140131

141132
func (s *state) processOption(opt Option) {
142133
switch opt := opt.(type) {
134+
case nil:
143135
case Options:
144136
for _, o := range opt {
145137
s.processOption(o)
146138
}
139+
case coreOption:
140+
type filtered interface {
141+
isFiltered() bool
142+
}
143+
if fopt, ok := opt.(filtered); ok && !fopt.isFiltered() {
144+
panic(fmt.Sprintf("cannot use an unfiltered option: %v", opt))
145+
}
146+
s.opts = append(s.opts, opt)
147147
case visibleStructs:
148148
if s.exporters == nil {
149149
s.exporters = make(map[reflect.Type]bool)
150150
}
151151
for t := range opt {
152152
s.exporters[t] = true
153153
}
154-
case option:
155-
if opt.typeFilter == nil && len(opt.pathFilters)+len(opt.valueFilters) == 0 {
156-
panic(fmt.Sprintf("cannot use an unfiltered option: %v", opt))
157-
}
158-
if opt.op == nil && len(opt.valueFilters) == 0 {
159-
s.optsIgn = append(s.optsIgn, opt)
160-
} else {
161-
s.opts = append(s.opts, opt)
162-
}
163154
case reporter:
164155
if s.reporter != nil {
165156
panic("difference reporter already registered")
@@ -205,9 +196,10 @@ func (s *state) compareAny(vx, vy reflect.Value) {
205196
s.curPath.push(&pathStep{typ: t})
206197
defer s.curPath.pop()
207198
}
199+
vx, vy = s.tryExporting(vx, vy)
208200

209201
// Rule 1: Check whether an option applies on this node in the value tree.
210-
if s.tryOptions(&vx, &vy, t) {
202+
if s.tryOptions(vx, vy, t) {
211203
return
212204
}
213205

@@ -284,90 +276,37 @@ func (s *state) compareAny(vx, vy reflect.Value) {
284276
}
285277
}
286278

287-
// tryOptions iterates through all of the options and evaluates whether any
288-
// of them can be applied. This may modify the underlying values vx and vy
289-
// if an unexported field is being forcibly exported.
290-
func (s *state) tryOptions(vx, vy *reflect.Value, t reflect.Type) bool {
291-
// Try all ignore options that do not depend on the value first.
292-
// This avoids possible panics when processing unexported fields.
293-
for _, opt := range s.optsIgn {
294-
var v reflect.Value // Dummy value; should never be used
295-
if s.applyFilters(v, v, t, opt) {
296-
return true // Ignore option applied
297-
}
298-
}
299-
300-
// Since the values must be used after this point, verify that the values
301-
// are either exported or can be forcibly exported.
279+
func (s *state) tryExporting(vx, vy reflect.Value) (reflect.Value, reflect.Value) {
302280
if sf, ok := s.curPath[len(s.curPath)-1].(*structField); ok && sf.unexported {
303-
if !sf.force {
304-
const help = "consider using AllowUnexported or cmpopts.IgnoreUnexported"
305-
panic(fmt.Sprintf("cannot handle unexported field: %#v\n%s", s.curPath, help))
306-
}
307-
308-
// Use unsafe pointer arithmetic to get read-write access to an
309-
// unexported field in the struct.
310-
*vx = unsafeRetrieveField(sf.pvx, sf.field)
311-
*vy = unsafeRetrieveField(sf.pvy, sf.field)
312-
}
313-
314-
// Try all other options now.
315-
optIdx := -1 // Index of Option to apply
316-
for i, opt := range s.opts {
317-
if !s.applyFilters(*vx, *vy, t, opt) {
318-
continue
319-
}
320-
if opt.op == nil {
321-
return true // Ignored comparison
322-
}
323-
if optIdx >= 0 {
324-
panic(fmt.Sprintf("ambiguous set of options at %#v\n\n%v\n\n%v\n", s.curPath, s.opts[optIdx], opt))
281+
if sf.force {
282+
// Use unsafe pointer arithmetic to get read-write access to an
283+
// unexported field in the struct.
284+
vx = unsafeRetrieveField(sf.pvx, sf.field)
285+
vy = unsafeRetrieveField(sf.pvy, sf.field)
286+
} else {
287+
// We are not allowed to export the value, so invalidate them
288+
// so that tryOptions can panic later if not explicitly ignored.
289+
vx = nothing
290+
vy = nothing
325291
}
326-
optIdx = i
327-
}
328-
if optIdx >= 0 {
329-
s.applyOption(*vx, *vy, t, s.opts[optIdx])
330-
return true
331292
}
332-
return false
293+
return vx, vy
333294
}
334295

335-
func (s *state) applyFilters(vx, vy reflect.Value, t reflect.Type, opt option) bool {
336-
if opt.typeFilter != nil {
337-
if !t.AssignableTo(opt.typeFilter) {
338-
return false
339-
}
340-
}
341-
for _, f := range opt.pathFilters {
342-
if !f(s.curPath) {
343-
return false
344-
}
345-
}
346-
for _, f := range opt.valueFilters {
347-
if !t.AssignableTo(f.in) || !s.callTTBFunc(f.fnc, vx, vy) {
348-
return false
349-
}
296+
func (s *state) tryOptions(vx, vy reflect.Value, t reflect.Type) bool {
297+
// If there were no FilterValues, we will not detect invalid inputs,
298+
// so manually check for them and append invalid if necessary.
299+
// We still evaluate the options since an ignore can override invalid.
300+
opts := s.opts
301+
if !vx.IsValid() || !vy.IsValid() {
302+
opts = Options{opts, invalid{}}
350303
}
351-
return true
352-
}
353-
354-
func (s *state) applyOption(vx, vy reflect.Value, t reflect.Type, opt option) {
355-
switch op := opt.op.(type) {
356-
case *transformer:
357-
// Update path before calling the Transformer so that dynamic checks
358-
// will use the updated path.
359-
s.curPath.push(&transform{pathStep{op.fnc.Type().Out(0)}, op})
360-
defer s.curPath.pop()
361304

362-
vx = s.callTRFunc(op.fnc, vx)
363-
vy = s.callTRFunc(op.fnc, vy)
364-
s.compareAny(vx, vy)
365-
return
366-
case *comparer:
367-
eq := s.callTTBFunc(op.fnc, vx, vy)
368-
s.report(eq, vx, vy)
369-
return
305+
// Evaluate all filters and apply the remaining options.
306+
if opt := opts.filter(s, vx, vy, t); opt != nil {
307+
return opt.apply(s, vx, vy)
370308
}
309+
return false
371310
}
372311

373312
func (s *state) tryMethod(vx, vy reflect.Value, t reflect.Type) bool {

cmp/compare_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ func comparerTests() []test {
113113
cmp.Comparer(func(x, y int) bool { return true }),
114114
cmp.Transformer("", func(x int) float64 { return float64(x) }),
115115
},
116-
wantPanic: "ambiguous set of options",
116+
wantPanic: "ambiguous set of applicable options",
117117
}, {
118118
label: label,
119119
x: 1,
@@ -380,7 +380,7 @@ func transformerTests() []test {
380380
cmp.Transformer("", func(in int) int { return in / 2 }),
381381
cmp.Transformer("", func(in int) int { return in }),
382382
},
383-
wantPanic: "ambiguous set of options",
383+
wantPanic: "ambiguous set of applicable options",
384384
}, {
385385
label: label,
386386
x: []int{0, -5, 0, -1},

0 commit comments

Comments
 (0)