Skip to content

Commit

Permalink
Support embedded interfaces in cmg (#277)
Browse files Browse the repository at this point in the history
Before, Clue Mock Generator would not include the methods from embedded
interfaces, for example:

```golang
package something

import (
    "goa.design/clue/health"
)

type Client interface {
    Get() (string, error)

    health.Pinger
}
```

Would not include the methods from `health.Pinger` in the generated
mock. Now it will!
  • Loading branch information
douglaswth committed Aug 29, 2023
1 parent 13c6d8b commit 4d1dc69
Show file tree
Hide file tree
Showing 13 changed files with 397 additions and 123 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,8 @@ package imported

type (
Type byte

Interface interface {
Imported(Type) Type
}
)
7 changes: 7 additions & 0 deletions mock/cmd/cmg/pkg/generate/_tests/extensive/extensive.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,13 @@ type (
NamedTypes(Struct, Array, io.Reader, imported.Type, goa.Endpoint, Generic[uint, string, Struct, Array]) (Struct, Array, io.Reader, imported.Type, goa.Endpoint, Generic[uint, string, Struct, Array])
FuncNamedTypes(func(Struct, Array, io.Reader, imported.Type, goa.Endpoint, Generic[uint, string, Struct, Array])) func(Struct, Array, io.Reader, imported.Type, goa.Endpoint, Generic[uint, string, Struct, Array])
VariableConflicts(f, m uint)

Embedded
imported.Interface
}

Embedded interface {
Embedded(int8) int8
}

Generic[K comparable, V ~int | bool | string, X, Y any] interface {
Expand Down
72 changes: 72 additions & 0 deletions mock/cmd/cmg/pkg/generate/_tests/extensive/mocks/extensive.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 12 additions & 0 deletions mock/cmd/cmg/pkg/parse/_tests/doer/doer.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,22 @@
package doer

import (
"example.com/a/b/external"
)

type (
Doer interface {
Do(a, b int, c float64) (d, e int, err error)
}

EmbeddedDoer interface {
Doer
}

ExternalEmbeddedDoer interface {
external.Doer
}

doer interface { //nolint:unused
do(a, b int, c float64) (d, e int, err error)
}
Expand Down
7 changes: 7 additions & 0 deletions mock/cmd/cmg/pkg/parse/_tests/external/doer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package external

type (
Doer interface {
Do(a, b int, c float64) (d, e int, err error)
}
)
26 changes: 23 additions & 3 deletions mock/cmd/cmg/pkg/parse/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,13 +51,33 @@ func (i *interface_) TypeParameters() (typeParameters []Type) {
return
}

func (i *interface_) Methods() (methods []Method) {
for _, m := range i.interfaceType.Methods.List {
func (i *interface_) Methods() []Method {
return i.methods(i.interfaceType)
}

func (i *interface_) methods(it *ast.InterfaceType) (methods []Method) {
for _, m := range it.Methods.List {
switch t := m.Type.(type) {
case *ast.FuncType:
for _, n := range m.Names {
o, _, _ := types.LookupFieldOrMethod(i.p.Types.Scope().Lookup(i.Name()).Type(), true, i.p.Types, n.Name)
methods = append(methods, newMethod(i.p, n, t, o.Type().Underlying().(*types.Signature).Variadic()))
methods = append(methods, newASTMethod(i.p, n, t, o.Type().Underlying().(*types.Signature).Variadic()))
}
case *ast.Ident:
switch dt := t.Obj.Decl.(type) {
case *ast.TypeSpec:
switch t := dt.Type.(type) {
case *ast.InterfaceType:
methods = append(methods, i.methods(t)...)
}
}
case *ast.SelectorExpr:
if tv, ok := i.p.TypesInfo.Types[t]; ok {
if ti, ok := tv.Type.Underlying().(*types.Interface); ok {
for j := 0; j < ti.NumMethods(); j++ {
methods = append(methods, newTypesMethod(ti.Method(j)))
}
}
}
}
}
Expand Down
68 changes: 67 additions & 1 deletion mock/cmd/cmg/pkg/parse/interface_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package parse

import (
"go/ast"
"go/types"
"testing"

"github.com/stretchr/testify/assert"
Expand Down Expand Up @@ -134,6 +135,28 @@ func TestInterface_Methods(t *testing.T) {
require.Len(t, ps, 1)
p := ps[0]

var (
externalDoerSelectorExpr *ast.SelectorExpr
externalDoerInterface *types.Interface
)
for at, tv := range p.TypesInfo.Types {
if se, ok := at.(*ast.SelectorExpr); ok {
if i, ok := se.X.(*ast.Ident); ok {
if i.Name != "external" {
continue
}
if se.Sel.Name == "Doer" {
externalDoerSelectorExpr = se
externalDoerInterface, _ = tv.Type.Underlying().(*types.Interface)
break
}
}
}
}
require.NotNil(t, externalDoerSelectorExpr)
require.NotNil(t, externalDoerInterface)
require.Greater(t, externalDoerInterface.NumMethods(), 0)

cases := []struct {
Name string
TypeSpec *ast.TypeSpec
Expand All @@ -159,7 +182,7 @@ func TestInterface_Methods(t *testing.T) {
},
}}},
Expected: []Method{
newMethod(p, ast.NewIdent("Do"), &ast.FuncType{
newASTMethod(p, ast.NewIdent("Do"), &ast.FuncType{
Params: &ast.FieldList{List: []*ast.Field{
{Names: []*ast.Ident{ast.NewIdent("a"), ast.NewIdent("b")}, Type: ast.NewIdent("int")},
{Names: []*ast.Ident{ast.NewIdent("c")}, Type: ast.NewIdent("float64")},
Expand All @@ -171,6 +194,49 @@ func TestInterface_Methods(t *testing.T) {
}, false),
},
},
{
Name: "embedded interface",
TypeSpec: &ast.TypeSpec{Name: ast.NewIdent("EmbeddedDoer")},
InterfaceType: &ast.InterfaceType{Methods: &ast.FieldList{List: []*ast.Field{{
Type: &ast.Ident{Name: "Doer", Obj: &ast.Object{Kind: ast.Typ, Decl: &ast.TypeSpec{
Type: &ast.InterfaceType{Methods: &ast.FieldList{List: []*ast.Field{
{
Names: []*ast.Ident{ast.NewIdent("Do")},
Type: &ast.FuncType{
Params: &ast.FieldList{List: []*ast.Field{
{Names: []*ast.Ident{ast.NewIdent("a"), ast.NewIdent("b")}, Type: ast.NewIdent("int")},
{Names: []*ast.Ident{ast.NewIdent("c")}, Type: ast.NewIdent("float64")},
}},
Results: &ast.FieldList{List: []*ast.Field{
{Names: []*ast.Ident{ast.NewIdent("d"), ast.NewIdent("e")}, Type: ast.NewIdent("int")},
{Names: []*ast.Ident{ast.NewIdent("err")}, Type: ast.NewIdent("error")},
}},
},
},
}}},
}}}}},
}},
Expected: []Method{
newASTMethod(p, ast.NewIdent("Do"), &ast.FuncType{
Params: &ast.FieldList{List: []*ast.Field{
{Names: []*ast.Ident{ast.NewIdent("a"), ast.NewIdent("b")}, Type: ast.NewIdent("int")},
{Names: []*ast.Ident{ast.NewIdent("c")}, Type: ast.NewIdent("float64")},
}},
Results: &ast.FieldList{List: []*ast.Field{
{Names: []*ast.Ident{ast.NewIdent("d"), ast.NewIdent("e")}, Type: ast.NewIdent("int")},
{Names: []*ast.Ident{ast.NewIdent("err")}, Type: ast.NewIdent("error")},
}},
}, false),
},
},
{
Name: "external embedded interface",
TypeSpec: &ast.TypeSpec{Name: ast.NewIdent("ExternalEmbeddedDoer")},
InterfaceType: &ast.InterfaceType{Methods: &ast.FieldList{List: []*ast.Field{{Type: externalDoerSelectorExpr}}}},
Expected: []Method{
newTypesMethod(externalDoerInterface.Method(0)),
},
},
}

for _, tc := range cases {
Expand Down
68 changes: 52 additions & 16 deletions mock/cmd/cmg/pkg/parse/method.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package parse

import (
"go/ast"
"go/types"

"golang.org/x/tools/go/packages"
)
Expand All @@ -15,54 +16,89 @@ type (
Variadic() bool
}

method struct {
astMethod struct {
p *packages.Package
ident *ast.Ident
funcType *ast.FuncType
variadic bool
}

typesMethod struct {
f *types.Func
s *types.Signature
}
)

func newMethod(p *packages.Package, ident *ast.Ident, funcType *ast.FuncType, variadic bool) Method {
return &method{p: p, ident: ident, funcType: funcType, variadic: variadic}
func newASTMethod(p *packages.Package, ident *ast.Ident, funcType *ast.FuncType, variadic bool) Method {
return &astMethod{p: p, ident: ident, funcType: funcType, variadic: variadic}
}

func (m *method) Name() string {
return m.ident.Name
func (am *astMethod) Name() string {
return am.ident.Name
}

func (m *method) IsExported() bool {
return m.ident.IsExported()
func (am *astMethod) IsExported() bool {
return am.ident.IsExported()
}

func (m *method) Parameters() (parameters []Value) {
for _, p := range m.funcType.Params.List {
func (am *astMethod) Parameters() (parameters []Value) {
for _, p := range am.funcType.Params.List {
idents := []*ast.Ident{nil}
if p.Names != nil {
idents = p.Names
}
for _, ident := range idents {
parameters = append(parameters, newValue(m.p, ident, p.Type))
parameters = append(parameters, newASTValue(am.p, ident, p.Type))
}
}
return
}

func (m *method) Results() (results []Value) {
if m.funcType.Results != nil {
for _, r := range m.funcType.Results.List {
func (am *astMethod) Results() (results []Value) {
if am.funcType.Results != nil {
for _, r := range am.funcType.Results.List {
idents := []*ast.Ident{nil}
if r.Names != nil {
idents = r.Names
}
for _, ident := range idents {
results = append(results, newValue(m.p, ident, r.Type))
results = append(results, newASTValue(am.p, ident, r.Type))
}
}
}
return
}

func (m *method) Variadic() bool {
return m.variadic
func (am *astMethod) Variadic() bool {
return am.variadic
}

func newTypesMethod(f *types.Func) Method {
return &typesMethod{f: f, s: f.Type().(*types.Signature)}
}

func (tm *typesMethod) Name() string {
return tm.f.Name()
}

func (tm *typesMethod) IsExported() bool {
return tm.f.Exported()
}

func (tm *typesMethod) Parameters() (parameters []Value) {
for i := 0; i < tm.s.Params().Len(); i++ {
parameters = append(parameters, newTypesValue(tm.s.Params().At(i)))
}
return
}

func (tm *typesMethod) Results() (results []Value) {
for i := 0; i < tm.s.Results().Len(); i++ {
results = append(results, newTypesValue(tm.s.Results().At(i)))
}
return
}

func (tm *typesMethod) Variadic() bool {
return tm.s.Variadic()
}
Loading

0 comments on commit 4d1dc69

Please sign in to comment.