reflect/protoregistry: initial commit

Package protoregistry provides data structures to register and lookup
protobuf descriptor types.

High-level API:
	var GlobalFiles = new(Files)
	var NotFound = errors.New("not found")
	type Files struct{ ... }
		func NewFiles(...pref.FileDescriptor) *Files
		func (*Files) Register(...pref.FileDescriptor) error
		func (*Files) FindDescriptorByName(pref.FullName) (pref.Descriptor, error)
		func (*Files) RangeFiles(func(pref.FileDescriptor) bool)
		func (*Files) RangeFilesByPackage(pref.FullName, func(pref.FileDescriptor) bool)
		func (*Files) RangeFilesByPath(string, func(pref.FileDescriptor) bool)

To support the FindDescriptorByName method, we add a DescriptorByName to
protoreflect.FileDescriptor and associated logic to prototype.

Change-Id: I14d65f74d2bd9f4f48641da9dfa70190310e5878
Reviewed-on: https://go-review.googlesource.com/129499
Reviewed-by: Damien Neil <dneil@google.com>
diff --git a/reflect/protoreflect/type.go b/reflect/protoreflect/type.go
index d46f402..427aea9 100644
--- a/reflect/protoreflect/type.go
+++ b/reflect/protoreflect/type.go
@@ -178,6 +178,9 @@
 	Extensions() ExtensionDescriptors
 	// Services is a list of the top-level service declarations.
 	Services() ServiceDescriptors
+	// DescriptorByName looks up any descriptor declared within this file
+	// by full name. It returns nil if not found.
+	DescriptorByName(FullName) Descriptor
 
 	isFileDescriptor
 }
