reflect/prototype: implement Format methods

Implement Format methods to provide the ability to pretty-print the descriptor
in a humanly readable fashion. While this functionality is not strictly necessary
for the operation of Go protobuf, it is a useful aid for humans.

Change-Id: I88807b38b5be713867f2f2aab5a0843fc147dc35
Reviewed-on: https://go-review.googlesource.com/131255
Reviewed-by: Herbie Ong <herbie@google.com>
This commit is contained in:
Joe Tsai 2018-08-24 03:03:21 -07:00 committed by Joe Tsai
parent 3cf6e62f69
commit 17764983d4
2 changed files with 450 additions and 23 deletions

View File

@ -6,21 +6,253 @@ package prototype
import (
"fmt"
"io"
"math/rand"
"reflect"
"strconv"
"strings"
"google.golang.org/proto/internal/pragma"
pref "google.golang.org/proto/reflect/protoreflect"
)
// TODO: This is useful for print descriptor types in a human readable way.
// This is not strictly necessary.
// list is an interface that matches any of the list interfaces defined in the
// protoreflect package.
type list interface {
Len() int
pragma.DoNotImplement
}
func formatList(s fmt.State, r rune, vs list) {}
func formatList(s fmt.State, r rune, vs list) {
io.WriteString(s, formatListOpt(vs, true, r == 'v' && (s.Flag('+') || s.Flag('#'))))
}
func formatListOpt(vs list, isRoot, allowMulti bool) string {
start, end := "[", "]"
if isRoot {
var name string
switch vs.(type) {
case pref.FieldNumbers:
name = "FieldNumbers"
case pref.FieldRanges:
name = "FieldRanges"
case pref.FileImports:
name = "FileImports"
case pref.Descriptor:
name = reflect.ValueOf(vs).MethodByName("Get").Type().Out(0).Name() + "s"
}
start, end = name+randomSpace()+"{", "}"
}
func formatDesc(s fmt.State, r rune, t pref.Descriptor) {}
var ss []string
switch vs := vs.(type) {
case pref.FieldNumbers:
for i := 0; i < vs.Len(); i++ {
ss = append(ss, fmt.Sprint(vs.Get(i)))
}
return start + joinStrings(ss, false) + end
case pref.FieldRanges:
for i := 0; i < vs.Len(); i++ {
r := vs.Get(i)
if r[0]+1 == r[1] {
ss = append(ss, fmt.Sprintf("%d", r[0]))
} else {
ss = append(ss, fmt.Sprintf("%d:%d", r[0], r[1]))
}
}
return start + joinStrings(ss, false) + end
case pref.FileImports:
for i := 0; i < vs.Len(); i++ {
var rs records
rs.Append(reflect.ValueOf(vs.Get(i)), "Path", "Package", "IsPublic", "IsWeak")
ss = append(ss, "{"+rs.Join()+"}")
}
return start + joinStrings(ss, allowMulti) + end
default:
_, isEnumValue := vs.(pref.EnumValueDescriptors)
for i := 0; i < vs.Len(); i++ {
m := reflect.ValueOf(vs).MethodByName("Get")
v := m.Call([]reflect.Value{reflect.ValueOf(i)})[0].Interface()
ss = append(ss, formatDescOpt(v.(pref.Descriptor), false, allowMulti && !isEnumValue))
}
return start + joinStrings(ss, allowMulti && isEnumValue) + end
}
}
// descriptorAccessors is a list of accessors to print for each descriptor.
//
// Do not print all accessors since some contain redundant information,
// while others are pointers that we do not want to follow since the descriptor
// is actually a cyclic graph.
//
// Using a list allows us to print the accessors in a sensible order.
var descriptorAccessors = map[reflect.Type][]string{
reflect.TypeOf((*pref.FileDescriptor)(nil)).Elem(): {"Path", "Package", "Imports", "Messages", "Enums", "Extensions", "Services"},
reflect.TypeOf((*pref.MessageDescriptor)(nil)).Elem(): {"IsMapEntry", "Fields", "Oneofs", "RequiredNumbers", "ExtensionRanges", "Messages", "Enums", "Extensions"},
reflect.TypeOf((*pref.FieldDescriptor)(nil)).Elem(): {"Number", "Cardinality", "Kind", "JSONName", "IsPacked", "IsMap", "IsWeak", "Default", "OneofType", "ExtendedType", "MessageType", "EnumType"},
reflect.TypeOf((*pref.OneofDescriptor)(nil)).Elem(): {"Fields"}, // not directly used; must keep in sync with formatDescOpt
reflect.TypeOf((*pref.EnumDescriptor)(nil)).Elem(): {"Values"},
reflect.TypeOf((*pref.EnumValueDescriptor)(nil)).Elem(): {"Number"},
reflect.TypeOf((*pref.ServiceDescriptor)(nil)).Elem(): {"Methods"},
reflect.TypeOf((*pref.MethodDescriptor)(nil)).Elem(): {"InputType", "OutputType", "IsStreamingClient", "IsStreamingServer"},
}
func formatDesc(s fmt.State, r rune, t pref.Descriptor) {
io.WriteString(s, formatDescOpt(t, true, r == 'v' && (s.Flag('+') || s.Flag('#'))))
}
func formatDescOpt(t pref.Descriptor, isRoot, allowMulti bool) string {
rv := reflect.ValueOf(t)
rt := rv.MethodByName("ProtoType").Type().In(0)
start, end := "{", "}"
if isRoot {
start = rt.Name() + randomSpace() + "{"
}
_, isFile := t.(pref.FileDescriptor)
rs := records{allowMulti: allowMulti}
if t.IsPlaceholder() {
if isFile {
rs.Append(rv, "Path", "Package", "IsPlaceholder")
} else {
rs.Append(rv, "FullName", "IsPlaceholder")
}
} else {
switch {
case isFile:
rs.Append(rv, "Syntax")
case isRoot:
rs.Append(rv, "Syntax", "FullName")
default:
rs.Append(rv, "Name")
}
if t, ok := t.(pref.OneofDescriptor); ok {
var ss []string
fs := t.Fields()
for i := 0; i < fs.Len(); i++ {
ss = append(ss, string(fs.Get(i).Name()))
}
if len(ss) > 0 {
rs.recs = append(rs.recs, [2]string{"Fields", "[" + joinStrings(ss, false) + "]"})
}
} else {
rs.Append(rv, descriptorAccessors[rt]...)
}
// TODO: Print GoType
}
return start + rs.Join() + end
}
type records struct {
recs [][2]string
allowMulti bool
}
func (rs *records) Append(v reflect.Value, accessors ...string) {
for _, a := range accessors {
var rv reflect.Value
if m := v.MethodByName(a); m.IsValid() {
rv = m.Call(nil)[0]
}
if v.Kind() == reflect.Struct && !rv.IsValid() {
rv = v.FieldByName(a)
}
if !rv.IsValid() {
panic(fmt.Sprintf("unknown accessor: %v.%s", v.Type(), a))
}
if _, ok := rv.Interface().(pref.Value); ok {
rv = rv.MethodByName("Interface").Call(nil)[0]
if !rv.IsNil() {
rv = rv.Elem()
}
}
// Ignore zero values.
var isZero bool
switch rv.Kind() {
case reflect.Interface, reflect.Slice:
isZero = rv.IsNil()
case reflect.Bool:
isZero = rv.Bool() == false
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
isZero = rv.Int() == 0
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
isZero = rv.Uint() == 0
case reflect.String:
isZero = rv.String() == ""
}
if n, ok := rv.Interface().(list); ok {
isZero = n.Len() == 0
}
if isZero {
continue
}
// Format the value.
var s string
v := rv.Interface()
switch v := v.(type) {
case list:
s = formatListOpt(v, false, rs.allowMulti)
case pref.FieldDescriptor, pref.OneofDescriptor, pref.EnumValueDescriptor, pref.MethodDescriptor:
s = string(v.(pref.Descriptor).Name())
case pref.Descriptor:
s = string(v.FullName())
case string:
s = strconv.Quote(v)
case []byte:
s = fmt.Sprintf("%q", v)
default:
s = fmt.Sprint(v)
}
rs.recs = append(rs.recs, [2]string{a, s})
}
}
func (rs *records) Join() string {
var ss []string
// In single line mode, simply join all records with commas.
if !rs.allowMulti {
for _, r := range rs.recs {
ss = append(ss, r[0]+": "+r[1])
}
return joinStrings(ss, false)
}
// In allowMulti line mode, align single line records for more readable output.
var maxLen int
flush := func(i int) {
for _, r := range rs.recs[len(ss):i] {
padding := strings.Repeat(" ", maxLen-len(r[0]))
ss = append(ss, r[0]+": "+padding+r[1])
}
maxLen = 0
}
for i, r := range rs.recs {
if isMulti := strings.Contains(r[1], "\n"); isMulti {
flush(i)
ss = append(ss, r[0]+": "+strings.Join(strings.Split(r[1], "\n"), "\n\t"))
} else if maxLen < len(r[0]) {
maxLen = len(r[0])
}
}
flush(len(rs.recs))
return joinStrings(ss, true)
}
func joinStrings(ss []string, isMulti bool) string {
if len(ss) == 0 {
return ""
}
if isMulti {
return "\n\t" + strings.Join(ss, "\n\t") + "\n"
}
return strings.Join(ss, ", ")
}
// randomSpace randomly returns a string that is either empty or a single space.
// This is done deliberately to ensure that the output is slightly non-stable.
//
// These makes it harder for people to depend on the debug string as stable
// and provides us the flexibility to make changes.
func randomSpace() string {
return " "[:rand.Intn(2)]
}

