Add Exporter option (#176)
Add an Exporter option that accepts a function to determine
which struct types to permit access to unexported fields.
Treat this as a first-class option and implement AllowUnexported
in terms of the new Exporter option.
The new Exporter option:
* Better matches the existing style of top-level options
both by name (e.g., Comparer, Transformer, and Reporter)
and by API style (all accept a function).
* Is more flexible as it enables users to functionally
implement AllowAllUnexported by simply doing:
Exporter(func(reflect.Type) bool { return true })
Fixes #40
diff --git a/cmp/compare.go b/cmp/compare.go
index 2133562..8419fcc 100644
--- a/cmp/compare.go
+++ b/cmp/compare.go
@@ -22,8 +22,8 @@
// equality is determined by recursively comparing the primitive kinds on both
// values, much like reflect.DeepEqual. Unlike reflect.DeepEqual, unexported
// fields are not compared by default; they result in panics unless suppressed
-// by using an Ignore option (see cmpopts.IgnoreUnexported) or explicitly compared
-// using the AllowUnexported option.
+// by using an Ignore option (see cmpopts.IgnoreUnexported) or explicitly
+// compared using the Exporter option.
package cmp
import (
@@ -62,8 +62,8 @@
//
// Structs are equal if recursively calling Equal on all fields report equal.
// If a struct contains unexported fields, Equal panics unless an Ignore option
-// (e.g., cmpopts.IgnoreUnexported) ignores that field or the AllowUnexported
-// option explicitly permits comparing the unexported field.
+// (e.g., cmpopts.IgnoreUnexported) ignores that field or the Exporter option
+// explicitly permits comparing the unexported field.
//
// Slices are equal if they are both nil or both non-nil, where recursively
// calling Equal on all non-ignored slice or array elements report equal.
@@ -148,8 +148,8 @@
dynChecker dynChecker
// These fields, once set by processOption, will not change.
- exporters map[reflect.Type]bool // Set of structs with unexported field visibility
- opts Options // List of all fundamental and filter options
+ exporters []exporter // List of exporters for structs with unexported fields
+ opts Options // List of all fundamental and filter options
}
func newState(opts []Option) *state {
@@ -174,13 +174,8 @@
panic(fmt.Sprintf("cannot use an unfiltered option: %v", opt))
}
s.opts = append(s.opts, opt)
- case visibleStructs:
- if s.exporters == nil {
- s.exporters = make(map[reflect.Type]bool)
- }
- for t := range opt {
- s.exporters[t] = true
- }
+ case exporter:
+ s.exporters = append(s.exporters, opt)
case reporter:
s.reporters = append(s.reporters, opt)
default:
@@ -354,6 +349,7 @@
func (s *state) compareStruct(t reflect.Type, vx, vy reflect.Value) {
var vax, vay reflect.Value // Addressable versions of vx and vy
+ var mayForce, mayForceInit bool
step := StructField{&structField{}}
for i := 0; i < t.NumField(); i++ {
step.typ = t.Field(i).Type
@@ -375,7 +371,13 @@
vax = makeAddressable(vx)
vay = makeAddressable(vy)
}
- step.mayForce = s.exporters[t]
+ if !mayForceInit {
+ for _, xf := range s.exporters {
+ mayForce = mayForce || xf(t)
+ }
+ mayForceInit = true
+ }
+ step.mayForce = mayForce
step.pvx = vax
step.pvy = vay
step.field = t.Field(i)
diff --git a/cmp/export_panic.go b/cmp/export_panic.go
index abc3a1c..dd03235 100644
--- a/cmp/export_panic.go
+++ b/cmp/export_panic.go
@@ -8,8 +8,8 @@
import "reflect"
-const supportAllowUnexported = false
+const supportExporters = false
func retrieveUnexportedField(reflect.Value, reflect.StructField) reflect.Value {
- panic("retrieveUnexportedField is not implemented")
+ panic("no support for forcibly accessing unexported fields")
}
diff --git a/cmp/export_unsafe.go b/cmp/export_unsafe.go
index 5f6ed93..57020e2 100644
--- a/cmp/export_unsafe.go
+++ b/cmp/export_unsafe.go
@@ -11,7 +11,7 @@
"unsafe"
)
-const supportAllowUnexported = true
+const supportExporters = true
// retrieveUnexportedField uses unsafe to forcibly retrieve any field from
// a struct such that the value has read-write permissions.
diff --git a/cmp/options.go b/cmp/options.go
index a20390d..409e803 100644
--- a/cmp/options.go
+++ b/cmp/options.go
@@ -225,7 +225,7 @@
// Unable to Interface implies unexported field without visibility access.
if !vx.CanInterface() || !vy.CanInterface() {
- const help = "consider using a custom Comparer; if you control the implementation of type, you can also consider AllowUnexported or cmpopts.IgnoreUnexported"
+ const help = "consider using a custom Comparer; if you control the implementation of type, you can also consider using an Exporter, AllowUnexported, or cmpopts.IgnoreUnexported"
var name string
if t := s.curPath.Index(-2).Type(); t.Name() != "" {
// Named type with unexported fields.
@@ -372,9 +372,8 @@
return fmt.Sprintf("Comparer(%s)", function.NameOf(cm.fnc))
}
-// AllowUnexported returns an Option that forcibly allows operations on
-// unexported fields in certain structs, which are specified by passing in a
-// value of each struct type.
+// Exporter returns an Option that specifies whether Equal is allowed to
+// introspect into the unexported fields of certain struct types.
//
// Users of this option must understand that comparing on unexported fields
// from external packages is not safe since changes in the internal
@@ -398,10 +397,24 @@
//
// In other cases, the cmpopts.IgnoreUnexported option can be used to ignore
// all unexported fields on specified struct types.
-func AllowUnexported(types ...interface{}) Option {
- if !supportAllowUnexported {
- panic("AllowUnexported is not supported on purego builds, Google App Engine Standard, or GopherJS")
+func Exporter(f func(reflect.Type) bool) Option {
+ if !supportExporters {
+ panic("Exporter is not supported on purego builds")
}
+ return exporter(f)
+}
+
+type exporter func(reflect.Type) bool
+
+func (exporter) filter(_ *state, _ reflect.Type, _, _ reflect.Value) applicableOption {
+ panic("not implemented")
+}
+
+// AllowUnexported returns an Options that allows Equal to forcibly introspect
+// unexported fields of the specified struct types.
+//
+// See Exporter for the proper use of this option.
+func AllowUnexported(types ...interface{}) Option {
m := make(map[reflect.Type]bool)
for _, typ := range types {
t := reflect.TypeOf(typ)
@@ -410,13 +423,7 @@
}
m[t] = true
}
- return visibleStructs(m)
-}
-
-type visibleStructs map[reflect.Type]bool
-
-func (visibleStructs) filter(_ *state, _ reflect.Type, _, _ reflect.Value) applicableOption {
- panic("not implemented")
+ return exporter(func(t reflect.Type) bool { return m[t] })
}
// Result represents the comparison result for a single node and
diff --git a/cmp/path.go b/cmp/path.go
index 96fffd2..d7a420d 100644
--- a/cmp/path.go
+++ b/cmp/path.go
@@ -41,7 +41,7 @@
// In some cases, one or both may be invalid or have restrictions:
// • For StructField, both are not interface-able if the current field
// is unexported and the struct type is not explicitly permitted by
- // AllowUnexported to traverse unexported fields.
+ // an Exporter to traverse unexported fields.
// • For SliceIndex, one may be invalid if an element is missing from
// either the x or y slice.
// • For MapIndex, one may be invalid if an entry is missing from