diff --git a/reflect/protoregistry/registry.go b/reflect/protoregistry/registry.go
new file mode 100644
index 0000000..5e21043
--- /dev/null
+++ b/reflect/protoregistry/registry.go
@@ -0,0 +1,291 @@
+// 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.
+
+// Package protoregistry provides data structures to register and lookup
+// protobuf descriptor types.
+package protoregistry
+
+import (
+	"sort"
+	"strings"
+
+	"google.golang.org/proto/internal/errors"
+	"google.golang.org/proto/reflect/protoreflect"
+)
+
+// TODO: Perhaps Register should record the frame of where the function was
+// called and surface that in the error? That would help users debug duplicate
+// registration issues. This presumes that we provide a way to disable automatic
+// registration in generated code.
+
+// TODO: Add a type registry:
+/*
+var GlobalTypes = new(Types)
+
+type Type interface {
+	protoreflect.Descriptor
+	GoType() reflect.Type
+}
+type Types struct {
+	Parent   *Types
+	Resolver func(url string) (Type, error)
+}
+func NewTypes(typs ...Type) *Types
+func (*Types) Register(typs ...Type) error
+func (*Types) FindEnumByName(enum protoreflect.FullName) (protoreflect.EnumType, error)
+func (*Types) FindMessageByName(message protoreflect.FullName) (protoreflect.MessageType, error)
+func (*Types) FindMessageByURL(url string) (protoreflect.MessageType, error)
+func (*Types) FindExtensionByName(field protoreflect.FullName) (protoreflect.ExtensionType, error)
+func (*Types) FindExtensionByNumber(message protoreflect.FullName, field protoreflect.FieldNumber) (protoreflect.ExtensionType, error)
+func (*Types) RangeEnums(f func(protoreflect.EnumType) bool)
+func (*Types) RangeMessages(f func(protoreflect.MessageType) bool)
+func (*Types) RangeExtensions(f func(protoreflect.ExtensionType) bool)
+func (*Types) RangeExtensionsByMessage(message protoreflect.FullName, f func(protoreflect.ExtensionType) bool)
+*/
+
+// GlobalFiles is a global registry of file descriptors.
+var GlobalFiles = new(Files)
+
+// NotFound is a sentinel error value to indicate that the type was not found.
+var NotFound = errors.New("not found")
+
+// Files is a registry for looking up or iterating over files and the
+// descriptors contained within them.
+// The Find and Range methods are safe for concurrent use.
+type Files struct {
+	filesByPackage filesByPackage
+	filesByPath    filesByPath
+}
+
+type (
+	filesByPackage struct {
+		// files is a list of files all in the same package.
+		files []protoreflect.FileDescriptor
+		// subs is a tree of files all in a sub-package scope.
+		// It also maps all top-level identifiers declared in files
+		// as the notProtoPackage sentinel value.
+		subs map[protoreflect.Name]*filesByPackage // invariant: len(Name) > 0
+	}
+	filesByPath map[string][]protoreflect.FileDescriptor
+)
+
+// notProtoPackage is a sentinel value to indicate that some identifier maps
+// to an actual protobuf declaration and is not a sub-package.
+var notProtoPackage = new(filesByPackage)
+
+// NewFiles returns a registry initialized with the provided set of files.
+// If there are duplicates, the first one takes precedence.
+func NewFiles(files ...protoreflect.FileDescriptor) *Files {
+	// TODO: Should last take precedence? This allows a user to intentionally
+	// overwrite an existing registration.
+	//
+	// The use case is for implementing the existing v1 proto.RegisterFile
+	// function where the behavior is last wins. However, it could be argued
+	// that the v1 behavior is broken, and we can switch to first wins
+	// without violating compatibility.
+	r := new(Files)
+	r.Register(files...) // ignore errors; first takes precedence
+	return r
+}
+
+// Register registers the provided list of file descriptors.
+// Placeholder files are ignored.
+//
+// If any descriptor within a file conflicts with the descriptor of any
+// previously registered file (e.g., two enums with the same full name),
+// then that file is not registered and an error is returned.
+//
+// It is permitted for multiple files to have the same file path.
+func (r *Files) Register(files ...protoreflect.FileDescriptor) error {
+	var firstErr error
+fileLoop:
+	for _, file := range files {
+		if file.IsPlaceholder() {
+			continue // TODO: Should this be an error instead?
+		}
+
+		// Register the file into the filesByPackage tree.
+		//
+		// The prototype package validates that a FileDescriptor is internally
+		// consistent such it does not have conflicts within itself.
+		// However, we need to ensure that the inserted file does not conflict
+		// with other previously inserted files.
+		pkg := file.Package()
+		root := &r.filesByPackage
+		for len(pkg) > 0 {
+			var prefix protoreflect.Name
+			prefix, pkg = splitPrefix(pkg)
+
+			// Add a new sub-package segment.
+			switch nextRoot := root.subs[prefix]; nextRoot {
+			case nil:
+				nextRoot = new(filesByPackage)
+				if root.subs == nil {
+					root.subs = make(map[protoreflect.Name]*filesByPackage)
+				}
+				root.subs[prefix] = nextRoot
+				root = nextRoot
+			case notProtoPackage:
+				if firstErr == nil {
+					name := strings.TrimSuffix(strings.TrimSuffix(string(file.Package()), string(pkg)), ".")
+					firstErr = errors.New("file %q has a name conflict over %v", file.Path(), name)
+				}
+				continue fileLoop
+			default:
+				root = nextRoot
+			}
+		}
+		// Check for top-level conflicts within the same package.
+		// The current file cannot add any top-level declaration that conflict
+		// with another top-level declaration or sub-package name.
+		var conflicts []protoreflect.Name
+		rangeTopLevelDeclarations(file, func(s protoreflect.Name) {
+			if root.subs[s] == nil {
+				if root.subs == nil {
+					root.subs = make(map[protoreflect.Name]*filesByPackage)
+				}
+				root.subs[s] = notProtoPackage
+			} else {
+				conflicts = append(conflicts, s)
+			}
+		})
+		if len(conflicts) > 0 {
+			// Remove inserted identifiers to make registration failure atomic.
+			sort.Slice(conflicts, func(i, j int) bool { return conflicts[i] < conflicts[j] })
+			rangeTopLevelDeclarations(file, func(s protoreflect.Name) {
+				i := sort.Search(len(conflicts), func(i int) bool { return conflicts[i] >= s })
+				if has := i < len(conflicts) && conflicts[i] == s; !has {
+					delete(root.subs, s) // remove everything not in conflicts
+				}
+			})
+
+			if firstErr == nil {
+				name := file.Package().Append(conflicts[0])
+				firstErr = errors.New("file %q has a name conflict over %v", file.Path(), name)
+			}
+			continue fileLoop
+		}
+		root.files = append(root.files, file)
+
+		// Register the file into the filesByPath map.
+		//
+		// There is no check for conflicts in file path since the path is
+		// heavily dependent on how protoc is invoked. When protoc is being
+		// invoked by different parties in a distributed manner, it is
+		// unreasonable to assume nor ensure that the path is unique.
+		if r.filesByPath == nil {
+			r.filesByPath = make(filesByPath)
+		}
+		r.filesByPath[file.Path()] = append(r.filesByPath[file.Path()], file)
+	}
+	return firstErr
+}
+
+// FindDescriptorByName looks up any descriptor (except files) by its full name.
+// Files are not handled since multiple file descriptors may belong in
+// the same package and have the same full name (see RangeFilesByPackage).
+//
+// This return (nil, NotFound) if not found.
+func (r *Files) FindDescriptorByName(name protoreflect.FullName) (protoreflect.Descriptor, error) {
+	pkg := name
+	root := &r.filesByPackage
+	for len(pkg) > 0 {
+		var prefix protoreflect.Name
+		prefix, pkg = splitPrefix(pkg)
+		switch nextRoot := root.subs[prefix]; nextRoot {
+		case nil:
+			return nil, NotFound
+		case notProtoPackage:
+			// Search current root's package for the descriptor.
+			for _, fd := range root.files {
+				if d := fd.DescriptorByName(name); d != nil {
+					return d, nil
+				}
+			}
+			return nil, NotFound
+		default:
+			root = nextRoot
+		}
+	}
+	return nil, NotFound
+}
+
+// RangeFiles iterates over all registered files.
+// The iteration order is undefined.
+func (r *Files) RangeFiles(f func(protoreflect.FileDescriptor) bool) {
+	r.RangeFilesByPackage("", f) // empty package is a prefix for all packages
+}
+
+// RangeFilesByPackage iterates over all registered files filtered by
+// the given proto package prefix. It iterates over files with an exact package
+// match before iterating over files with general prefix match.
+// The iteration order is undefined within exact matches or prefix matches.
+func (r *Files) RangeFilesByPackage(pkg protoreflect.FullName, f func(protoreflect.FileDescriptor) bool) {
+	if strings.HasSuffix(string(pkg), ".") {
+		return // avoid edge case where splitPrefix allows trailing dot
+	}
+	root := &r.filesByPackage
+	for len(pkg) > 0 && root != nil {
+		var prefix protoreflect.Name
+		prefix, pkg = splitPrefix(pkg)
+		root = root.subs[prefix]
+	}
+	rangeFiles(root, f)
+}
+func rangeFiles(fs *filesByPackage, f func(protoreflect.FileDescriptor) bool) bool {
+	if fs == nil {
+		return true
+	}
+	// Iterate over exact matches.
+	for _, fd := range fs.files { // TODO: iterate non-deterministically
+		if !f(fd) {
+			return false
+		}
+	}
+	// Iterate over prefix matches.
+	for _, fs := range fs.subs {
+		if !rangeFiles(fs, f) {
+			return false
+		}
+	}
+	return true
+}
+
+// RangeFilesByPath iterates over all registered files filtered by
+// the given proto path. The iteration order is undefined.
+func (r *Files) RangeFilesByPath(path string, f func(protoreflect.FileDescriptor) bool) {
+	for _, fd := range r.filesByPath[path] { // TODO: iterate non-deterministically
+		if !f(fd) {
+			return
+		}
+	}
+}
+
+func splitPrefix(name protoreflect.FullName) (protoreflect.Name, protoreflect.FullName) {
+	if i := strings.IndexByte(string(name), '.'); i >= 0 {
+		return protoreflect.Name(name[:i]), name[i+len("."):]
+	}
+	return protoreflect.Name(name), ""
+}
+
+// rangeTopLevelDeclarations iterates over the name of all top-level
+// declarations in the proto file.
+func rangeTopLevelDeclarations(fd protoreflect.FileDescriptor, f func(protoreflect.Name)) {
+	for i := 0; i < fd.Enums().Len(); i++ {
+		e := fd.Enums().Get(i)
+		f(e.Name())
+		for i := 0; i < e.Values().Len(); i++ {
+			f(e.Values().Get(i).Name())
+		}
+	}
+	for i := 0; i < fd.Messages().Len(); i++ {
+		f(fd.Messages().Get(i).Name())
+	}
+	for i := 0; i < fd.Extensions().Len(); i++ {
+		f(fd.Extensions().Get(i).Name())
+	}
+	for i := 0; i < fd.Services().Len(); i++ {
+		f(fd.Services().Get(i).Name())
+	}
+}
diff --git a/reflect/protoregistry/registry_test.go b/reflect/protoregistry/registry_test.go
new file mode 100644
index 0000000..d05f8ed
--- /dev/null
+++ b/reflect/protoregistry/registry_test.go
@@ -0,0 +1,313 @@
+// 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.
+
+package protoregistry
+
+import (
+	"fmt"
+	"strings"
+	"testing"
+
+	"github.com/google/go-cmp/cmp"
+	"github.com/google/go-cmp/cmp/cmpopts"
+
+	pref "google.golang.org/proto/reflect/protoreflect"
+	ptype "google.golang.org/proto/reflect/prototype"
+)
+
+func TestFiles(t *testing.T) {
+	type (
+		file struct {
+			Path string
+			Pkg  pref.FullName
+		}
+		testFile struct {
+			inFile  *ptype.File
+			wantErr string
+		}
+		testFindDesc struct {
+			inName pref.FullName
+			wantOk bool
+		}
+		testRangePkg struct {
+			inPkg     pref.FullName
+			wantFiles []file
+		}
+		testRangePath struct {
+			inPath    string
+			wantFiles []file
+		}
+	)
+
+	tests := []struct {
+		files      []testFile
+		findDescs  []testFindDesc
+		rangePkgs  []testRangePkg
+		rangePaths []testRangePath
+	}{{
+		// Test that overlapping packages and files are permitted.
+		files: []testFile{
+			{inFile: &ptype.File{Syntax: pref.Proto2, Package: "foo.bar"}},
+			{inFile: &ptype.File{Syntax: pref.Proto2, Path: "foo/bar/test.proto", Package: "my.test"}},
+			{inFile: &ptype.File{Syntax: pref.Proto2, Path: "foo/bar/test.proto", Package: "foo.bar.baz"}},
+			{inFile: &ptype.File{Syntax: pref.Proto2, Package: "my.test.package"}},
+			{inFile: &ptype.File{Syntax: pref.Proto2, Package: "foo.bar"}},
+			{inFile: &ptype.File{Syntax: pref.Proto2, Path: "foo/bar/baz/../test.proto", Package: "my.test"}},
+		},
+
+		rangePkgs: []testRangePkg{{
+			inPkg: "nothing",
+		}, {
+			inPkg: "",
+			wantFiles: []file{
+				{"", "foo.bar"},
+				{"", "foo.bar"},
+				{"foo/bar/test.proto", "foo.bar.baz"},
+				{"foo/bar/test.proto", "my.test"},
+				{"", "my.test.package"},
+				{"foo/bar/baz/../test.proto", "my.test"},
+			},
+		}, {
+			inPkg: ".",
+		}, {
+			inPkg: "foo",
+			wantFiles: []file{
+				{"", "foo.bar"},
+				{"", "foo.bar"},
+				{"foo/bar/test.proto", "foo.bar.baz"},
+			},
+		}, {
+			inPkg: "foo.",
+		}, {
+			inPkg: "foo..",
+		}, {
+			inPkg: "foo.bar.baz",
+			wantFiles: []file{
+				{"foo/bar/test.proto", "foo.bar.baz"},
+			},
+		}, {
+			inPkg: "fo",
+		}},
+
+		rangePaths: []testRangePath{{
+			inPath: "nothing",
+		}, {
+			inPath: "",
+			wantFiles: []file{
+				{"", "foo.bar"},
+				{"", "foo.bar"},
+				{"", "my.test.package"},
+			},
+		}, {
+			inPath: "foo/bar/test.proto",
+			wantFiles: []file{
+				{"foo/bar/test.proto", "foo.bar.baz"},
+				{"foo/bar/test.proto", "my.test"},
+			},
+		}},
+	}, {
+		// Test when new enum conflicts with existing package.
+		files: []testFile{{
+			inFile: &ptype.File{Syntax: pref.Proto2, Path: "test1a.proto", Package: "foo.bar.baz"},
+		}, {
+			inFile:  &ptype.File{Syntax: pref.Proto2, Path: "test1b.proto", Enums: []ptype.Enum{{Name: "foo"}}},
+			wantErr: `file "test1b.proto" has a name conflict over foo`,
+		}},
+	}, {
+		// Test when new package conflicts with existing enum.
+		files: []testFile{{
+			inFile: &ptype.File{Syntax: pref.Proto2, Path: "test2a.proto", Enums: []ptype.Enum{{Name: "foo"}}},
+		}, {
+			inFile:  &ptype.File{Syntax: pref.Proto2, Path: "test2b.proto", Package: "foo.bar.baz"},
+			wantErr: `file "test2b.proto" has a name conflict over foo`,
+		}},
+	}, {
+		// Test when new enum conflicts with existing enum in same package.
+		files: []testFile{{
+			inFile: &ptype.File{Syntax: pref.Proto2, Path: "test3a.proto", Package: "foo", Enums: []ptype.Enum{{Name: "BAR"}}},
+		}, {
+			inFile:  &ptype.File{Syntax: pref.Proto2, Path: "test3b.proto", Package: "foo", Enums: []ptype.Enum{{Name: "BAR"}}},
+			wantErr: `file "test3b.proto" has a name conflict over foo.BAR`,
+		}},
+	}, {
+		files: []testFile{{
+			inFile: &ptype.File{
+				Syntax:  pref.Proto2,
+				Package: "fizz.buzz",
+				Messages: []ptype.Message{{
+					Name: "Message",
+					Fields: []ptype.Field{{
+						Name:        "Field",
+						Number:      1,
+						Cardinality: pref.Optional,
+						Kind:        pref.StringKind,
+						OneofName:   "Oneof",
+					}},
+					Oneofs:          []ptype.Oneof{{Name: "Oneof"}},
+					ExtensionRanges: [][2]pref.FieldNumber{{1000, 2000}},
+				}},
+				Enums: []ptype.Enum{{
+					Name:   "Enum",
+					Values: []ptype.EnumValue{{Name: "EnumValue", Number: 0}},
+				}},
+				Extensions: []ptype.Extension{{
+					Name:         "Extension",
+					Number:       1000,
+					Cardinality:  pref.Optional,
+					Kind:         pref.StringKind,
+					ExtendedType: ptype.PlaceholderMessage("fizz.buzz.Message"),
+				}},
+				Services: []ptype.Service{{
+					Name: "Service",
+					Methods: []ptype.Method{{
+						Name:              "Method",
+						InputType:         ptype.PlaceholderMessage("fizz.buzz.Message"),
+						OutputType:        ptype.PlaceholderMessage("fizz.buzz.Message"),
+						IsStreamingClient: true,
+						IsStreamingServer: true,
+					}},
+				}},
+			},
+		}, {
+			inFile: &ptype.File{
+				Syntax:  pref.Proto2,
+				Package: "fizz.buzz.gazz",
+				Enums: []ptype.Enum{{
+					Name:   "Enum",
+					Values: []ptype.EnumValue{{Name: "EnumValue", Number: 0}},
+				}},
+			},
+		}, {
+			// Conflict over a single declaration.
+			inFile: &ptype.File{
+				Syntax:  pref.Proto2,
+				Package: "fizz.buzz",
+				Enums: []ptype.Enum{{
+					Name:   "Enum1",
+					Values: []ptype.EnumValue{{Name: "EnumValue1", Number: 0}},
+				}, {
+					Name:   "Enum2",
+					Values: []ptype.EnumValue{{Name: "EnumValue2", Number: 0}},
+				}, {
+					Name:   "Enum3",
+					Values: []ptype.EnumValue{{Name: "Enum", Number: 0}}, // conflict
+				}},
+			},
+			wantErr: "name conflict over fizz.buzz.Enum",
+		}, {
+			// Previously failed registration should not pollute the namespace.
+			inFile: &ptype.File{
+				Syntax:  pref.Proto2,
+				Package: "fizz.buzz",
+				Enums: []ptype.Enum{{
+					Name:   "Enum1",
+					Values: []ptype.EnumValue{{Name: "EnumValue1", Number: 0}},
+				}, {
+					Name:   "Enum2",
+					Values: []ptype.EnumValue{{Name: "EnumValue2", Number: 0}},
+				}},
+			},
+		}, {
+			// Make sure we can register without package name.
+			inFile: &ptype.File{
+				Syntax: pref.Proto2,
+				Messages: []ptype.Message{{
+					Name: "Message",
+					Messages: []ptype.Message{{
+						Name: "Message",
+						Messages: []ptype.Message{{
+							Name: "Message",
+						}},
+					}},
+				}},
+			},
+		}},
+
+		findDescs: []testFindDesc{
+			{"", false},
+			{"Enum", false},
+			{"Message", true},
+			{"Message.", false},
+			{"Message.Message", true},
+			{"Message.Message.Message", true},
+			{"Message.Message.Message.Message", false},
+			{"fizz.buzz", false},
+			{"fizz.buzz.Enum", true},
+			{"fizz.buzz.Enum1", true},
+			{"fizz.buzz.Enum1.EnumValue", false},
+			{"fizz.buzz.EnumValue", true},
+			{"fizz.buzz.Message", true},
+			{"fizz.buzz.Message.Field", true},
+			{"fizz.buzz.Message.Oneof", true},
+			{"fizz.buzz.Extension", true},
+			{"fizz.buzz.Service", true},
+			{"fizz.buzz.Service.Method", true},
+			{"fizz.buzz.Method", false},
+		},
+	}}
+
+	sortFiles := cmpopts.SortSlices(func(x, y file) bool {
+		return x.Path < y.Path || (x.Path == y.Path && x.Pkg < y.Pkg)
+	})
+	for _, tt := range tests {
+		t.Run("", func(t *testing.T) {
+			var files Files
+			for i, tc := range tt.files {
+				fd, err := ptype.NewFile(tc.inFile)
+				if err != nil {
+					t.Fatalf("file %d, prototype.NewFile() error: %v", i, err)
+				}
+				gotErr := files.Register(fd)
+				if (gotErr == nil && tc.wantErr != "") || !strings.Contains(fmt.Sprint(gotErr), tc.wantErr) {
+					t.Errorf("file %d, Register() = %v, want %v", i, gotErr, tc.wantErr)
+				}
+			}
+
+			for _, tc := range tt.findDescs {
+				got, err := files.FindDescriptorByName(tc.inName)
+				if (got == nil) == (err == nil) {
+					if tc.wantOk {
+						t.Errorf("FindDescriptorByName(%v) = (%v, %v), want (non-nil, nil)", tc.inName, got, err)
+					} else {
+						t.Errorf("FindDescriptorByName(%v) = (%v, %v), want (nil, NotFound)", tc.inName, got, err)
+					}
+				}
+
+				gotName := pref.FullName("<nil>")
+				if got != nil {
+					gotName = got.FullName()
+				}
+				wantName := pref.FullName("<nil>")
+				if tc.wantOk {
+					wantName = tc.inName
+				}
+				if gotName != wantName {
+					t.Errorf("FindDescriptorByName(%v) = %v, want %v", tc.inName, gotName, wantName)
+				}
+			}
+
+			for _, tc := range tt.rangePkgs {
+				var gotFiles []file
+				files.RangeFilesByPackage(tc.inPkg, func(fd pref.FileDescriptor) bool {
+					gotFiles = append(gotFiles, file{fd.Path(), fd.Package()})
+					return true
+				})
+				if diff := cmp.Diff(tc.wantFiles, gotFiles, sortFiles); diff != "" {
+					t.Errorf("RangeFilesByPackage(%v) mismatch (-want +got):\n%v", tc.inPkg, diff)
+				}
+			}
+
+			for _, tc := range tt.rangePaths {
+				var gotFiles []file
+				files.RangeFilesByPath(tc.inPath, func(fd pref.FileDescriptor) bool {
+					gotFiles = append(gotFiles, file{fd.Path(), fd.Package()})
+					return true
+				})
+				if diff := cmp.Diff(tc.wantFiles, gotFiles, sortFiles); diff != "" {
+					t.Errorf("RangeFilesByPath(%v) mismatch (-want +got):\n%v", tc.inPath, diff)
+				}
+			}
+		})
+	}
+}
diff --git a/reflect/prototype/placeholder_type.go b/reflect/prototype/placeholder_type.go
index 1aed8de..387d75b 100644
--- a/reflect/prototype/placeholder_type.go
+++ b/reflect/prototype/placeholder_type.go
@@ -40,15 +40,16 @@
 	placeholderName
 }
 
