Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Generation from interface aliases #109

Merged
merged 7 commits into from
Aug 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ generate:
go run ./cmd/minimock/minimock.go -i ./tests.contextAccepter -o ./tests/context_accepter_mock.go
go run ./cmd/minimock/minimock.go -i github.com/gojuno/minimock/v3.Tester -o ./tests/package_name_specified_test.go -p tests_test
go run ./cmd/minimock/minimock.go -i ./tests.actor -o ./tests/actor_mock.go
go run ./cmd/minimock/minimock.go -i ./tests.formatterAlias -o ./tests/formatter_alias_mock.go
go run ./cmd/minimock/minimock.go -i ./tests.formatterType -o ./tests/formatter_type_mock.go
go run ./cmd/minimock/minimock.go -i ./tests.reader -o ./tests/reader_mock.go

./bin:
mkdir ./bin
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ The main features of minimock are:
* It supports generics.
* It works well with [table driven tests](https://dave.cheney.net/2013/06/09/writing-table-driven-tests-in-go) because you can set up mocks for several methods in one line of code using the builder pattern.
* It can generate several mocks in one run.
* It can generate mocks from interface aliases.
* It generates code that passes default set of [golangci-lint](https://github.com/golangci/golangci-lint) checks.
* It puts //go:generate instruction into the generated code, so all you need to do when the source interface is updated is to run the `go generate ./...` command from within the project's directory.
* It makes sure that all mocked methods have been called during the test and keeps your test code clean and up to date.
Expand Down
3 changes: 2 additions & 1 deletion equal.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ func Diff(e, a interface{}) string {
return ""
}

initialKind := k
if k == reflect.Ptr {
t = t.Elem()
k = t.Kind()
Expand All @@ -73,7 +74,7 @@ func Diff(e, a interface{}) string {
return ""
}

if k == reflect.Struct {
if initialKind == reflect.Struct {
a = setAnyContext(e, a)
}

Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ module github.com/gojuno/minimock/v3

require (
github.com/davecgh/go-spew v1.1.1
github.com/hexdigest/gowrap v1.3.7
github.com/hexdigest/gowrap v1.3.10
github.com/pkg/errors v0.9.1
github.com/pmezard/go-difflib v1.0.0
github.com/stretchr/testify v1.8.4
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaS
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/hexdigest/gowrap v1.3.7 h1:vgfh7NQEZQyMe17PBaMX/d4+7hTLyxo2q4DFTfx3p1E=
github.com/hexdigest/gowrap v1.3.7/go.mod h1:5KTYxPjK1RRfD+9L4Oo9gjP3XNAs4rkoVK2E7eAEFyM=
github.com/hexdigest/gowrap v1.3.10 h1:e8NwAdtnwpRJ4Ks76+GctDNCFIkHR2TCFCWshug/zIE=
github.com/hexdigest/gowrap v1.3.10/go.mod h1:5KTYxPjK1RRfD+9L4Oo9gjP3XNAs4rkoVK2E7eAEFyM=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
Expand Down
134 changes: 123 additions & 11 deletions internal/types/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ import (
"go/ast"
"go/printer"
"go/token"
"strings"

"github.com/hexdigest/gowrap/pkg"
)

// InterfaceSpecification represents abstraction over interface type. It contains all the metadata
Expand All @@ -23,17 +26,13 @@ type InterfaceSpecificationParam struct {
}

func FindAllInterfaces(p *ast.Package, pattern string) []InterfaceSpecification {
// Find all declared types in a single package
types := []*ast.TypeSpec{}
for _, file := range p.Files {
types = append(types, findAllTypeSpecsInFile(file)...)
}

// Filter interfaces from all the declarations
interfaces := []*ast.TypeSpec{}
for _, typeSpec := range types {
if isInterface(typeSpec) {
interfaces = append(interfaces, typeSpec)
for _, file := range p.Files {
for _, typeSpec := range findAllTypeSpecsInFile(file) {
if isInterface(typeSpec, file.Imports) {
interfaces = append(interfaces, typeSpec)
}
}
}

Expand All @@ -57,8 +56,121 @@ func FindAllInterfaces(p *ast.Package, pattern string) []InterfaceSpecification
return interfaceSpecifications
}

func isInterface(typeSpec *ast.TypeSpec) bool {
// Check if this type declaration is specifically an interface declaration
func isInterface(typeSpec *ast.TypeSpec, fileImports []*ast.ImportSpec) bool {
// we are generating mocks for interfaces,
// interface aliases to types from the same package
// and aliases to types from another package
return isInterfaceType(typeSpec) ||
isInterfaceAlias(typeSpec, fileImports) ||
isExportedInterfaceAlias(typeSpec, fileImports)
}

// isInterfaceAlias checks if type is an alias to other
// interface type that is in the same package
func isInterfaceAlias(typeSpec *ast.TypeSpec, fileImports []*ast.ImportSpec) bool {
ident, ok := typeSpec.Type.(*ast.Ident)
if !ok {
return false
}

if ident.Obj == nil {
return false
}

if ts, ok := ident.Obj.Decl.(*ast.TypeSpec); ok {
return isInterface(ts, fileImports)
}

return false
}

// isExportedInterfaceAlias checks if type is an alias to other
// interface type that is in other exported package
func isExportedInterfaceAlias(typeSpec *ast.TypeSpec, fileImports []*ast.ImportSpec) bool {
selector, ok := typeSpec.Type.(*ast.SelectorExpr)
if !ok {
return false
}

ident, ok := selector.X.(*ast.Ident)
if !ok {
return false
}

srcPkgPath := findSourcePackage(ident, fileImports)
srcAst, err := getPackageAst(srcPkgPath)
if err != nil {
return false
}

typeSpec, imports := findTypeSpecInPackage(srcAst, selector.Sel.Name)

// we have to check recursively because checked typed might be
// another alias to other interface
return isInterface(typeSpec, imports)
}

func getPackageAst(packagePath string) (*ast.Package, error) {
srcPkg, err := pkg.Load(packagePath)
if err != nil {
return nil, err
}

fs := token.NewFileSet()
srcAst, err := pkg.AST(fs, srcPkg)
if err != nil {
return nil, err
}

return srcAst, nil
}

func findTypeSpecInPackage(p *ast.Package, name string) (typeSpec *ast.TypeSpec, imports []*ast.ImportSpec) {
for _, f := range p.Files {
if f == nil {
continue
}
types := findAllTypeSpecsInFile(f)
typeSpec, found := findTypeByName(types, name)
if found {
return typeSpec, f.Imports
}
}

return
}

func findTypeByName(types []*ast.TypeSpec, name string) (*ast.TypeSpec, bool) {
for _, ts := range types {
if ts.Name.Name == name {
return ts, true
}
}

return nil, false
}

func findSourcePackage(ident *ast.Ident, imports []*ast.ImportSpec) string {
for _, imp := range imports {
cleanPath := strings.Trim(imp.Path.Value, "\"")
if imp.Name != nil {
if ident.Name == imp.Name.Name {
return cleanPath
}

continue
}

slash := strings.LastIndex(cleanPath, "/")
if ident.Name == cleanPath[slash+1:] {
return cleanPath
}
}

return ""
}

func isInterfaceType(typeSpec *ast.TypeSpec) bool {
_, ok := typeSpec.Type.(*ast.InterfaceType)
return ok
}
Expand Down
Loading
Loading