View File

@ -5,6 +5,7 @@
package prototype
import (
"fmt"
"reflect"
"strconv"
"strings"
@ -63,6 +64,43 @@ func TestDescriptors(t *testing.T) {
}
}
// TestDescriptorAccessors tests that descriptorAccessors is up-to-date.
func TestDescriptorAccessors(t *testing.T) {
ignore := map[string]bool{"ProtoType": true, "DescriptorByName": true}
rt := reflect.TypeOf((*pref.Descriptor)(nil)).Elem()
for i := 0; i < rt.NumMethod(); i++ {
ignore[rt.Method(i).Name] = true
}
for rt, m := range descriptorAccessors {
got := map[string]bool{}
for _, s := range m {
got[s] = true
}
want := map[string]bool{}
for i := 0; i < rt.NumMethod(); i++ {
want[rt.Method(i).Name] = true
}
// Check if descriptorAccessors contains a non-existent accessor.
// If this test fails, remove the accessor from descriptorAccessors.
for s := range got {
if !want[s] && !ignore[s] {
t.Errorf("%v.%v does not exist", rt, s)
}
}
// Check if there are new protoreflect interface methods that are not
// handled by the formatter. If this fails, either add the method to
// ignore or add them to descriptorAccessors.
for s := range want {
if !got[s] && !ignore[s] {
t.Errorf("%v.%v is not called by formatter", rt, s)
}
}
}
}
// TODO: Test NewFileFromDescriptorProto with imported files.
func TestFile(t *testing.T) {
@ -127,18 +165,18 @@ func TestFile(t *testing.T) {
Name: "field_six", // "test.B.field_six"
Number: 6,
Cardinality: pref.Required,
Kind: pref.StringKind,
Kind: pref.BytesKind,
}},
Oneofs: []Oneof{
{Name: "O1"}, // "test.B.O1"
{Name: "O2"}, // "test.B.O2"
},
ExtensionRanges: [][2]pref.FieldNumber{{1000, 2000}},
ExtensionRanges: [][2]pref.FieldNumber{{1000, 2000}, {3000, 3001}},
}, {
Name: "C", // "test.C"
Messages: []Message{{
Name: "A", // "test.C.A"
Fields: []Field{{Name: "F", Number: 1, Cardinality: pref.Required, Kind: pref.BytesKind}},
Fields: []Field{{Name: "F", Number: 1, Cardinality: pref.Required, Kind: pref.BytesKind, Default: pref.ValueOf([]byte("dead\xbe\xef"))}},
}},
Enums: []Enum{{
Name: "E1", // "test.C.E1"
@ -244,7 +282,7 @@ func TestFile(t *testing.T) {
Name: protoV1.String("field_six"),
Number: protoV1.Int32(6),
Label: descriptorV1.FieldDescriptorProto_Label(pref.Required).Enum(),
Type: descriptorV1.FieldDescriptorProto_Type(pref.StringKind).Enum(),
Type: descriptorV1.FieldDescriptorProto_Type(pref.BytesKind).Enum(),
}},
OneofDecl: []*descriptorV1.OneofDescriptorProto{
{Name: protoV1.String("O1")},
@ -252,16 +290,18 @@ func TestFile(t *testing.T) {
},
ExtensionRange: []*descriptorV1.DescriptorProto_ExtensionRange{
{Start: protoV1.Int32(1000), End: protoV1.Int32(2000)},
{Start: protoV1.Int32(3000), End: protoV1.Int32(3001)},
},
}, {
Name: protoV1.String("C"),
NestedType: []*descriptorV1.DescriptorProto{{
Name: protoV1.String("A"),
Field: []*descriptorV1.FieldDescriptorProto{{
Name: protoV1.String("F"),
Number: protoV1.Int32(1),
Label: descriptorV1.FieldDescriptorProto_Label(pref.Required).Enum(),
Type: descriptorV1.FieldDescriptorProto_Type(pref.BytesKind).Enum(),
Name: protoV1.String("F"),
Number: protoV1.Int32(1),
Label: descriptorV1.FieldDescriptorProto_Label(pref.Required).Enum(),
Type: descriptorV1.FieldDescriptorProto_Type(pref.BytesKind).Enum(),
DefaultValue: protoV1.String(`dead\276\357`),
}},
}},
EnumType: []*descriptorV1.EnumDescriptorProto{{
@ -320,12 +360,14 @@ func TestFile(t *testing.T) {
{"NewFileFromDescriptorProto", fd2},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Run("Accessors", func(t *testing.T) {
// Run sub-tests in parallel to induce potential races.
t.Run("", func(t *testing.T) { t.Parallel(); testFileAccessors(t, tt.desc) })
t.Run("", func(t *testing.T) { t.Parallel(); testFileAccessors(t, tt.desc) })
})
// Run sub-tests in parallel to induce potential races.
for i := 0; i < 2; i++ {
t.Run("Accessors", func(t *testing.T) { t.Parallel(); testFileAccessors(t, tt.desc) })
t.Run("FormatCompact", func(t *testing.T) { t.Parallel(); testFileFormatCompact(t, tt.desc) })
t.Run("FormatMulti", func(t *testing.T) { t.Parallel(); testFileFormatMulti(t, tt.desc) })
}
})
}
}
@ -442,7 +484,7 @@ func testFileAccessors(t *testing.T, fd pref.FileDescriptor) {
},
"ByNumber:6": M{
"Cardinality": pref.Required,
"Default": "",
"Default": []byte(nil),
"OneofType": nil,
},
},
@ -474,13 +516,14 @@ func testFileAccessors(t *testing.T, fd pref.FileDescriptor) {
"Has:6": true,
},
"ExtensionRanges": M{
"Len": 1,
"Len": 2,
"Get:0": [2]pref.FieldNumber{1000, 2000},
"Has:999": false,
"Has:1000": true,
"Has:1500": true,
"Has:1999": true,
"Has:2000": false,
"Has:3000": true,
},
},
"Get:2": M{
@ -576,7 +619,6 @@ func testFileAccessors(t *testing.T, fd pref.FileDescriptor) {
}
checkAccessors(t, "", reflect.ValueOf(fd), want)
}
func checkAccessors(t *testing.T, p string, rv reflect.Value, want map[string]interface{}) {
if rv.Interface() == nil {
t.Errorf("%v is nil, want non-nil", p)
@ -629,6 +671,159 @@ func checkAccessors(t *testing.T, p string, rv reflect.Value, want map[string]in
}
}
func testFileFormatCompact(t *testing.T, fd pref.FileDescriptor) {
const want = `FileDescriptor{Syntax: proto2, Path: "path/to/file.proto", Package: test, Messages: [{Name: A, IsMapEntry: true, Fields: [{Name: key, Number: 1, Cardinality: optional, Kind: string, JSONName: "key"}, {Name: value, Number: 2, Cardinality: optional, Kind: message, JSONName: "value", MessageType: test.B}]}, {Name: B, Fields: [{Name: field_one, Number: 1, Cardinality: optional, Kind: string, JSONName: "fieldOne", Default: "hello", OneofType: O1}, {Name: field_two, Number: 2, Cardinality: optional, Kind: enum, JSONName: "Field2", Default: 1, OneofType: O2, EnumType: test.E1}, {Name: field_three, Number: 3, Cardinality: optional, Kind: message, JSONName: "fieldThree", OneofType: O2, MessageType: test.C}, {Name: field_four, Number: 4, Cardinality: repeated, Kind: message, JSONName: "Field4", IsMap: true, MessageType: test.A}, {Name: field_five, Number: 5, Cardinality: repeated, Kind: int32, JSONName: "fieldFive", IsPacked: true}, {Name: field_six, Number: 6, Cardinality: required, Kind: bytes, JSONName: "fieldSix"}], Oneofs: [{Name: O1, Fields: [field_one]}, {Name: O2, Fields: [field_two, field_three]}], RequiredNumbers: [6], ExtensionRanges: [1000:2000, 3000]}, {Name: C, Messages: [{Name: A, Fields: [{Name: F, Number: 1, Cardinality: required, Kind: bytes, JSONName: "F", Default: "dead\xbe\xef"}], RequiredNumbers: [1]}], Enums: [{Name: E1, Values: [{Name: FOO}, {Name: BAR, Number: 1}]}], Extensions: [{Name: X, Number: 1000, Cardinality: repeated, Kind: message, ExtendedType: test.B, MessageType: test.C}]}], Enums: [{Name: E1, Values: [{Name: FOO}, {Name: BAR, Number: 1}]}], Extensions: [{Name: X, Number: 1000, Cardinality: repeated, Kind: message, IsPacked: true, ExtendedType: test.B, MessageType: test.C}], Services: [{Name: S, Methods: [{Name: M, InputType: test.A, OutputType: test.C.A, IsStreamingClient: true, IsStreamingServer: true}]}]}`
got := fmt.Sprintf("%v", fd)
got = strings.Replace(got, "FileDescriptor ", "FileDescriptor", 1) // cleanup randomizer
if got != want {
t.Errorf("fmt.Sprintf(%q, fd):\ngot: %s\nwant: %s", "%v", got, want)
}
}
func testFileFormatMulti(t *testing.T, fd pref.FileDescriptor) {
const want = `FileDescriptor{
Syntax: proto2
Path: "path/to/file.proto"
Package: test
Messages: [{
Name: A
IsMapEntry: true
Fields: [{
Name: key
Number: 1
Cardinality: optional
Kind: string
JSONName: "key"
}, {
Name: value
Number: 2
Cardinality: optional
Kind: message
JSONName: "value"
MessageType: test.B
}]
}, {
Name: B
Fields: [{
Name: field_one
Number: 1
Cardinality: optional
Kind: string
JSONName: "fieldOne"
Default: "hello"
OneofType: O1
}, {
Name: field_two
Number: 2
Cardinality: optional
Kind: enum
JSONName: "Field2"
Default: 1
OneofType: O2
EnumType: test.E1
}, {
Name: field_three
Number: 3
Cardinality: optional
Kind: message
JSONName: "fieldThree"
OneofType: O2
MessageType: test.C
}, {
Name: field_four
Number: 4
Cardinality: repeated
Kind: message
JSONName: "Field4"
IsMap: true
MessageType: test.A
}, {
Name: field_five
Number: 5
Cardinality: repeated
Kind: int32
JSONName: "fieldFive"
IsPacked: true
}, {
Name: field_six
Number: 6
Cardinality: required
Kind: bytes
JSONName: "fieldSix"
}]
Oneofs: [{
Name: O1
Fields: [field_one]
}, {
Name: O2
Fields: [field_two, field_three]
}]
RequiredNumbers: [6]
ExtensionRanges: [1000:2000, 3000]
}, {
Name: C
Messages: [{
Name: A
Fields: [{
Name: F
Number: 1
Cardinality: required
Kind: bytes
JSONName: "F"
Default: "dead\xbe\xef"
}]
RequiredNumbers: [1]
}]
Enums: [{
Name: E1
Values: [
{Name: FOO}
{Name: BAR, Number: 1}
]
}]
Extensions: [{
Name: X
Number: 1000
Cardinality: repeated
Kind: message
ExtendedType: test.B
MessageType: test.C
}]
}]
Enums: [{
Name: E1
Values: [
{Name: FOO}
{Name: BAR, Number: 1}
]
}]
Extensions: [{
Name: X
Number: 1000
Cardinality: repeated
Kind: message
IsPacked: true
ExtendedType: test.B
MessageType: test.C
}]
Services: [{
Name: S
Methods: [{
Name: M
InputType: test.A
OutputType: test.C.A
IsStreamingClient: true
IsStreamingServer: true
}]
}]
}`
got := fmt.Sprintf("%+v", fd)
got = strings.Replace(got, "FileDescriptor ", "FileDescriptor", 1) // cleanup randomizer
if got != want {
t.Errorf("fmt.Sprintf(%q, fd):\ngot: %s\nwant: %s", "%+v", got, want)
}
}
func TestResolve(t *testing.T) {
f := &File{
Syntax: pref.Proto2,