Skip to content

Commit

Permalink
Merge pull request #255 from TheSpiritXIII/parse-generics
Browse files Browse the repository at this point in the history
Support parsing type parameters
  • Loading branch information
k8s-ci-robot authored Mar 26, 2024
2 parents 9cff633 + 239d3d4 commit a8d1235
Show file tree
Hide file tree
Showing 7 changed files with 323 additions and 1 deletion.
39 changes: 38 additions & 1 deletion v2/parser/parse.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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 {
Expand Down
243 changes: 243 additions & 0 deletions v2/parser/parse_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"encoding/json"
"fmt"
"path/filepath"
"reflect"
"sort"
"testing"

Expand Down Expand Up @@ -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)
}
})
}
}
12 changes: 12 additions & 0 deletions v2/parser/testdata/basic/file.go
Original file line number Diff line number Diff line change
@@ -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"`
}
10 changes: 10 additions & 0 deletions v2/parser/testdata/generic-multi/file.go
Original file line number Diff line number Diff line change
@@ -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"`
}
10 changes: 10 additions & 0 deletions v2/parser/testdata/generic-recursive/file.go
Original file line number Diff line number Diff line change
@@ -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"`
}
6 changes: 6 additions & 0 deletions v2/parser/testdata/generic/file.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package foo

type Blah[T any] struct {
// V is the first field.
V T `json:"v"`
}
4 changes: 4 additions & 0 deletions v2/types/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ const (
DeclarationOf Kind = "DeclarationOf"
Unknown Kind = ""
Unsupported Kind = "Unsupported"
TypeParam Kind = "TypeParam"

// Protobuf is protobuf type.
Protobuf Kind = "Protobuf"
Expand Down Expand Up @@ -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

Expand Down

0 comments on commit a8d1235

Please sign in to comment.