Skip to content

Commit

Permalink
Use x/tools/go/packages instead of x/tools/go/loader to parse Go code,
Browse files Browse the repository at this point in the history
in order to support Go 1.11 modules. Fixes vektra#213.
  • Loading branch information
atombender committed Nov 13, 2018
1 parent ea26575 commit 0ecc4a9
Show file tree
Hide file tree
Showing 5 changed files with 110 additions and 112 deletions.
21 changes: 5 additions & 16 deletions mockery/generator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ type GeneratorSuite struct {
}

func (s *GeneratorSuite) SetupTest() {
s.parser = NewParser()
s.parser = NewParser(nil)
}

func (s *GeneratorSuite) getInterfaceFromFile(interfacePath, interfaceName string) *Interface {
Expand All @@ -36,16 +36,15 @@ func (s *GeneratorSuite) getInterfaceFromFile(interfacePath, interfaceName strin
)

iface, err := s.parser.Find(interfaceName)
s.NoError(err, "The requested interface was found.")
s.Require().NoError(err)
s.Require().NotNil(iface)
return iface
}

func (s *GeneratorSuite) getGenerator(
filepath, interfaceName string, inPackage bool,
) *Generator {
return NewGenerator(
s.getInterfaceFromFile(filepath, interfaceName), pkg, inPackage,
)
return NewGenerator(s.getInterfaceFromFile(filepath, interfaceName), pkg, inPackage)
}

func (s *GeneratorSuite) checkGeneration(
Expand Down Expand Up @@ -85,14 +84,6 @@ func (s *GeneratorSuite) checkPrologueGeneration(
)
}

func (s *GeneratorSuite) getInterfaceRelPath(iface *Interface) string {
local, err := filepath.Rel(getGoPathSrc(), filepath.Dir(iface.Path))
s.NoError(err, "No errors with relative path generation.")

// Align w/ Generator.getLocalizedPath and enforce '/' slashes for import paths in every OS.
return filepath.ToSlash(local)
}

func (s *GeneratorSuite) TestCalculateImport() {
gp := []string{"a/src", "b/src"}

Expand Down Expand Up @@ -1014,15 +1005,13 @@ func (s *GeneratorSuite) TestPrologueWithImportSameAsLocalPackage() {
generator := s.getGenerator(
"imports_same_as_package.go", "ImportsSameAsPackage", false,
)
s.getInterfaceRelPath(generator.iface)
expected := `package mocks
import fixtures "` + s.getInterfaceRelPath(generator.iface) + `"
import fixtures "` + generator.iface.Path + `"
import mock "github.com/stretchr/testify/mock"
import test "github.com/vektra/mockery/mockery/fixtures/test"
`

s.checkPrologueGeneration(generator, expected)
}

Expand Down
170 changes: 87 additions & 83 deletions mockery/parse.go
Original file line number Diff line number Diff line change
@@ -1,54 +1,47 @@
package mockery

import (
"fmt"
"go/ast"
"go/build"
"go/importer"
"go/types"
"io/ioutil"
"path/filepath"
"sort"
"strings"
"sync"

"golang.org/x/tools/go/loader"
"golang.org/x/tools/go/packages"
)

type Parser struct {
configMapping map[string][]*ast.File
pathToInterfaces map[string][]string
pathToASTFile map[string]*ast.File
parserPackages []*types.Package
conf loader.Config
type parserEntry struct {
fileName string
pkg *packages.Package
syntax *ast.File
interfaces []string
}

func NewParser() *Parser {
var conf loader.Config

conf.TypeCheckFuncBodies = func(_ string) bool { return false }
conf.TypeChecker.DisableUnusedImportCheck = true
conf.TypeChecker.Importer = importer.Default()

// Initialize the build context (e.g. GOARCH/GOOS fields) so we can use it for respecting
// build tags during Parse.
buildCtx := build.Default
conf.Build = &buildCtx
type Parser struct {
entries []*parserEntry
entriesByFileName map[string]*parserEntry
packages []*packages.Package
parserPackages []*types.Package
conf packages.Config
}

func NewParser(buildTags []string) *Parser {
var conf packages.Config
conf.Mode = packages.LoadSyntax
if len(buildTags) > 0 {
conf.BuildFlags = []string{"-tags", strings.Join(buildTags, ",")}
}
return &Parser{
parserPackages: make([]*types.Package, 0),
configMapping: make(map[string][]*ast.File),
pathToInterfaces: make(map[string][]string),
pathToASTFile: make(map[string]*ast.File),
conf: conf,
parserPackages: make([]*types.Package, 0),
entriesByFileName: map[string]*parserEntry{},
conf: conf,
}
}

func (p *Parser) AddBuildTags(buildTags ...string) {
p.conf.Build.BuildTags = append(p.conf.Build.BuildTags, buildTags...)
}

func (p *Parser) Parse(path string) error {

// To support relative paths to mock targets w/ vendor deps, we need to provide eventual
// calls to build.Context.Import with an absolute path. It needs to be absolute because
// Import will only find the vendor directory if our target path for parsing is under
Expand All @@ -75,27 +68,59 @@ func (p *Parser) Parse(path string) error {

fname := fi.Name()
fpath := filepath.Join(dir, fname)
if _, ok := p.entriesByFileName[fpath]; ok {
continue
}

// If go/build would ignore this file, e.g. based on build tags, also ignore it here.
//
// (Further coupling with go internals and x/tools may of course bear a cost eventually
// e.g. https://github.com/vektra/mockery/pull/117#issue-199337071, but should add
// worthwhile consistency in this tool's behavior in the meantime.)
match, matchErr := p.conf.Build.MatchFile(dir, fname)
if matchErr != nil {
return matchErr
// match, matchErr := p.conf.Build.MatchFile(dir, fname)
// if matchErr != nil {
// return matchErr
// }
// if !match {
// continue
// }

pkgs, err := packages.Load(&p.conf, "file="+fpath)
if err != nil {
return err
}
if !match {
if len(pkgs) == 0 {
continue
}
if len(pkgs) > 1 {
names := make([]string, len(pkgs))
for i, p := range pkgs {
names[i] = p.Name
}
panic(fmt.Sprintf("file %s resolves to multiple packages: %s", fpath, strings.Join(names, ", ")))
}

f, parseErr := p.conf.ParseFile(fpath, nil)
if parseErr != nil {
return parseErr
pkg := pkgs[0]
if len(pkg.Errors) > 0 {
return pkg.Errors[0]
}
if len(pkg.GoFiles) == 0 {
continue
}

p.configMapping[path] = append(p.configMapping[path], f)
p.pathToASTFile[fpath] = f
for idx, f := range pkg.GoFiles {
if _, ok := p.entriesByFileName[f]; ok {
continue
}
entry := parserEntry{
fileName: f,
pkg: pkg,
syntax: pkg.Syntax[idx],
}
p.entries = append(p.entries, &entry)
p.entriesByFileName[f] = &entry
}
p.packages = append(p.packages, pkg)
}

return nil
Expand Down Expand Up @@ -129,61 +154,35 @@ func (p *Parser) Load() error {
var wg sync.WaitGroup
wg.Add(1)
go func() {
for path, fi := range p.pathToASTFile {
for _, entry := range p.entries {
nv := NewNodeVisitor()
ast.Walk(nv, fi)
p.pathToInterfaces[path] = nv.DeclaredInterfaces()
ast.Walk(nv, entry.syntax)
entry.interfaces = nv.DeclaredInterfaces()
}
wg.Done()
}()

// Type-check a package consisting of this file.
// Type information for the imported packages
// comes from $GOROOT/pkg/$GOOS_$GOOARCH/fmt.a.
for path, files := range p.configMapping {
p.conf.CreateFromFiles(path, files...)
}

prog, err := p.conf.Load()
if err != nil {
return err
}

for _, pkgInfo := range prog.Created {
p.parserPackages = append(p.parserPackages, pkgInfo.Pkg)
}

wg.Wait()
return nil
}

func (p *Parser) Find(name string) (*Interface, error) {
for _, pkg := range p.parserPackages {
if iface := p.FindInPackage(name, pkg); iface != nil {
return iface, nil
for _, entry := range p.entries {
for _, iface := range entry.interfaces {
if iface == name {
list := p.packageInterfaces(entry.pkg.Types, entry.syntax, entry.fileName, []string{name}, nil)
if len(list) > 0 {
return list[0], nil
}
}
}
}
return nil, ErrNotInterface
}

func (p *Parser) FindInPackage(name string, pkg *types.Package) *Interface {
iFaces := p.pathToInterfaces[pkg.Path()]
for i := 0; i < len(iFaces); i++ {
iface := iFaces[i]
if iface == name {
list := make([]*Interface, 0)
file := p.pathToASTFile[pkg.Path()]
list = p.packageInterfaces(pkg, file, []string{name}, list)
return list[0]
}
}

return nil
}

type Interface struct {
Name string
Path string
FileName string
File *ast.File
Pkg *types.Package
Type *types.Interface
Expand All @@ -206,18 +205,22 @@ func (s sortableIFaceList) Less(i, j int) bool {

func (p *Parser) Interfaces() []*Interface {
ifaces := make(sortableIFaceList, 0)
for _, pkg := range p.parserPackages {
path := pkg.Path()
declaredIfaces := p.pathToInterfaces[path]
astFile := p.pathToASTFile[path]
ifaces = p.packageInterfaces(pkg, astFile, declaredIfaces, ifaces)
for _, entry := range p.entries {
declaredIfaces := entry.interfaces
astFile := entry.syntax
ifaces = p.packageInterfaces(entry.pkg.Types, astFile, entry.fileName, declaredIfaces, ifaces)
}

sort.Sort(ifaces)
return ifaces
}

func (p *Parser) packageInterfaces(pkg *types.Package, file *ast.File, declaredInterfaces []string, ifaces []*Interface) []*Interface {
func (p *Parser) packageInterfaces(
pkg *types.Package,
file *ast.File,
fileName string,
declaredInterfaces []string,
ifaces []*Interface) []*Interface {
scope := pkg.Scope()
for _, name := range declaredInterfaces {
obj := scope.Lookup(name)
Expand All @@ -244,6 +247,7 @@ func (p *Parser) packageInterfaces(pkg *types.Package, file *ast.File, declaredI
Name: name,
Pkg: pkg,
Path: pkg.Path(),
FileName: fileName,
Type: iface.Complete(),
NamedType: typ,
File: file,
Expand Down
22 changes: 13 additions & 9 deletions mockery/parse_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ func init() {
}

func TestFileParse(t *testing.T) {
parser := NewParser()
parser := NewParser(nil)

err := parser.Parse(testFile)
assert.NoError(t, err)
Expand All @@ -29,7 +29,7 @@ func TestFileParse(t *testing.T) {
}

func noTestFileInterfaces(t *testing.T) {
parser := NewParser()
parser := NewParser(nil)

err := parser.Parse(testFile)
assert.NoError(t, err)
Expand All @@ -43,7 +43,7 @@ func noTestFileInterfaces(t *testing.T) {
}

func TestBuildTagInFilename(t *testing.T) {
parser := NewParser()
parser := NewParser(nil)

// Include the major OS values found on https://golang.org/dl/ so we're likely to match
// anywhere the test is executed.
Expand All @@ -65,7 +65,7 @@ func TestBuildTagInFilename(t *testing.T) {
}

func TestBuildTagInComment(t *testing.T) {
parser := NewParser()
parser := NewParser(nil)

// Include the major OS values found on https://golang.org/dl/ so we're likely to match
// anywhere the test is executed.
Expand All @@ -87,8 +87,7 @@ func TestBuildTagInComment(t *testing.T) {
}

func TestCustomBuildTag(t *testing.T) {
parser := NewParser()
parser.AddBuildTags("custom")
parser := NewParser([]string{"custom"})

// Include two files that define the same interface, but with different
// build tags. Only one should be loaded.
Expand All @@ -100,7 +99,12 @@ func TestCustomBuildTag(t *testing.T) {
err = parser.Load()
assert.NoError(t, err) // Expect "redeclared in this block" if tags aren't respected

nodes := parser.Interfaces()
assert.Equal(t, 1, len(nodes))
assert.Equal(t, "IfaceWithCustomBuildTagInComment", nodes[0].Name)
found := false
for _, node := range parser.Interfaces() {
if node.Name == "IfaceWithCustomBuildTagInComment" {
found = true
break
}
}
assert.True(t, found, "IfaceWithCustomBuildTagInComment not parsed")
}
3 changes: 1 addition & 2 deletions mockery/walker.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,7 @@ type WalkerVisitor interface {
}

func (this *Walker) Walk(visitor WalkerVisitor) (generated bool) {
parser := NewParser()
parser.AddBuildTags(this.BuildTags...)
parser := NewParser(this.BuildTags)
this.doWalk(parser, this.BaseDir, visitor)

err := parser.Load()
Expand Down
Loading

0 comments on commit 0ecc4a9

Please sign in to comment.