Marvin Steadfast
104e72bd86
All checks were successful
continuous-integration/drone/push Build is passing
295 lines
6.9 KiB
Go
295 lines
6.9 KiB
Go
package pkg
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"go/ast"
|
|
"go/types"
|
|
"io/ioutil"
|
|
"path/filepath"
|
|
"sort"
|
|
"strings"
|
|
|
|
"github.com/rs/zerolog"
|
|
"github.com/vektra/mockery/v2/pkg/logging"
|
|
"golang.org/x/tools/go/packages"
|
|
)
|
|
|
|
type parserEntry struct {
|
|
fileName string
|
|
pkg *packages.Package
|
|
syntax *ast.File
|
|
interfaces []string
|
|
}
|
|
|
|
type packageLoadEntry struct {
|
|
pkgs []*packages.Package
|
|
err error
|
|
}
|
|
|
|
type Parser struct {
|
|
entries []*parserEntry
|
|
entriesByFileName map[string]*parserEntry
|
|
parserPackages []*types.Package
|
|
conf packages.Config
|
|
packageLoadCache map[string]packageLoadEntry
|
|
}
|
|
|
|
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),
|
|
entriesByFileName: map[string]*parserEntry{},
|
|
conf: conf,
|
|
packageLoadCache: map[string]packageLoadEntry{},
|
|
}
|
|
}
|
|
|
|
func (p *Parser) loadPackages(fpath string) ([]*packages.Package, error) {
|
|
if result, ok := p.packageLoadCache[fpath]; ok {
|
|
return result.pkgs, result.err
|
|
}
|
|
pkgs, err := packages.Load(&p.conf, "file="+fpath)
|
|
p.packageLoadCache[fpath] = packageLoadEntry{pkgs, err}
|
|
return pkgs, err
|
|
}
|
|
|
|
func (p *Parser) Parse(ctx context.Context, 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
|
|
// a "root" (GOROOT or a GOPATH). Only absolute paths will pass the prefix-based validation.
|
|
//
|
|
// For example, if our parse target is "./ifaces", Import will check if any "roots" are a
|
|
// prefix of "ifaces" and decide to skip the vendor search.
|
|
path, err := filepath.Abs(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
dir := filepath.Dir(path)
|
|
|
|
files, err := ioutil.ReadDir(dir)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for _, fi := range files {
|
|
log := zerolog.Ctx(ctx).With().
|
|
Str(logging.LogKeyDir, dir).
|
|
Str(logging.LogKeyFile, fi.Name()).
|
|
Logger()
|
|
ctx = log.WithContext(ctx)
|
|
|
|
if filepath.Ext(fi.Name()) != ".go" || strings.HasSuffix(fi.Name(), "_test.go") || strings.HasPrefix(fi.Name(), "mock_") {
|
|
continue
|
|
}
|
|
|
|
log.Debug().Msgf("parsing")
|
|
|
|
fname := fi.Name()
|
|
fpath := filepath.Join(dir, fname)
|
|
if _, ok := p.entriesByFileName[fpath]; ok {
|
|
continue
|
|
}
|
|
|
|
pkgs, err := p.loadPackages(fpath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
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, ", ")))
|
|
}
|
|
|
|
pkg := pkgs[0]
|
|
if len(pkg.Errors) > 0 {
|
|
return pkg.Errors[0]
|
|
}
|
|
if len(pkg.GoFiles) == 0 {
|
|
continue
|
|
}
|
|
|
|
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
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
type NodeVisitor struct {
|
|
declaredInterfaces []string
|
|
}
|
|
|
|
func NewNodeVisitor() *NodeVisitor {
|
|
return &NodeVisitor{
|
|
declaredInterfaces: make([]string, 0),
|
|
}
|
|
}
|
|
|
|
func (nv *NodeVisitor) DeclaredInterfaces() []string {
|
|
return nv.declaredInterfaces
|
|
}
|
|
|
|
func (nv *NodeVisitor) Visit(node ast.Node) ast.Visitor {
|
|
switch n := node.(type) {
|
|
case *ast.TypeSpec:
|
|
switch n.Type.(type) {
|
|
case *ast.InterfaceType, *ast.FuncType:
|
|
nv.declaredInterfaces = append(nv.declaredInterfaces, n.Name.Name)
|
|
}
|
|
}
|
|
return nv
|
|
}
|
|
|
|
func (p *Parser) Load() error {
|
|
for _, entry := range p.entries {
|
|
nv := NewNodeVisitor()
|
|
ast.Walk(nv, entry.syntax)
|
|
entry.interfaces = nv.DeclaredInterfaces()
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (p *Parser) Find(name string) (*Interface, error) {
|
|
for _, entry := range p.entries {
|
|
for _, iface := range entry.interfaces {
|
|
if iface == name {
|
|
list := p.packageInterfaces(entry.pkg.Types, entry.fileName, []string{name}, nil)
|
|
if len(list) > 0 {
|
|
return list[0], nil
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return nil, ErrNotInterface
|
|
}
|
|
|
|
type Method struct {
|
|
Name string
|
|
Signature *types.Signature
|
|
}
|
|
|
|
// Interface type represents the target type that we will generate a mock for.
|
|
// It could be an interface, or a function type.
|
|
// Function type emulates: an interface it has 1 method with the function signature
|
|
// and a general name, e.g. "Execute".
|
|
type Interface struct {
|
|
Name string // Name of the type to be mocked.
|
|
QualifiedName string // Path to the package of the target type.
|
|
FileName string
|
|
File *ast.File
|
|
Pkg *types.Package
|
|
NamedType *types.Named
|
|
IsFunction bool // If true, this instance represents a function, otherwise it's an interface.
|
|
ActualInterface *types.Interface // Holds the actual interface type, in case it's an interface.
|
|
SingleFunction *Method // Holds the function type information, in case it's a function type.
|
|
}
|
|
|
|
func (iface *Interface) Methods() []*Method {
|
|
if iface.IsFunction {
|
|
return []*Method{iface.SingleFunction}
|
|
}
|
|
methods := make([]*Method, iface.ActualInterface.NumMethods())
|
|
for i := 0; i < iface.ActualInterface.NumMethods(); i++ {
|
|
fn := iface.ActualInterface.Method(i)
|
|
methods[i] = &Method{Name: fn.Name(), Signature: fn.Type().(*types.Signature)}
|
|
}
|
|
return methods
|
|
}
|
|
|
|
type sortableIFaceList []*Interface
|
|
|
|
func (s sortableIFaceList) Len() int {
|
|
return len(s)
|
|
}
|
|
|
|
func (s sortableIFaceList) Swap(i, j int) {
|
|
s[i], s[j] = s[j], s[i]
|
|
}
|
|
|
|
func (s sortableIFaceList) Less(i, j int) bool {
|
|
return strings.Compare(s[i].Name, s[j].Name) == -1
|
|
}
|
|
|
|
func (p *Parser) Interfaces() []*Interface {
|
|
ifaces := make(sortableIFaceList, 0)
|
|
for _, entry := range p.entries {
|
|
declaredIfaces := entry.interfaces
|
|
ifaces = p.packageInterfaces(entry.pkg.Types, entry.fileName, declaredIfaces, ifaces)
|
|
}
|
|
|
|
sort.Sort(ifaces)
|
|
return ifaces
|
|
}
|
|
|
|
func (p *Parser) packageInterfaces(
|
|
pkg *types.Package,
|
|
fileName string,
|
|
declaredInterfaces []string,
|
|
ifaces []*Interface) []*Interface {
|
|
scope := pkg.Scope()
|
|
for _, name := range declaredInterfaces {
|
|
obj := scope.Lookup(name)
|
|
if obj == nil {
|
|
continue
|
|
}
|
|
|
|
typ, ok := obj.Type().(*types.Named)
|
|
if !ok {
|
|
continue
|
|
}
|
|
|
|
name = typ.Obj().Name()
|
|
|
|
if typ.Obj().Pkg() == nil {
|
|
continue
|
|
}
|
|
|
|
elem := &Interface{
|
|
Name: name,
|
|
Pkg: pkg,
|
|
QualifiedName: pkg.Path(),
|
|
FileName: fileName,
|
|
NamedType: typ,
|
|
}
|
|
|
|
iface, ok := typ.Underlying().(*types.Interface)
|
|
if ok {
|
|
elem.IsFunction = false
|
|
elem.ActualInterface = iface
|
|
} else {
|
|
sig, ok := typ.Underlying().(*types.Signature)
|
|
if !ok {
|
|
continue
|
|
}
|
|
elem.IsFunction = true
|
|
elem.SingleFunction = &Method{Name: "Execute", Signature: sig}
|
|
}
|
|
|
|
ifaces = append(ifaces, elem)
|
|
}
|
|
|
|
return ifaces
|
|
}
|