Просмотр исходного кода

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>
Damien Neil 7 лет назад
Родитель
Сommit
162c12703c

+ 8 - 4
cmd/protoc-gen-go-grpc/internal_gengogrpc/grpc.go

@@ -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
 		}

+ 2 - 2
cmd/protoc-gen-go-grpc/internal_gengogrpc/options.go

@@ -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
 	}

+ 68 - 0
cmd/protoc-gen-go/golden_test.go

@@ -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))
+	}
+}

+ 16 - 11
cmd/protoc-gen-go/internal_gengo/main.go

@@ -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
 		}

+ 8 - 3
cmd/protoc-gen-go/internal_gengo/oneof.go

@@ -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)

+ 4 - 4
cmd/protoc-gen-go/internal_gengo/options.go

@@ -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
 	}

+ 121 - 0
cmd/protoc-gen-go/testdata/annotations/annotations.pb.go

@@ -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,
+}

+ 17 - 0
cmd/protoc-gen-go/testdata/annotations/annotations.proto

@@ -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;
+}

+ 4 - 2
internal/protogen/goldentest/goldentest.go

@@ -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

+ 159 - 49
protogen/protogen.go

@@ -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 {
+// 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, path...)
+	n = append(n, loc.Path...)
 	n = append(n, a...)
-	return n
+	return Location{
+		SourceFile: loc.SourceFile,
+		Path:       n,
+	}
 }

+ 4 - 4
protogen/protogen_test.go

@@ -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: