reflect/protoreflect: improve source information usability

Added API:
  SourceLocations.ByPath
  SourceLocations.ByDescriptor
  SourceLocation.Next
  SourcePath.String
  SourcePath.Equal

We modify compiler/protogen to use SourceLocations.ByDescriptor.
In retrospect, if this had existed during the development of protogen,
we would not have exposed protogen.Location and related fields.

Change-Id: I58f17e59f90b9ba16f0982c4b71c2542e4ff6e75
Reviewed-on: https://go-review.googlesource.com/c/protobuf/+/238000
Reviewed-by: Damien Neil <dneil@google.com>
diff --git a/cmd/protoc-gen-go/internal_gengo/main.go b/cmd/protoc-gen-go/internal_gengo/main.go
index ea5d2e6..5f55976 100644
--- a/cmd/protoc-gen-go/internal_gengo/main.go
+++ b/cmd/protoc-gen-go/internal_gengo/main.go
@@ -115,17 +115,14 @@
 // genStandaloneComments prints all leading comments for a FileDescriptorProto
 // location identified by the field number n.
 func genStandaloneComments(g *protogen.GeneratedFile, f *fileInfo, n int32) {
-	for _, loc := range f.Proto.GetSourceCodeInfo().GetLocation() {
-		if len(loc.Path) == 1 && loc.Path[0] == n {
-			for _, s := range loc.GetLeadingDetachedComments() {
-				g.P(protogen.Comments(s))
-				g.P()
-			}
-			if s := loc.GetLeadingComments(); s != "" {
-				g.P(protogen.Comments(s))
-				g.P()
-			}
-		}
+	loc := f.Desc.SourceLocations().ByPath(protoreflect.SourcePath{n})
+	for _, s := range loc.LeadingDetachedComments {
+		g.P(protogen.Comments(s))
+		g.P()
+	}
+	if s := loc.LeadingComments; s != "" {
+		g.P(protogen.Comments(s))
+		g.P()
 	}
 }
 
diff --git a/compiler/protogen/protogen.go b/compiler/protogen/protogen.go
index 3892d05..c380a03 100644
--- a/compiler/protogen/protogen.go
+++ b/compiler/protogen/protogen.go
@@ -13,7 +13,6 @@
 import (
 	"bufio"
 	"bytes"
-	"encoding/binary"
 	"fmt"
 	"go/ast"
 	"go/parser"
@@ -482,7 +481,7 @@
 	// of "dir/foo". Appending ".pb.go" produces an output file of "dir/foo.pb.go".
 	GeneratedFilenamePrefix string
 
-	comments map[pathKey]CommentSet
+	location Location
 }
 
 func newFile(gen *Plugin, p *descriptorpb.FileDescriptorProto, packageName GoPackageName, importPath GoImportPath) (*File, error) {
@@ -498,7 +497,7 @@
 		Proto:         p,
 		GoPackageName: packageName,
 		GoImportPath:  importPath,
-		comments:      make(map[pathKey]CommentSet),
+		location:      Location{SourceFile: desc.Path()},
 	}
 
 	// Determine the prefix for generated Go files.
@@ -526,19 +525,6 @@
 	}
 	f.GeneratedFilenamePrefix = prefix
 
-	for _, loc := range p.GetSourceCodeInfo().GetLocation() {
-		// Descriptors declarations are guaranteed to have unique comment sets.
-		// Other locations may not be unique, but we don't use them.
-		var leadingDetached []Comments
-		for _, s := range loc.GetLeadingDetachedComments() {
-			leadingDetached = append(leadingDetached, Comments(s))
-		}
-		f.comments[newPathKey(loc.Path)] = CommentSet{
-			LeadingDetached: leadingDetached,
-			Leading:         Comments(loc.GetLeadingComments()),
-			Trailing:        Comments(loc.GetTrailingComments()),
-		}
-	}
 	for i, eds := 0, desc.Enums(); i < eds.Len(); i++ {
 		f.Enums = append(f.Enums, newEnum(gen, f, nil, eds.Get(i)))
 	}
@@ -571,13 +557,6 @@
 	return f, nil
 }
 
-func (f *File) location(idxPath ...int32) Location {
-	return Location{
-		SourceFile: f.Desc.Path(),
-		Path:       idxPath,
-	}
-}
-
 // 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, "").
@@ -625,15 +604,15 @@
 func newEnum(gen *Plugin, f *File, parent *Message, desc protoreflect.EnumDescriptor) *Enum {
 	var loc Location
 	if parent != nil {
-		loc = parent.Location.appendPath(int32(genid.DescriptorProto_EnumType_field_number), int32(desc.Index()))
+		loc = parent.Location.appendPath(genid.DescriptorProto_EnumType_field_number, desc.Index())
 	} else {
-		loc = f.location(int32(genid.FileDescriptorProto_EnumType_field_number), int32(desc.Index()))
+		loc = f.location.appendPath(genid.FileDescriptorProto_EnumType_field_number, desc.Index())
 	}
 	enum := &Enum{
 		Desc:     desc,
 		GoIdent:  newGoIdent(f, desc),
 		Location: loc,
-		Comments: f.comments[newPathKey(loc.Path)],
+		Comments: makeCommentSet(f.Desc.SourceLocations().ByDescriptor(desc)),
 	}
 	gen.enumsByName[desc.FullName()] = enum
 	for i, vds := 0, enum.Desc.Values(); i < vds.Len(); i++ {
@@ -664,13 +643,13 @@
 		parentIdent = message.GoIdent
 	}
 	name := parentIdent.GoName + "_" + string(desc.Name())
-	loc := enum.Location.appendPath(int32(genid.EnumDescriptorProto_Value_field_number), int32(desc.Index()))
+	loc := enum.Location.appendPath(genid.EnumDescriptorProto_Value_field_number, desc.Index())
 	return &EnumValue{
 		Desc:     desc,
 		GoIdent:  f.GoImportPath.Ident(name),
 		Parent:   enum,
 		Location: loc,
-		Comments: f.comments[newPathKey(loc.Path)],
+		Comments: makeCommentSet(f.Desc.SourceLocations().ByDescriptor(desc)),
 	}
 }
 
@@ -694,15 +673,15 @@
 func newMessage(gen *Plugin, f *File, parent *Message, desc protoreflect.MessageDescriptor) *Message {
 	var loc Location
 	if parent != nil {
-		loc = parent.Location.appendPath(int32(genid.DescriptorProto_NestedType_field_number), int32(desc.Index()))
+		loc = parent.Location.appendPath(genid.DescriptorProto_NestedType_field_number, desc.Index())
 	} else {
-		loc = f.location(int32(genid.FileDescriptorProto_MessageType_field_number), int32(desc.Index()))
+		loc = f.location.appendPath(genid.FileDescriptorProto_MessageType_field_number, desc.Index())
 	}
 	message := &Message{
 		Desc:     desc,
 		GoIdent:  newGoIdent(f, desc),
 		Location: loc,
-		Comments: f.comments[newPathKey(loc.Path)],
+		Comments: makeCommentSet(f.Desc.SourceLocations().ByDescriptor(desc)),
 	}
 	gen.messagesByName[desc.FullName()] = message
 	for i, eds := 0, desc.Enums(); i < eds.Len(); i++ {
@@ -852,11 +831,11 @@
 	var loc Location
 	switch {
 	case desc.IsExtension() && message == nil:
-		loc = f.location(int32(genid.FileDescriptorProto_Extension_field_number), int32(desc.Index()))
+		loc = f.location.appendPath(genid.FileDescriptorProto_Extension_field_number, desc.Index())
 	case desc.IsExtension() && message != nil:
-		loc = message.Location.appendPath(int32(genid.DescriptorProto_Extension_field_number), int32(desc.Index()))
+		loc = message.Location.appendPath(genid.DescriptorProto_Extension_field_number, desc.Index())
 	default:
-		loc = message.Location.appendPath(int32(genid.DescriptorProto_Field_field_number), int32(desc.Index()))
+		loc = message.Location.appendPath(genid.DescriptorProto_Field_field_number, desc.Index())
 	}
 	camelCased := strs.GoCamelCase(string(desc.Name()))
 	var parentPrefix string
@@ -872,7 +851,7 @@
 		},
 		Parent:   message,
 		Location: loc,
-		Comments: f.comments[newPathKey(loc.Path)],
+		Comments: makeCommentSet(f.Desc.SourceLocations().ByDescriptor(desc)),
 	}
 	return field
 }
