feat(depinject): codegen part 2 types and values (#12616)
## Description Ref #12556 This PR continues with basic codegen infrastructure for depinject, this time adding the ability to generate `ast.Expr`'s for `reflect.Type` and `reflect.Value`. --- ### Author Checklist *All items are required. Please add a note to the item if the item is not applicable and please add links to any relevant follow up issues.* I have... - [ ] included the correct [type prefix](https://github.com/commitizen/conventional-commit-types/blob/v3.0.0/index.json) in the PR title - [ ] added `!` to the type prefix if API or client breaking change - [ ] targeted the correct branch (see [PR Targeting](https://github.com/cosmos/cosmos-sdk/blob/main/CONTRIBUTING.md#pr-targeting)) - [ ] provided a link to the relevant issue or specification - [ ] followed the guidelines for [building modules](https://github.com/cosmos/cosmos-sdk/blob/main/docs/building-modules) - [ ] included the necessary unit and integration [tests](https://github.com/cosmos/cosmos-sdk/blob/main/CONTRIBUTING.md#testing) - [ ] added a changelog entry to `CHANGELOG.md` - [ ] included comments for [documenting Go code](https://blog.golang.org/godoc) - [ ] updated the relevant documentation or specification - [ ] reviewed "Files changed" and left comments if necessary - [ ] confirmed all CI checks have passed ### Reviewers Checklist *All items are required. Please add a note if the item is not applicable and please add your handle next to the items reviewed if you only reviewed selected items.* I have... - [ ] confirmed the correct [type prefix](https://github.com/commitizen/conventional-commit-types/blob/v3.0.0/index.json) in the PR title - [ ] confirmed `!` in the type prefix if API or client breaking change - [ ] confirmed all author checklist items have been addressed - [ ] reviewed state machine logic - [ ] reviewed API design and naming - [ ] reviewed documentation is accurate - [ ] reviewed tests and test coverage - [ ] manually tested (if applicable)
This commit is contained in:
@@ -0,0 +1,154 @@
|
||||
package codegen
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"go/ast"
|
||||
"go/token"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// TypeExpr generates an ast.Expr to be used in the context of the file for the
|
||||
// provided reflect.Type, adding any needed imports.
|
||||
func (g *FileGen) TypeExpr(typ reflect.Type) (ast.Expr, error) {
|
||||
if name := typ.Name(); name != "" {
|
||||
name = g.importGenericTypeParams(name, typ.PkgPath())
|
||||
importPrefix := g.AddOrGetImport(typ.PkgPath())
|
||||
if importPrefix == "" {
|
||||
return ast.NewIdent(name), nil
|
||||
}
|
||||
|
||||
return ast.NewIdent(fmt.Sprintf("%s.%s", importPrefix, name)), nil
|
||||
}
|
||||
|
||||
switch typ.Kind() {
|
||||
|
||||
case reflect.Array:
|
||||
elt, err := g.TypeExpr(typ.Elem())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &ast.ArrayType{
|
||||
Len: &ast.BasicLit{Kind: token.INT, Value: fmt.Sprintf("%d", typ.Len())},
|
||||
Elt: elt,
|
||||
}, nil
|
||||
|
||||
case reflect.Slice:
|
||||
elt, err := g.TypeExpr(typ.Elem())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &ast.ArrayType{Elt: elt}, nil
|
||||
|
||||
case reflect.Chan:
|
||||
elt, err := g.TypeExpr(typ.Elem())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
e := &ast.ChanType{Value: elt}
|
||||
switch typ.ChanDir() {
|
||||
case reflect.SendDir:
|
||||
e.Dir = ast.SEND
|
||||
case reflect.RecvDir:
|
||||
e.Dir = ast.RECV
|
||||
default:
|
||||
e.Dir = ast.SEND | ast.RECV
|
||||
}
|
||||
return e, nil
|
||||
|
||||
case reflect.Func:
|
||||
e := &ast.FuncType{
|
||||
Params: &ast.FieldList{},
|
||||
Results: &ast.FieldList{},
|
||||
}
|
||||
|
||||
numIn := typ.NumIn()
|
||||
for i := 0; i < numIn; i++ {
|
||||
in, err := g.TypeExpr(typ.In(i))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
e.Params.List = append(e.Params.List, &ast.Field{Type: in})
|
||||
}
|
||||
|
||||
if typ.IsVariadic() {
|
||||
in, err := g.TypeExpr(typ.In(numIn - 1).Elem())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
e.Params.List[numIn-1] = &ast.Field{Type: &ast.Ellipsis{Elt: in}}
|
||||
}
|
||||
|
||||
for i := 0; i < typ.NumOut(); i++ {
|
||||
out, err := g.TypeExpr(typ.Out(i))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
e.Results.List = append(e.Results.List, &ast.Field{Type: out})
|
||||
}
|
||||
|
||||
return e, nil
|
||||
|
||||
case reflect.Map:
|
||||
k, err := g.TypeExpr(typ.Key())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
v, err := g.TypeExpr(typ.Elem())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &ast.MapType{Key: k, Value: v}, nil
|
||||
|
||||
case reflect.Pointer:
|
||||
elem, err := g.TypeExpr(typ.Elem())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &ast.StarExpr{X: elem}, nil
|
||||
|
||||
default:
|
||||
return nil, fmt.Errorf("unexpected type %v", typ)
|
||||
}
|
||||
}
|
||||
|
||||
var genericTypeNameRegex = regexp.MustCompile(`(\w+)\[(.*)]`)
|
||||
|
||||
func (g *FileGen) importGenericTypeParams(typeName string, pkgPath string) (newTypeName string) {
|
||||
// a generic type parameter from the same package the generic type is defined won't have the
|
||||
// full package name so we need to compare it with the final package part (the default import prefix)
|
||||
// ex: for a/b.C in package a/b, we'll just see the type param b.C.
|
||||
pkgParts := strings.Split(pkgPath, "/")
|
||||
pkgDefaultPrefix := pkgParts[len(pkgParts)-1]
|
||||
|
||||
matches := genericTypeNameRegex.FindStringSubmatch(typeName)
|
||||
if len(matches) == 3 {
|
||||
typeParamExpr := matches[2]
|
||||
typeParams := strings.Split(typeParamExpr, ",")
|
||||
var importedTypeParams []string
|
||||
for _, param := range typeParams {
|
||||
param = strings.TrimSpace(param)
|
||||
i := strings.LastIndex(param, ".")
|
||||
if i > 0 {
|
||||
pkg := param[:i]
|
||||
name := param[i+1:]
|
||||
var prefix string
|
||||
if pkg == pkgDefaultPrefix {
|
||||
prefix = pkg
|
||||
} else {
|
||||
prefix = g.AddOrGetImport(pkg)
|
||||
}
|
||||
param = fmt.Sprintf("%s.%s", prefix, name)
|
||||
}
|
||||
importedTypeParams = append(importedTypeParams, param)
|
||||
}
|
||||
return fmt.Sprintf("%s[%s]", matches[1], strings.Join(importedTypeParams, ", "))
|
||||
}
|
||||
|
||||
return typeName
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
package codegen
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"go/ast"
|
||||
"go/printer"
|
||||
"go/token"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"gotest.tools/v3/assert"
|
||||
|
||||
"cosmossdk.io/depinject/internal/graphviz"
|
||||
)
|
||||
|
||||
type MyInt int
|
||||
|
||||
type AStruct struct {
|
||||
Foo int
|
||||
}
|
||||
|
||||
type AGenericStruct[A, B any] struct {
|
||||
A A
|
||||
B B
|
||||
}
|
||||
|
||||
type AStructWrapper AStruct
|
||||
|
||||
type AnInterface interface{}
|
||||
|
||||
func TestTypeExpr(t *testing.T) {
|
||||
expectTypeExpr(t, false, "bool")
|
||||
expectTypeExpr(t, uint(0), "uint")
|
||||
expectTypeExpr(t, uint8(0), "uint8")
|
||||
expectTypeExpr(t, uint16(0), "uint16")
|
||||
expectTypeExpr(t, uint32(0), "uint32")
|
||||
expectTypeExpr(t, uint64(0), "uint64")
|
||||
expectTypeExpr(t, int(0), "int")
|
||||
expectTypeExpr(t, int8(0), "int8")
|
||||
expectTypeExpr(t, int16(0), "int16")
|
||||
expectTypeExpr(t, int32(0), "int32")
|
||||
expectTypeExpr(t, int64(0), "int64")
|
||||
expectTypeExpr(t, float32(0), "float32")
|
||||
expectTypeExpr(t, float64(0), "float64")
|
||||
expectTypeExpr(t, complex64(0), "complex64")
|
||||
expectTypeExpr(t, complex128(0), "complex128")
|
||||
expectTypeExpr(t, MyInt(0), "codegen.MyInt")
|
||||
expectTypeExpr(t, [1]int{0}, "[1]int")
|
||||
expectTypeExpr(t, []int{}, "[]int")
|
||||
expectTypeExpr(t, make(chan int), "chan int")
|
||||
expectTypeExpr(t, make(<-chan int), "<-chan int")
|
||||
expectTypeExpr(t, make(chan<- int), "chan<- int")
|
||||
expectTypeExpr(t, func(int, string) (bool, error) { return false, nil },
|
||||
"func(int, string) (bool, error)",
|
||||
)
|
||||
expectTypeExpr(t, func(int, ...string) (bool, error) { return false, nil },
|
||||
"func(int, ...string) (bool, error)",
|
||||
)
|
||||
expectTypeExpr(t, AStruct{}, "codegen.AStruct")
|
||||
expectTypeExpr(t, map[string]graphviz.Attributes{}, "map[string]graphviz.Attributes")
|
||||
expectTypeExpr(t, &AStruct{}, "*codegen.AStruct")
|
||||
expectTypeExpr(t, AGenericStruct[graphviz.Node, FileGen]{}, "codegen.AGenericStruct[graphviz.Node, codegen.FileGen]")
|
||||
expectTypeExpr(t, AStructWrapper{}, "codegen.AStructWrapper")
|
||||
expectTypeExpr(t, "abc", "string")
|
||||
expectTypeExpr(t, uintptr(0), "uintptr")
|
||||
expectTypeExpr(t, (*AnInterface)(nil), "*codegen.AnInterface")
|
||||
}
|
||||
|
||||
func expectTypeExpr(t *testing.T, value interface{}, expected string) {
|
||||
t.Helper()
|
||||
g, err := NewFileGen(&ast.File{}, "")
|
||||
assert.NilError(t, err)
|
||||
e, err := g.TypeExpr(reflect.TypeOf(value))
|
||||
assert.NilError(t, err)
|
||||
expectExpr(t, e, expected)
|
||||
}
|
||||
|
||||
func expectExpr(t *testing.T, e ast.Expr, expected string) {
|
||||
t.Helper()
|
||||
fset := token.NewFileSet()
|
||||
buf := &bytes.Buffer{}
|
||||
assert.NilError(t, printer.Fprint(buf, fset, e))
|
||||
errBuf := &bytes.Buffer{}
|
||||
assert.NilError(t, ast.Fprint(errBuf, fset, e, nil))
|
||||
assert.Equal(t, expected, buf.String(), errBuf.String())
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
package codegen
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"go/ast"
|
||||
"go/token"
|
||||
"reflect"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// ValueExpr generates an ast.Expr to be used in the context of the file for the
|
||||
// provided reflect.Value, adding any needed imports. Values with kind Chan,
|
||||
// Func, Interface, Uintptr, and UnsafePointer cannot be generated and only
|
||||
// pointers to structs can be generated.
|
||||
func (g *FileGen) ValueExpr(value reflect.Value) (ast.Expr, error) {
|
||||
typ := value.Type()
|
||||
switch typ.Kind() {
|
||||
|
||||
case reflect.Bool:
|
||||
return &ast.BasicLit{Kind: token.IDENT, Value: fmt.Sprintf("%t", value.Bool())}, nil
|
||||
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||
return &ast.BasicLit{Kind: token.INT, Value: fmt.Sprintf("%d", value.Uint())}, nil
|
||||
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
return &ast.BasicLit{Kind: token.INT, Value: fmt.Sprintf("%d", value.Int())}, nil
|
||||
|
||||
case reflect.Float32, reflect.Float64:
|
||||
return &ast.BasicLit{Kind: token.FLOAT, Value: strconv.FormatFloat(value.Float(), 'e', -1, 64)}, nil
|
||||
|
||||
case reflect.Complex64, reflect.Complex128:
|
||||
return &ast.BasicLit{Kind: token.FLOAT, Value: strconv.FormatComplex(value.Complex(), 'e', -1, 128)}, nil
|
||||
|
||||
case reflect.Array:
|
||||
return g.arraySliceExpr(value)
|
||||
|
||||
case reflect.Map:
|
||||
if value.IsNil() {
|
||||
return ast.NewIdent("nil"), nil
|
||||
}
|
||||
|
||||
t, err := g.TypeExpr(typ)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
n := value.Len()
|
||||
lit := &ast.CompositeLit{
|
||||
Type: t,
|
||||
Elts: make([]ast.Expr, n),
|
||||
}
|
||||
|
||||
for i, key := range value.MapKeys() {
|
||||
k, err := g.ValueExpr(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
v, err := g.ValueExpr(value.MapIndex(key))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
lit.Elts[i] = &ast.KeyValueExpr{Key: k, Value: v}
|
||||
}
|
||||
|
||||
return lit, nil
|
||||
|
||||
case reflect.Slice:
|
||||
if value.IsNil() {
|
||||
return ast.NewIdent("nil"), nil
|
||||
}
|
||||
|
||||
return g.arraySliceExpr(value)
|
||||
|
||||
case reflect.String:
|
||||
return &ast.BasicLit{Kind: token.STRING, Value: fmt.Sprintf("%q", value.String())}, nil
|
||||
|
||||
case reflect.Struct:
|
||||
t, err := g.TypeExpr(typ)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
n := typ.NumField()
|
||||
lit := &ast.CompositeLit{
|
||||
Type: t,
|
||||
}
|
||||
|
||||
for i := 0; i < n; i++ {
|
||||
f := typ.Field(i)
|
||||
v := value.FieldByName(f.Name)
|
||||
if v.IsZero() {
|
||||
continue
|
||||
}
|
||||
|
||||
vExpr, err := g.ValueExpr(v)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
lit.Elts = append(lit.Elts, &ast.KeyValueExpr{
|
||||
Key: ast.NewIdent(f.Name),
|
||||
Value: vExpr,
|
||||
})
|
||||
}
|
||||
|
||||
return lit, nil
|
||||
case reflect.Pointer:
|
||||
if value.IsNil() {
|
||||
return ast.NewIdent("nil"), nil
|
||||
}
|
||||
|
||||
if typ.Elem().Kind() == reflect.Struct {
|
||||
v, err := g.ValueExpr(value.Elem())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &ast.UnaryExpr{Op: token.AND, X: v}, nil
|
||||
} else {
|
||||
return nil, fmt.Errorf("invalid type %s", typ)
|
||||
}
|
||||
case reflect.Invalid, reflect.Uintptr, reflect.Chan, reflect.Func, reflect.Interface, reflect.UnsafePointer:
|
||||
return nil, fmt.Errorf("invalid type %s", typ)
|
||||
|
||||
default:
|
||||
return nil, fmt.Errorf("invalid type %s", typ)
|
||||
}
|
||||
}
|
||||
|
||||
func (g *FileGen) arraySliceExpr(value reflect.Value) (ast.Expr, error) {
|
||||
astTyp, err := g.TypeExpr(value.Type())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
n := value.Len()
|
||||
lit := &ast.CompositeLit{Type: astTyp, Elts: make([]ast.Expr, n)}
|
||||
|
||||
for i := 0; i < n; i++ {
|
||||
lit.Elts[i], err = g.ValueExpr(value.Index(i))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return lit, nil
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
package codegen
|
||||
|
||||
import (
|
||||
"go/ast"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"gotest.tools/v3/assert"
|
||||
)
|
||||
|
||||
func TestValueExpr(t *testing.T) {
|
||||
// bool
|
||||
expectValueExpr(t, true, `true`)
|
||||
expectValueExpr(t, false, `false`)
|
||||
|
||||
// uints
|
||||
expectValueExpr(t, uint(0), `0`)
|
||||
expectValueExpr(t, uint8(1), `1`)
|
||||
expectValueExpr(t, uint16(2), `2`)
|
||||
expectValueExpr(t, uint32(3), `3`)
|
||||
expectValueExpr(t, uint64(12345678), `12345678`)
|
||||
|
||||
// ints
|
||||
expectValueExpr(t, 0, `0`)
|
||||
expectValueExpr(t, int8(-1), `-1`)
|
||||
expectValueExpr(t, int16(-2), `-2`)
|
||||
expectValueExpr(t, int32(-3), `-3`)
|
||||
expectValueExpr(t, int64(-12345678), `-12345678`)
|
||||
|
||||
// floats
|
||||
expectValueExpr(t, float32(0.0), `0e+00`)
|
||||
expectValueExpr(t, float64(1.32e-9), `1.32e-09`)
|
||||
|
||||
// complex
|
||||
expectValueExpr(t, complex64(1+2i), `(1e+00+2e+00i)`)
|
||||
expectValueExpr(t, complex128(1.32e-9+-3.03i), `(1.32e-09-3.03e+00i)`)
|
||||
|
||||
// array
|
||||
expectValueExpr(t, [3]uint32{1, 4, 9}, `[3]uint32{1, 4, 9}`)
|
||||
|
||||
// slice
|
||||
expectValueExpr(t, []uint32{1, 4, 9}, `[]uint32{1, 4, 9}`)
|
||||
|
||||
// map
|
||||
expectValueExpr(t, map[string]int{"a": 1}, `map[string]int{"a": 1}`)
|
||||
|
||||
// struct
|
||||
expectValueExpr(t, AStruct{Foo: 2}, `codegen.AStruct{Foo: 2}`)
|
||||
expectValueExpr(t, AStruct{}, `codegen.AStruct{}`) // empty default fields
|
||||
|
||||
// struct pointer
|
||||
expectValueExpr(t, &AStruct{Foo: 2}, `&codegen.AStruct{Foo: 2}`)
|
||||
var nilStruct *AStruct
|
||||
expectValueExpr(t, nilStruct, `nil`)
|
||||
|
||||
// struct wrapper
|
||||
expectValueExpr(t, &AStructWrapper{Foo: 2}, `&codegen.AStructWrapper{Foo: 2}`)
|
||||
|
||||
// string
|
||||
expectValueExpr(t, "abc", `"abc"`)
|
||||
}
|
||||
|
||||
func expectValueExpr(t *testing.T, value interface{}, expected string) {
|
||||
t.Helper()
|
||||
g, err := NewFileGen(&ast.File{}, "")
|
||||
assert.NilError(t, err)
|
||||
e, err := g.ValueExpr(reflect.ValueOf(value))
|
||||
assert.NilError(t, err)
|
||||
expectExpr(t, e, expected)
|
||||
}
|
||||
Reference in New Issue
Block a user