-func (t placeholderFile) Path() string                          { return t.path }
-func (t placeholderFile) Package() pref.FullName                { return t.FullName() }
-func (t placeholderFile) Imports() pref.FileImports             { return &emptyFiles }
-func (t placeholderFile) Messages() pref.MessageDescriptors     { return &emptyMessages }
-func (t placeholderFile) Enums() pref.EnumDescriptors           { return &emptyEnums }
-func (t placeholderFile) Extensions() pref.ExtensionDescriptors { return &emptyExtensions }
-func (t placeholderFile) Services() pref.ServiceDescriptors     { return &emptyServices }
-func (t placeholderFile) Format(s fmt.State, r rune)            { formatDesc(s, r, t) }
-func (t placeholderFile) ProtoType(pref.FileDescriptor)         {}
+func (t placeholderFile) Path() string                                   { return t.path }
+func (t placeholderFile) Package() pref.FullName                         { return t.FullName() }
+func (t placeholderFile) Imports() pref.FileImports                      { return &emptyFiles }
+func (t placeholderFile) Messages() pref.MessageDescriptors              { return &emptyMessages }
+func (t placeholderFile) Enums() pref.EnumDescriptors                    { return &emptyEnums }
+func (t placeholderFile) Extensions() pref.ExtensionDescriptors          { return &emptyExtensions }
+func (t placeholderFile) Services() pref.ServiceDescriptors              { return &emptyServices }
+func (t placeholderFile) DescriptorByName(pref.FullName) pref.Descriptor { return nil }
+func (t placeholderFile) Format(s fmt.State, r rune)                     { formatDesc(s, r, t) }
+func (t placeholderFile) ProtoType(pref.FileDescriptor)                  {}
 
 type placeholderMessage struct {
 	placeholderName
diff --git a/reflect/prototype/protofile_type.go b/reflect/prototype/protofile_type.go
index 2929232..e82de98 100644
--- a/reflect/prototype/protofile_type.go
+++ b/reflect/prototype/protofile_type.go
@@ -46,6 +46,7 @@
 	es enumsMeta
 	xs extensionsMeta
 	ss servicesMeta
+	ds descriptorsMeta
 }
 type fileDesc struct{ f *File }
 
@@ -70,10 +71,86 @@
 func (t fileDesc) Enums() pref.EnumDescriptors                       { return t.f.es.lazyInit(t, t.f.Enums) }
 func (t fileDesc) Extensions() pref.ExtensionDescriptors             { return t.f.xs.lazyInit(t, t.f.Extensions) }
 func (t fileDesc) Services() pref.ServiceDescriptors                 { return t.f.ss.lazyInit(t, t.f.Services) }
+func (t fileDesc) DescriptorByName(s pref.FullName) pref.Descriptor  { return t.f.ds.lookup(t, s) }
 func (t fileDesc) Format(s fmt.State, r rune)                        { formatDesc(s, r, t) }
 func (t fileDesc) ProtoType(pref.FileDescriptor)                     {}
 func (t fileDesc) ProtoInternal(pragma.DoNotImplement)               {}
 
+// descriptorsMeta is a lazily initialized map of all descriptors declared in
+// the file by full name.
+type descriptorsMeta struct {
+	once sync.Once
+	m    map[pref.FullName]pref.Descriptor
+}
+
+func (m *descriptorsMeta) lookup(fd pref.FileDescriptor, s pref.FullName) pref.Descriptor {
+	m.once.Do(func() {
+		m.m = make(map[pref.FullName]pref.Descriptor)
+		m.initMap(fd)
+		delete(m.m, fd.Package()) // avoid registering the file descriptor itself
+	})
+	return m.m[s]
+}
+func (m *descriptorsMeta) initMap(d pref.Descriptor) {
+	m.m[d.FullName()] = d
+	if ds, ok := d.(interface {
+		Enums() pref.EnumDescriptors
+	}); ok {
+		for i := 0; i < ds.Enums().Len(); i++ {
+			m.initMap(ds.Enums().Get(i))
+		}
+	}
+	if ds, ok := d.(interface {
+		Values() pref.EnumValueDescriptors
+	}); ok {
+		for i := 0; i < ds.Values().Len(); i++ {
+			m.initMap(ds.Values().Get(i))
+		}
+	}
+	if ds, ok := d.(interface {
+		Messages() pref.MessageDescriptors
+	}); ok {
+		for i := 0; i < ds.Messages().Len(); i++ {
+			m.initMap(ds.Messages().Get(i))
+		}
+	}
+	if ds, ok := d.(interface {
+		Fields() pref.FieldDescriptors
+	}); ok {
+		for i := 0; i < ds.Fields().Len(); i++ {
+			m.initMap(ds.Fields().Get(i))
+		}
+	}
+	if ds, ok := d.(interface {
+		Oneofs() pref.OneofDescriptors
+	}); ok {
+		for i := 0; i < ds.Oneofs().Len(); i++ {
+			m.initMap(ds.Oneofs().Get(i))
+		}
+	}
+	if ds, ok := d.(interface {
+		Extensions() pref.ExtensionDescriptors
+	}); ok {
+		for i := 0; i < ds.Extensions().Len(); i++ {
+			m.initMap(ds.Extensions().Get(i))
+		}
+	}
+	if ds, ok := d.(interface {
+		Services() pref.ServiceDescriptors
+	}); ok {
+		for i := 0; i < ds.Services().Len(); i++ {
+			m.initMap(ds.Services().Get(i))
+		}
+	}
+	if ds, ok := d.(interface {
+		Methods() pref.MethodDescriptors
+	}); ok {
+		for i := 0; i < ds.Methods().Len(); i++ {
+			m.initMap(ds.Methods().Get(i))
+		}
+	}
+}
+
 type messageMeta struct {
 	inheritedMeta
 
diff --git a/reflect/prototype/type_test.go b/reflect/prototype/type_test.go
index 5ecfc1a..a5ef61d 100644
--- a/reflect/prototype/type_test.go
+++ b/reflect/prototype/type_test.go
@@ -84,14 +84,14 @@
 		}, {
 			Name: "B", // "test.B"
 			Fields: []Field{{
-				Name:        "field_one",
+				Name:        "field_one", // "test.B.field_one"
 				Number:      1,
 				Cardinality: pref.Optional,
 				Kind:        pref.StringKind,
 				Default:     pref.ValueOf("hello"),
 				OneofName:   "O1",
 			}, {
-				Name:        "field_two",
+				Name:        "field_two", // "test.B.field_two"
 				JSONName:    "Field2",
 				Number:      2,
 				Cardinality: pref.Optional,
@@ -100,32 +100,35 @@
 				EnumType:    PlaceholderEnum("test.E1"),
 				OneofName:   "O2",
 			}, {
-				Name:        "field_three",
+				Name:        "field_three", // "test.B.field_three"
 				Number:      3,
 				Cardinality: pref.Optional,
 				Kind:        pref.MessageKind,
 				MessageType: PlaceholderMessage("test.C"),
 				OneofName:   "O2",
 			}, {
-				Name:        "field_four",
+				Name:        "field_four", // "test.B.field_four"
 				JSONName:    "Field4",
 				Number:      4,
 				Cardinality: pref.Repeated,
 				Kind:        pref.MessageKind,
 				MessageType: PlaceholderMessage("test.A"),
 			}, {
-				Name:        "field_five",
+				Name:        "field_five", // "test.B.field_five"
 				Number:      5,
 				Cardinality: pref.Repeated,
 				Kind:        pref.Int32Kind,
 				IsPacked:    true,
 			}, {
-				Name:        "field_six",
+				Name:        "field_six", // "test.B.field_six"
 				Number:      6,
 				Cardinality: pref.Required,
 				Kind:        pref.StringKind,
 			}},
-			Oneofs:          []Oneof{{Name: "O1"}, {Name: "O2"}},
+			Oneofs: []Oneof{
+				{Name: "O1"}, // "test.B.O1"
+				{Name: "O2"}, // "test.B.O2"
+			},
 			ExtensionRanges: [][2]pref.FieldNumber{{1000, 2000}},
 		}, {
 			Name: "C", // "test.C"
@@ -382,6 +385,33 @@
 				},
 			},
 		},
