|
- // Copyright 2017, The Go Authors. All rights reserved.
- // Use of this source code is governed by a BSD-style
- // license that can be found in the LICENSE.md file.
-
- package cmpopts
-
- import (
- "fmt"
- "reflect"
- "strings"
-
- "github.com/google/go-cmp/cmp"
- )
-
- // filterField returns a new Option where opt is only evaluated on paths that
- // include a specific exported field on a single struct type.
- // The struct type is specified by passing in a value of that type.
- //
- // The name may be a dot-delimited string (e.g., "Foo.Bar") to select a
- // specific sub-field that is embedded or nested within the parent struct.
- func filterField(typ interface{}, name string, opt cmp.Option) cmp.Option {
- // TODO: This is currently unexported over concerns of how helper filters
- // can be composed together easily.
- // TODO: Add tests for FilterField.
-
- sf := newStructFilter(typ, name)
- return cmp.FilterPath(sf.filter, opt)
- }
-
- type structFilter struct {
- t reflect.Type // The root struct type to match on
- ft fieldTree // Tree of fields to match on
- }
-
- func newStructFilter(typ interface{}, names ...string) structFilter {
- // TODO: Perhaps allow * as a special identifier to allow ignoring any
- // number of path steps until the next field match?
- // This could be useful when a concrete struct gets transformed into
- // an anonymous struct where it is not possible to specify that by type,
- // but the transformer happens to provide guarantees about the names of
- // the transformed fields.
-
- t := reflect.TypeOf(typ)
- if t == nil || t.Kind() != reflect.Struct {
- panic(fmt.Sprintf("%T must be a struct", typ))
- }
- var ft fieldTree
- for _, name := range names {
- cname, err := canonicalName(t, name)
- if err != nil {
- panic(fmt.Sprintf("%s: %v", strings.Join(cname, "."), err))
- }
- ft.insert(cname)
- }
- return structFilter{t, ft}
- }
-
- func (sf structFilter) filter(p cmp.Path) bool {
- for i, ps := range p {
- if ps.Type().AssignableTo(sf.t) && sf.ft.matchPrefix(p[i+1:]) {
- return true
- }
- }
- return false
- }
-
- // fieldTree represents a set of dot-separated identifiers.
- //
- // For example, inserting the following selectors:
- // Foo
- // Foo.Bar.Baz
- // Foo.Buzz
- // Nuka.Cola.Quantum
- //
- // Results in a tree of the form:
- // {sub: {
- // "Foo": {ok: true, sub: {
- // "Bar": {sub: {
- // "Baz": {ok: true},
- // }},
- // "Buzz": {ok: true},
- // }},
- // "Nuka": {sub: {
- // "Cola": {sub: {
- // "Quantum": {ok: true},
- // }},
- // }},
- // }}
- type fieldTree struct {
- ok bool // Whether this is a specified node
- sub map[string]fieldTree // The sub-tree of fields under this node
- }
-
- // insert inserts a sequence of field accesses into the tree.
- func (ft *fieldTree) insert(cname []string) {
- if ft.sub == nil {
- ft.sub = make(map[string]fieldTree)
- }
- if len(cname) == 0 {
- ft.ok = true
- return
- }
- sub := ft.sub[cname[0]]
- sub.insert(cname[1:])
- ft.sub[cname[0]] = sub
- }
-
- // matchPrefix reports whether any selector in the fieldTree matches
- // the start of path p.
- func (ft fieldTree) matchPrefix(p cmp.Path) bool {
- for _, ps := range p {
- switch ps := ps.(type) {
- case cmp.StructField:
- ft = ft.sub[ps.Name()]
- if ft.ok {
- return true
- }
- if len(ft.sub) == 0 {
- return false
- }
- case cmp.Indirect:
- default:
- return false
- }
- }
- return false
- }
-
- // canonicalName returns a list of identifiers where any struct field access
- // through an embedded field is expanded to include the names of the embedded
- // types themselves.
- //
- // For example, suppose field "Foo" is not directly in the parent struct,
- // but actually from an embedded struct of type "Bar". Then, the canonical name
- // of "Foo" is actually "Bar.Foo".
- //
- // Suppose field "Foo" is not directly in the parent struct, but actually
- // a field in two different embedded structs of types "Bar" and "Baz".
- // Then the selector "Foo" causes a panic since it is ambiguous which one it
- // refers to. The user must specify either "Bar.Foo" or "Baz.Foo".
- func canonicalName(t reflect.Type, sel string) ([]string, error) {
- var name string
- sel = strings.TrimPrefix(sel, ".")
- if sel == "" {
- return nil, fmt.Errorf("name must not be empty")
- }
- if i := strings.IndexByte(sel, '.'); i < 0 {
- name, sel = sel, ""
- } else {
- name, sel = sel[:i], sel[i:]
- }
-
- // Type must be a struct or pointer to struct.
- if t.Kind() == reflect.Ptr {
- t = t.Elem()
- }
- if t.Kind() != reflect.Struct {
- return nil, fmt.Errorf("%v must be a struct", t)
- }
-
- // Find the canonical name for this current field name.
- // If the field exists in an embedded struct, then it will be expanded.
- if !isExported(name) {
- // Disallow unexported fields:
- // * To discourage people from actually touching unexported fields
- // * FieldByName is buggy (https://golang.org/issue/4876)
- return []string{name}, fmt.Errorf("name must be exported")
- }
- sf, ok := t.FieldByName(name)
- if !ok {
- return []string{name}, fmt.Errorf("does not exist")
- }
- var ss []string
- for i := range sf.Index {
- ss = append(ss, t.FieldByIndex(sf.Index[:i+1]).Name)
- }
- if sel == "" {
- return ss, nil
- }
- ssPost, err := canonicalName(sf.Type, sel)
- return append(ss, ssPost...), err
- }
|