Skip to content

Commit

Permalink
Parsing interface (#32)
Browse files Browse the repository at this point in the history
* WIP: Parsing interface

Signed-off-by: Joffref <[email protected]>

* Resolve comments and factorize code

Signed-off-by: Joffref <[email protected]>

* Resolve comments and improve documentation

Signed-off-by: Joffref <[email protected]>

* Resolve comments and add more tests

Signed-off-by: Joffref <[email protected]>

---------

Signed-off-by: Joffref <[email protected]>
  • Loading branch information
Joffref authored Dec 10, 2023
1 parent a0f6879 commit 95239fc
Show file tree
Hide file tree
Showing 20 changed files with 977 additions and 317 deletions.
7 changes: 3 additions & 4 deletions cmd/genz/genz.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package genz
import (
"flag"
"fmt"
"github.com/Joffref/genz/internal/parser"
"io"
"log"
"net/http"
Expand All @@ -13,7 +14,6 @@ import (

"github.com/Joffref/genz/internal/command"
"github.com/Joffref/genz/internal/generator"
"github.com/Joffref/genz/internal/parser"
"github.com/Joffref/genz/internal/utils"
)

Expand All @@ -29,7 +29,7 @@ Flags:`

var (
generateCmd = flag.NewFlagSet("", flag.ExitOnError)
typeName = generateCmd.String("type", "", "name of the struct to parse")
typeName = generateCmd.String("type", "", "name of the type to parse")
templateLocation = generateCmd.String("template", "", "go-template local or remote file")
output = generateCmd.String("output", "", "output file name; default srcdir/<type>.gen.go")
buildTags = generateCmd.String("tags", "", "comma-separated list of build tags to apply")
Expand Down Expand Up @@ -93,12 +93,11 @@ func (c generateCommand) Run() error {
// Default: process whole package in current directory.
args = []string{"."}
}

buf, err := generator.Generate(
utils.LoadPackage(args, tags),
string(template),
*typeName,
parser.Parse,
parser.Parser,
)
if err != nil {
return err
Expand Down
14 changes: 14 additions & 0 deletions examples/4_mock/mock.tmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package {{ .PackageName }}

{{ range .PackageImports }}import "{{ . }}"{{ end }}

type {{.Type.InternalName}}Mock struct {
{{ range .Methods }}{{ .Name }}Func func({{ range $index, $element := .Params }} param{{$index}} {{ .Name }}{{ end }}) {{ range .Returns }}{{ .InternalName }}{{ end }}
{{ end }}
}

{{ range .Methods }}
func (m *{{ $.Type.InternalName }}Mock) {{ .Name }}({{ range $index, $element := .Params }}param{{$index}} {{ .Name }} {{ end }}) {{ range .Returns }}{{ .InternalName }}{{ end }} {
{{ if .Returns }}return {{ end }}m.{{ .Name }}Func({{ range $index, $element := .Params }}param{{$index}} {{ end }})
}
{{ end }}
14 changes: 14 additions & 0 deletions examples/4_mock/test/expected.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package test

type HelloMock struct {
SayHelloToFunc func(param0 string) string
HelloFunc func() string
}

func (m *HelloMock) SayHelloTo(param0 string) string {
return m.SayHelloToFunc(param0)
}

func (m *HelloMock) Hello() string {
return m.HelloFunc()
}
7 changes: 7 additions & 0 deletions examples/4_mock/test/hello.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package test

//go:generate genz -template ../mock.tmpl -output hello.gen.go -type Hello
type Hello interface {
SayHelloTo(name string) string
Hello() string
}
41 changes: 41 additions & 0 deletions examples/4_mock/test/hello_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package test

import "testing"

func TestHelloMock_SayHelloTo(t *testing.T) {
type fields struct {
SayHelloToFunc func(param0 string) string
}
type args struct {
param0 string
}
tests := []struct {
name string
fields fields
args args
want string
}{
{
name: "dummy_test",
fields: fields{
SayHelloToFunc: func(param0 string) string {
return ""
},
},
args: args{
param0: "",
},
want: "",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
m := &HelloMock{
SayHelloToFunc: tt.fields.SayHelloToFunc,
}
if got := m.SayHelloTo(tt.args.param0); got != tt.want {
t.Errorf("SayHelloTo() = %v, want %v", got, tt.want)
}
})
}
}
8 changes: 4 additions & 4 deletions internal/generator/generator.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,16 @@ package generator
import (
"bytes"
"fmt"
"github.com/Joffref/genz/pkg/models"
"go/format"
"html/template"
"log"

"github.com/Joffref/genz/internal/parser"
"github.com/Masterminds/sprig/v3"
"golang.org/x/tools/go/packages"
)

type parseFunc func(pkg *packages.Package, structName string) (parser.Struct, error)
type parseFunc func(pkg *packages.Package, typeName string) (models.ParsedElement, error)

func Generate(
pkg *packages.Package,
Expand All @@ -23,7 +23,7 @@ func Generate(
) (bytes.Buffer, error) {
log.Printf("generating template for type %s", typeName)

parsedType, err := parse(pkg, typeName)
parsedElement, err := parse(pkg, typeName)
if err != nil {
return bytes.Buffer{}, fmt.Errorf("failed to inspect package: %v", err)
}
Expand All @@ -33,7 +33,7 @@ func Generate(
return bytes.Buffer{}, fmt.Errorf("failed to parse template: %v", err)
}
buf := bytes.Buffer{}
err = tmpl.Execute(&buf, parsedType)
err = tmpl.Execute(&buf, parsedElement)
if err != nil {
return bytes.Buffer{}, fmt.Errorf("failed to execute template: %v", err)
}
Expand Down
24 changes: 13 additions & 11 deletions internal/generator/generator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,17 @@ package generator_test
import (
"bytes"
"errors"
"github.com/Joffref/genz/pkg/models"
"strings"
"testing"

"github.com/Joffref/genz/internal/generator"
"github.com/Joffref/genz/internal/parser"
"golang.org/x/tools/go/packages"
)

func TestGenerateErrorParse(t *testing.T) {
parseFunc := func(pkg *packages.Package, structName string) (parser.Struct, error) {
return parser.Struct{}, errors.New("failed to parse package")
parseFunc := func(pkg *packages.Package, structName string) (models.ParsedElement, error) {
return models.ParsedElement{}, errors.New("failed to parse package")
}
_, err := generator.Generate(nil, "template", "typeName", parseFunc)
if err == nil {
Expand All @@ -25,8 +25,8 @@ func TestGenerateErrorParse(t *testing.T) {
}

func TestGenerateErrorTemplateParse(t *testing.T) {
parseFunc := func(pkg *packages.Package, structName string) (parser.Struct, error) {
return parser.Struct{}, nil
parseFunc := func(pkg *packages.Package, structName string) (models.ParsedElement, error) {
return models.ParsedElement{}, nil
}
_, err := generator.Generate(nil, "{{", "typeName", parseFunc)
if err == nil {
Expand All @@ -38,8 +38,8 @@ func TestGenerateErrorTemplateParse(t *testing.T) {
}

func TestGenerateErrorTemplateExecute(t *testing.T) {
parseFunc := func(pkg *packages.Package, structName string) (parser.Struct, error) {
return parser.Struct{}, nil
parseFunc := func(pkg *packages.Package, structName string) (models.ParsedElement, error) {
return models.ParsedElement{}, nil
}
_, err := generator.Generate(nil, "{{.Foo}}", "typeName", parseFunc)
if err == nil {
Expand All @@ -51,10 +51,12 @@ func TestGenerateErrorTemplateExecute(t *testing.T) {
}

func TestGenerateSuccess(t *testing.T) {
parseFunc := func(pkg *packages.Package, structName string) (parser.Struct, error) {
return parser.Struct{
Type: parser.Type{
Name: "TypeName",
parseFunc := func(pkg *packages.Package, structName string) (models.ParsedElement, error) {
return models.ParsedElement{
Element: models.Element{
Type: models.Type{
Name: "TypeName",
},
},
}, nil
}
Expand Down
79 changes: 79 additions & 0 deletions internal/parser/helper.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package parser

import (
"fmt"
"github.com/Joffref/genz/pkg/models"
"go/ast"
"go/types"
"golang.org/x/tools/go/packages"
)

// parseElementType initializes a models.Element with the given name and package.
// It does not parse the element's methods or attributes.
// It should be called first by every parse* function.
func parseElementType(pkg *packages.Package, name string) (models.Element, error) {
if pkg.Types == nil {
return models.Element{}, fmt.Errorf("package %s has no types", pkg.Name)
}
return models.Element{
Type: models.Type{
Name: fmt.Sprintf("%s.%s", pkg.Name, name),
InternalName: name,
},
}, nil
}

// objectAsNamedType returns the given object as a *types.Named.
// It returns an error if the object is not a *types.TypeName or if the type of the *types.TypeName is not a *types.Named.
func objectAsNamedType(object types.Object) (*types.Named, error) {
typeName, isTypeName := object.(*types.TypeName)
if !isTypeName {
return nil, fmt.Errorf("%s is not a TypeName", object.Name())
}
namedType, isNamedType := typeName.Type().(*types.Named)
if !isNamedType {
return nil, fmt.Errorf("%s is not a named type", object.Name())
}

return namedType, nil
}

// parseType returns a models.Type from the given types.Type.
// It returns the type name with the package qualifier and without the package qualifier.
func parseType(t types.Type) models.Type {
// Remove every qualifier before the type name
// transforming "github.com/google/uuid.UUID" into "UUID"
noPackageQualifier := func(_ *types.Package) string { return "" }

// Adds the package name qualifier before the type name
// transforming "github.com/google/uuid.UUID" into "uuid.UUID"
packageNameQualifier := func(pkg *types.Package) string {
return pkg.Name()
}

return models.Type{
Name: types.TypeString(t, packageNameQualifier), // (e.g. "uuid.UUID")
InternalName: types.TypeString(t, noPackageQualifier), // (e.g. "UUID")
}

}

// loadAstExpr returns the ast.Expr of the given typeName in the given package.
// It returns an error if the typeName is not found in the package.
// Be aware that ast.Expr is an interface, so the returned value can be of any type.
func loadAstExpr(pkg *packages.Package, typeName string) (ast.Expr, error) {
for ident := range pkg.TypesInfo.Defs {
if ident.Name == typeName {
if ident.Obj == nil { // could be a name overlapping with a type.
continue
}
switch ident.Obj.Decl.(type) {
case *ast.TypeSpec:
return ident.Obj.Decl.(*ast.TypeSpec).Type, nil
default: // could be a name overlapping with a type.
continue
}
}
}
return nil, fmt.Errorf("%s not found in package %s", typeName, pkg.Name)
}
54 changes: 54 additions & 0 deletions internal/parser/interface.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package parser

import (
"github.com/Joffref/genz/pkg/models"
"go/ast"
"go/types"
"golang.org/x/tools/go/packages"
)

// parseInterface parses the given interface and returns a models.Element.
// It also parses the subInterfaces of the given interface and the methods of the subInterfaces recursively.
func parseInterface(pkg *packages.Package, interfaceName string, interfaceType *ast.InterfaceType) (models.Element, error) {

parsedInterface, err := parseElementType(pkg, interfaceName)
if err != nil {
return models.Element{}, err
}

var methods []models.Method

for _, method := range interfaceType.Methods.List {
switch pkg.TypesInfo.TypeOf(method.Type).(type) {
case *types.Signature:
methodModel, err := parseMethod(method.Names[0].Name, pkg.TypesInfo.TypeOf(method.Type).(*types.Signature))
if err != nil {
return models.Element{}, err
}
if method.Doc != nil {
for _, comment := range method.Doc.List {
methodModel.Comments = append(methodModel.Comments, comment.Text[2:])
}
}
methods = append(methods, methodModel)
case *types.Named: // Embedded interface
namedType := pkg.TypesInfo.TypeOf(method.Type).(*types.Named)
iface := namedType.Origin().Underlying().(*types.Interface).Complete()
for i := 0; i < iface.NumMethods(); i++ {
methodModel, err := parseMethod(iface.Method(i).Name(), iface.Method(i).Type().(*types.Signature))
if err != nil {
return models.Element{}, err
}
if method.Doc != nil {
for _, comment := range method.Doc.List {
methodModel.Comments = append(methodModel.Comments, comment.Text[2:])
}
}
methods = append(methods, methodModel)
}
}

}
parsedInterface.Methods = methods
return parsedInterface, nil
}
Loading

0 comments on commit 95239fc

Please sign in to comment.