Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support parsing type parameters #255

Merged
merged 1 commit into from
Mar 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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