// Copyright 2018 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 file. package packagestest import ( "fmt" "go/token" "io/ioutil" "os" "path/filepath" "reflect" "regexp" "strings" "golang.org/x/tools/go/expect" "golang.org/x/tools/go/packages" "golang.org/x/tools/internal/span" ) const ( markMethod = "mark" eofIdentifier = "EOF" ) // Expect invokes the supplied methods for all expectation notes found in // the exported source files. // // All exported go source files are parsed to collect the expectation // notes. // See the documentation for expect.Parse for how the notes are collected // and parsed. // // The methods are supplied as a map of name to function, and those functions // will be matched against the expectations by name. // Notes with no matching function will be skipped, and functions with no // matching notes will not be invoked. // If there are no registered markers yet, a special pass will be run first // which adds any markers declared with @mark(Name, pattern) or @name. These // call the Mark method to add the marker to the global set. // You can register the "mark" method to override these in your own call to // Expect. The bound Mark function is usable directly in your method map, so // exported.Expect(map[string]interface{}{"mark": exported.Mark}) // replicates the built in behavior. // // Method invocation // // When invoking a method the expressions in the parameter list need to be // converted to values to be passed to the method. // There are a very limited set of types the arguments are allowed to be. // expect.Note : passed the Note instance being evaluated. // string : can be supplied either a string literal or an identifier. // int : can only be supplied an integer literal. // *regexp.Regexp : can only be supplied a regular expression literal // token.Pos : has a file position calculated as described below. // token.Position : has a file position calculated as described below. // expect.Range: has a start and end position as described below. // interface{} : will be passed any value // // Position calculation // // There is some extra handling when a parameter is being coerced into a // token.Pos, token.Position or Range type argument. // // If the parameter is an identifier, it will be treated as the name of an // marker to look up (as if markers were global variables). // // If it is a string or regular expression, then it will be passed to // expect.MatchBefore to look up a match in the line at which it was declared. // // It is safe to call this repeatedly with different method sets, but it is // not safe to call it concurrently. func (e *Exported) Expect(methods map[string]interface{}) error { if err := e.getNotes(); err != nil { return err } if err := e.getMarkers(); err != nil { return err } var err error ms := make(map[string]method, len(methods)) for name, f := range methods { mi := method{f: reflect.ValueOf(f)} mi.converters = make([]converter, mi.f.Type().NumIn()) for i := 0; i < len(mi.converters); i++ { mi.converters[i], err = e.buildConverter(mi.f.Type().In(i)) if err != nil { return fmt.Errorf("invalid method %v: %v", name, err) } } ms[name] = mi } for _, n := range e.notes { if n.Args == nil { // simple identifier form, convert to a call to mark n = &expect.Note{ Pos: n.Pos, Name: markMethod, Args: []interface{}{n.Name, n.Name}, } } mi, ok := ms[n.Name] if !ok { continue } params := make([]reflect.Value, len(mi.converters)) args := n.Args for i, convert := range mi.converters { params[i], args, err = convert(n, args) if err != nil { return fmt.Errorf("%v: %v", e.ExpectFileSet.Position(n.Pos), err) } } if len(args) > 0 { return fmt.Errorf("%v: unwanted args got %+v extra", e.ExpectFileSet.Position(n.Pos), args) } //TODO: catch the error returned from the method mi.f.Call(params) } return nil } // Range is a type alias for span.Range for backwards compatibility, prefer // using span.Range directly. type Range = span.Range // Mark adds a new marker to the known set. func (e *Exported) Mark(name string, r Range) { if e.markers == nil { e.markers = make(map[string]span.Range) } e.markers[name] = r } func (e *Exported) getNotes() error { if e.notes != nil { return nil } notes := []*expect.Note{} var dirs []string for _, module := range e.written { for _, filename := range module { dirs = append(dirs, filepath.Dir(filename)) } } for filename := range e.Config.Overlay { dirs = append(dirs, filepath.Dir(filename)) } pkgs, err := packages.Load(e.Config, dirs...) if err != nil { return fmt.Errorf("unable to load packages for directories %s: %v", dirs, err) } seen := make(map[token.Position]struct{}) for _, pkg := range pkgs { for _, filename := range pkg.GoFiles { content, err := e.FileContents(filename) if err != nil { return err } l, err := expect.Parse(e.ExpectFileSet, filename, content) if err != nil { return fmt.Errorf("failed to extract expectations: %v", err) } for _, note := range l { pos := e.ExpectFileSet.Position(note.Pos) if _, ok := seen[pos]; ok { continue } notes = append(notes, note) seen[pos] = struct{}{} } } } if _, ok := e.written[e.primary]; !ok { e.notes = notes return nil } // Check go.mod markers regardless of mode, we need to do this so that our marker count // matches the counts in the summary.txt.golden file for the test directory. if gomod, found := e.written[e.primary]["go.mod"]; found { // If we are in Modules mode, then we need to check the contents of the go.mod.temp. if e.Exporter == Modules { gomod += ".temp" } l, err := goModMarkers(e, gomod) if err != nil { return fmt.Errorf("failed to extract expectations for go.mod: %v", err) } notes = append(notes, l...) } e.notes = notes return nil } func goModMarkers(e *Exported, gomod string) ([]*expect.Note, error) { if _, err := os.Stat(gomod); os.IsNotExist(err) { // If there is no go.mod file, we want to be able to continue. return nil, nil } content, err := e.FileContents(gomod) if err != nil { return nil, err } if e.Exporter == GOPATH { return expect.Parse(e.ExpectFileSet, gomod, content) } gomod = strings.TrimSuffix(gomod, ".temp") // If we are in Modules mode, copy the original contents file back into go.mod if err := ioutil.WriteFile(gomod, content, 0644); err != nil { return nil, nil } return expect.Parse(e.ExpectFileSet, gomod, content) } func (e *Exported) getMarkers() error { if e.markers != nil { return nil } // set markers early so that we don't call getMarkers again from Expect e.markers = make(map[string]span.Range) return e.Expect(map[string]interface{}{ markMethod: e.Mark, }) } var ( noteType = reflect.TypeOf((*expect.Note)(nil)) identifierType = reflect.TypeOf(expect.Identifier("")) posType = reflect.TypeOf(token.Pos(0)) positionType = reflect.TypeOf(token.Position{}) rangeType = reflect.TypeOf(span.Range{}) spanType = reflect.TypeOf(span.Span{}) fsetType = reflect.TypeOf((*token.FileSet)(nil)) regexType = reflect.TypeOf((*regexp.Regexp)(nil)) exportedType = reflect.TypeOf((*Exported)(nil)) ) // converter converts from a marker's argument parsed from the comment to // reflect values passed to the method during Invoke. // It takes the args remaining, and returns the args it did not consume. // This allows a converter to consume 0 args for well known types, or multiple // args for compound types. type converter func(*expect.Note, []interface{}) (reflect.Value, []interface{}, error) // method is used to track information about Invoke methods that is expensive to // calculate so that we can work it out once rather than per marker. type method struct { f reflect.Value // the reflect value of the passed in method converters []converter // the parameter converters for the method } // buildConverter works out what function should be used to go from an ast expressions to a reflect // value of the type expected by a method. // It is called when only the target type is know, it returns converters that are flexible across // all supported expression types for that target type. func (e *Exported) buildConverter(pt reflect.Type) (converter, error) { switch { case pt == noteType: return func(n *expect.Note, args []interface{}) (reflect.Value, []interface{}, error) { return reflect.ValueOf(n), args, nil }, nil case pt == fsetType: return func(n *expect.Note, args []interface{}) (reflect.Value, []interface{}, error) { return reflect.ValueOf(e.ExpectFileSet), args, nil }, nil case pt == exportedType: return func(n *expect.Note, args []interface{}) (reflect.Value, []interface{}, error) { return reflect.ValueOf(e), args, nil }, nil case pt == posType: return func(n *expect.Note, args []interface{}) (reflect.Value, []interface{}, error) { r, remains, err := e.rangeConverter(n, args) if err != nil { return reflect.Value{}, nil, err } return reflect.ValueOf(r.Start), remains, nil }, nil case pt == positionType: return func(n *expect.Note, args []interface{}) (reflect.Value, []interface{}, error) { r, remains, err := e.rangeConverter(n, args) if err != nil { return reflect.Value{}, nil, err } return reflect.ValueOf(e.ExpectFileSet.Position(r.Start)), remains, nil }, nil case pt == rangeType: return func(n *expect.Note, args []interface{}) (reflect.Value, []interface{}, error) { r, remains, err := e.rangeConverter(n, args) if err != nil { return reflect.Value{}, nil, err } return reflect.ValueOf(r), remains, nil }, nil case pt == spanType: return func(n *expect.Note, args []interface{}) (reflect.Value, []interface{}, error) { r, remains, err := e.rangeConverter(n, args) if err != nil { return reflect.Value{}, nil, err } spn, err := r.Span() if err != nil { return reflect.Value{}, nil, err } return reflect.ValueOf(spn), remains, nil }, nil case pt == identifierType: return func(n *expect.Note, args []interface{}) (reflect.Value, []interface{}, error) { if len(args) < 1 { return reflect.Value{}, nil, fmt.Errorf("missing argument") } arg := args[0] args = args[1:] switch arg := arg.(type) { case expect.Identifier: return reflect.ValueOf(arg), args, nil default: return reflect.Value{}, nil, fmt.Errorf("cannot convert %v to string", arg) } }, nil case pt == regexType: return func(n *expect.Note, args []interface{}) (reflect.Value, []interface{}, error) { if len(args) < 1 { return reflect.Value{}, nil, fmt.Errorf("missing argument") } arg := args[0] args = args[1:] if _, ok := arg.(*regexp.Regexp); !ok { return reflect.Value{}, nil, fmt.Errorf("cannot convert %v to *regexp.Regexp", arg) } return reflect.ValueOf(arg), args, nil }, nil case pt.Kind() == reflect.String: return func(n *expect.Note, args []interface{}) (reflect.Value, []interface{}, error) { if len(args) < 1 { return reflect.Value{}, nil, fmt.Errorf("missing argument") } arg := args[0] args = args[1:] switch arg := arg.(type) { case expect.Identifier: return reflect.ValueOf(string(arg)), args, nil case string: return reflect.ValueOf(arg), args, nil default: return reflect.Value{}, nil, fmt.Errorf("cannot convert %v to string", arg) } }, nil case pt.Kind() == reflect.Int64: return func(n *expect.Note, args []interface{}) (reflect.Value, []interface{}, error) { if len(args) < 1 { return reflect.Value{}, nil, fmt.Errorf("missing argument") } arg := args[0] args = args[1:] switch arg := arg.(type) { case int64: return reflect.ValueOf(arg), args, nil default: return reflect.Value{}, nil, fmt.Errorf("cannot convert %v to int", arg) } }, nil case pt.Kind() == reflect.Bool: return func(n *expect.Note, args []interface{}) (reflect.Value, []interface{}, error) { if len(args) < 1 { return reflect.Value{}, nil, fmt.Errorf("missing argument") } arg := args[0] args = args[1:] b, ok := arg.(bool) if !ok { return reflect.Value{}, nil, fmt.Errorf("cannot convert %v to bool", arg) } return reflect.ValueOf(b), args, nil }, nil case pt.Kind() == reflect.Slice: return func(n *expect.Note, args []interface{}) (reflect.Value, []interface{}, error) { converter, err := e.buildConverter(pt.Elem()) if err != nil { return reflect.Value{}, nil, err } result := reflect.MakeSlice(reflect.SliceOf(pt.Elem()), 0, len(args)) for range args { value, remains, err := converter(n, args) if err != nil { return reflect.Value{}, nil, err } result = reflect.Append(result, value) args = remains } return result, args, nil }, nil default: if pt.Kind() == reflect.Interface && pt.NumMethod() == 0 { return func(n *expect.Note, args []interface{}) (reflect.Value, []interface{}, error) { if len(args) < 1 { return reflect.Value{}, nil, fmt.Errorf("missing argument") } return reflect.ValueOf(args[0]), args[1:], nil }, nil } return nil, fmt.Errorf("param has unexpected type %v (kind %v)", pt, pt.Kind()) } } func (e *Exported) rangeConverter(n *expect.Note, args []interface{}) (span.Range, []interface{}, error) { if len(args) < 1 { return span.Range{}, nil, fmt.Errorf("missing argument") } arg := args[0] args = args[1:] switch arg := arg.(type) { case expect.Identifier: // handle the special identifiers switch arg { case eofIdentifier: // end of file identifier, look up the current file f := e.ExpectFileSet.File(n.Pos) eof := f.Pos(f.Size()) return span.Range{FileSet: e.ExpectFileSet, Start: eof, End: token.NoPos}, args, nil default: // look up an marker by name mark, ok := e.markers[string(arg)] if !ok { return span.Range{}, nil, fmt.Errorf("cannot find marker %v", arg) } return mark, args, nil } case string: start, end, err := expect.MatchBefore(e.ExpectFileSet, e.FileContents, n.Pos, arg) if err != nil { return span.Range{}, nil, err } if start == token.NoPos { return span.Range{}, nil, fmt.Errorf("%v: pattern %s did not match", e.ExpectFileSet.Position(n.Pos), arg) } return span.Range{FileSet: e.ExpectFileSet, Start: start, End: end}, args, nil case *regexp.Regexp: start, end, err := expect.MatchBefore(e.ExpectFileSet, e.FileContents, n.Pos, arg) if err != nil { return span.Range{}, nil, err } if start == token.NoPos { return span.Range{}, nil, fmt.Errorf("%v: pattern %s did not match", e.ExpectFileSet.Position(n.Pos), arg) } return span.Range{FileSet: e.ExpectFileSet, Start: start, End: end}, args, nil default: return span.Range{}, nil, fmt.Errorf("cannot convert %v to pos", arg) } }