diff --git a/cmp/compare.go b/cmp/compare.go index 5527f01..a62ce04 100644 --- a/cmp/compare.go +++ b/cmp/compare.go @@ -49,11 +49,12 @@ var nothing = reflect.Value{} // • If two values are not of the same type, then they are never equal // and the overall result is false. // -// • Let S be the set of all Ignore, Transformer, and Comparer options that -// remain after applying all path filters, value filters, and type filters. +// • Let S be the set of all Ignore, Transformer, Comparer, and Default options +// that remain after applying all implicit and explicit filters. // If at least one Ignore exists in S, then the comparison is ignored. -// If the number of Transformer and Comparer options in S is greater than one, -// then Equal panics because it is ambiguous which option to use. +// If S contains multiple Default options, they are coalesced into one Default. +// If any Transformer, Comparer, or Default options coexist in S, +// then Equal panics because it is ambiguous which to use. // If S contains a single Transformer, then use that to transform the current // values and recursively call Equal on the output values. // If S contains a single Comparer, then use that to compare the current values. diff --git a/cmp/options.go b/cmp/options.go index a4e159a..e3d6a70 100644 --- a/cmp/options.go +++ b/cmp/options.go @@ -14,11 +14,11 @@ import ( ) // Option configures for specific behavior of Equal and Diff. In particular, -// the fundamental Option functions (Ignore, Transformer, and Comparer), +// the fundamental options (Ignore, Transformer, Comparer, and Default), // configure how equality is determined. // -// The fundamental options may be composed with filters (FilterPath and -// FilterValues) to control the scope over which they are applied. +// The fundamental options may be composed with filters (FilterPath, FilterValues, +// and FilterPriority) to control the scope over which they are applied. // // The cmp/cmpopts package provides helper functions for creating options that // may be used with Equal and Diff. @@ -33,7 +33,7 @@ type Option interface { } // applicableOption represents the following types: -// Fundamental: ignore | invalid | *comparer | *transformer +// Fundamental: noop | ignore | invalid | *comparer | *transformer // Grouping: Options type applicableOption interface { Option @@ -44,8 +44,8 @@ type applicableOption interface { } // coreOption represents the following types: -// Fundamental: ignore | invalid | *comparer | *transformer -// Filters: *pathFilter | *valuesFilter +// Fundamental: noop | ignore | invalid | *comparer | *transformer +// Filters: *pathFilter | *valuesFilter | *priorityFilter type coreOption interface { Option isCore() @@ -57,28 +57,28 @@ func (core) isCore() {} // Options is a list of Option values that also satisfies the Option interface. // Helper comparison packages may return an Options value when packing multiple -// Option values into a single Option. When this package processes an Options, -// it will be implicitly expanded into a flat list. -// -// Applying a filter on an Options is equivalent to applying that same filter -// on all individual options held within. +// Option values into a single Option. When this package processes Options +// or nested Options, it will be implicitly expanded into a flat set. type Options []Option func (opts Options) filter(s *state, vx, vy reflect.Value, t reflect.Type) (out applicableOption) { for _, opt := range opts { switch opt := opt.filter(s, vx, vy, t); opt.(type) { case ignore: - return ignore{} // Only ignore can short-circuit evaluation + return ignore{} // Highest precedence; can short-circuit filtering case invalid: - out = invalid{} // Takes precedence over comparer or transformer - case *comparer, *transformer, Options: + out = invalid{} // Second highest precedence + case noop, *comparer, *transformer, Options: switch out.(type) { case nil: out = opt case invalid: // Keep invalid - case *comparer, *transformer, Options: - out = Options{out, opt} // Conflicting comparers or transformers + case noop, *comparer, *transformer, Options: + if opt == (noop{}) && out == (noop{}) { + break // Coelesce redundant Default together + } + out = Options{out, opt} // Conflicting Comparer, Tranformer, or Default } } } @@ -87,7 +87,7 @@ func (opts Options) filter(s *state, vx, vy reflect.Value, t reflect.Type) (out func (opts Options) apply(s *state, _, _ reflect.Value) bool { const warning = "ambiguous set of applicable options" - const help = "consider using filters to ensure at most one Comparer or Transformer may apply" + const help = "consider using filters to ensure at most one Comparer, Transformer, or Default may apply" var ss []string for _, opt := range flattenOptions(nil, opts) { ss = append(ss, fmt.Sprint(opt)) @@ -104,11 +104,12 @@ func (opts Options) String() string { return fmt.Sprintf("Options{%s}", strings.Join(ss, ", ")) } -// FilterPath returns a new Option where opt is only evaluated if filter f +// FilterPath returns a new Option where opt is only applicable if filter f // returns true for the current Path in the value tree. // -// The option passed in may be an Ignore, Transformer, Comparer, Options, or -// a previously filtered Option. +// The Option passed in may be a filtered option (via the Filter functions), +// fundamental option (like Ignore, Transformer, Comparer, or Default), or +// Options group containing elements of the former. func FilterPath(f func(Path) bool, opt Option) Option { if f == nil { panic("invalid path filter function") @@ -137,7 +138,7 @@ func (f pathFilter) String() string { return fmt.Sprintf("FilterPath(%s, %v)", fn, f.opt) } -// FilterValues returns a new Option where opt is only evaluated if filter f, +// FilterValues returns a new Option where opt is only applicable if filter f, // which is a function of the form "func(T, T) bool", returns true for the // current pair of values being compared. If the type of the values is not // assignable to T, then this filter implicitly returns false. @@ -148,8 +149,9 @@ func (f pathFilter) String() string { // If T is an interface, it is possible that f is called with two values with // different concrete types that both implement T. // -// The option passed in may be an Ignore, Transformer, Comparer, Options, or -// a previously filtered Option. +// The Option passed in may be a filtered option (via the Filter functions), +// fundamental option (like Ignore, Transformer, Comparer, or Default), or +// Options group containing elements of the former. func FilterValues(f interface{}, opt Option) Option { v := reflect.ValueOf(f) if !function.IsType(v.Type(), function.ValueFilter) || v.IsNil() { @@ -187,6 +189,65 @@ func (f valuesFilter) String() string { return fmt.Sprintf("FilterValues(%s, %v)", fn, f.opt) } +// FilterPriority returns a new Option where an option, opts[i], +// is only applicable if no fundamental options remain after applying all filters +// in all prior options, opts[:i]. +// +// In order to prevent further options from being applicable, the Default option +// can be used to ensure that some fundamental option remains. +// +// The Option passed in may be a filtered option (via the Filter functions), +// fundamental option (like Ignore, Transformer, Comparer, or Default), or +// Options group containing elements of the former. +func FilterPriority(opts ...Option) Option { + var newOpts []Option + for _, opt := range opts { + if opt := normalizeOption(opt); opt != nil { + newOpts = append(newOpts, opt) + } + } + if len(newOpts) > 0 { + return &priorityFilter{opts: newOpts} + } + return nil +} + +type priorityFilter struct { + core + opts []Option +} + +func (f priorityFilter) filter(s *state, vx, vy reflect.Value, t reflect.Type) applicableOption { + for _, opt := range f.opts { + if opt := opt.filter(s, vx, vy, t); opt != nil { + return opt + } + } + return nil +} + +func (f priorityFilter) String() string { + var ss []string + for _, opt := range f.opts { + ss = append(ss, fmt.Sprint(opt)) + } + return fmt.Sprintf("FilterPriority(%s)", strings.Join(ss, ", ")) +} + +// Default is an Option that configures Equal to stop processing options and +// to proceed to the next evaluation rule (i.e., checking for the Equal method). +// This value is intended to be combined with FilterPriority to act as a +// sentinel type that prevents other options from being applicable. +// It is an error to pass an unfiltered Default option to Equal. +func Default() Option { return noop{} } + +type noop struct{ core } + +func (noop) isFiltered() bool { return false } +func (noop) filter(_ *state, _, _ reflect.Value, _ reflect.Type) applicableOption { return noop{} } +func (noop) apply(_ *state, _, _ reflect.Value) bool { return false } +func (noop) String() string { return "Default()" } + // Ignore is an Option that causes all comparisons to be ignored. // This value is intended to be combined with FilterPath or FilterValues. // It is an error to pass an unfiltered Ignore option to Equal.