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

View File

@ -17,7 +17,7 @@ import (
// serviceOptions returns the options for a service. // serviceOptions returns the options for a service.
func serviceOptions(gen *protogen.Plugin, service *protogen.Service) *descpb.ServiceOptions { 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 { if d == nil {
return nil return nil
} }
@ -26,7 +26,7 @@ func serviceOptions(gen *protogen.Plugin, service *protogen.Service) *descpb.Ser
// methodOptions returns the options for a method. // methodOptions returns the options for a method.
func methodOptions(gen *protogen.Plugin, method *protogen.Method) *descpb.MethodOptions { 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 { if d == nil {
return nil return nil
} }

View File

@ -7,9 +7,15 @@
package main package main
import ( import (
"bytes"
"flag" "flag"
"io/ioutil"
"os"
"path/filepath"
"testing" "testing"
"github.com/golang/protobuf/proto"
descpb "github.com/golang/protobuf/protoc-gen-go/descriptor"
"github.com/golang/protobuf/v2/internal/protogen/goldentest" "github.com/golang/protobuf/v2/internal/protogen/goldentest"
) )
@ -23,3 +29,65 @@ func init() {
func TestGolden(t *testing.T) { func TestGolden(t *testing.T) {
goldentest.Run(t, *regenerate) 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() g.P()
const filePackageField = 2 // FileDescriptorProto.package const filePackageField = 2 // FileDescriptorProto.package
genComment(g, f, []int32{filePackageField}) genComment(g, f, protogen.Location{Path: []int32{filePackageField}})
g.P() g.P()
g.P("package ", f.GoPackageName) g.P("package ", f.GoPackageName)
g.P() 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) { 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", g.P("type ", enum.GoIdent, " int32",
deprecationComment(enumOptions(gen, enum).GetDeprecated())) deprecationComment(enumOptions(gen, enum).GetDeprecated()))
g.P("const (") g.P("const (")
for _, value := range enum.Values { 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(), g.P(value.GoIdent, " ", enum.GoIdent, " = ", value.Desc.Number(),
deprecationComment(enumValueOptions(gen, value).GetDeprecated())) deprecationComment(enumValueOptions(gen, value).GetDeprecated()))
} }
@ -294,8 +296,8 @@ func genEnum(gen *protogen.Plugin, g *protogen.GeneratedFile, f *fileInfo, enum
} }
var indexes []string var indexes []string
for i := 1; i < len(enum.Path); i += 2 { for i := 1; i < len(enum.Location.Path); i += 2 {
indexes = append(indexes, strconv.Itoa(int(enum.Path[i]))) indexes = append(indexes, strconv.Itoa(int(enum.Location.Path[i])))
} }
g.P("func (", enum.GoIdent, ") EnumDescriptor() ([]byte, []int) {") g.P("func (", enum.GoIdent, ") EnumDescriptor() ([]byte, []int) {")
g.P("return ", f.descriptorVar, ", []int{", strings.Join(indexes, ","), "}") 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 return
} }
hasComment := genComment(g, f, message.Path) hasComment := genComment(g, f, message.Location)
if messageOptions(gen, message).GetDeprecated() { if messageOptions(gen, message).GetDeprecated() {
if hasComment { if hasComment {
g.P("//") g.P("//")
} }
g.P(deprecationComment(true)) g.P(deprecationComment(true))
} }
g.Annotate(message.GoIdent.GoName, message.Location)
g.P("type ", message.GoIdent, " struct {") g.P("type ", message.GoIdent, " struct {")
for _, field := range message.Fields { for _, field := range message.Fields {
if field.OneofType != nil { if field.OneofType != nil {
@ -352,7 +355,7 @@ func genMessage(gen *protogen.Plugin, g *protogen.GeneratedFile, f *fileInfo, me
} }
continue continue
} }
genComment(g, f, field.Path) genComment(g, f, field.Location)
goType, pointer := fieldGoType(g, field) goType, pointer := fieldGoType(g, field)
if pointer { if pointer {
goType = "*" + goType goType = "*" + goType
@ -369,6 +372,7 @@ func genMessage(gen *protogen.Plugin, g *protogen.GeneratedFile, f *fileInfo, me
fmt.Sprintf("protobuf_val:%q", fieldProtobufTag(val)), fmt.Sprintf("protobuf_val:%q", fieldProtobufTag(val)),
) )
} }
g.Annotate(message.GoIdent.GoName+"."+field.GoName, field.Location)
g.P(field.GoName, " ", goType, " `", strings.Join(tags, " "), "`", g.P(field.GoName, " ", goType, " `", strings.Join(tags, " "), "`",
deprecationComment(fieldOptions(gen, field).GetDeprecated())) 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() {}") g.P("func (*", message.GoIdent, ") ProtoMessage() {}")
// Descriptor // Descriptor
var indexes []string var indexes []string
for i := 1; i < len(message.Path); i += 2 { for i := 1; i < len(message.Location.Path); i += 2 {
indexes = append(indexes, strconv.Itoa(int(message.Path[i]))) indexes = append(indexes, strconv.Itoa(int(message.Location.Path[i])))
} }
g.P("func (*", message.GoIdent, ") Descriptor() ([]byte, []int) {") g.P("func (*", message.GoIdent, ") Descriptor() ([]byte, []int) {")
g.P("return ", f.descriptorVar, ", []int{", strings.Join(indexes, ","), "}") 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() { if fieldOptions(gen, field).GetDeprecated() {
g.P(deprecationComment(true)) g.P(deprecationComment(true))
} }
g.Annotate(message.GoIdent.GoName+".Get"+field.GoName, field.Location)
g.P("func (m *", message.GoIdent, ") Get", field.GoName, "() ", goType, " {") g.P("func (m *", message.GoIdent, ") Get", field.GoName, "() ", goType, " {")
if field.OneofType != nil { if field.OneofType != nil {
g.P("if x, ok := m.Get", field.OneofType.GoName, "().(*", fieldOneofType(field), "); ok {") 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) { func genComment(g *protogen.GeneratedFile, f *fileInfo, loc protogen.Location) (hasComment bool) {
for _, loc := range f.locationMap[pathKey(path)] { for _, loc := range f.locationMap[pathKey(loc.Path)] {
if loc.LeadingComments == nil { if loc.LeadingComments == nil {
continue continue
} }

View File

@ -15,14 +15,15 @@ import (
// genOneofField generates the struct field for a oneof. // genOneofField generates the struct field for a oneof.
func genOneofField(gen *protogen.Plugin, g *protogen.GeneratedFile, f *fileInfo, message *protogen.Message, oneof *protogen.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("//")
} }
g.P("// Types that are valid to be assigned to ", oneof.GoName, ":") g.P("// Types that are valid to be assigned to ", oneof.GoName, ":")
for _, field := range oneof.Fields { for _, field := range oneof.Fields {
genComment(g, f, field.Path) genComment(g, f, field.Location)
g.P("//\t*", fieldOneofType(field)) 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(), "\"`") 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("}")
g.P() g.P()
for _, field := range oneof.Fields { 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) goType, _ := fieldGoType(g, field)
tags := []string{ tags := []string{
fmt.Sprintf("protobuf:%q", fieldProtobufTag(field)), 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("func (*", fieldOneofType(field), ") ", ifName, "() {}")
g.P() 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("func (m *", message.GoIdent.GoName, ") Get", oneof.GoName, "() ", ifName, " {")
g.P("if m != nil {") g.P("if m != nil {")
g.P("return m.", oneof.GoName) g.P("return m.", oneof.GoName)

View File

@ -17,7 +17,7 @@ import (
// messageOptions returns the MessageOptions for a message. // messageOptions returns the MessageOptions for a message.
func messageOptions(gen *protogen.Plugin, message *protogen.Message) *descpb.MessageOptions { 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 { if d == nil {
return nil return nil
} }
@ -26,7 +26,7 @@ func messageOptions(gen *protogen.Plugin, message *protogen.Message) *descpb.Mes
// fieldOptions returns the FieldOptions for a message. // fieldOptions returns the FieldOptions for a message.
func fieldOptions(gen *protogen.Plugin, field *protogen.Field) *descpb.FieldOptions { 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 { if d == nil {
return nil return nil
} }
@ -35,7 +35,7 @@ func fieldOptions(gen *protogen.Plugin, field *protogen.Field) *descpb.FieldOpti
// enumOptions returns the EnumOptions for an enum // enumOptions returns the EnumOptions for an enum
func enumOptions(gen *protogen.Plugin, enum *protogen.Enum) *descpb.EnumOptions { 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 { if d == nil {
return 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 // enumValueOptions returns the EnumValueOptions for an enum value
func enumValueOptions(gen *protogen.Plugin, value *protogen.EnumValue) *descpb.EnumValueOptions { 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 { if d == nil {
return 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 { for _, sources := range packages {
args := []string{"-Itestdata", "--go_out=paths=source_relative:" + workdir} args := []string{"-Itestdata", "--go_out=paths=source_relative:" + workdir}
args = append(args, sources...) args = append(args, sources...)
protoc(t, args) Protoc(t, args)
} }
// Compare each generated file to the golden version. // 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.*}`) 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 := exec.Command("protoc", "--plugin=protoc-gen-go="+os.Args[0])
cmd.Args = append(cmd.Args, args...) cmd.Args = append(cmd.Args, args...)
// We set the RUN_AS_PROTOC_PLUGIN environment variable to indicate that // We set the RUN_AS_PROTOC_PLUGIN environment variable to indicate that

View File

@ -101,6 +101,7 @@ type Plugin struct {
fileReg *protoregistry.Files fileReg *protoregistry.Files
messagesByName map[protoreflect.FullName]*Message messagesByName map[protoreflect.FullName]*Message
enumsByName map[protoreflect.FullName]*Enum enumsByName map[protoreflect.FullName]*Enum
annotateCode bool
pathType pathType pathType pathType
genFiles []*GeneratedFile genFiles []*GeneratedFile
opts *Options 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) return nil, fmt.Errorf(`unknown path type %q: want "import" or "source_relative"`, value)
} }
case "annotate_code": 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: default:
if param[0] == 'M' { if param[0] == 'M' {
importPaths[param[1:]] = GoImportPath(value) importPaths[param[1:]] = GoImportPath(value)
@ -331,17 +338,29 @@ func (gen *Plugin) Response() *pluginpb.CodeGeneratorResponse {
resp.Error = proto.String(gen.err.Error()) resp.Error = proto.String(gen.err.Error())
return resp return resp
} }
for _, gf := range gen.genFiles { for _, g := range gen.genFiles {
content, err := gf.Content() content, err := g.content()
if err != nil { if err != nil {
return &pluginpb.CodeGeneratorResponse{ return &pluginpb.CodeGeneratorResponse{
Error: proto.String(err.Error()), Error: proto.String(err.Error()),
} }
} }
resp.File = append(resp.File, &pluginpb.CodeGeneratorResponse_File{ resp.File = append(resp.File, &pluginpb.CodeGeneratorResponse_File{
Name: proto.String(gf.filename), Name: proto.String(g.filename),
Content: proto.String(string(content)), 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 return resp
} }
@ -438,6 +457,13 @@ func newFile(gen *Plugin, p *descpb.FileDescriptorProto, packageName GoPackageNa
return f, nil 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. // goPackageOption interprets a file's go_package option.
// If there is no go_package, it returns ("", ""). // If there is no go_package, it returns ("", "").
// If there's a simple name, it returns (pkg, ""). // If there's a simple name, it returns (pkg, "").
@ -468,20 +494,20 @@ type Message struct {
Messages []*Message // nested message declarations Messages []*Message // nested message declarations
Enums []*Enum // nested enum declarations Enums []*Enum // nested enum declarations
Extensions []*Extension // nested extension 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 { func newMessage(gen *Plugin, f *File, parent *Message, desc protoreflect.MessageDescriptor) *Message {
var path []int32 var loc Location
if parent != nil { if parent != nil {
path = pathAppend(parent.Path, messageMessageField, int32(desc.Index())) loc = parent.Location.appendPath(messageMessageField, int32(desc.Index()))
} else { } else {
path = []int32{fileMessageField, int32(desc.Index())} loc = f.location(fileMessageField, int32(desc.Index()))
} }
message := &Message{ message := &Message{
Desc: desc, Desc: desc,
GoIdent: newGoIdent(f, desc), GoIdent: newGoIdent(f, desc),
Path: path, Location: loc,
} }
gen.messagesByName[desc.FullName()] = message gen.messagesByName[desc.FullName()] = message
for i, mdescs := 0, desc.Messages(); i < mdescs.Len(); i++ { 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 MessageType *Message // type for message or group fields; nil otherwise
EnumType *Enum // type for enum fields; nil otherwise EnumType *Enum // type for enum fields; nil otherwise
OneofType *Oneof // containing oneof; nil if not part of a oneof 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 { func newField(gen *Plugin, f *File, message *Message, desc protoreflect.FieldDescriptor) *Field {
var path []int32 var loc Location
switch { switch {
case desc.ExtendedType() != nil && message == nil: 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: case desc.ExtendedType() != nil && message != nil:
path = pathAppend(message.Path, messageExtensionField, int32(desc.Index())) loc = message.Location.appendPath(messageExtensionField, int32(desc.Index()))
default: default:
path = pathAppend(message.Path, messageFieldField, int32(desc.Index())) loc = message.Location.appendPath(messageFieldField, int32(desc.Index()))
} }
field := &Field{ field := &Field{
Desc: desc, Desc: desc,
GoName: camelCase(string(desc.Name())), GoName: camelCase(string(desc.Name())),
ParentMessage: message, ParentMessage: message,
Path: path, Location: loc,
} }
if desc.OneofType() != nil { if desc.OneofType() != nil {
field.OneofType = message.Oneofs[desc.OneofType().Index()] field.OneofType = message.Oneofs[desc.OneofType().Index()]
@ -649,7 +675,7 @@ type Oneof struct {
GoName string // Go field name of this oneof GoName string // Go field name of this oneof
ParentMessage *Message // message in which this oneof occurs ParentMessage *Message // message in which this oneof occurs
Fields []*Field // fields that are part of this oneof 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 { 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, Desc: desc,
ParentMessage: message, ParentMessage: message,
GoName: camelCase(string(desc.Name())), 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 { type Enum struct {
Desc protoreflect.EnumDescriptor Desc protoreflect.EnumDescriptor
GoIdent GoIdent // name of the generated Go type GoIdent GoIdent // name of the generated Go type
Values []*EnumValue // enum values Values []*EnumValue // enum values
Path []int32 // location path of this enum Location Location // location of this enum
} }
func newEnum(gen *Plugin, f *File, parent *Message, desc protoreflect.EnumDescriptor) *Enum { func newEnum(gen *Plugin, f *File, parent *Message, desc protoreflect.EnumDescriptor) *Enum {
var path []int32 var loc Location
if parent != nil { if parent != nil {
path = pathAppend(parent.Path, messageEnumField, int32(desc.Index())) loc = parent.Location.appendPath(messageEnumField, int32(desc.Index()))
} else { } else {
path = []int32{fileEnumField, int32(desc.Index())} loc = f.location(fileEnumField, int32(desc.Index()))
} }
enum := &Enum{ enum := &Enum{
Desc: desc, Desc: desc,
GoIdent: newGoIdent(f, desc), GoIdent: newGoIdent(f, desc),
Path: path, Location: loc,
} }
gen.enumsByName[desc.FullName()] = enum gen.enumsByName[desc.FullName()] = enum
for i, evdescs := 0, enum.Desc.Values(); i < evdescs.Len(); i++ { 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 { type EnumValue struct {
Desc protoreflect.EnumValueDescriptor Desc protoreflect.EnumValueDescriptor
GoIdent GoIdent // name of the generated Go type GoIdent GoIdent // name of the generated Go type
Path []int32 // location path of this enum value Location Location // location of this enum value
} }
func newEnumValue(gen *Plugin, f *File, message *Message, enum *Enum, desc protoreflect.EnumValueDescriptor) *EnumValue { 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, GoName: name,
GoImportPath: f.GoImportPath, 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 packageNames map[GoImportPath]GoPackageName
usedPackageNames map[GoPackageName]bool usedPackageNames map[GoPackageName]bool
manualImports map[GoImportPath]bool manualImports map[GoImportPath]bool
annotations map[string][]Location
} }
// NewGeneratedFile creates a new generated file with the given filename // 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), packageNames: make(map[GoImportPath]GoPackageName),
usedPackageNames: make(map[GoPackageName]bool), usedPackageNames: make(map[GoPackageName]bool),
manualImports: make(map[GoImportPath]bool), manualImports: make(map[GoImportPath]bool),
annotations: make(map[string][]Location),
} }
gen.genFiles = append(gen.genFiles, g) gen.genFiles = append(gen.genFiles, g)
return g return g
@ -753,16 +781,16 @@ func (gen *Plugin) NewGeneratedFile(filename string, goImportPath GoImportPath)
type Service struct { type Service struct {
Desc protoreflect.ServiceDescriptor Desc protoreflect.ServiceDescriptor
GoName string GoName string
Path []int32 // location path of this service Location Location // location of this service
Methods []*Method // service method definitions Methods []*Method // service method definitions
} }
func newService(gen *Plugin, f *File, desc protoreflect.ServiceDescriptor) *Service { func newService(gen *Plugin, f *File, desc protoreflect.ServiceDescriptor) *Service {
service := &Service{ service := &Service{
Desc: desc, Desc: desc,
GoName: camelCase(string(desc.Name())), GoName: camelCase(string(desc.Name())),
Path: []int32{fileServiceField, int32(desc.Index())}, Location: f.location(fileServiceField, int32(desc.Index())),
} }
for i, mdescs := 0, desc.Methods(); i < mdescs.Len(); i++ { for i, mdescs := 0, desc.Methods(); i < mdescs.Len(); i++ {
service.Methods = append(service.Methods, newMethod(gen, f, service, mdescs.Get(i))) service.Methods = append(service.Methods, newMethod(gen, f, service, mdescs.Get(i)))
@ -776,7 +804,7 @@ type Method struct {
GoName string GoName string
ParentService *Service ParentService *Service
Path []int32 // location path of this method Location Location // location of this method
InputType *Message InputType *Message
OutputType *Message OutputType *Message
} }
@ -786,7 +814,7 @@ func newMethod(gen *Plugin, f *File, service *Service, desc protoreflect.MethodD
Desc: desc, Desc: desc,
GoName: camelCase(string(desc.Name())), GoName: camelCase(string(desc.Name())),
ParentService: service, ParentService: service,
Path: pathAppend(service.Path, serviceMethodField, int32(desc.Index())), Location: service.Location.appendPath(serviceMethodField, int32(desc.Index())),
} }
return method 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 // 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 // string following the same rules as fmt.Print. It never inserts spaces
// between parameters. // between parameters.
//
// TODO: .meta file annotations.
func (g *GeneratedFile) P(v ...interface{}) { func (g *GeneratedFile) P(v ...interface{}) {
for _, x := range v { for _, x := range v {
switch x := x.(type) { switch x := x.(type) {
@ -863,8 +889,18 @@ func (g *GeneratedFile) Write(p []byte) (n int, err error) {
return g.buf.Write(p) return g.buf.Write(p)
} }
// Content returns the contents of the generated file. // Annotate associates a symbol in a generated Go file with a location in a
func (g *GeneratedFile) Content() ([]byte, error) { // 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") { if !strings.HasSuffix(g.filename, ".go") {
return g.buf.Bytes(), nil 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 { 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) return nil, fmt.Errorf("%v: can not reformat Go source: %v", g.filename, err)
} }
// TODO: Annotations.
return out.Bytes(), nil 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 type pathType int
@ -952,11 +1051,22 @@ const (
serviceStreamField = 4 // stream serviceStreamField = 4 // stream
) )
// pathAppend appends elements to a location path. // A Location is a location in a .proto source file.
// It does not alias the original path. //
func pathAppend(path []int32, a ...int32) []int32 { // See the google.protobuf.SourceCodeInfo documentation in descriptor.proto
var n []int32 // for details.
n = append(n, path...) type Location struct {
n = append(n, a...) SourceFile string
return n 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 _ = baz.X // "golang.org/x/baz"
var _ = string1.X // "golang.org/z/string" var _ = string1.X // "golang.org/z/string"
` `
got, err := g.Content() got, err := g.content()
if err != nil { if err != nil {
t.Fatalf("g.Content() = %v", err) t.Fatalf("g.content() = %v", err)
} }
if want != string(got) { if want != string(got) {
t.Fatalf(`want: t.Fatalf(`want:
@ -331,9 +331,9 @@ import bar "prefix/golang.org/x/bar"
var _ = bar.X var _ = bar.X
` `
got, err := g.Content() got, err := g.content()
if err != nil { if err != nil {
t.Fatalf("g.Content() = %v", err) t.Fatalf("g.content() = %v", err)
} }
if want != string(got) { if want != string(got) {
t.Fatalf(`want: t.Fatalf(`want: