// 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 non-pointer 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. sf, _ := t.FieldByName(name) if !isExported(name) { // Avoid using reflect.Type.FieldByName for unexported fields due to // buggy behavior with regard to embeddeding and unexported fields. // See https://golang.org/issue/4876 for details. sf = reflect.StructField{} for i := 0; i < t.NumField() && sf.Name == ""; i++ { if t.Field(i).Name == name { sf = t.Field(i) } } } if sf.Name == "" { 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 }