@@ -927,7 +906,7 @@
 }
 
 func newOneof(gen *Plugin, f *File, message *Message, desc protoreflect.OneofDescriptor) *Oneof {
-	loc := message.Location.appendPath(int32(genid.DescriptorProto_OneofDecl_field_number), int32(desc.Index()))
+	loc := message.Location.appendPath(genid.DescriptorProto_OneofDecl_field_number, desc.Index())
 	camelCased := strs.GoCamelCase(string(desc.Name()))
 	parentPrefix := message.GoIdent.GoName + "_"
 	return &Oneof{
@@ -939,7 +918,7 @@
 			GoName:       parentPrefix + camelCased,
 		},
 		Location: loc,
-		Comments: f.comments[newPathKey(loc.Path)],
+		Comments: makeCommentSet(f.Desc.SourceLocations().ByDescriptor(desc)),
 	}
 }
 
@@ -959,12 +938,12 @@
 }
 
 func newService(gen *Plugin, f *File, desc protoreflect.ServiceDescriptor) *Service {
-	loc := f.location(int32(genid.FileDescriptorProto_Service_field_number), int32(desc.Index()))
+	loc := f.location.appendPath(genid.FileDescriptorProto_Service_field_number, desc.Index())
 	service := &Service{
 		Desc:     desc,
 		GoName:   strs.GoCamelCase(string(desc.Name())),
 		Location: loc,
-		Comments: f.comments[newPathKey(loc.Path)],
+		Comments: makeCommentSet(f.Desc.SourceLocations().ByDescriptor(desc)),
 	}
 	for i, mds := 0, desc.Methods(); i < mds.Len(); i++ {
 		service.Methods = append(service.Methods, newMethod(gen, f, service, mds.Get(i)))
@@ -988,13 +967,13 @@
 }
 
 func newMethod(gen *Plugin, f *File, service *Service, desc protoreflect.MethodDescriptor) *Method {
-	loc := service.Location.appendPath(int32(genid.ServiceDescriptorProto_Method_field_number), int32(desc.Index()))
+	loc := service.Location.appendPath(genid.ServiceDescriptorProto_Method_field_number, desc.Index())
 	method := &Method{
 		Desc:     desc,
 		GoName:   strs.GoCamelCase(string(desc.Name())),
 		Parent:   service,
 		Location: loc,
-		Comments: f.comments[newPathKey(loc.Path)],
+		Comments: makeCommentSet(f.Desc.SourceLocations().ByDescriptor(desc)),
 	}
 	return method
 }
@@ -1359,28 +1338,10 @@
 }
 
 // appendPath add elements to a Location's path, returning a new Location.
-func (loc Location) appendPath(a ...int32) Location {
-	var n protoreflect.SourcePath
-	n = append(n, loc.Path...)
-	n = append(n, a...)
-	return Location{
-		SourceFile: loc.SourceFile,
-		Path:       n,
-	}
-}
-
-// A pathKey is a representation of a location path suitable for use as a map key.
-type pathKey struct {
-	s string
-}
-
-// newPathKey converts a location path to a pathKey.
-func newPathKey(idxPath []int32) pathKey {
-	buf := make([]byte, 4*len(idxPath))
-	for i, x := range idxPath {
-		binary.LittleEndian.PutUint32(buf[i*4:], uint32(x))
-	}
-	return pathKey{string(buf)}
+func (loc Location) appendPath(num protoreflect.FieldNumber, idx int) Location {
+	loc.Path = append(protoreflect.SourcePath(nil), loc.Path...) // make copy
+	loc.Path = append(loc.Path, int32(num), int32(idx))
+	return loc
 }
 
 // CommentSet is a set of leading and trailing comments associated
@@ -1391,6 +1352,18 @@
 	Trailing        Comments
 }
 
+func makeCommentSet(loc protoreflect.SourceLocation) CommentSet {
+	var leadingDetached []Comments
+	for _, s := range loc.LeadingDetachedComments {
+		leadingDetached = append(leadingDetached, Comments(s))
+	}
+	return CommentSet{
+		LeadingDetached: leadingDetached,
+		Leading:         Comments(loc.LeadingComments),
+		Trailing:        Comments(loc.TrailingComments),
+	}
+}
+
 // Comments is a comments string as provided by protoc.
 type Comments string
 
diff --git a/internal/cmd/generate-protos/main.go b/internal/cmd/generate-protos/main.go
index 48dda63..f8778d7 100644
--- a/internal/cmd/generate-protos/main.go
+++ b/internal/cmd/generate-protos/main.go
@@ -93,6 +93,7 @@
 					gengo.GenerateVersionMarkers = false
 					gengo.GenerateFile(gen, file)
 					generateIdentifiers(gen, file)
+					generateSouceContextStringer(gen, file)
 				}
 			}
 			gen.SupportedFeatures = gengo.SupportedFeatures
@@ -361,6 +362,66 @@
 	processMessages(file.Messages)
 }
 
