From 954a6a153f8645e3a9fd268e5a4338b78648c591 Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Mon, 1 Aug 2022 15:57:37 +0200 Subject: [PATCH] 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) --- depinject/internal/codegen/type.go | 154 +++++++++++++++++++++++ depinject/internal/codegen/type_test.go | 86 +++++++++++++ depinject/internal/codegen/value.go | 149 ++++++++++++++++++++++ depinject/internal/codegen/value_test.go | 70 +++++++++++ 4 files changed, 459 insertions(+) create mode 100644 depinject/internal/codegen/type.go create mode 100644 depinject/internal/codegen/type_test.go create mode 100644 depinject/internal/codegen/value.go create mode 100644 depinject/internal/codegen/value_test.go diff --git a/depinject/internal/codegen/type.go b/depinject/internal/codegen/type.go new file mode 100644 index 0000000000..9234206bfb --- /dev/null +++ b/depinject/internal/codegen/type.go @@ -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 +} diff --git a/depinject/internal/codegen/type_test.go b/depinject/internal/codegen/type_test.go new file mode 100644 index 0000000000..8c33b9802d --- /dev/null +++ b/depinject/internal/codegen/type_test.go @@ -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()) +} diff --git a/depinject/internal/codegen/value.go b/depinject/internal/codegen/value.go new file mode 100644 index 0000000000..27738adc54 --- /dev/null +++ b/depinject/internal/codegen/value.go @@ -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 +} diff --git a/depinject/internal/codegen/value_test.go b/depinject/internal/codegen/value_test.go new file mode 100644 index 0000000000..f822333f7b --- /dev/null +++ b/depinject/internal/codegen/value_test.go @@ -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) +}