protogen: generate .meta file with code annotations

When the generator parameter 'annotate_code' is provided, generate a .meta
file containing a GeneratedCodeInfo message describing the generated code's
relation to the source .proto file.

Annotations are added with (*protogen.GeneratedFile).Annotate, which takes the
name of a Go identifier (e.g., "SomeMessage" or "SomeMessage.GetField") and an
associated source location. The generator examines the generated AST to
determine source offsets for the symbols.

Change the []int32 "Path" in protogen types to a "Location", which also captures
the source file name.

Change-Id: Icd2340875831f40a1f91d495e3bd7ea381475c77
Reviewed-on: https://go-review.googlesource.com/c/139759
Reviewed-by: Joe Tsai <thebrokentoaster@gmail.com>
This commit is contained in:
Damien Neil 2018-10-04 12:42:37 -07:00
parent d277b5236a
commit 162c12703c
11 changed files with 413 additions and 81 deletions

View File

@ -74,9 +74,11 @@ func genService(gen *protogen.Plugin, file *fileInfo, g *protogen.GeneratedFile,
g.P("//")
g.P(deprecationComment)
}
g.Annotate(clientName, service.Location)
g.P("type ", clientName, " interface {")
for _, method := range service.Methods {
genComment(g, file, method.Path)
genComment(g, file, method.Location)
g.Annotate(clientName+"."+method.GoName, method.Location)
g.P(clientSignature(g, method))
}
g.P("}")
@ -118,9 +120,11 @@ func genService(gen *protogen.Plugin, file *fileInfo, g *protogen.GeneratedFile,
g.P("//")
g.P(deprecationComment)
}
g.Annotate(serverType, service.Location)
g.P("type ", serverType, " interface {")
for _, method := range service.Methods {
genComment(g, file, method.Path)
genComment(g, file, method.Location)
g.Annotate(serverType+"."+method.GoName, method.Location)
g.P(serverSignature(g, method))
}
g.P("}")
@ -384,8 +388,8 @@ func ident(name string) protogen.GoIdent {
}
}
func genComment(g *protogen.GeneratedFile, file *fileInfo, path []int32) (hasComment bool) {
for _, loc := range file.locationMap[pathKey(path)] {
func genComment(g *protogen.GeneratedFile, file *fileInfo, loc protogen.Location) (hasComment bool) {
for _, loc := range file.locationMap[pathKey(loc.Path)] {
if loc.LeadingComments == nil {
continue
}

View File

@ -17,7 +17,7 @@ import (
// serviceOptions returns the options for a service.
func serviceOptions(gen *protogen.Plugin, service *protogen.Service) *descpb.ServiceOptions {
d := getDescriptorProto(gen, service.Desc, service.Path)
d := getDescriptorProto(gen, service.Desc, service.Location.Path)
if d == nil {
return nil
}
@ -26,7 +26,7 @@ func serviceOptions(gen *protogen.Plugin, service *protogen.Service) *descpb.Ser
// methodOptions returns the options for a method.
func methodOptions(gen *protogen.Plugin, method *protogen.Method) *descpb.MethodOptions {
d := getDescriptorProto(gen, method.Desc, method.Path)
d := getDescriptorProto(gen, method.Desc, method.Location.Path)
if d == nil {
return nil
}

View File

@ -7,9 +7,15 @@
package main
import (
"bytes"
"flag"
"io/ioutil"
"os"
"path/filepath"
"testing"
"github.com/golang/protobuf/proto"
descpb "github.com/golang/protobuf/protoc-gen-go/descriptor"
"github.com/golang/protobuf/v2/internal/protogen/goldentest"
)
@ -23,3 +29,65 @@ func init() {
func TestGolden(t *testing.T) {
goldentest.Run(t, *regenerate)
}
func TestAnnotations(t *testing.T) {
workdir, err := ioutil.TempDir("", "proto-test")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(workdir)
goldentest.Protoc(t, []string{"--go_out=paths=source_relative,annotate_code:" + workdir, "-Itestdata/annotations", "testdata/annotations/annotations.proto"})
sourceFile, err := ioutil.ReadFile(filepath.Join(workdir, "annotations.pb.go"))
if err != nil {
t.Fatal(err)
}
metaFile, err := ioutil.ReadFile(filepath.Join(workdir, "annotations.pb.go.meta"))
if err != nil {
t.Fatal(err)
}
gotInfo := &descpb.GeneratedCodeInfo{}
if err := proto.UnmarshalText(string(metaFile), gotInfo); err != nil {
t.Fatalf("can't parse meta file: %v", err)
}
wantInfo := &descpb.GeneratedCodeInfo{}
for _, want := range []struct {
prefix, text, suffix string
path []int32
}{{
"type ", "AnnotationsTestEnum", " int32",
[]int32{5 /* enum_type */, 0},
}, {
"\t", "AnnotationsTestEnum_ANNOTATIONS_TEST_ENUM_VALUE", " AnnotationsTestEnum = 0",
[]int32{5 /* enum_type */, 0, 2 /* value */, 0},
}, {
"type ", "AnnotationsTestMessage", " struct {",
[]int32{4 /* message_type */, 0},
}, {
"\t", "AnnotationsTestField", " ",
[]int32{4 /* message_type */, 0, 2 /* field */, 0},
}, {
"func (m *AnnotationsTestMessage) ", "GetAnnotationsTestField", "() string {",
[]int32{4 /* message_type */, 0, 2 /* field */, 0},
}} {
s := want.prefix + want.text + want.suffix
pos := bytes.Index(sourceFile, []byte(s))
if pos < 0 {
t.Errorf("source file does not contain: %v", s)
continue
}
begin := pos + len(want.prefix)
end := begin + len(want.text)
wantInfo.Annotation = append(wantInfo.Annotation, &descpb.GeneratedCodeInfo_Annotation{
Path: want.path,
Begin: proto.Int32(int32(begin)),
End: proto.Int32(int32(end)),
SourceFile: proto.String("annotations.proto"),
})
}
if !proto.Equal(gotInfo, wantInfo) {
t.Errorf("unexpected annotations for annotations.proto; got:\n%v\nwant:\n%v",
proto.MarshalTextString(gotInfo), proto.MarshalTextString(wantInfo))
}
}

View File

@ -76,7 +76,7 @@ func GenerateFile(gen *protogen.Plugin, file *protogen.File, g *protogen.Generat
}
g.P()
const filePackageField = 2 // FileDescriptorProto.package
genComment(g, f, []int32{filePackageField})
genComment(g, f, protogen.Location{Path: []int32{filePackageField}})
g.P()
g.P("package ", f.GoPackageName)
g.P()
@ -237,12 +237,14 @@ func genFileDescriptor(gen *protogen.Plugin, g *protogen.GeneratedFile, f *fileI
}
func genEnum(gen *protogen.Plugin, g *protogen.GeneratedFile, f *fileInfo, enum *protogen.Enum) {
genComment(g, f, enum.Path)
genComment(g, f, enum.Location)
g.Annotate(enum.GoIdent.GoName, enum.Location)
g.P("type ", enum.GoIdent, " int32",
deprecationComment(enumOptions(gen, enum).GetDeprecated()))
g.P("const (")
for _, value := range enum.Values {
genComment(g, f, value.Path)
genComment(g, f, value.Location)
g.Annotate(value.GoIdent.GoName, value.Location)
g.P(value.GoIdent, " ", enum.GoIdent, " = ", value.Desc.Number(),
deprecationComment(enumValueOptions(gen, value).GetDeprecated()))
}
@ -294,8 +296,8 @@ func genEnum(gen *protogen.Plugin, g *protogen.GeneratedFile, f *fileInfo, enum
}
var indexes []string
for i := 1; i < len(enum.Path); i += 2 {
indexes = append(indexes, strconv.Itoa(int(enum.Path[i])))
for i := 1; i < len(enum.Location.Path); i += 2 {
indexes = append(indexes, strconv.Itoa(int(enum.Location.Path[i])))
}
g.P("func (", enum.GoIdent, ") EnumDescriptor() ([]byte, []int) {")
g.P("return ", f.descriptorVar, ", []int{", strings.Join(indexes, ","), "}")
@ -333,13 +335,14 @@ func genMessage(gen *protogen.Plugin, g *protogen.GeneratedFile, f *fileInfo, me
return
}
hasComment := genComment(g, f, message.Path)
hasComment := genComment(g, f, message.Location)
if messageOptions(gen, message).GetDeprecated() {
if hasComment {
g.P("//")
}
g.P(deprecationComment(true))
}
g.Annotate(message.GoIdent.GoName, message.Location)
g.P("type ", message.GoIdent, " struct {")
for _, field := range message.Fields {
if field.OneofType != nil {
@ -352,7 +355,7 @@ func genMessage(gen *protogen.Plugin, g *protogen.GeneratedFile, f *fileInfo, me
}
continue
}
genComment(g, f, field.Path)
genComment(g, f, field.Location)
goType, pointer := fieldGoType(g, field)
if pointer {
goType = "*" + goType
@ -369,6 +372,7 @@ func genMessage(gen *protogen.Plugin, g *protogen.GeneratedFile, f *fileInfo, me
fmt.Sprintf("protobuf_val:%q", fieldProtobufTag(val)),
)
}
g.Annotate(message.GoIdent.GoName+"."+field.GoName, field.Location)
g.P(field.GoName, " ", goType, " `", strings.Join(tags, " "), "`",
deprecationComment(fieldOptions(gen, field).GetDeprecated()))
}
@ -402,8 +406,8 @@ func genMessage(gen *protogen.Plugin, g *protogen.GeneratedFile, f *fileInfo, me
g.P("func (*", message.GoIdent, ") ProtoMessage() {}")
// Descriptor
var indexes []string
for i := 1; i < len(message.Path); i += 2 {
indexes = append(indexes, strconv.Itoa(int(message.Path[i])))
for i := 1; i < len(message.Location.Path); i += 2 {
indexes = append(indexes, strconv.Itoa(int(message.Location.Path[i])))
}
g.P("func (*", message.GoIdent, ") Descriptor() ([]byte, []int) {")
g.P("return ", f.descriptorVar, ", []int{", strings.Join(indexes, ","), "}")
@ -547,6 +551,7 @@ func genMessage(gen *protogen.Plugin, g *protogen.GeneratedFile, f *fileInfo, me
if fieldOptions(gen, field).GetDeprecated() {
g.P(deprecationComment(true))
}
g.Annotate(message.GoIdent.GoName+".Get"+field.GoName, field.Location)
g.P("func (m *", message.GoIdent, ") Get", field.GoName, "() ", goType, " {")
if field.OneofType != nil {
g.P("if x, ok := m.Get", field.OneofType.GoName, "().(*", fieldOneofType(field), "); ok {")
@ -897,8 +902,8 @@ func genRegisterExtension(gen *protogen.Plugin, g *protogen.GeneratedFile, f *fi
}
}
func genComment(g *protogen.GeneratedFile, f *fileInfo, path []int32) (hasComment bool) {
for _, loc := range f.locationMap[pathKey(path)] {
func genComment(g *protogen.GeneratedFile, f *fileInfo, loc protogen.Location) (hasComment bool) {
for _, loc := range f.locationMap[pathKey(loc.Path)] {
if loc.LeadingComments == nil {
continue
}

View File

@ -15,14 +15,15 @@ import (
// genOneofField generates the struct field for a oneof.
func genOneofField(gen *protogen.Plugin, g *protogen.GeneratedFile, f *fileInfo, message *protogen.Message, oneof *protogen.Oneof) {
if genComment(g, f, oneof.Path) {
if genComment(g, f, oneof.Location) {
g.P("//")
}
g.P("// Types that are valid to be assigned to ", oneof.GoName, ":")
for _, field := range oneof.Fields {
genComment(g, f, field.Path)
genComment(g, f, field.Location)
g.P("//\t*", fieldOneofType(field))
}
g.Annotate(message.GoIdent.GoName+"."+oneof.GoName, oneof.Location)
g.P(oneof.GoName, " ", oneofInterfaceName(message, oneof), " `protobuf_oneof:\"", oneof.Desc.Name(), "\"`")
}
@ -38,7 +39,10 @@ func genOneofTypes(gen *protogen.Plugin, g *protogen.GeneratedFile, f *fileInfo,
g.P("}")
g.P()
for _, field := range oneof.Fields {
g.P("type ", fieldOneofType(field), " struct {")
name := fieldOneofType(field)
g.Annotate(name.GoName, field.Location)
g.Annotate(name.GoName+"."+field.GoName, field.Location)
g.P("type ", name, " struct {")
goType, _ := fieldGoType(g, field)
tags := []string{
fmt.Sprintf("protobuf:%q", fieldProtobufTag(field)),
@ -51,6 +55,7 @@ func genOneofTypes(gen *protogen.Plugin, g *protogen.GeneratedFile, f *fileInfo,
g.P("func (*", fieldOneofType(field), ") ", ifName, "() {}")
g.P()
}
g.Annotate(message.GoIdent.GoName+".Get"+oneof.GoName, oneof.Location)
g.P("func (m *", message.GoIdent.GoName, ") Get", oneof.GoName, "() ", ifName, " {")
g.P("if m != nil {")
g.P("return m.", oneof.GoName)

View File

@ -17,7 +17,7 @@ import (
// messageOptions returns the MessageOptions for a message.
func messageOptions(gen *protogen.Plugin, message *protogen.Message) *descpb.MessageOptions {
d := getDescriptorProto(gen, message.Desc, message.Path)
d := getDescriptorProto(gen, message.Desc, message.Location.Path)
if d == nil {
return nil
}
@ -26,7 +26,7 @@ func messageOptions(gen *protogen.Plugin, message *protogen.Message) *descpb.Mes
// fieldOptions returns the FieldOptions for a message.
func fieldOptions(gen *protogen.Plugin, field *protogen.Field) *descpb.FieldOptions {
d := getDescriptorProto(gen, field.Desc, field.Path)
d := getDescriptorProto(gen, field.Desc, field.Location.Path)
if d == nil {
return nil
}
@ -35,7 +35,7 @@ func fieldOptions(gen *protogen.Plugin, field *protogen.Field) *descpb.FieldOpti
// enumOptions returns the EnumOptions for an enum
func enumOptions(gen *protogen.Plugin, enum *protogen.Enum) *descpb.EnumOptions {
d := getDescriptorProto(gen, enum.Desc, enum.Path)
d := getDescriptorProto(gen, enum.Desc, enum.Location.Path)
if d == nil {
return nil
}
@ -44,7 +44,7 @@ func enumOptions(gen *protogen.Plugin, enum *protogen.Enum) *descpb.EnumOptions
// enumValueOptions returns the EnumValueOptions for an enum value
func enumValueOptions(gen *protogen.Plugin, value *protogen.EnumValue) *descpb.EnumValueOptions {
d := getDescriptorProto(gen, value.Desc, value.Path)
d := getDescriptorProto(gen, value.Desc, value.Location.Path)
if d == nil {
return nil
}

View File

@ -0,0 +1,121 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// source: annotations/annotations.proto
package annotations
import (
fmt "fmt"
proto "github.com/golang/protobuf/proto"
math "math"
)
// Reference imports to suppress errors if they are not otherwise used.
var _ = proto.Marshal
var _ = fmt.Errorf
var _ = math.Inf
// This is a compile-time assertion to ensure that this generated file
// is compatible with the proto package it is being compiled against.
// A compilation error at this line likely means your copy of the
// proto package needs to be updated.
const _ = proto.ProtoPackageIsVersion2 // please upgrade the proto package
type AnnotationsTestEnum int32
const (
AnnotationsTestEnum_ANNOTATIONS_TEST_ENUM_VALUE AnnotationsTestEnum = 0
)
var AnnotationsTestEnum_name = map[int32]string{
0: "ANNOTATIONS_TEST_ENUM_VALUE",
}
var AnnotationsTestEnum_value = map[string]int32{
"ANNOTATIONS_TEST_ENUM_VALUE": 0,
}
func (x AnnotationsTestEnum) Enum() *AnnotationsTestEnum {
p := new(AnnotationsTestEnum)
*p = x
return p
}
func (x AnnotationsTestEnum) String() string {
return proto.EnumName(AnnotationsTestEnum_name, int32(x))
}
func (x *AnnotationsTestEnum) UnmarshalJSON(data []byte) error {
value, err := proto.UnmarshalJSONEnum(AnnotationsTestEnum_value, data, "AnnotationsTestEnum")
if err != nil {
return err
}
*x = AnnotationsTestEnum(value)
return nil
}
func (AnnotationsTestEnum) EnumDescriptor() ([]byte, []int) {
return fileDescriptor_21dfaf6fd39fa3b7, []int{0}
}
type AnnotationsTestMessage struct {
AnnotationsTestField *string `protobuf:"bytes,1,opt,name=AnnotationsTestField" json:"AnnotationsTestField,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
func (m *AnnotationsTestMessage) Reset() { *m = AnnotationsTestMessage{} }
func (m *AnnotationsTestMessage) String() string { return proto.CompactTextString(m) }
func (*AnnotationsTestMessage) ProtoMessage() {}
func (*AnnotationsTestMessage) Descriptor() ([]byte, []int) {
return fileDescriptor_21dfaf6fd39fa3b7, []int{0}
}
func (m *AnnotationsTestMessage) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_AnnotationsTestMessage.Unmarshal(m, b)
}
func (m *AnnotationsTestMessage) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
return xxx_messageInfo_AnnotationsTestMessage.Marshal(b, m, deterministic)
}
func (m *AnnotationsTestMessage) XXX_Merge(src proto.Message) {
xxx_messageInfo_AnnotationsTestMessage.Merge(m, src)
}
func (m *AnnotationsTestMessage) XXX_Size() int {
return xxx_messageInfo_AnnotationsTestMessage.Size(m)
}
func (m *AnnotationsTestMessage) XXX_DiscardUnknown() {
xxx_messageInfo_AnnotationsTestMessage.DiscardUnknown(m)
}
var xxx_messageInfo_AnnotationsTestMessage proto.InternalMessageInfo
func (m *AnnotationsTestMessage) GetAnnotationsTestField() string {
if m != nil && m.AnnotationsTestField != nil {
return *m.AnnotationsTestField
}
return ""
}
func init() {
proto.RegisterEnum("goproto.protoc.annotations.AnnotationsTestEnum", AnnotationsTestEnum_name, AnnotationsTestEnum_value)
proto.RegisterType((*AnnotationsTestMessage)(nil), "goproto.protoc.annotations.AnnotationsTestMessage")
}
func init() { proto.RegisterFile("annotations/annotations.proto", fileDescriptor_21dfaf6fd39fa3b7) }
var fileDescriptor_21dfaf6fd39fa3b7 = []byte{
// 194 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0x92, 0x4d, 0xcc, 0xcb, 0xcb,
0x2f, 0x49, 0x2c, 0xc9, 0xcc, 0xcf, 0x2b, 0xd6, 0x47, 0x62, 0xeb, 0x15, 0x14, 0xe5, 0x97, 0xe4,
0x0b, 0x49, 0xa5, 0xe7, 0x83, 0x19, 0x10, 0x6e, 0xb2, 0x1e, 0x92, 0x0a, 0x25, 0x1f, 0x2e, 0x31,
0x47, 0x04, 0x37, 0x24, 0xb5, 0xb8, 0xc4, 0x37, 0xb5, 0xb8, 0x38, 0x31, 0x3d, 0x55, 0xc8, 0x88,
0x4b, 0x04, 0x4d, 0xc6, 0x2d, 0x33, 0x35, 0x27, 0x45, 0x82, 0x51, 0x81, 0x51, 0x83, 0x33, 0x08,
0xab, 0x9c, 0x96, 0x19, 0x97, 0x30, 0x9a, 0xb8, 0x6b, 0x5e, 0x69, 0xae, 0x90, 0x3c, 0x97, 0xb4,
0xa3, 0x9f, 0x9f, 0x7f, 0x88, 0x63, 0x88, 0xa7, 0xbf, 0x5f, 0x70, 0x7c, 0x88, 0x6b, 0x70, 0x48,
0xbc, 0xab, 0x5f, 0xa8, 0x6f, 0x7c, 0x98, 0xa3, 0x4f, 0xa8, 0xab, 0x00, 0x83, 0x93, 0x5b, 0x94,
0x4b, 0x7a, 0x66, 0x49, 0x46, 0x69, 0x92, 0x5e, 0x72, 0x7e, 0xae, 0x7e, 0x7a, 0x7e, 0x4e, 0x62,
0x5e, 0xba, 0x3e, 0xd8, 0xb5, 0x49, 0xa5, 0x69, 0xfa, 0x65, 0x46, 0xfa, 0xc9, 0xb9, 0x29, 0x10,
0x7e, 0xb2, 0x6e, 0x7a, 0x6a, 0x9e, 0x6e, 0x7a, 0xbe, 0x7e, 0x49, 0x6a, 0x71, 0x49, 0x4a, 0x62,
0x49, 0x22, 0xb2, 0x7f, 0x01, 0x01, 0x00, 0x00, 0xff, 0xff, 0x40, 0xd6, 0xe5, 0x9d, 0x09, 0x01,
0x00, 0x00,
}

View File

@ -0,0 +1,17 @@
// Copyright 2018 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
syntax = "proto2";
package goproto.protoc.annotations;
option go_package = "github.com/golang/protobuf/v2/cmd/protoc-gen-go/testdata/annotations";
message AnnotationsTestMessage {
optional string AnnotationsTestField = 1;
}
enum AnnotationsTestEnum {
ANNOTATIONS_TEST_ENUM_VALUE = 0;
}

View File

@ -55,7 +55,7 @@ func Run(t *testing.T, regenerate bool) {
for _, sources := range packages {
args := []string{"-Itestdata", "--go_out=paths=source_relative:" + workdir}
args = append(args, sources...)
protoc(t, args)
Protoc(t, args)
}
// Compare each generated file to the golden version.
@ -111,7 +111,9 @@ func Run(t *testing.T, regenerate bool) {
var fdescRE = regexp.MustCompile(`(?ms)^var fileDescriptor.*}`)
func protoc(t *testing.T, args []string) {
// Protoc runs protoc, using the function registered with Plugin as the protoc-gen-go plugin.
func Protoc(t *testing.T, args []string) {
t.Helper()
cmd := exec.Command("protoc", "--plugin=protoc-gen-go="+os.Args[0])
cmd.Args = append(cmd.Args, args...)
// We set the RUN_AS_PROTOC_PLUGIN environment variable to indicate that

View File

@ -101,6 +101,7 @@ type Plugin struct {
fileReg *protoregistry.Files
messagesByName map[protoreflect.FullName]*Message
enumsByName map[protoreflect.FullName]*Enum
annotateCode bool
pathType pathType
genFiles []*GeneratedFile
opts *Options
@ -180,7 +181,13 @@ func New(req *pluginpb.CodeGeneratorRequest, opts *Options) (*Plugin, error) {
return nil, fmt.Errorf(`unknown path type %q: want "import" or "source_relative"`, value)
}
case "annotate_code":
// TODO
switch value {
case "true", "":
gen.annotateCode = true
case "false":
default:
return nil, fmt.Errorf(`bad value for parameter %q: want "true" or "false"`, param)
}
default:
if param[0] == 'M' {
importPaths[param[1:]] = GoImportPath(value)
@ -331,17 +338,29 @@ func (gen *Plugin) Response() *pluginpb.CodeGeneratorResponse {
resp.Error = proto.String(gen.err.Error())
return resp
}
for _, gf := range gen.genFiles {
content, err := gf.Content()
for _, g := range gen.genFiles {
content, err := g.content()
if err != nil {
return &pluginpb.CodeGeneratorResponse{
Error: proto.String(err.Error()),
}
}
resp.File = append(resp.File, &pluginpb.CodeGeneratorResponse_File{
Name: proto.String(gf.filename),
Name: proto.String(g.filename),
Content: proto.String(string(content)),
})
if gen.annotateCode && strings.HasSuffix(g.filename, ".go") {
meta, err := g.metaFile(content)
if err != nil {
return &pluginpb.CodeGeneratorResponse{
Error: proto.String(err.Error()),
}
}
resp.File = append(resp.File, &pluginpb.CodeGeneratorResponse_File{
Name: proto.String(g.filename + ".meta"),
Content: proto.String(meta),
})
}
}
return resp
}
@ -438,6 +457,13 @@ func newFile(gen *Plugin, p *descpb.FileDescriptorProto, packageName GoPackageNa
return f, nil
}
func (f *File) location(path ...int32) Location {
return Location{
SourceFile: f.Desc.Path(),
Path: path,
}
}
// goPackageOption interprets a file's go_package option.
// If there is no go_package, it returns ("", "").
// If there's a simple name, it returns (pkg, "").
@ -468,20 +494,20 @@ type Message struct {
Messages []*Message // nested message declarations
Enums []*Enum // nested enum declarations
Extensions []*Extension // nested extension declarations
Path []int32 // location path of this message
Location Location // location of this message
}
func newMessage(gen *Plugin, f *File, parent *Message, desc protoreflect.MessageDescriptor) *Message {
var path []int32
var loc Location
if parent != nil {
path = pathAppend(parent.Path, messageMessageField, int32(desc.Index()))
loc = parent.Location.appendPath(messageMessageField, int32(desc.Index()))
} else {
path = []int32{fileMessageField, int32(desc.Index())}
loc = f.location(fileMessageField, int32(desc.Index()))
}
message := &Message{
Desc: desc,
GoIdent: newGoIdent(f, desc),
Path: path,
Desc: desc,
GoIdent: newGoIdent(f, desc),
Location: loc,
}
gen.messagesByName[desc.FullName()] = message
for i, mdescs := 0, desc.Messages(); i < mdescs.Len(); i++ {
@ -585,24 +611,24 @@ type Field struct {
MessageType *Message // type for message or group fields; nil otherwise
EnumType *Enum // type for enum fields; nil otherwise
OneofType *Oneof // containing oneof; nil if not part of a oneof
Path []int32 // location path of this field
Location Location // location of this field
}
func newField(gen *Plugin, f *File, message *Message, desc protoreflect.FieldDescriptor) *Field {
var path []int32
var loc Location
switch {
case desc.ExtendedType() != nil && message == nil:
path = []int32{fileExtensionField, int32(desc.Index())}
loc = f.location(fileExtensionField, int32(desc.Index()))
case desc.ExtendedType() != nil && message != nil:
path = pathAppend(message.Path, messageExtensionField, int32(desc.Index()))
loc = message.Location.appendPath(messageExtensionField, int32(desc.Index()))
default:
path = pathAppend(message.Path, messageFieldField, int32(desc.Index()))
loc = message.Location.appendPath(messageFieldField, int32(desc.Index()))
}
field := &Field{
Desc: desc,
GoName: camelCase(string(desc.Name())),
ParentMessage: message,
Path: path,
Location: loc,
}
if desc.OneofType() != nil {
field.OneofType = message.Oneofs[desc.OneofType().Index()]
@ -649,7 +675,7 @@ type Oneof struct {
GoName string // Go field name of this oneof
ParentMessage *Message // message in which this oneof occurs
Fields []*Field // fields that are part of this oneof
Path []int32 // location path of this oneof
Location Location // location of this oneof
}
func newOneof(gen *Plugin, f *File, message *Message, desc protoreflect.OneofDescriptor) *Oneof {
@ -657,7 +683,7 @@ func newOneof(gen *Plugin, f *File, message *Message, desc protoreflect.OneofDes
Desc: desc,
ParentMessage: message,
GoName: camelCase(string(desc.Name())),
Path: pathAppend(message.Path, messageOneofField, int32(desc.Index())),
Location: message.Location.appendPath(messageOneofField, int32(desc.Index())),
}
}
@ -671,22 +697,22 @@ func (oneof *Oneof) init(gen *Plugin, parent *Message) {
type Enum struct {
Desc protoreflect.EnumDescriptor
GoIdent GoIdent // name of the generated Go type
Values []*EnumValue // enum values
Path []int32 // location path of this enum
GoIdent GoIdent // name of the generated Go type
Values []*EnumValue // enum values
Location Location // location of this enum
}
func newEnum(gen *Plugin, f *File, parent *Message, desc protoreflect.EnumDescriptor) *Enum {
var path []int32
var loc Location
if parent != nil {
path = pathAppend(parent.Path, messageEnumField, int32(desc.Index()))
loc = parent.Location.appendPath(messageEnumField, int32(desc.Index()))
} else {
path = []int32{fileEnumField, int32(desc.Index())}
loc = f.location(fileEnumField, int32(desc.Index()))
}
enum := &Enum{
Desc: desc,
GoIdent: newGoIdent(f, desc),
Path: path,
Desc: desc,
GoIdent: newGoIdent(f, desc),
Location: loc,
}
gen.enumsByName[desc.FullName()] = enum
for i, evdescs := 0, enum.Desc.Values(); i < evdescs.Len(); i++ {
@ -699,8 +725,8 @@ func newEnum(gen *Plugin, f *File, parent *Message, desc protoreflect.EnumDescri
type EnumValue struct {
Desc protoreflect.EnumValueDescriptor
GoIdent GoIdent // name of the generated Go type
Path []int32 // location path of this enum value
GoIdent GoIdent // name of the generated Go type
Location Location // location of this enum value
}
func newEnumValue(gen *Plugin, f *File, message *Message, enum *Enum, desc protoreflect.EnumValueDescriptor) *EnumValue {
@ -719,7 +745,7 @@ func newEnumValue(gen *Plugin, f *File, message *Message, enum *Enum, desc proto
GoName: name,
GoImportPath: f.GoImportPath,
},
Path: pathAppend(enum.Path, enumValueField, int32(desc.Index())),
Location: enum.Location.appendPath(enumValueField, int32(desc.Index())),
}
}
@ -732,6 +758,7 @@ type GeneratedFile struct {
packageNames map[GoImportPath]GoPackageName
usedPackageNames map[GoPackageName]bool
manualImports map[GoImportPath]bool
annotations map[string][]Location
}
// NewGeneratedFile creates a new generated file with the given filename
@ -744,6 +771,7 @@ func (gen *Plugin) NewGeneratedFile(filename string, goImportPath GoImportPath)
packageNames: make(map[GoImportPath]GoPackageName),
usedPackageNames: make(map[GoPackageName]bool),
manualImports: make(map[GoImportPath]bool),
annotations: make(map[string][]Location),
}
gen.genFiles = append(gen.genFiles, g)
return g
@ -753,16 +781,16 @@ func (gen *Plugin) NewGeneratedFile(filename string, goImportPath GoImportPath)
type Service struct {
Desc protoreflect.ServiceDescriptor
GoName string
Path []int32 // location path of this service
Methods []*Method // service method definitions
GoName string
Location Location // location of this service
Methods []*Method // service method definitions
}
func newService(gen *Plugin, f *File, desc protoreflect.ServiceDescriptor) *Service {
service := &Service{
Desc: desc,
GoName: camelCase(string(desc.Name())),
Path: []int32{fileServiceField, int32(desc.Index())},
Desc: desc,
GoName: camelCase(string(desc.Name())),
Location: f.location(fileServiceField, int32(desc.Index())),
}
for i, mdescs := 0, desc.Methods(); i < mdescs.Len(); i++ {
service.Methods = append(service.Methods, newMethod(gen, f, service, mdescs.Get(i)))
@ -776,7 +804,7 @@ type Method struct {
GoName string
ParentService *Service
Path []int32 // location path of this method
Location Location // location of this method
InputType *Message
OutputType *Message
}
@ -786,7 +814,7 @@ func newMethod(gen *Plugin, f *File, service *Service, desc protoreflect.MethodD
Desc: desc,
GoName: camelCase(string(desc.Name())),
ParentService: service,
Path: pathAppend(service.Path, serviceMethodField, int32(desc.Index())),
Location: service.Location.appendPath(serviceMethodField, int32(desc.Index())),
}
return method
}
@ -814,8 +842,6 @@ func (method *Method) init(gen *Plugin) error {
// P prints a line to the generated output. It converts each parameter to a
// string following the same rules as fmt.Print. It never inserts spaces
// between parameters.
//
// TODO: .meta file annotations.
func (g *GeneratedFile) P(v ...interface{}) {
for _, x := range v {
switch x := x.(type) {
@ -863,8 +889,18 @@ func (g *GeneratedFile) Write(p []byte) (n int, err error) {
return g.buf.Write(p)
}
// Content returns the contents of the generated file.
func (g *GeneratedFile) Content() ([]byte, error) {
// Annotate associates a symbol in a generated Go file with a location in a
// source .proto file.
//
// The symbol may refer to a type, constant, variable, function, method, or
// struct field. The "T.sel" syntax is used to identify the method or field
// 'sel' on type 'T'.
func (g *GeneratedFile) Annotate(symbol string, loc Location) {
g.annotations[symbol] = append(g.annotations[symbol], loc)
}
// content returns the contents of the generated file.
func (g *GeneratedFile) content() ([]byte, error) {
if !strings.HasSuffix(g.filename, ".go") {
return g.buf.Bytes(), nil
}
@ -912,9 +948,72 @@ func (g *GeneratedFile) Content() ([]byte, error) {
if err = (&printer.Config{Mode: printer.TabIndent | printer.UseSpaces, Tabwidth: 8}).Fprint(&out, fset, file); err != nil {
return nil, fmt.Errorf("%v: can not reformat Go source: %v", g.filename, err)
}
// TODO: Annotations.
return out.Bytes(), nil
}
// metaFile returns the contents of the file's metadata file, which is a
// text formatted string of the google.protobuf.GeneratedCodeInfo.
func (g *GeneratedFile) metaFile(content []byte) (string, error) {
fset := token.NewFileSet()
astFile, err := parser.ParseFile(fset, "", content, 0)
if err != nil {
return "", err
}
info := &descpb.GeneratedCodeInfo{}
seenAnnotations := make(map[string]bool)
annotate := func(s string, ident *ast.Ident) {
seenAnnotations[s] = true
for _, loc := range g.annotations[s] {
info.Annotation = append(info.Annotation, &descpb.GeneratedCodeInfo_Annotation{
SourceFile: proto.String(loc.SourceFile),
Path: loc.Path,
Begin: proto.Int32(int32(fset.Position(ident.Pos()).Offset)),
End: proto.Int32(int32(fset.Position(ident.End()).Offset)),
})
}
}
for _, decl := range astFile.Decls {
switch decl := decl.(type) {
case *ast.GenDecl:
for _, spec := range decl.Specs {
switch spec := spec.(type) {
case *ast.TypeSpec:
annotate(spec.Name.Name, spec.Name)
if st, ok := spec.Type.(*ast.StructType); ok {
for _, field := range st.Fields.List {
for _, name := range field.Names {
annotate(spec.Name.Name+"."+name.Name, name)
}
}
}
case *ast.ValueSpec:
for _, name := range spec.Names {
annotate(name.Name, name)
}
}
}
case *ast.FuncDecl:
if decl.Recv == nil {
annotate(decl.Name.Name, decl.Name)
} else {
recv := decl.Recv.List[0].Type
if s, ok := recv.(*ast.StarExpr); ok {
recv = s.X
}
if id, ok := recv.(*ast.Ident); ok {
annotate(id.Name+"."+decl.Name.Name, decl.Name)
}
}
}
}
for a := range g.annotations {
if !seenAnnotations[a] {
return "", fmt.Errorf("%v: no symbol matching annotation %q", g.filename, a)
}
}
return proto.CompactTextString(info), nil
}
type pathType int
@ -952,11 +1051,22 @@ const (
serviceStreamField = 4 // stream
)
// pathAppend appends elements to a location path.
// It does not alias the original path.
func pathAppend(path []int32, a ...int32) []int32 {
var n []int32
n = append(n, path...)
n = append(n, a...)
return n
// A Location is a location in a .proto source file.
//
// See the google.protobuf.SourceCodeInfo documentation in descriptor.proto
// for details.
type Location struct {
SourceFile string
Path []int32
}
// appendPath add elements to a Location's path, returning a new Location.
func (loc Location) appendPath(a ...int32) Location {
var n []int32
n = append(n, loc.Path...)
n = append(n, a...)
return Location{
SourceFile: loc.SourceFile,
Path: n,
}
}

View File

@ -295,9 +295,9 @@ var _ = bar1.X // "golang.org/y/bar"
var _ = baz.X // "golang.org/x/baz"
var _ = string1.X // "golang.org/z/string"
`
got, err := g.Content()
got, err := g.content()
if err != nil {
t.Fatalf("g.Content() = %v", err)
t.Fatalf("g.content() = %v", err)
}
if want != string(got) {
t.Fatalf(`want:
@ -331,9 +331,9 @@ import bar "prefix/golang.org/x/bar"
var _ = bar.X
`
got, err := g.Content()
got, err := g.content()
if err != nil {
t.Fatalf("g.Content() = %v", err)
t.Fatalf("g.content() = %v", err)
}
if want != string(got) {
t.Fatalf(`want: