@@ -338,17 +338,15 @@ func (cm comparer) String() string {
338
338
}
339
339
340
340
// AllowUnexported returns an Option that forcibly allows operations on
341
- // unexported fields in certain structs, which are specified by passing in a
342
- // value of each struct type.
341
+ // unexported fields in certain structs. Struct types with permitted visibility
342
+ // are specified by passing in a value of the struct type.
343
343
//
344
- // Users of this option must understand that comparing on unexported fields
345
- // from external packages is not safe since changes in the internal
346
- // implementation of some external package may cause the result of Equal
347
- // to unexpectedly change. However, it may be valid to use this option on types
348
- // defined in an internal package where the semantic meaning of an unexported
349
- // field is in the control of the user.
344
+ // Comparing unexported fields from packages that are not owned by the user
345
+ // is unsafe since changes in the internal implementation may cause the result
346
+ // of Equal to unexpectedly change. This option should only be used on types
347
+ // where the semantic meaning of unexported fields is in full control of the user.
350
348
//
351
- // For some cases, a custom Comparer should be used instead that defines
349
+ // For most cases, a custom Comparer should be used instead that defines
352
350
// equality as a function of the public API of a type rather than the underlying
353
351
// unexported implementation.
354
352
//
@@ -363,24 +361,90 @@ func (cm comparer) String() string {
363
361
//
364
362
// In other cases, the cmpopts.IgnoreUnexported option can be used to ignore
365
363
// all unexported fields on specified struct types.
366
- func AllowUnexported (types ... interface {}) Option {
364
+ func AllowUnexported (typs ... interface {}) Option {
367
365
if ! supportAllowUnexported {
368
366
panic ("AllowUnexported is not supported on purego builds, Google App Engine Standard, or GopherJS" )
369
367
}
370
- m := make ( map [reflect. Type ] bool )
371
- for _ , typ := range types {
372
- t := reflect .TypeOf (typ )
368
+ var x fieldExporter
369
+ for _ , v := range typs {
370
+ t := reflect .TypeOf (v )
373
371
if t .Kind () != reflect .Struct {
374
- panic (fmt .Sprintf ("invalid struct type: %T" , typ ))
372
+ panic (fmt .Sprintf ("invalid struct type: %T" , v ))
375
373
}
376
- m [ t ] = true
374
+ x . insertType ( t )
377
375
}
378
- return visibleStructs ( m )
376
+ return x
379
377
}
380
378
381
- type visibleStructs map [reflect.Type ]bool
379
+ // AllowUnexportedWithin returns an Option that forcibly allows
380
+ // operations on unexported fields in certain structs.
381
+ // See AllowUnexported for proper guidance on comparing unexported fields.
382
+ //
383
+ // Unexported visibility is permitted for any struct type declared within a
384
+ // package where the pkgPrefix is path prefix match of the full package path.
385
+ // A path prefix match is defined as a string prefix match where the next
386
+ // character is either the first character, a forward slash,
387
+ // or the end of the string.
388
+ //
389
+ // For example, the package path "example.com/foo/bar" is matched by:
390
+ // • "example.com/foo/bar"
391
+ // • "example.com/foo"
392
+ // • "example.com"
393
+ // and is not matched by:
394
+ // • "example.com/foo/ba"
395
+ // • "example.com/fizz"
396
+ // • "example.org"
397
+ func AllowUnexportedWithin (pkgPrefix string ) Option {
398
+ if ! supportAllowUnexported {
399
+ panic ("AllowUnexported is not supported on purego builds, Google App Engine Standard, or GopherJS" )
400
+ }
401
+ var x fieldExporter
402
+ x .insertPrefix (pkgPrefix )
403
+ return x
404
+ }
405
+
406
+ type fieldExporter struct {
407
+ typs map [reflect.Type ]struct {}
408
+ pkgs map [string ]struct {}
409
+ }
410
+
411
+ func (x * fieldExporter ) insertType (t reflect.Type ) {
412
+ if x .typs == nil {
413
+ x .typs = make (map [reflect.Type ]struct {})
414
+ }
415
+ x .typs [t ] = struct {}{}
416
+ }
417
+
418
+ func (x * fieldExporter ) insertPrefix (p string ) {
419
+ if x .pkgs == nil {
420
+ x .pkgs = make (map [string ]struct {})
421
+ }
422
+ x .pkgs [p ] = struct {}{}
423
+ }
424
+
425
+ func (x fieldExporter ) mayExport (t reflect.Type , sf reflect.StructField ) bool {
426
+ // TODO(dsnet): Workaround for reflect bug (https://golang.org/issue/21122).
427
+ // The upstream fix landed in Go1.10, so we can remove this when dropping
428
+ // support for Go1.9 and below.
429
+ if len (x .pkgs ) > 0 && sf .PkgPath == "" {
430
+ return true // Liberally allow exporting since we lack path information
431
+ }
432
+
433
+ if _ , ok := x .typs [t ]; ok {
434
+ return true
435
+ }
436
+ for pkgPrefix := range x .pkgs {
437
+ if ! strings .HasPrefix (sf .PkgPath , string (pkgPrefix )) {
438
+ continue
439
+ }
440
+ if len (sf .PkgPath ) == len (pkgPrefix ) || sf .PkgPath [len (pkgPrefix )] == '/' || len (pkgPrefix ) == 0 {
441
+ return true
442
+ }
443
+ }
444
+ return false
445
+ }
382
446
383
- func (visibleStructs ) filter (_ * state , _ , _ reflect.Value , _ reflect.Type ) applicableOption {
447
+ func (fieldExporter ) filter (_ * state , _ , _ reflect.Value , _ reflect.Type ) applicableOption {
384
448
panic ("not implemented" )
385
449
}
386
450
0 commit comments