Files
Jay Brown 7d78e65d0c Merged in feature/semver (pull request #93)
Add semantic versioning to all services

* working

* Merge branch 'main' of bitbucket.org:aarete/query-orchestration into feature/semver

* 1.24 mod

* go upgrade

* tool

* Merge branch 'main' of bitbucket.org:aarete/query-orchestration into feature/semver
2025-03-06 22:33:39 +00:00

422 lines
9.8 KiB
Go

package pkg
import (
"context"
"errors"
"fmt"
"go/ast"
"go/types"
"os"
"path/filepath"
"sort"
"strings"
"github.com/vektra/mockery/v2/pkg/logging"
"github.com/rs/zerolog"
"golang.org/x/tools/go/packages"
)
type fileEntry struct {
fileName string
pkg *packages.Package
syntax *ast.File
interfaces []string
disableFuncMocks bool
}
func (f *fileEntry) ParseInterfaces(ctx context.Context) {
nv := NewNodeVisitor(ctx, f.disableFuncMocks)
ast.Walk(nv, f.syntax)
f.interfaces = nv.DeclaredInterfaces()
}
type packageLoadEntry struct {
pkgs []*packages.Package
err error
}
type Parser struct {
files []*fileEntry
entriesByFileName map[string]*fileEntry
parserPackages []*types.Package
conf packages.Config
packageLoadCache map[string]packageLoadEntry
disableFuncMocks bool
}
func ParserDisableFuncMocks(disable bool) func(*Parser) {
return func(p *Parser) {
p.disableFuncMocks = disable
}
}
func NewParser(buildTags []string, opts ...func(*Parser)) *Parser {
var conf packages.Config
conf.Mode = packages.NeedTypes |
packages.NeedTypesSizes |
packages.NeedSyntax |
packages.NeedTypesInfo |
packages.NeedImports |
packages.NeedName |
packages.NeedFiles |
packages.NeedCompiledGoFiles
if len(buildTags) > 0 {
conf.BuildFlags = []string{"-tags", strings.Join(buildTags, ",")}
}
p := &Parser{
parserPackages: make([]*types.Package, 0),
entriesByFileName: map[string]*fileEntry{},
conf: conf,
packageLoadCache: map[string]packageLoadEntry{},
}
for _, opt := range opts {
opt(p)
}
return p
}
func (p *Parser) loadPackages(fpath string) ([]*packages.Package, error) {
if result, ok := p.packageLoadCache[filepath.Dir(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) ParsePackages(ctx context.Context, packageNames []string) error {
log := zerolog.Ctx(ctx)
packages, err := packages.Load(&p.conf, packageNames...)
if err != nil {
return err
}
for _, pkg := range packages {
if len(pkg.GoFiles) == 0 {
continue
}
for _, err := range pkg.Errors {
log.Err(err).Msg("encountered error when loading package")
}
if len(pkg.Errors) != 0 {
return errors.New("error occurred when loading packages")
}
for fileIdx, file := range pkg.GoFiles {
log.Debug().
Str("package", pkg.PkgPath).
Str("file", file).
Msgf("found file")
entry := fileEntry{
fileName: file,
pkg: pkg,
syntax: pkg.Syntax[fileIdx],
disableFuncMocks: p.disableFuncMocks,
}
entry.ParseInterfaces(ctx)
p.files = append(p.files, &entry)
p.entriesByFileName[file] = &entry
}
}
return nil
}
// DEPRECATED: Parse is part of the deprecated, legacy mockery behavior. This is not
// used when the packages feature is enabled.
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 := os.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 := fileEntry{
fileName: f,
pkg: pkg,
syntax: pkg.Syntax[idx],
}
p.files = append(p.files, &entry)
p.entriesByFileName[f] = &entry
}
}
return nil
}
func (p *Parser) Load(ctx context.Context) error {
for _, entry := range p.files {
entry.ParseInterfaces(ctx)
}
return nil
}
func (p *Parser) Find(name string) (*Interface, error) {
for _, entry := range p.files {
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
}
func (p *Parser) Has(packageName, interfaceName string) bool {
for _, entry := range p.files {
if entry.pkg.PkgPath != packageName {
continue
}
for _, ifaceName := range entry.interfaces {
if ifaceName != interfaceName {
continue
}
return true
}
}
return false
}
func (p *Parser) Interfaces() []*Interface {
ifaces := make(sortableIFaceList, 0)
for _, entry := range p.files {
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
}
var typ *types.Named
var name string
ttyp := obj.Type()
if talias, ok := obj.Type().(*types.Alias); ok {
name = talias.Obj().Name()
ttyp = types.Unalias(obj.Type())
}
typ, ok := ttyp.(*types.Named)
if !ok {
continue
}
if name == "" {
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
}
type Method struct {
Name string
Signature *types.Signature
}
type TypesPackage interface {
Name() string
Path() string
}
// 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 TypesPackage
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
}
type NodeVisitor struct {
declaredInterfaces []string
disableFuncMocks bool
ctx context.Context
}
func NewNodeVisitor(ctx context.Context, disableFuncMocks bool) *NodeVisitor {
return &NodeVisitor{
declaredInterfaces: make([]string, 0),
disableFuncMocks: disableFuncMocks,
ctx: ctx,
}
}
func (nv *NodeVisitor) DeclaredInterfaces() []string {
return nv.declaredInterfaces
}
func (nv *NodeVisitor) add(ctx context.Context, n *ast.TypeSpec) {
log := zerolog.Ctx(ctx)
log.Debug().
Str("node-name", n.Name.Name).
Str("node-type", fmt.Sprintf("%T", n.Type)).
Msg("found node with acceptable type for mocking")
nv.declaredInterfaces = append(nv.declaredInterfaces, n.Name.Name)
}
func (nv *NodeVisitor) Visit(node ast.Node) ast.Visitor {
log := zerolog.Ctx(nv.ctx)
switch n := node.(type) {
case *ast.TypeSpec:
log := log.With().
Str("node-name", n.Name.Name).
Str("node-type", fmt.Sprintf("%T", n.Type)).
Logger()
switch n.Type.(type) {
case *ast.FuncType:
if nv.disableFuncMocks {
break
}
nv.add(nv.ctx, n)
case *ast.InterfaceType, *ast.IndexExpr, *ast.IndexListExpr, *ast.SelectorExpr, *ast.Ident:
nv.add(nv.ctx, n)
default:
log.Debug().Msg("found node with unacceptable type for mocking. Rejecting.")
}
}
return nv
}