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:
Aaron Craelius
2022-08-01 13:57:37 +00:00
committed by GitHub
parent 4a6c1c26d1
commit 954a6a153f
4 changed files with 459 additions and 0 deletions
+154
View File
@@ -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
}
+86
View File
@@ -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())
}
+149
View File
@@ -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
}
+70
View File
@@ -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)
}