From 239d3d48564a0b8e2bf8a60dc7734d33b5a89575 Mon Sep 17 00:00:00 2001 From: Daniel Hrabovcak Date: Tue, 19 Mar 2024 10:22:29 -0400 Subject: [PATCH] Support parsing type parameters --- v2/parser/parse.go | 39 ++- v2/parser/parse_test.go | 243 +++++++++++++++++++ v2/parser/testdata/basic/file.go | 12 + v2/parser/testdata/generic-multi/file.go | 10 + v2/parser/testdata/generic-recursive/file.go | 10 + v2/parser/testdata/generic/file.go | 6 + v2/types/types.go | 4 + 7 files changed, 323 insertions(+), 1 deletion(-) create mode 100644 v2/parser/testdata/basic/file.go create mode 100644 v2/parser/testdata/generic-multi/file.go create mode 100644 v2/parser/testdata/generic-recursive/file.go create mode 100644 v2/parser/testdata/generic/file.go diff --git a/v2/parser/parse.go b/v2/parser/parse.go index 28ef6cdd..e7acf495 100644 --- a/v2/parser/parse.go +++ b/v2/parser/parse.go @@ -587,9 +587,16 @@ func goNameToName(in string) types.Name { return types.Name{Name: in} } + // There may be '.' characters within a generic. Temporarily remove + // the generic. + genericIndex := strings.IndexRune(in, '[') + if genericIndex == -1 { + genericIndex = len(in) + } + // Otherwise, if there are '.' characters present, the name has a // package path in front. - nameParts := strings.Split(in, ".") + nameParts := strings.Split(in[:genericIndex], ".") name := types.Name{Name: in} if n := len(nameParts); n >= 2 { // The final "." is the name of the type--previous ones must @@ -738,6 +745,27 @@ func (p *Parser) walkType(u types.Universe, useName *types.Name, in gotypes.Type } out.Kind = types.Alias out.Underlying = p.walkType(u, nil, t.Underlying()) + case *gotypes.Struct: + name := goNameToName(t.String()) + tpMap := map[string]*types.Type{} + if t.TypeParams().Len() != 0 { + // Remove generics, then readd them without the encoded + // type, e.g. Foo[T any] => Foo[T] + var tpNames []string + for i := 0; i < t.TypeParams().Len(); i++ { + tp := t.TypeParams().At(i) + tpName := tp.Obj().Name() + tpNames = append(tpNames, tpName) + tpMap[tpName] = p.walkType(u, nil, tp.Constraint()) + } + name.Name = fmt.Sprintf("%s[%s]", strings.SplitN(name.Name, "[", 2)[0], strings.Join(tpNames, ",")) + } + + if out := u.Type(name); out.Kind != types.Unknown { + return out // short circuit if we've already made this. + } + out = p.walkType(u, &name, t.Underlying()) + out.TypeParams = tpMap default: // gotypes package makes everything "named" with an // underlying anonymous type--we remove that annoying @@ -764,6 +792,15 @@ func (p *Parser) walkType(u types.Universe, useName *types.Name, in gotypes.Type } } return out + case *gotypes.TypeParam: + // DO NOT retrieve the type from the universe. The default type-param name is only the + // generic variable name. Ideally, it would be namespaced by package and struct but it is + // not. Thus, if we try to use the universe, we would start polluting it. + // e.g. if Foo[T] and Bar[T] exists, we'd mistakenly use the same type T for both. + return &types.Type{ + Name: name, + Kind: types.TypeParam, + } default: out := u.Type(name) if out.Kind != types.Unknown { diff --git a/v2/parser/parse_test.go b/v2/parser/parse_test.go index cac241a4..6bc99019 100644 --- a/v2/parser/parse_test.go +++ b/v2/parser/parse_test.go @@ -20,6 +20,7 @@ import ( "encoding/json" "fmt" "path/filepath" + "reflect" "sort" "testing" @@ -816,3 +817,245 @@ func TestAddOnePkgToUniverse(t *testing.T) { } } } + +func TestStructParse(t *testing.T) { + testCases := []struct { + description string + testFile string + expected func() *types.Type + }{ + { + description: "basic comments", + testFile: "k8s.io/gengo/v2/parser/testdata/basic", + expected: func() *types.Type { + return &types.Type{ + Name: types.Name{ + Package: "k8s.io/gengo/v2/parser/testdata/basic", + Name: "Blah", + }, + Kind: types.Struct, + CommentLines: []string{"Blah is a test.", "A test, I tell you."}, + SecondClosestCommentLines: []string{""}, + Members: []types.Member{ + { + Name: "A", + Embedded: false, + CommentLines: []string{"A is the first field."}, + Tags: `json:"a"`, + Type: types.Int64, + }, + { + Name: "B", + Embedded: false, + CommentLines: []string{"B is the second field.", "Multiline comments work."}, + Tags: `json:"b"`, + Type: types.String, + }, + }, + TypeParams: map[string]*types.Type{}, + } + }, + }, + { + description: "generic", + testFile: "./testdata/generic", + expected: func() *types.Type { + return &types.Type{ + Name: types.Name{ + Package: "k8s.io/gengo/v2/parser/testdata/generic", + Name: "Blah[T]", + }, + Kind: types.Struct, + CommentLines: []string{""}, + SecondClosestCommentLines: []string{""}, + Members: []types.Member{ + { + Name: "V", + Embedded: false, + CommentLines: []string{"V is the first field."}, + Tags: `json:"v"`, + Type: &types.Type{ + Kind: types.TypeParam, + Name: types.Name{ + Name: "T", + }, + }, + }, + }, + TypeParams: map[string]*types.Type{ + "T": { + Name: types.Name{ + Name: "any", + }, + Kind: types.Interface, + }, + }, + } + }, + }, + { + description: "generic multiple", + testFile: "./testdata/generic-multi", + expected: func() *types.Type { + return &types.Type{ + Name: types.Name{ + Package: "k8s.io/gengo/v2/parser/testdata/generic-multi", + Name: "Blah[T,U,V]", + }, + Kind: types.Struct, + CommentLines: []string{""}, + SecondClosestCommentLines: []string{""}, + Members: []types.Member{ + { + Name: "V1", + Embedded: false, + CommentLines: []string{"V1 is the first field."}, + Tags: `json:"v1"`, + Type: &types.Type{ + Kind: types.TypeParam, + Name: types.Name{ + Name: "T", + }, + }, + }, + { + Name: "V2", + Embedded: false, + CommentLines: []string{"V2 is the second field."}, + Tags: `json:"v2"`, + Type: &types.Type{ + Kind: types.TypeParam, + Name: types.Name{ + Name: "U", + }, + }, + }, + { + Name: "V3", + Embedded: false, + CommentLines: []string{"V3 is the third field."}, + Tags: `json:"v3"`, + Type: &types.Type{ + Kind: types.TypeParam, + Name: types.Name{ + Name: "V", + }, + }, + }, + }, + TypeParams: map[string]*types.Type{ + "T": { + Name: types.Name{ + Name: "any", + }, + Kind: types.Interface, + }, + "U": { + Name: types.Name{ + Name: "any", + }, + Kind: types.Interface, + }, + "V": { + Name: types.Name{ + Name: "any", + }, + Kind: types.Interface, + }, + }, + } + }, + }, + { + description: "generic recursive", + testFile: "./testdata/generic-recursive", + expected: func() *types.Type { + recursiveT := &types.Type{ + Name: types.Name{ + Package: "k8s.io/gengo/v2/parser/testdata/generic-recursive", + Name: "DeepCopyable", + }, + Kind: types.Interface, + CommentLines: []string{""}, + SecondClosestCommentLines: []string{""}, + Methods: map[string]*types.Type{}, + } + recursiveT.Methods["DeepCopy"] = &types.Type{ + Name: types.Name{ + Name: "func (k8s.io/gengo/v2/parser/testdata/generic-recursive.DeepCopyable[T]).DeepCopy() T", + }, + Kind: types.Func, + CommentLines: []string{""}, + Signature: &types.Signature{ + Receiver: recursiveT, + Results: []*types.ParamResult{ + { + Name: "", + Type: &types.Type{ + Name: types.Name{ + Name: "T", + }, + Kind: types.TypeParam, + }, + }, + }, + }, + } + return &types.Type{ + Name: types.Name{ + Package: "k8s.io/gengo/v2/parser/testdata/generic-recursive", + Name: "Blah[T]", + }, + Kind: types.Struct, + CommentLines: []string{""}, + SecondClosestCommentLines: []string{""}, + Members: []types.Member{ + { + Name: "V", + Embedded: false, + CommentLines: []string{"V is the first field."}, + Tags: `json:"v"`, + Type: &types.Type{ + Name: types.Name{ + Name: "T", + }, + Kind: types.TypeParam, + }, + }, + }, + TypeParams: map[string]*types.Type{ + "T": recursiveT, + }, + } + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.description, func(t *testing.T) { + parser := New() + + pkgs, err := parser.loadPackages(tc.testFile) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + u := types.Universe{} + if err := parser.addPkgToUniverse(pkgs[0], &u); err != nil { + t.Errorf("unexpected error: %v", err) + } + + expected := tc.expected() + pkg, ok := u[expected.Name.Package] + if !ok { + t.Fatalf("package %s not found", expected.Name.Package) + } + st := pkg.Type(expected.Name.Name) + if st == nil || st.Kind == types.Unknown { + t.Fatalf("type %s not found", expected.Name.Name) + } + if e, a := expected, st; !reflect.DeepEqual(e, a) { + t.Errorf("wanted, got:\n%#v\n%#v", e, a) + } + }) + } +} diff --git a/v2/parser/testdata/basic/file.go b/v2/parser/testdata/basic/file.go new file mode 100644 index 00000000..0d5bb24b --- /dev/null +++ b/v2/parser/testdata/basic/file.go @@ -0,0 +1,12 @@ +package foo + +// Blah is a test. +// A test, I tell you. +type Blah struct { + // A is the first field. + A int64 `json:"a"` + + // B is the second field. + // Multiline comments work. + B string `json:"b"` +} diff --git a/v2/parser/testdata/generic-multi/file.go b/v2/parser/testdata/generic-multi/file.go new file mode 100644 index 00000000..fd5ce5f7 --- /dev/null +++ b/v2/parser/testdata/generic-multi/file.go @@ -0,0 +1,10 @@ +package foo + +type Blah[T any, U any, V any] struct { + // V1 is the first field. + V1 T `json:"v1"` + // V2 is the second field. + V2 U `json:"v2"` + // V3 is the third field. + V3 V `json:"v3"` +} diff --git a/v2/parser/testdata/generic-recursive/file.go b/v2/parser/testdata/generic-recursive/file.go new file mode 100644 index 00000000..1392eed1 --- /dev/null +++ b/v2/parser/testdata/generic-recursive/file.go @@ -0,0 +1,10 @@ +package foo + +type DeepCopyable[T any] interface { + DeepCopy() T +} + +type Blah[T DeepCopyable[T]] struct { + // V is the first field. + V T `json:"v"` +} diff --git a/v2/parser/testdata/generic/file.go b/v2/parser/testdata/generic/file.go new file mode 100644 index 00000000..41abb5b7 --- /dev/null +++ b/v2/parser/testdata/generic/file.go @@ -0,0 +1,6 @@ +package foo + +type Blah[T any] struct { + // V is the first field. + V T `json:"v"` +} diff --git a/v2/types/types.go b/v2/types/types.go index fad4416e..7bbca017 100644 --- a/v2/types/types.go +++ b/v2/types/types.go @@ -98,6 +98,7 @@ const ( DeclarationOf Kind = "DeclarationOf" Unknown Kind = "" Unsupported Kind = "Unsupported" + TypeParam Kind = "TypeParam" // Protobuf is protobuf type. Protobuf Kind = "Protobuf" @@ -324,6 +325,9 @@ type Type struct { // If Kind == Struct Members []Member + // If Kind == Struct + TypeParams map[string]*Type + // If Kind == Map, Slice, Pointer, or Chan Elem *Type