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>
diff --git a/reflect/prototype/stringer.go b/reflect/prototype/stringer.go
index 7541b8d..20805fa 100644
--- a/reflect/prototype/stringer.go
+++ b/reflect/prototype/stringer.go
@@ -6,21 +6,253 @@
 
 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)]
+}
diff --git a/reflect/prototype/type_test.go b/reflect/prototype/type_test.go
index 0c1d93f..00404ff 100644
--- a/reflect/prototype/type_test.go
+++ b/reflect/prototype/type_test.go
@@ -5,6 +5,7 @@
 package prototype
 
 import (
+	"fmt"
 	"reflect"
 	"strconv"
 	"strings"
@@ -63,6 +64,43 @@
 	}
 }
 
+// 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 @@
 				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 @@
 				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 @@
 			},
 			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 @@
 		{"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 @@
 					},
 					"ByNumber:6": M{
 						"Cardinality": pref.Required,
-						"Default":     "",
+						"Default":     []byte(nil),
 						"OneofType":   nil,
 					},
 				},
@@ -474,13 +516,14 @@
 					"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 @@
 	}
 	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 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,