+		"DescriptorByName:":                 nil,
+		"DescriptorByName:A":                nil,
+		"DescriptorByName:test":             nil,
+		"DescriptorByName:test.":            nil,
+		"DescriptorByName:test.A":           M{"FullName": pref.FullName("test.A")},
+		"DescriptorByName:test.A.key":       M{"FullName": pref.FullName("test.A.key")},
+		"DescriptorByName:test.A.A":         nil,
+		"DescriptorByName:test.A.field_one": nil,
+		"DescriptorByName:test.B.field_one": M{"FullName": pref.FullName("test.B.field_one")},
+		"DescriptorByName:test.B.O1":        M{"FullName": pref.FullName("test.B.O1")},
+		"DescriptorByName:test.B.O3":        nil,
+		"DescriptorByName:test.C.E1":        M{"FullName": pref.FullName("test.C.E1")},
+		"DescriptorByName:test.C.E1.FOO":    nil,
+		"DescriptorByName:test.C.FOO":       M{"FullName": pref.FullName("test.C.FOO")},
+		"DescriptorByName:test.C.Foo":       nil,
+		"DescriptorByName:test.C.BAZ":       nil,
+		"DescriptorByName:test.E1":          M{"FullName": pref.FullName("test.E1")},
+		"DescriptorByName:test.E1.FOO":      nil,
+		"DescriptorByName:test.FOO":         M{"FullName": pref.FullName("test.FOO")},
+		"DescriptorByName:test.Foo":         nil,
+		"DescriptorByName:test.BAZ":         nil,
+		"DescriptorByName:test.C.X":         M{"FullName": pref.FullName("test.C.X")},
+		"DescriptorByName:test.X":           M{"FullName": pref.FullName("test.X")},
+		"DescriptorByName:test.X.":          nil,
+		"DescriptorByName:test.S":           M{"FullName": pref.FullName("test.S")},
+		"DescriptorByName:test.S.M":         M{"FullName": pref.FullName("test.S.M")},
+		"DescriptorByName:test.M":           nil,
 	}
 
 	// Concurrently explore the file tree to induce races.