cmd/protoc-gen-go: add support for protobuf reflection
Implement support in protoc-gen-go for generating messages and enums
that satisfy the v2 protobuf reflection interfaces. Specifically, the following
are added:
* top-level variable representing the file descriptor
* ProtoReflect method on enums (to implement protoV2.Enum)
* ProtoReflect method on messages (to implement protoV2.Message)
The following are not supported yet:
* resolving transitive dependencies for file imports
* Extension descriptors
* Service descriptors
The implementation approach creates a single array for all the message and enum
declarations and references sections of that array to complete dependencies.
Since protobuf declarations can form a graph (a message may depend on itself),
it is difficult to construct a graph as a single literal. One way is to use
placeholder descriptors, but that is not efficient as it requires encoding
the full name of each dependent enum and message and then later resolving it;
thus, both expanding the binary size and also increasing initialization cost.
Instead, we add a prototype.{Enum,Message}.Reference method to obtain a
descriptor reference for the purposes for satisfying dependencies.
As such, nested declarations and dependencies are populated in an init function.
Other changes to support the implementation:
* Added a protoimpl package to expose the MessageType type and also the
MessageTypeOf and EnumTypeOf helper functions.
* Added a protogen.File.GoIdent field to provide a suggested variable name
for the file descriptor.
* Added prototype.{Enum,Message}.Reference that provides a descriptor reference
for the purposes for satisfying cyclic dependencies.
* Added protoreflect.{Syntax,Cardinality,Kind}.GoString to obtain a Go source
identifier that represents the given constant.
Change-Id: I9455764882dee6ad10f251901e7d419091e8bf1d
Reviewed-on: https://go-review.googlesource.com/c/150074
Reviewed-by: Damien Neil <dneil@google.com>
diff --git a/protogen/names.go b/protogen/names.go
index d561a5f..dc4c86c 100644
--- a/protogen/names.go
+++ b/protogen/names.go
@@ -46,18 +46,36 @@
// A GoPackageName is the name of a Go package. e.g., "protobuf".
type GoPackageName string
-// cleanPacakgeName converts a string to a valid Go package name.
+// cleanPackageName converts a string to a valid Go package name.
func cleanPackageName(name string) GoPackageName {
- name = strings.Map(badToUnderscore, name)
- // Identifier must not be keyword: insert _.
- if token.Lookup(name).IsKeyword() {
- name = "_" + name
+ return GoPackageName(cleanGoName(name, false))
+}
+
+// cleanGoName converts a string to a valid Go identifier.
+// If mustExport, then the returned identifier is exported if not already.
+func cleanGoName(name string, mustExport bool) string {
+ name = strings.Map(func(r rune) rune {
+ if unicode.IsLetter(r) || unicode.IsDigit(r) {
+ return r
+ }
+ return '_'
+ }, name)
+ prefix := "_"
+ if mustExport {
+ prefix = "X"
}
- // Identifier must not begin with digit: insert _.
- if r, _ := utf8.DecodeRuneInString(name); unicode.IsDigit(r) {
- name = "_" + name
+ switch r, n := utf8.DecodeRuneInString(name); {
+ case token.Lookup(name).IsKeyword():
+ return prefix + name
+ case unicode.IsDigit(r):
+ return prefix + name
+ case mustExport && !unicode.IsUpper(r):
+ if unicode.IsLower(r) {
+ return string(unicode.ToUpper(r)) + name[n:]
+ }
+ return prefix + name
}
- return GoPackageName(name)
+ return name
}
var isGoPredeclaredIdentifier = map[string]bool{
@@ -102,16 +120,6 @@
"uintptr": true,
}
-// badToUnderscore is the mapping function used to generate Go names from package names,
-// which can be dotted in the input .proto file. It replaces non-identifier characters such as
-// dot or dash with underscore.
-func badToUnderscore(r rune) rune {
- if unicode.IsLetter(r) || unicode.IsDigit(r) || r == '_' {
- return r
- }
- return '_'
-}
-
// baseName returns the last path element of the name, with the last dotted suffix removed.
func baseName(name string) string {
// First, find the last element
diff --git a/protogen/names_test.go b/protogen/names_test.go
index 3271954..e253429 100644
--- a/protogen/names_test.go
+++ b/protogen/names_test.go
@@ -25,6 +25,11 @@
{"_one._two", "XOne_XTwo"},
{"SCREAMING_SNAKE_CASE", "SCREAMING_SNAKE_CASE"},
{"double__underscore", "Double_Underscore"},
+ {"camelCase", "CamelCase"},
+ {"go2proto", "Go2Proto"},
+ {"世界", "世界"},
+ {"x世界", "X世界"},
+ {"foo_bar世界", "FooBar世界"},
}
for _, tc := range tests {
if got := camelCase(tc.in); got != tc.want {
@@ -32,3 +37,24 @@
}
}
}
+
+func TestCleanGoName(t *testing.T) {
+ tests := []struct {
+ in, want, wantExported string
+ }{
+ {"", "", "X"},
+ {"hello", "hello", "Hello"},
+ {"hello-world!!", "hello_world__", "Hello_world__"},
+ {"hello-\xde\xad\xbe\xef\x00", "hello_____", "Hello_____"},
+ {"hello 世界", "hello_世界", "Hello_世界"},
+ {"世界", "世界", "X世界"},
+ }
+ for _, tc := range tests {
+ if got := cleanGoName(tc.in, false); got != tc.want {
+ t.Errorf("cleanGoName(%q, false) = %q, want %q", tc.in, got, tc.want)
+ }
+ if got := cleanGoName(tc.in, true); got != tc.wantExported {
+ t.Errorf("cleanGoName(%q, true) = %q, want %q", tc.in, got, tc.wantExported)
+ }
+ }
+}
diff --git a/protogen/protogen.go b/protogen/protogen.go
index 95a499f..cdfccce 100644
--- a/protogen/protogen.go
+++ b/protogen/protogen.go
@@ -379,13 +379,14 @@
Desc protoreflect.FileDescriptor
Proto *descriptorpb.FileDescriptorProto
- GoPackageName GoPackageName // name of this file's Go package
- GoImportPath GoImportPath // import path of this file's Go package
- Messages []*Message // top-level message declarations
- Enums []*Enum // top-level enum declarations
- Extensions []*Extension // top-level extension declarations
- Services []*Service // top-level service declarations
- Generate bool // true if we should generate code for this file
+ GoDescriptorIdent GoIdent // name of Go variable for the file descriptor
+ GoPackageName GoPackageName // name of this file's Go package
+ GoImportPath GoImportPath // import path of this file's Go package
+ Messages []*Message // top-level message declarations
+ Enums []*Enum // top-level enum declarations
+ Extensions []*Extension // top-level extension declarations
+ Services []*Service // top-level service declarations
+ Generate bool // true if we should generate code for this file
// GeneratedFilenamePrefix is used to construct filenames for generated
// files associated with this source file.
@@ -429,6 +430,10 @@
prefix = path.Join(string(importPath), path.Base(prefix))
}
}
+ f.GoDescriptorIdent = GoIdent{
+ GoName: camelCase(cleanGoName(path.Base(prefix), true)) + "_ProtoFile",
+ GoImportPath: f.GoImportPath,
+ }
f.GeneratedFilenamePrefix = prefix
for _, loc := range p.GetSourceCodeInfo().GetLocation() {