+// generateSouceContextStringer generates the implementation for the
+// protoreflect.SourcePath.String method by using information present
+// in the descriptor.proto.
+func generateSouceContextStringer(gen *protogen.Plugin, file *protogen.File) {
+	if file.Desc.Path() != "google/protobuf/descriptor.proto" {
+		return
+	}
+
+	importPath := modulePath + "/reflect/protoreflect"
+	g := gen.NewGeneratedFile(importPath+"/source_gen.go", protogen.GoImportPath(importPath))
+	for _, s := range generatedPreamble {
+		g.P(s)
+	}
+	g.P("package ", path.Base(importPath))
+	g.P()
+
+	var messages []*protogen.Message
+	for _, message := range file.Messages {
+		if message.Desc.Name() == "FileDescriptorProto" {
+			messages = append(messages, message)
+		}
+	}
+	seen := make(map[*protogen.Message]bool)
+
+	for len(messages) > 0 {
+		m := messages[0]
+		messages = messages[1:]
+		if seen[m] {
+			continue
+		}
+		seen[m] = true
+
+		g.P("func (p *SourcePath) append", m.GoIdent.GoName, "(b []byte) []byte {")
+		g.P("if len(*p) == 0 { return b }")
+		g.P("switch (*p)[0] {")
+		for _, f := range m.Fields {
+			g.P("case ", f.Desc.Number(), ":")
+			var cardinality string
+			switch {
+			case f.Desc.IsMap():
+				panic("maps are not supported")
+			case f.Desc.IsList():
+				cardinality = "Repeated"
+			default:
+				cardinality = "Singular"
+			}
+			nextAppender := "nil"
+			if f.Message != nil {
+				nextAppender = "(*SourcePath).append" + f.Message.GoIdent.GoName
+				messages = append(messages, f.Message)
+			}
+			g.P("b = p.append", cardinality, "Field(b, ", strconv.Quote(string(f.Desc.Name())), ", ", nextAppender, ")")
+		}
+		g.P("}")
+		g.P("return b")
+		g.P("}")
+		g.P()
+	}
+}
+
 func syncOutput(dstDir, srcDir string) {
 	filepath.Walk(srcDir, func(srcPath string, _ os.FileInfo, _ error) error {
 		if !strings.HasSuffix(srcPath, ".go") && !strings.HasSuffix(srcPath, ".meta") {
diff --git a/internal/filedesc/build.go b/internal/filedesc/build.go
index d02d770..b293b69 100644
--- a/internal/filedesc/build.go
+++ b/internal/filedesc/build.go
@@ -3,6 +3,9 @@
 // license that can be found in the LICENSE file.
 
 // Package filedesc provides functionality for constructing descriptors.
+//
+// The types in this package implement interfaces in the protoreflect package
+// related to protobuf descripriptors.
 package filedesc
 
 import (
diff --git a/internal/filedesc/desc_list.go b/internal/filedesc/desc_list.go
index 4187d66..aa294ff 100644
--- a/internal/filedesc/desc_list.go
+++ b/internal/filedesc/desc_list.go
@@ -6,9 +6,12 @@
 
 import (
 	"fmt"
+	"math"
 	"sort"
 	"sync"
 
+	"google.golang.org/protobuf/internal/genid"
+
 	"google.golang.org/protobuf/encoding/protowire"
 	"google.golang.org/protobuf/internal/descfmt"
 	"google.golang.org/protobuf/internal/errors"
@@ -278,9 +281,170 @@
 }
 
 type SourceLocations struct {
+	// List is a list of SourceLocations.
+	// The SourceLocation.Next field does not need to be populated
+	// as it will be lazily populated upon first need.
 	List []pref.SourceLocation
+
+	// File is the parent file descriptor that these locations are relative to.
+	// If non-nil, ByDescriptor verifies that the provided descriptor
+	// is a child of this file descriptor.
+	File pref.FileDescriptor
+
+	once   sync.Once
+	byPath map[pathKey]int
 }
 
-func (p *SourceLocations) Len() int                            { return len(p.List) }
-func (p *SourceLocations) Get(i int) pref.SourceLocation       { return p.List[i] }
+func (p *SourceLocations) Len() int                      { return len(p.List) }
+func (p *SourceLocations) Get(i int) pref.SourceLocation { return p.lazyInit().List[i] }
+func (p *SourceLocations) byKey(k pathKey) pref.SourceLocation {
+	if i, ok := p.lazyInit().byPath[k]; ok {
+		return p.List[i]
+	}
+	return pref.SourceLocation{}
+}
+func (p *SourceLocations) ByPath(path pref.SourcePath) pref.SourceLocation {
+	return p.byKey(newPathKey(path))
+}
+func (p *SourceLocations) ByDescriptor(desc pref.Descriptor) pref.SourceLocation {
+	if p.File != nil && desc != nil && p.File != desc.ParentFile() {
+		return pref.SourceLocation{} // mismatching parent files
+	}
+	var pathArr [16]int32
+	path := pathArr[:0]
+	for {
+		switch desc.(type) {
+		case pref.FileDescriptor:
+			// Reverse the path since it was constructed in reverse.
+			for i, j := 0, len(path)-1; i < j; i, j = i+1, j-1 {
+				path[i], path[j] = path[j], path[i]
+			}
+			return p.byKey(newPathKey(path))
+		case pref.MessageDescriptor:
+			path = append(path, int32(desc.Index()))
+			desc = desc.Parent()
+			switch desc.(type) {
+			case pref.FileDescriptor:
+				path = append(path, int32(genid.FileDescriptorProto_MessageType_field_number))
+			case pref.MessageDescriptor:
+				path = append(path, int32(genid.DescriptorProto_NestedType_field_number))
+			default:
+				return pref.SourceLocation{}
+			}
+		case pref.FieldDescriptor:
+			isExtension := desc.(pref.FieldDescriptor).IsExtension()
+			path = append(path, int32(desc.Index()))
+			desc = desc.Parent()
+			if isExtension {
+				switch desc.(type) {
+				case pref.FileDescriptor:
+					path = append(path, int32(genid.FileDescriptorProto_Extension_field_number))
+				case pref.MessageDescriptor:
+					path = append(path, int32(genid.DescriptorProto_Extension_field_number))
+				default:
+					return pref.SourceLocation{}
+				}
+			} else {
+				switch desc.(type) {
+				case pref.MessageDescriptor:
+					path = append(path, int32(genid.DescriptorProto_Field_field_number))
+				default:
+					return pref.SourceLocation{}
+				}
+			}
+		case pref.OneofDescriptor:
+			path = append(path, int32(desc.Index()))
+			desc = desc.Parent()
+			switch desc.(type) {
+			case pref.MessageDescriptor:
+				path = append(path, int32(genid.DescriptorProto_OneofDecl_field_number))
+			default:
+				return pref.SourceLocation{}
+			}
+		case pref.EnumDescriptor:
+			path = append(path, int32(desc.Index()))
+			desc = desc.Parent()
+			switch desc.(type) {
+			case pref.FileDescriptor:
+				path = append(path, int32(genid.FileDescriptorProto_EnumType_field_number))
+			case pref.MessageDescriptor:
+				path = append(path, int32(genid.DescriptorProto_EnumType_field_number))
+			default:
+				return pref.SourceLocation{}
+			}
+		case pref.EnumValueDescriptor:
+			path = append(path, int32(desc.Index()))
+			desc = desc.Parent()
+			switch desc.(type) {
+			case pref.EnumDescriptor:
+				path = append(path, int32(genid.EnumDescriptorProto_Value_field_number))
+			default:
+				return pref.SourceLocation{}
+			}
+		case pref.ServiceDescriptor:
+			path = append(path, int32(desc.Index()))
+			desc = desc.Parent()
+			switch desc.(type) {
+			case pref.FileDescriptor:
+				path = append(path, int32(genid.FileDescriptorProto_Service_field_number))
+			default:
+				return pref.SourceLocation{}
+			}
+		case pref.MethodDescriptor:
+			path = append(path, int32(desc.Index()))
+			desc = desc.Parent()
+			switch desc.(type) {
+			case pref.ServiceDescriptor:
+				path = append(path, int32(genid.ServiceDescriptorProto_Method_field_number))
+			default:
+				return pref.SourceLocation{}
+			}
+		default:
+			return pref.SourceLocation{}
+		}
+	}
+}
+func (p *SourceLocations) lazyInit() *SourceLocations {
+	p.once.Do(func() {
+		if len(p.List) > 0 {
+			// Collect all the indexes for a given path.
+			pathIdxs := make(map[pathKey][]int, len(p.List))
+			for i, l := range p.List {
+				k := newPathKey(l.Path)
+				pathIdxs[k] = append(pathIdxs[k], i)
+			}
+
+			// Update the next index for all locations.
+			p.byPath = make(map[pathKey]int, len(p.List))
+			for k, idxs := range pathIdxs {
+				for i := 0; i < len(idxs)-1; i++ {
+					p.List[idxs[i]].Next = idxs[i+1]
+				}
+				p.List[idxs[len(idxs)-1]].Next = 0
+				p.byPath[k] = idxs[0] // record the first location for this path
+			}
+		}
+	})
+	return p
+}
 func (p *SourceLocations) ProtoInternal(pragma.DoNotImplement) {}
+
+// pathKey is a comparable representation of protoreflect.SourcePath.
+type pathKey struct {
+	arr [16]uint8 // first n-1 path segments; last element is the length
+	str string    // used if the path does not fit in arr
+}
+
+func newPathKey(p pref.SourcePath) (k pathKey) {
+	if len(p) < len(k.arr) {
+		for i, ps := range p {
+			if ps < 0 || math.MaxUint8 <= ps {
+				return pathKey{str: p.String()}
+			}
+			k.arr[i] = uint8(ps)
+		}
+		k.arr[len(k.arr)-1] = uint8(len(p))
+		return k
+	}
+	return pathKey{str: p.String()}
+}
diff --git a/reflect/protodesc/desc.go b/reflect/protodesc/desc.go
index 37f254d..e4dfb12 100644
--- a/reflect/protodesc/desc.go
+++ b/reflect/protodesc/desc.go
@@ -144,6 +144,7 @@
 	}
 
 	// Handle source locations.
+	f.L2.Locations.File = f
 	for _, loc := range fd.GetSourceCodeInfo().GetLocation() {
 		var l protoreflect.SourceLocation
 		// TODO: Validate that the path points to an actual declaration?
diff --git a/reflect/protodesc/file_test.go b/reflect/protodesc/file_test.go
index 9b7e8dd..50a0ed9 100644
--- a/reflect/protodesc/file_test.go
+++ b/reflect/protodesc/file_test.go
@@ -994,3 +994,241 @@
 		t.Fatal("NewFiles with import cycle: success, want error")
 	}
 }
+
+func TestSourceLocations(t *testing.T) {
+	fd := mustParseFile(`
+		name: "comments.proto"
+		message_type: [{
+			name: "Message1"
+			field: [
+				{name:"field1" number:1 label:LABEL_OPTIONAL type:TYPE_STRING},
+				{name:"field2" number:2 label:LABEL_OPTIONAL type:TYPE_STRING},
+				{name:"field3" number:3 label:LABEL_OPTIONAL type:TYPE_STRING oneof_index:0},
+				{name:"field4" number:4 label:LABEL_OPTIONAL type:TYPE_STRING oneof_index:0},
+				{name:"field5" number:5 label:LABEL_OPTIONAL type:TYPE_STRING oneof_index:1},
+				{name:"field6" number:6 label:LABEL_OPTIONAL type:TYPE_STRING oneof_index:1}
+			]
+			extension: [
+				{name:"extension1" number:100 label:LABEL_OPTIONAL type:TYPE_STRING extendee:".Message1"},
+				{name:"extension2" number:101 label:LABEL_OPTIONAL type:TYPE_STRING extendee:".Message1"}
+			]
+			nested_type: [{name:"Message1"}, {name:"Message2"}]
+			extension_range: {start:100 end:536870912}
+			oneof_decl: [{name:"oneof1"}, {name:"oneof2"}]
+		}, {
+			name: "Message2"
+			enum_type: {
+				name: "Enum1"
+				value: [
+					{name: "FOO", number: 0},
+					{name: "BAR", number: 1}
+				]
+			}
+		}]
+		enum_type: {
+			name: "Enum1"
+			value: [
+				{name: "FOO", number: 0},
+				{name: "BAR", number: 1}
+			]
+		}
+		service: {
+			name: "Service1"
+			method: [
+				{name:"Method1" input_type:".Message1" output_type:".Message1"},
+				{name:"Method2" input_type:".Message2" output_type:".Message2"}
+			]
+		}
+		extension: [
+			{name:"extension1" number:102 label:LABEL_OPTIONAL type:TYPE_STRING extendee:".Message1"},
+			{name:"extension2" number:103 label:LABEL_OPTIONAL type:TYPE_STRING extendee:".Message1"}
+		]
+		source_code_info: {
+			location: [
+				{span:[0,0,69,1]},
+				{path:[12] span:[0,0,18]},
+				{path:[5,0] span:[3,0,8,1] leading_comments:" Enum1\r\n"},
+				{path:[5,0,1] span:[3,5,10]},
+				{path:[5,0,2,0] span:[5,2,10] leading_comments:" FOO\r\n"},
+				{path:[5,0,2,0,1] span:[5,2,5]},
+				{path:[5,0,2,0,2] span:[5,8,9]},
+				{path:[5,0,2,1] span:[7,2,10] leading_comments:" BAR\r\n"},
+				{path:[5,0,2,1,1] span:[7,2,5]},
+				{path:[5,0,2,1,2] span:[7,8,9]},
+				{path:[4,0] span:[11,0,43,1] leading_comments:" Message1\r\n"},
+				{path:[4,0,1] span:[11,8,16]},
+				{path:[4,0,3,0] span:[13,2,21] leading_comments:" Message1.Message1\r\n"},
+				{path:[4,0,3,0,1] span:[13,10,18]},
+				{path:[4,0,3,1] span:[15,2,21] leading_comments:" Message1.Message2\r\n"},
+				{path:[4,0,3,1,1] span:[15,10,18]},
+				{path:[4,0,2,0] span:[18,2,29] leading_comments:" Message1.field1\r\n"},
+				{path:[4,0,2,0,4] span:[18,2,10]},
+				{path:[4,0,2,0,5] span:[18,11,17]},
+				{path:[4,0,2,0,1] span:[18,18,24]},
+				{path:[4,0,2,0,3] span:[18,27,28]},
+				{path:[4,0,2,1] span:[20,2,29] leading_comments:" Message1.field2\r\n"},
+				{path:[4,0,2,1,4] span:[20,2,10]},
+				{path:[4,0,2,1,5] span:[20,11,17]},
+				{path:[4,0,2,1,1] span:[20,18,24]},
+				{path:[4,0,2,1,3] span:[20,27,28]},
+				{path:[4,0,8,0] span:[22,2,27,3] leading_comments:" Message1.oneof1\r\n"},
+				{path:[4,0,8,0,1] span:[22,8,14]},
+				{path:[4,0,2,2] span:[24,4,22] leading_comments:" Message1.field3\r\n"},
+				{path:[4,0,2,2,5] span:[24,4,10]},
+				{path:[4,0,2,2,1] span:[24,11,17]},
+				{path:[4,0,2,2,3] span:[24,20,21]},
+				{path:[4,0,2,3] span:[26,4,22] leading_comments:" Message1.field4\r\n"},
+				{path:[4,0,2,3,5] span:[26,4,10]},
+				{path:[4,0,2,3,1] span:[26,11,17]},
+				{path:[4,0,2,3,3] span:[26,20,21]},
+				{path:[4,0,8,1] span:[29,2,34,3] leading_comments:" Message1.oneof2\r\n"},
+				{path:[4,0,8,1,1] span:[29,8,14]},
+				{path:[4,0,2,4] span:[31,4,22] leading_comments:" Message1.field5\r\n"},
+				{path:[4,0,2,4,5] span:[31,4,10]},
+				{path:[4,0,2,4,1] span:[31,11,17]},
+				{path:[4,0,2,4,3] span:[31,20,21]},
+				{path:[4,0,2,5] span:[33,4,22] leading_comments:" Message1.field6\r\n"},
+				{path:[4,0,2,5,5] span:[33,4,10]},
+				{path:[4,0,2,5,1] span:[33,11,17]},
+				{path:[4,0,2,5,3] span:[33,20,21]},
+				{path:[4,0,5] span:[36,2,24]},
+				{path:[4,0,5,0] span:[36,13,23]},
+				{path:[4,0,5,0,1] span:[36,13,16]},
+				{path:[4,0,5,0,2] span:[36,20,23]},
+				{path:[4,0,6] span:[37,2,42,3]},
+				{path:[4,0,6,0] span:[39,4,37] leading_comments:" Message1.extension1\r\n"},
+				{path:[4,0,6,0,2] span:[37,9,18]},
+				{path:[4,0,6,0,4] span:[39,4,12]},
+				{path:[4,0,6,0,5] span:[39,13,19]},
+				{path:[4,0,6,0,1] span:[39,20,30]},
+				{path:[4,0,6,0,3] span:[39,33,36]},
+				{path:[4,0,6,1] span:[41,4,37] leading_comments:" Message1.extension2\r\n"},
+				{path:[4,0,6,1,2] span:[37,9,18]},
+				{path:[4,0,6,1,4] span:[41,4,12]},
+				{path:[4,0,6,1,5] span:[41,13,19]},
+				{path:[4,0,6,1,1] span:[41,20,30]},
+				{path:[4,0,6,1,3] span:[41,33,36]},
+				{path:[7] span:[45,0,50,1]},
+				{path:[7,0] span:[47,2,35] leading_comments:" extension1\r\n"},
+				{path:[7,0,2] span:[45,7,15]},
+				{path:[7,0,4] span:[47,2,10]},
+				{path:[7,0,5] span:[47,11,17]},
+				{path:[7,0,1] span:[47,18,28]},
+				{path:[7,0,3] span:[47,31,34]},
+				{path:[7,1] span:[49,2,35] leading_comments:" extension2\r\n"},
+				{path:[7,1,2] span:[45,7,15]},
+				{path:[7,1,4] span:[49,2,10]},
+				{path:[7,1,5] span:[49,11,17]},
+				{path:[7,1,1] span:[49,18,28]},
+				{path:[7,1,3] span:[49,31,34]},
+				{path:[4,1] span:[53,0,61,1] leading_comments:" Message2\r\n"},
+				{path:[4,1,1] span:[53,8,16]},
+				{path:[4,1,4,0] span:[55,2,60,3] leading_comments:" Message2.Enum1\r\n"},
+				{path:[4,1,4,0,1] span:[55,7,12]},
+				{path:[4,1,4,0,2,0] span:[57,4,12] leading_comments:" Message2.FOO\r\n"},
+				{path:[4,1,4,0,2,0,1] span:[57,4,7]},
+				{path:[4,1,4,0,2,0,2] span:[57,10,11]},
+				{path:[4,1,4,0,2,1] span:[59,4,12] leading_comments:" Message2.BAR\r\n"},
+				{path:[4,1,4,0,2,1,1] span:[59,4,7]},
+				{path:[4,1,4,0,2,1,2] span:[59,10,11]},
+				{path:[6,0] span:[64,0,69,1] leading_comments:" Service1\r\n"},
+				{path:[6,0,1] span:[64,8,16]},
+				{path:[6,0,2,0] span:[66,2,43] leading_comments:" Service1.Method1\r\n"},
+				{path:[6,0,2,0,1] span:[66,6,13]},
+				{path:[6,0,2,0,2] span:[66,14,22]},
+				{path:[6,0,2,0,3] span:[66,33,41]},
+				{path:[6,0,2,1] span:[68,2,43] leading_comments:" Service1.Method2\r\n"},
+				{path:[6,0,2,1,1] span:[68,6,13]},
+				{path:[6,0,2,1,2] span:[68,14,22]},
+				{path:[6,0,2,1,3] span:[68,33,41]}
+			]
+		}
+	`)
+	fileDesc, err := NewFile(fd, nil)
+	if err != nil {
+		t.Fatalf("NewFile error: %v", err)
+	}
+
+	var walkDescs func(protoreflect.Descriptor, func(protoreflect.Descriptor))
+	walkDescs = func(d protoreflect.Descriptor, f func(protoreflect.Descriptor)) {
+		f(d)
+		if d, ok := d.(interface {
+			Enums() protoreflect.EnumDescriptors
+		}); ok {
+			eds := d.Enums()
+			for i := 0; i < eds.Len(); i++ {
+				walkDescs(eds.Get(i), f)
+			}
+		}
+		if d, ok := d.(interface {
+			Values() protoreflect.EnumValueDescriptors
+		}); ok {
+			vds := d.Values()
+			for i := 0; i < vds.Len(); i++ {
+				walkDescs(vds.Get(i), f)
+			}
+		}
+		if d, ok := d.(interface {
+			Messages() protoreflect.MessageDescriptors
+		}); ok {
+			mds := d.Messages()
+			for i := 0; i < mds.Len(); i++ {
+				walkDescs(mds.Get(i), f)
+			}
+		}
+		if d, ok := d.(interface {
+			Fields() protoreflect.FieldDescriptors
+		}); ok {
+			fds := d.Fields()
+			for i := 0; i < fds.Len(); i++ {
+				walkDescs(fds.Get(i), f)
+			}
+		}
+		if d, ok := d.(interface {
+			Oneofs() protoreflect.OneofDescriptors
+		}); ok {
+			ods := d.Oneofs()
+			for i := 0; i < ods.Len(); i++ {
+				walkDescs(ods.Get(i), f)
+			}
+		}
+		if d, ok := d.(interface {
+			Extensions() protoreflect.ExtensionDescriptors
+		}); ok {
+			xds := d.Extensions()
+			for i := 0; i < xds.Len(); i++ {
+				walkDescs(xds.Get(i), f)
+			}
+		}
+		if d, ok := d.(interface {
+			Services() protoreflect.ServiceDescriptors
+		}); ok {
+			sds := d.Services()
+			for i := 0; i < sds.Len(); i++ {
+				walkDescs(sds.Get(i), f)
+			}
+		}
+		if d, ok := d.(interface {
+			Methods() protoreflect.MethodDescriptors
+		}); ok {
+			mds := d.Methods()
+			for i := 0; i < mds.Len(); i++ {
+				walkDescs(mds.Get(i), f)
+			}
+		}
+	}
+
+	var numDescs int
+	walkDescs(fileDesc, func(d protoreflect.Descriptor) {
+		// The comment for every descriptor should be the full name itself.
+		got := strings.TrimSpace(fileDesc.SourceLocations().ByDescriptor(d).LeadingComments)
+		want := string(d.FullName())
+		if got != want {
+			t.Errorf("comment mismatch: got %v, want %v", got, want)
+		}
+		numDescs++
+	})
+	if numDescs != 30 {
+		t.Errorf("visited %d descriptor, expected 30", numDescs)
+	}
+}
diff --git a/reflect/protoreflect/source.go b/reflect/protoreflect/source.go
index 32ea3d9..121ba3a 100644
--- a/reflect/protoreflect/source.go
+++ b/reflect/protoreflect/source.go
@@ -4,6 +4,10 @@
 
 package protoreflect
 
+import (
+	"strconv"
+)
+
 // SourceLocations is a list of source locations.
 type SourceLocations interface {
 	// Len reports the number of source locations in the proto file.
@@ -11,9 +15,20 @@
 	// Get returns the ith SourceLocation. It panics if out of bounds.
 	Get(int) SourceLocation
 
-	doNotImplement
+	// ByPath returns the SourceLocation for the given path,
+	// returning the first location if multiple exist for the same path.
+	// If multiple locations exist for the same path,
+	// then SourceLocation.Next index can be used to identify the
+	// index of the next SourceLocation.
+	// If no location exists for this path, it returns the zero value.
+	ByPath(path SourcePath) SourceLocation
 
-	// TODO: Add ByPath and ByDescriptor helper methods.
+	// ByDescriptor returns the SourceLocation for the given descriptor,
+	// returning the first location if multiple exist for the same path.
+	// If no location exists for this descriptor, it returns the zero value.
+	ByDescriptor(desc Descriptor) SourceLocation
+
+	doNotImplement
 }
 
 // SourceLocation describes a source location and
@@ -39,6 +54,10 @@
 	LeadingComments string
 	// TrailingComments is the trailing attached comment for the declaration.
 	TrailingComments string
+
+	// Next is an index into SourceLocations for the next source location that
+	// has the same Path. It is zero if there is no next location.
+	Next int
 }
 
 // SourcePath identifies part of a file descriptor for a source location.
@@ -48,5 +67,62 @@
 // See google.protobuf.SourceCodeInfo.Location.path.
 type SourcePath []int32
 
-// TODO: Add SourcePath.String method to pretty-print the path. For example:
-//	".message_type[6].nested_type[15].field[3]"
+// Equal reports whether p1 equals p2.
+func (p1 SourcePath) Equal(p2 SourcePath) bool {
+	if len(p1) != len(p2) {
+		return false
+	}
+	for i := range p1 {
+		if p1[i] != p2[i] {
+			return false
+		}
+	}
+	return true
+}
+
+// String formats the path in a humanly readable manner.
+// The output is guaranteed to be deterministic,
+// making it suitable for use as a key into a Go map.
+// It is not guaranteed to be stable as the exact output could change
+// in a future version of this module.
+//
+// Example output:
+//	.message_type[6].nested_type[15].field[3]
+func (p SourcePath) String() string {
+	b := p.appendFileDescriptorProto(nil)
+	for _, i := range p {
+		b = append(b, '.')
+		b = strconv.AppendInt(b, int64(i), 10)
+	}
+	return string(b)
+}
+
+type appendFunc func(*SourcePath, []byte) []byte
+
+func (p *SourcePath) appendSingularField(b []byte, name string, f appendFunc) []byte {
+	if len(*p) == 0 {
+		return b
+	}
+	b = append(b, '.')
+	b = append(b, name...)
+	*p = (*p)[1:]
+	if f != nil {
+		b = f(p, b)
+	}
+	return b
+}
+
+func (p *SourcePath) appendRepeatedField(b []byte, name string, f appendFunc) []byte {
+	b = p.appendSingularField(b, name, nil)
+	if len(*p) == 0 || (*p)[0] < 0 {
+		return b
+	}
+	b = append(b, '[')
+	b = strconv.AppendUint(b, uint64((*p)[0]), 10)
+	b = append(b, ']')
+	*p = (*p)[1:]
+	if f != nil {
+		b = f(p, b)
+	}
+	return b
+}
diff --git a/reflect/protoreflect/source_gen.go b/reflect/protoreflect/source_gen.go
new file mode 100644
index 0000000..b03c122
--- /dev/null
+++ b/reflect/protoreflect/source_gen.go
@@ -0,0 +1,461 @@
+// Copyright 2019 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.
+
+// Code generated by generate-protos. DO NOT EDIT.
+
+package protoreflect
+
+func (p *SourcePath) appendFileDescriptorProto(b []byte) []byte {
+	if len(*p) == 0 {
+		return b
+	}
+	switch (*p)[0] {
+	case 1:
+		b = p.appendSingularField(b, "name", nil)
+	case 2:
+		b = p.appendSingularField(b, "package", nil)
+	case 3:
+		b = p.appendRepeatedField(b, "dependency", nil)
+	case 10:
+		b = p.appendRepeatedField(b, "public_dependency", nil)
+	case 11:
+		b = p.appendRepeatedField(b, "weak_dependency", nil)
+	case 4:
+		b = p.appendRepeatedField(b, "message_type", (*SourcePath).appendDescriptorProto)
+	case 5:
+		b = p.appendRepeatedField(b, "enum_type", (*SourcePath).appendEnumDescriptorProto)
+	case 6:
+		b = p.appendRepeatedField(b, "service", (*SourcePath).appendServiceDescriptorProto)
+	case 7:
+		b = p.appendRepeatedField(b, "extension", (*SourcePath).appendFieldDescriptorProto)
+	case 8:
+		b = p.appendSingularField(b, "options", (*SourcePath).appendFileOptions)
+	case 9:
+		b = p.appendSingularField(b, "source_code_info", (*SourcePath).appendSourceCodeInfo)
+	case 12:
+		b = p.appendSingularField(b, "syntax", nil)
+	}
+	return b
+}
+
+func (p *SourcePath) appendDescriptorProto(b []byte) []byte {
+	if len(*p) == 0 {
+		return b
+	}
+	switch (*p)[0] {
+	case 1:
+		b = p.appendSingularField(b, "name", nil)
+	case 2:
+		b = p.appendRepeatedField(b, "field", (*SourcePath).appendFieldDescriptorProto)
+	case 6:
+		b = p.appendRepeatedField(b, "extension", (*SourcePath).appendFieldDescriptorProto)
+	case 3:
+		b = p.appendRepeatedField(b, "nested_type", (*SourcePath).appendDescriptorProto)
+	case 4:
+		b = p.appendRepeatedField(b, "enum_type", (*SourcePath).appendEnumDescriptorProto)
+	case 5:
+		b = p.appendRepeatedField(b, "extension_range", (*SourcePath).appendDescriptorProto_ExtensionRange)
+	case 8:
+		b = p.appendRepeatedField(b, "oneof_decl", (*SourcePath).appendOneofDescriptorProto)
+	case 7:
+		b = p.appendSingularField(b, "options", (*SourcePath).appendMessageOptions)
+	case 9:
+		b = p.appendRepeatedField(b, "reserved_range", (*SourcePath).appendDescriptorProto_ReservedRange)
+	case 10:
+		b = p.appendRepeatedField(b, "reserved_name", nil)
+	}
+	return b
+}
+
+func (p *SourcePath) appendEnumDescriptorProto(b []byte) []byte {
+	if len(*p) == 0 {
+		return b
+	}
+	switch (*p)[0] {
+	case 1:
+		b = p.appendSingularField(b, "name", nil)
+	case 2:
+		b = p.appendRepeatedField(b, "value", (*SourcePath).appendEnumValueDescriptorProto)
+	case 3:
+		b = p.appendSingularField(b, "options", (*SourcePath).appendEnumOptions)
+	case 4:
+		b = p.appendRepeatedField(b, "reserved_range", (*SourcePath).appendEnumDescriptorProto_EnumReservedRange)
+	case 5:
+		b = p.appendRepeatedField(b, "reserved_name", nil)
+	}
+	return b
+}
+
+func (p *SourcePath) appendServiceDescriptorProto(b []byte) []byte {
+	if len(*p) == 0 {
+		return b
+	}
+	switch (*p)[0] {
+	case 1:
+		b = p.appendSingularField(b, "name", nil)
+	case 2:
+		b = p.appendRepeatedField(b, "method", (*SourcePath).appendMethodDescriptorProto)
+	case 3:
+		b = p.appendSingularField(b, "options", (*SourcePath).appendServiceOptions)
+	}
+	return b
+}
+
+func (p *SourcePath) appendFieldDescriptorProto(b []byte) []byte {
+	if len(*p) == 0 {
+		return b
+	}
+	switch (*p)[0] {
+	case 1:
+		b = p.appendSingularField(b, "name", nil)
+	case 3:
+		b = p.appendSingularField(b, "number", nil)
+	case 4:
+		b = p.appendSingularField(b, "label", nil)
+	case 5:
+		b = p.appendSingularField(b, "type", nil)
+	case 6:
+		b = p.appendSingularField(b, "type_name", nil)
+	case 2:
+		b = p.appendSingularField(b, "extendee", nil)
+	case 7:
+		b = p.appendSingularField(b, "default_value", nil)
+	case 9:
+		b = p.appendSingularField(b, "oneof_index", nil)
+	case 10:
+		b = p.appendSingularField(b, "json_name", nil)
+	case 8:
+		b = p.appendSingularField(b, "options", (*SourcePath).appendFieldOptions)
+	case 17:
+		b = p.appendSingularField(b, "proto3_optional", nil)
+	}
+	return b
+}
+
+func (p *SourcePath) appendFileOptions(b []byte) []byte {
+	if len(*p) == 0 {
+		return b
+	}
+	switch (*p)[0] {
+	case 1:
+		b = p.appendSingularField(b, "java_package", nil)
+	case 8:
+		b = p.appendSingularField(b, "java_outer_classname", nil)
+	case 10:
+		b = p.appendSingularField(b, "java_multiple_files", nil)
+	case 20:
+		b = p.appendSingularField(b, "java_generate_equals_and_hash", nil)
+	case 27:
+		b = p.appendSingularField(b, "java_string_check_utf8", nil)
+	case 9:
+		b = p.appendSingularField(b, "optimize_for", nil)
+	case 11:
+		b = p.appendSingularField(b, "go_package", nil)
+	case 16:
+		b = p.appendSingularField(b, "cc_generic_services", nil)
+	case 17:
+		b = p.appendSingularField(b, "java_generic_services", nil)
+	case 18:
+		b = p.appendSingularField(b, "py_generic_services", nil)
+	case 42:
+		b = p.appendSingularField(b, "php_generic_services", nil)
+	case 23:
+		b = p.appendSingularField(b, "deprecated", nil)
+	case 31:
+		b = p.appendSingularField(b, "cc_enable_arenas", nil)
+	case 36:
+		b = p.appendSingularField(b, "objc_class_prefix", nil)
+	case 37:
+		b = p.appendSingularField(b, "csharp_namespace", nil)
+	case 39:
+		b = p.appendSingularField(b, "swift_prefix", nil)
+	case 40:
+		b = p.appendSingularField(b, "php_class_prefix", nil)
+	case 41:
+		b = p.appendSingularField(b, "php_namespace", nil)
+	case 44:
+		b = p.appendSingularField(b, "php_metadata_namespace", nil)
+	case 45:
+		b = p.appendSingularField(b, "ruby_package", nil)
+	case 999:
+		b = p.appendRepeatedField(b, "uninterpreted_option", (*SourcePath).appendUninterpretedOption)
+	}
+	return b
+}
+
+func (p *SourcePath) appendSourceCodeInfo(b []byte) []byte {
+	if len(*p) == 0 {
+		return b
+	}
+	switch (*p)[0] {
+	case 1:
+		b = p.appendRepeatedField(b, "location", (*SourcePath).appendSourceCodeInfo_Location)
+	}
+	return b
+}
+
+func (p *SourcePath) appendDescriptorProto_ExtensionRange(b []byte) []byte {
+	if len(*p) == 0 {
+		return b
+	}
+	switch (*p)[0] {
+	case 1:
+		b = p.appendSingularField(b, "start", nil)
+	case 2:
+		b = p.appendSingularField(b, "end", nil)
+	case 3:
+		b = p.appendSingularField(b, "options", (*SourcePath).appendExtensionRangeOptions)
+	}
+	return b
+}
+
+func (p *SourcePath) appendOneofDescriptorProto(b []byte) []byte {
+	if len(*p) == 0 {
+		return b
+	}
+	switch (*p)[0] {
+	case 1:
+		b = p.appendSingularField(b, "name", nil)
+	case 2:
+		b = p.appendSingularField(b, "options", (*SourcePath).appendOneofOptions)
+	}
+	return b
+}
+
+func (p *SourcePath) appendMessageOptions(b []byte) []byte {
+	if len(*p) == 0 {
+		return b
+	}
+	switch (*p)[0] {
+	case 1:
+		b = p.appendSingularField(b, "message_set_wire_format", nil)
+	case 2:
+		b = p.appendSingularField(b, "no_standard_descriptor_accessor", nil)
+	case 3:
+		b = p.appendSingularField(b, "deprecated", nil)
+	case 7:
+		b = p.appendSingularField(b, "map_entry", nil)
+	case 999:
+		b = p.appendRepeatedField(b, "uninterpreted_option", (*SourcePath).appendUninterpretedOption)
+	}
+	return b
+}
+
+func (p *SourcePath) appendDescriptorProto_ReservedRange(b []byte) []byte {
+	if len(*p) == 0 {
+		return b
+	}
+	switch (*p)[0] {
+	case 1:
+		b = p.appendSingularField(b, "start", nil)
+	case 2:
+		b = p.appendSingularField(b, "end", nil)
+	}
+	return b
+}
+
+func (p *SourcePath) appendEnumValueDescriptorProto(b []byte) []byte {
+	if len(*p) == 0 {
+		return b
+	}
+	switch (*p)[0] {
+	case 1:
+		b = p.appendSingularField(b, "name", nil)
+	case 2:
+		b = p.appendSingularField(b, "number", nil)
+	case 3:
+		b = p.appendSingularField(b, "options", (*SourcePath).appendEnumValueOptions)
+	}
+	return b
+}
+
+func (p *SourcePath) appendEnumOptions(b []byte) []byte {
+	if len(*p) == 0 {
+		return b
+	}
+	switch (*p)[0] {
+	case 2:
+		b = p.appendSingularField(b, "allow_alias", nil)
+	case 3:
+		b = p.appendSingularField(b, "deprecated", nil)
+	case 999:
+		b = p.appendRepeatedField(b, "uninterpreted_option", (*SourcePath).appendUninterpretedOption)
+	}
+	return b
+}
+
+func (p *SourcePath) appendEnumDescriptorProto_EnumReservedRange(b []byte) []byte {
+	if len(*p) == 0 {
+		return b
+	}
+	switch (*p)[0] {
+	case 1:
+		b = p.appendSingularField(b, "start", nil)
+	case 2:
+		b = p.appendSingularField(b, "end", nil)
+	}
+	return b
+}
+
+func (p *SourcePath) appendMethodDescriptorProto(b []byte) []byte {
+	if len(*p) == 0 {
+		return b
+	}
+	switch (*p)[0] {
+	case 1:
+		b = p.appendSingularField(b, "name", nil)
+	case 2:
+		b = p.appendSingularField(b, "input_type", nil)
+	case 3:
+		b = p.appendSingularField(b, "output_type", nil)
+	case 4:
+		b = p.appendSingularField(b, "options", (*SourcePath).appendMethodOptions)
+	case 5:
+		b = p.appendSingularField(b, "client_streaming", nil)
+	case 6:
+		b = p.appendSingularField(b, "server_streaming", nil)
+	}
+	return b
+}
+
+func (p *SourcePath) appendServiceOptions(b []byte) []byte {
+	if len(*p) == 0 {
+		return b
+	}
+	switch (*p)[0] {
+	case 33:
+		b = p.appendSingularField(b, "deprecated", nil)
+	case 999:
+		b = p.appendRepeatedField(b, "uninterpreted_option", (*SourcePath).appendUninterpretedOption)
+	}
+	return b
+}
+
+func (p *SourcePath) appendFieldOptions(b []byte) []byte {
+	if len(*p) == 0 {
+		return b
+	}
+	switch (*p)[0] {
+	case 1:
+		b = p.appendSingularField(b, "ctype", nil)
+	case 2:
+		b = p.appendSingularField(b, "packed", nil)
+	case 6:
+		b = p.appendSingularField(b, "jstype", nil)
+	case 5:
+		b = p.appendSingularField(b, "lazy", nil)
+	case 3:
+		b = p.appendSingularField(b, "deprecated", nil)
+	case 10:
+		b = p.appendSingularField(b, "weak", nil)
+	case 999:
+		b = p.appendRepeatedField(b, "uninterpreted_option", (*SourcePath).appendUninterpretedOption)
+	}
+	return b
+}
+
+func (p *SourcePath) appendUninterpretedOption(b []byte) []byte {
+	if len(*p) == 0 {
+		return b
+	}
+	switch (*p)[0] {
+	case 2:
+		b = p.appendRepeatedField(b, "name", (*SourcePath).appendUninterpretedOption_NamePart)
+	case 3:
+		b = p.appendSingularField(b, "identifier_value", nil)
+	case 4:
+		b = p.appendSingularField(b, "positive_int_value", nil)
+	case 5:
+		b = p.appendSingularField(b, "negative_int_value", nil)
+	case 6:
+		b = p.appendSingularField(b, "double_value", nil)
+	case 7:
+		b = p.appendSingularField(b, "string_value", nil)
+	case 8:
+		b = p.appendSingularField(b, "aggregate_value", nil)
+	}
+	return b
+}
+
+func (p *SourcePath) appendSourceCodeInfo_Location(b []byte) []byte {
+	if len(*p) == 0 {
+		return b
+	}
+	switch (*p)[0] {
+	case 1:
+		b = p.appendRepeatedField(b, "path", nil)
+	case 2:
+		b = p.appendRepeatedField(b, "span", nil)
+	case 3:
+		b = p.appendSingularField(b, "leading_comments", nil)
+	case 4:
+		b = p.appendSingularField(b, "trailing_comments", nil)
+	case 6:
+		b = p.appendRepeatedField(b, "leading_detached_comments", nil)
+	}
+	return b
+}
+
+func (p *SourcePath) appendExtensionRangeOptions(b []byte) []byte {
+	if len(*p) == 0 {
+		return b
+	}
+	switch (*p)[0] {
+	case 999:
+		b = p.appendRepeatedField(b, "uninterpreted_option", (*SourcePath).appendUninterpretedOption)
+	}
+	return b
+}
+
+func (p *SourcePath) appendOneofOptions(b []byte) []byte {
+	if len(*p) == 0 {
+		return b
+	}
+	switch (*p)[0] {
+	case 999:
+		b = p.appendRepeatedField(b, "uninterpreted_option", (*SourcePath).appendUninterpretedOption)
+	}
+	return b
+}
+
+func (p *SourcePath) appendEnumValueOptions(b []byte) []byte {
+	if len(*p) == 0 {
+		return b
+	}
+	switch (*p)[0] {
+	case 1:
+		b = p.appendSingularField(b, "deprecated", nil)
+	case 999:
+		b = p.appendRepeatedField(b, "uninterpreted_option", (*SourcePath).appendUninterpretedOption)
+	}
+	return b
+}
+
+func (p *SourcePath) appendMethodOptions(b []byte) []byte {
+	if len(*p) == 0 {
+		return b
+	}
+	switch (*p)[0] {
+	case 33:
+		b = p.appendSingularField(b, "deprecated", nil)
+	case 34:
+		b = p.appendSingularField(b, "idempotency_level", nil)
+	case 999:
+		b = p.appendRepeatedField(b, "uninterpreted_option", (*SourcePath).appendUninterpretedOption)
+	}
+	return b
+}
+
+func (p *SourcePath) appendUninterpretedOption_NamePart(b []byte) []byte {
+	if len(*p) == 0 {
+		return b
+	}
+	switch (*p)[0] {
+	case 1:
+		b = p.appendSingularField(b, "name_part", nil)
+	case 2:
+		b = p.appendSingularField(b, "is_extension", nil)
+	}
+	return b
+}
diff --git a/reflect/protoreflect/source_test.go b/reflect/protoreflect/source_test.go
new file mode 100644
index 0000000..877ede5
--- /dev/null
+++ b/reflect/protoreflect/source_test.go
@@ -0,0 +1,35 @@
+// Copyright 2020 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.
+
+package protoreflect
+
+import "testing"
+
+func TestSourcePathString(t *testing.T) {
+	tests := []struct {
+		in   SourcePath
+		want string
+	}{
+		{nil, ""},
+		{SourcePath{}, ""},
+		{SourcePath{0}, ".0"},
+		{SourcePath{1}, ".name"},
+		{SourcePath{1, 1}, ".name.1"},
+		{SourcePath{1, 1, -2, 3}, ".name.1.-2.3"},
+		{SourcePath{3}, ".dependency"},
+		{SourcePath{3, 0}, ".dependency[0]"},
+		{SourcePath{3, -1}, ".dependency.-1"},
+		{SourcePath{3, 1, 2}, ".dependency[1].2"},
+		{SourcePath{4}, ".message_type"},
+		{SourcePath{4, 0}, ".message_type[0]"},
+		{SourcePath{4, -1}, ".message_type.-1"},
+		{SourcePath{4, 1, 0}, ".message_type[1].0"},
+		{SourcePath{4, 1, 1}, ".message_type[1].name"},
+	}
+	for _, tt := range tests {
+		if got := tt.in.String(); got != tt.want {
+			t.Errorf("SourcePath(%d).String() = %v, want %v", tt.in, got, tt.want)
+		}
+	}
+}