From d421b963aed7f7c3ba3718edfc6fbd787fa8f0dd Mon Sep 17 00:00:00 2001 From: Jeff Thompson Date: Thu, 23 Nov 2023 19:48:03 +0100 Subject: [PATCH] chore: In amino.pkg, add optional WithComments, use them in GenerateProto3MessagePartial (#1235) This PR resolves issue #1157 by adding support for the optional `WithComments`. See the issue for motivation. - In `amino.pkg.Type`, add optional fields `Comment` and ` FieldComments` . - Add the `Package` method `WithComments` which can optionally be used after `WithTypes`. This reads the Go source file and scans the AST for struct and field comments which are added to the `Type` object. - Update `GenerateProto3MessagePartial` to copy the comments to the `P3Doc` and related `P3Field` objects which already have an optional `Comment` field for comments that are already included in the Protobuf file. - Add test file `comments_test.go` . As shown in the test, you can use `WithComments` after `WithTypes`: pkg := amino.RegisterPackage( amino.NewPackage( "github.com/gnolang/gno/tm2/pkg/amino/genproto", "amino_test", amino.GetCallersDirname(), ).WithTypes( &TestMessageName{}, &TestMessageName2{}, // Add comments from this same source file. ).WithComments(path.Join(amino.GetCallersDirname(), "comments_test.go"))) If your Go struct looks like: // message comment type TestMessageName struct { // field comment 1 FieldName1 string // field comment 2 FieldName2 []uint64 } then your Protobuf file has: // message comment message TestMessageName { // field comment 1 string FieldName1 = 1; // field comment 2 repeated uint64 FieldName2 = 2; }
Contributors' checklist... - [x] Added new tests, or not needed, or not feasible - [x] Provided an example (e.g. screenshot) to aid review or the PR is self-explanatory - [x] Updated the official documentation or not needed - [x] No breaking changes were made, or a `BREAKING CHANGE: xxx` message was included in the description - [x] Added references to related issues and PRs - [x] Provided any useful hints for running manual tests - [ ] Added new benchmarks to [generated graphs](https://gnoland.github.io/benchmarks), if any. More info [here](https://github.com/gnolang/gno/blob/master/.benchmarks/README.md).
--------- Signed-off-by: Jeff Thompson Co-authored-by: Morgan Bazalgette --- tm2/pkg/amino/genproto/comments_test.go | 61 +++++++++++++++++++++ tm2/pkg/amino/genproto/genproto.go | 12 +++++ tm2/pkg/amino/pkg/pkg.go | 72 ++++++++++++++++++++++++- 3 files changed, 143 insertions(+), 2 deletions(-) create mode 100644 tm2/pkg/amino/genproto/comments_test.go diff --git a/tm2/pkg/amino/genproto/comments_test.go b/tm2/pkg/amino/genproto/comments_test.go new file mode 100644 index 00000000000..5910a244b6b --- /dev/null +++ b/tm2/pkg/amino/genproto/comments_test.go @@ -0,0 +1,61 @@ +package genproto + +import ( + "path" + "testing" + + "github.com/gnolang/gno/tm2/pkg/amino" + "github.com/jaekwon/testify/assert" +) + +// message comment +type TestMessageName struct { + // field comment 1 + FieldName1 string + // field comment 2 + FieldName2 []uint64 +} + +// message comment 2 +type TestMessageName2 struct { + // another field comment + FieldName string +} + +func TestComments(t *testing.T) { + pkg := amino.RegisterPackage( + amino.NewPackage( + "github.com/gnolang/gno/tm2/pkg/amino/genproto", + "amino_test", + amino.GetCallersDirname(), + ).WithTypes( + &TestMessageName{}, + &TestMessageName2{}, + // Add comments from this same source file. + ).WithComments(path.Join(amino.GetCallersDirname(), "comments_test.go"))) + + p3c := NewP3Context() + p3c.RegisterPackage(pkg) + p3c.ValidateBasic() + p3doc := p3c.GenerateProto3SchemaForTypes(pkg, pkg.ReflectTypes()...) + proto3Schema := p3doc.Print() + assert.Equal(t, proto3Schema, `syntax = "proto3"; +package amino_test; + +option go_package = "github.com/gnolang/gno/tm2/pkg/amino/genproto/pb"; + +// messages +// message comment +message TestMessageName { + // field comment 1 + string field_name1 = 1 [json_name = "FieldName1"]; + // field comment 2 + repeated uint64 field_name2 = 2 [json_name = "FieldName2"]; +} + +// message comment 2 +message TestMessageName2 { + // another field comment + string field_name = 1 [json_name = "FieldName"]; +}`) +} diff --git a/tm2/pkg/amino/genproto/genproto.go b/tm2/pkg/amino/genproto/genproto.go index 01452639245..fb71f32783e 100644 --- a/tm2/pkg/amino/genproto/genproto.go +++ b/tm2/pkg/amino/genproto/genproto.go @@ -190,6 +190,15 @@ func (p3c *P3Context) GenerateProto3MessagePartial(p3doc *P3Doc, rt reflect.Type p3msg.Name = info.Name // not rinfo. + var fieldComments map[string]string + if rinfo.Package != nil { + if pkgType, ok := rinfo.Package.GetType(rt); ok { + p3msg.Comment = pkgType.Comment + // We will check for optional field comments below. + fieldComments = pkgType.FieldComments + } + } + // Append to p3msg.Fields, fields of the struct. for _, field := range rsfields { // rinfo. fp3, fp3IsRepeated, implicit := typeToP3Type(info.Package, field.TypeInfo, field.FieldOptions) @@ -209,6 +218,9 @@ func (p3c *P3Context) GenerateProto3MessagePartial(p3doc *P3Doc, rt reflect.Type JSONName: field.JSONName, Number: field.FieldOptions.BinFieldNum, } + if fieldComments != nil { + p3Field.Comment = fieldComments[field.Name] + } p3msg.Fields = append(p3msg.Fields, p3Field) } diff --git a/tm2/pkg/amino/pkg/pkg.go b/tm2/pkg/amino/pkg/pkg.go index fad46c4cad7..72e9f891489 100644 --- a/tm2/pkg/amino/pkg/pkg.go +++ b/tm2/pkg/amino/pkg/pkg.go @@ -2,6 +2,9 @@ package pkg import ( "fmt" + "go/ast" + "go/parser" + "go/token" "path/filepath" "reflect" "regexp" @@ -11,8 +14,10 @@ import ( type Type struct { Type reflect.Type - Name string // proto3 name (override) - PointerPreferred bool // whether pointer is preferred for decoding interface. + Name string // proto3 name (override) + PointerPreferred bool // whether pointer is preferred for decoding interface. + Comment string // optional doc comment for the type + FieldComments map[string]string // If not nil, the optional doc comment for each field name } func (t *Type) FullName(pkg *Package) string { @@ -196,6 +201,69 @@ func (pkg *Package) WithP3SchemaFile(file string) *Package { return pkg } +// Parse the Go code in filename and scan the AST looking for struct doc comments. +// Find the Type in pkg.Types and set its Comment and FieldComments, which are +// used by genproto.GenerateProto3MessagePartial to set the Comment in the P3Doc +// and related P3Field objects. +func (pkg *Package) WithComments(filename string) *Package { + fset := token.NewFileSet() + f, err := parser.ParseFile(fset, filename, nil, parser.ParseComments) + if err != nil { + panic(err) + } + + ast.Inspect(f, func(node ast.Node) bool { + genDecl, ok := node.(*ast.GenDecl) + if !ok { + return true + } + for _, spec := range genDecl.Specs { + typeSpec, ok := spec.(*ast.TypeSpec) + if !ok { + continue + } + + pkgType := pkg.getTypeByName(typeSpec.Name.Name) + if pkgType == nil { + continue + } + if genDecl.Doc != nil { + // Set the type comment. + pkgType.Comment = strings.TrimSpace(genDecl.Doc.Text()) + } + + structType, ok := typeSpec.Type.(*ast.StructType) + if !ok { + continue + } + for _, field := range structType.Fields.List { + if field.Names != nil && len(field.Names) == 1 && field.Doc != nil { + // Set the field comment. + if pkgType.FieldComments == nil { + pkgType.FieldComments = make(map[string]string) + } + + pkgType.FieldComments[field.Names[0].Name] = strings.TrimSpace(field.Doc.Text()) + } + } + } + return true + }) + + return pkg +} + +// Get the Type by name. If not found, return nil. +func (pkg *Package) getTypeByName(name string) *Type { + for _, t := range pkg.Types { + if t.Name == name { + return t + } + } + + return nil +} + // Result cannot be modified. func (pkg *Package) GetType(rt reflect.Type) (t Type, ok bool) { if rt.Kind() == reflect.Ptr {