stringer.go

  1// Copyright 2018 The Go Authors. All rights reserved.
  2// Use of this source code is governed by a BSD-style
  3// license that can be found in the LICENSE file.
  4
  5// Package descfmt provides functionality to format descriptors.
  6package descfmt
  7
  8import (
  9	"fmt"
 10	"io"
 11	"reflect"
 12	"strconv"
 13	"strings"
 14
 15	"google.golang.org/protobuf/internal/detrand"
 16	"google.golang.org/protobuf/internal/pragma"
 17	"google.golang.org/protobuf/reflect/protoreflect"
 18)
 19
 20type list interface {
 21	Len() int
 22	pragma.DoNotImplement
 23}
 24
 25func FormatList(s fmt.State, r rune, vs list) {
 26	io.WriteString(s, formatListOpt(vs, true, r == 'v' && (s.Flag('+') || s.Flag('#'))))
 27}
 28func formatListOpt(vs list, isRoot, allowMulti bool) string {
 29	start, end := "[", "]"
 30	if isRoot {
 31		var name string
 32		switch vs.(type) {
 33		case protoreflect.Names:
 34			name = "Names"
 35		case protoreflect.FieldNumbers:
 36			name = "FieldNumbers"
 37		case protoreflect.FieldRanges:
 38			name = "FieldRanges"
 39		case protoreflect.EnumRanges:
 40			name = "EnumRanges"
 41		case protoreflect.FileImports:
 42			name = "FileImports"
 43		case protoreflect.Descriptor:
 44			name = reflect.ValueOf(vs).MethodByName("Get").Type().Out(0).Name() + "s"
 45		default:
 46			name = reflect.ValueOf(vs).Elem().Type().Name()
 47		}
 48		start, end = name+"{", "}"
 49	}
 50
 51	var ss []string
 52	switch vs := vs.(type) {
 53	case protoreflect.Names:
 54		for i := 0; i < vs.Len(); i++ {
 55			ss = append(ss, fmt.Sprint(vs.Get(i)))
 56		}
 57		return start + joinStrings(ss, false) + end
 58	case protoreflect.FieldNumbers:
 59		for i := 0; i < vs.Len(); i++ {
 60			ss = append(ss, fmt.Sprint(vs.Get(i)))
 61		}
 62		return start + joinStrings(ss, false) + end
 63	case protoreflect.FieldRanges:
 64		for i := 0; i < vs.Len(); i++ {
 65			r := vs.Get(i)
 66			if r[0]+1 == r[1] {
 67				ss = append(ss, fmt.Sprintf("%d", r[0]))
 68			} else {
 69				ss = append(ss, fmt.Sprintf("%d:%d", r[0], r[1])) // enum ranges are end exclusive
 70			}
 71		}
 72		return start + joinStrings(ss, false) + end
 73	case protoreflect.EnumRanges:
 74		for i := 0; i < vs.Len(); i++ {
 75			r := vs.Get(i)
 76			if r[0] == r[1] {
 77				ss = append(ss, fmt.Sprintf("%d", r[0]))
 78			} else {
 79				ss = append(ss, fmt.Sprintf("%d:%d", r[0], int64(r[1])+1)) // enum ranges are end inclusive
 80			}
 81		}
 82		return start + joinStrings(ss, false) + end
 83	case protoreflect.FileImports:
 84		for i := 0; i < vs.Len(); i++ {
 85			var rs records
 86			rv := reflect.ValueOf(vs.Get(i))
 87			rs.Append(rv, []methodAndName{
 88				{rv.MethodByName("Path"), "Path"},
 89				{rv.MethodByName("Package"), "Package"},
 90				{rv.MethodByName("IsPublic"), "IsPublic"},
 91				{rv.MethodByName("IsWeak"), "IsWeak"},
 92			}...)
 93			ss = append(ss, "{"+rs.Join()+"}")
 94		}
 95		return start + joinStrings(ss, allowMulti) + end
 96	default:
 97		_, isEnumValue := vs.(protoreflect.EnumValueDescriptors)
 98		for i := 0; i < vs.Len(); i++ {
 99			m := reflect.ValueOf(vs).MethodByName("Get")
100			v := m.Call([]reflect.Value{reflect.ValueOf(i)})[0].Interface()
101			ss = append(ss, formatDescOpt(v.(protoreflect.Descriptor), false, allowMulti && !isEnumValue, nil))
102		}
103		return start + joinStrings(ss, allowMulti && isEnumValue) + end
104	}
105}
106
107type methodAndName struct {
108	method reflect.Value
109	name   string
110}
111
112func FormatDesc(s fmt.State, r rune, t protoreflect.Descriptor) {
113	io.WriteString(s, formatDescOpt(t, true, r == 'v' && (s.Flag('+') || s.Flag('#')), nil))
114}
115
116func InternalFormatDescOptForTesting(t protoreflect.Descriptor, isRoot, allowMulti bool, record func(string)) string {
117	return formatDescOpt(t, isRoot, allowMulti, record)
118}
119
120func formatDescOpt(t protoreflect.Descriptor, isRoot, allowMulti bool, record func(string)) string {
121	rv := reflect.ValueOf(t)
122	rt := rv.MethodByName("ProtoType").Type().In(0)
123
124	start, end := "{", "}"
125	if isRoot {
126		start = rt.Name() + "{"
127	}
128
129	_, isFile := t.(protoreflect.FileDescriptor)
130	rs := records{
131		allowMulti: allowMulti,
132		record:     record,
133	}
134	if t.IsPlaceholder() {
135		if isFile {
136			rs.Append(rv, []methodAndName{
137				{rv.MethodByName("Path"), "Path"},
138				{rv.MethodByName("Package"), "Package"},
139				{rv.MethodByName("IsPlaceholder"), "IsPlaceholder"},
140			}...)
141		} else {
142			rs.Append(rv, []methodAndName{
143				{rv.MethodByName("FullName"), "FullName"},
144				{rv.MethodByName("IsPlaceholder"), "IsPlaceholder"},
145			}...)
146		}
147	} else {
148		switch {
149		case isFile:
150			rs.Append(rv, methodAndName{rv.MethodByName("Syntax"), "Syntax"})
151		case isRoot:
152			rs.Append(rv, []methodAndName{
153				{rv.MethodByName("Syntax"), "Syntax"},
154				{rv.MethodByName("FullName"), "FullName"},
155			}...)
156		default:
157			rs.Append(rv, methodAndName{rv.MethodByName("Name"), "Name"})
158		}
159		switch t := t.(type) {
160		case protoreflect.FieldDescriptor:
161			accessors := []methodAndName{
162				{rv.MethodByName("Number"), "Number"},
163				{rv.MethodByName("Cardinality"), "Cardinality"},
164				{rv.MethodByName("Kind"), "Kind"},
165				{rv.MethodByName("HasJSONName"), "HasJSONName"},
166				{rv.MethodByName("JSONName"), "JSONName"},
167				{rv.MethodByName("HasPresence"), "HasPresence"},
168				{rv.MethodByName("IsExtension"), "IsExtension"},
169				{rv.MethodByName("IsPacked"), "IsPacked"},
170				{rv.MethodByName("IsWeak"), "IsWeak"},
171				{rv.MethodByName("IsList"), "IsList"},
172				{rv.MethodByName("IsMap"), "IsMap"},
173				{rv.MethodByName("MapKey"), "MapKey"},
174				{rv.MethodByName("MapValue"), "MapValue"},
175				{rv.MethodByName("HasDefault"), "HasDefault"},
176				{rv.MethodByName("Default"), "Default"},
177				{rv.MethodByName("ContainingOneof"), "ContainingOneof"},
178				{rv.MethodByName("ContainingMessage"), "ContainingMessage"},
179				{rv.MethodByName("Message"), "Message"},
180				{rv.MethodByName("Enum"), "Enum"},
181			}
182			for _, s := range accessors {
183				switch s.name {
184				case "MapKey":
185					if k := t.MapKey(); k != nil {
186						rs.recs = append(rs.recs, [2]string{"MapKey", k.Kind().String()})
187					}
188				case "MapValue":
189					if v := t.MapValue(); v != nil {
190						switch v.Kind() {
191						case protoreflect.EnumKind:
192							rs.AppendRecs("MapValue", [2]string{"MapValue", string(v.Enum().FullName())})
193						case protoreflect.MessageKind, protoreflect.GroupKind:
194							rs.AppendRecs("MapValue", [2]string{"MapValue", string(v.Message().FullName())})
195						default:
196							rs.AppendRecs("MapValue", [2]string{"MapValue", v.Kind().String()})
197						}
198					}
199				case "ContainingOneof":
200					if od := t.ContainingOneof(); od != nil {
201						rs.AppendRecs("ContainingOneof", [2]string{"Oneof", string(od.Name())})
202					}
203				case "ContainingMessage":
204					if t.IsExtension() {
205						rs.AppendRecs("ContainingMessage", [2]string{"Extendee", string(t.ContainingMessage().FullName())})
206					}
207				case "Message":
208					if !t.IsMap() {
209						rs.Append(rv, s)
210					}
211				default:
212					rs.Append(rv, s)
213				}
214			}
215		case protoreflect.OneofDescriptor:
216			var ss []string
217			fs := t.Fields()
218			for i := 0; i < fs.Len(); i++ {
219				ss = append(ss, string(fs.Get(i).Name()))
220			}
221			if len(ss) > 0 {
222				rs.AppendRecs("Fields", [2]string{"Fields", "[" + joinStrings(ss, false) + "]"})
223			}
224
225		case protoreflect.FileDescriptor:
226			rs.Append(rv, []methodAndName{
227				{rv.MethodByName("Path"), "Path"},
228				{rv.MethodByName("Package"), "Package"},
229				{rv.MethodByName("Imports"), "Imports"},
230				{rv.MethodByName("Messages"), "Messages"},
231				{rv.MethodByName("Enums"), "Enums"},
232				{rv.MethodByName("Extensions"), "Extensions"},
233				{rv.MethodByName("Services"), "Services"},
234			}...)
235
236		case protoreflect.MessageDescriptor:
237			rs.Append(rv, []methodAndName{
238				{rv.MethodByName("IsMapEntry"), "IsMapEntry"},
239				{rv.MethodByName("Fields"), "Fields"},
240				{rv.MethodByName("Oneofs"), "Oneofs"},
241				{rv.MethodByName("ReservedNames"), "ReservedNames"},
242				{rv.MethodByName("ReservedRanges"), "ReservedRanges"},
243				{rv.MethodByName("RequiredNumbers"), "RequiredNumbers"},
244				{rv.MethodByName("ExtensionRanges"), "ExtensionRanges"},
245				{rv.MethodByName("Messages"), "Messages"},
246				{rv.MethodByName("Enums"), "Enums"},
247				{rv.MethodByName("Extensions"), "Extensions"},
248			}...)
249
250		case protoreflect.EnumDescriptor:
251			rs.Append(rv, []methodAndName{
252				{rv.MethodByName("Values"), "Values"},
253				{rv.MethodByName("ReservedNames"), "ReservedNames"},
254				{rv.MethodByName("ReservedRanges"), "ReservedRanges"},
255				{rv.MethodByName("IsClosed"), "IsClosed"},
256			}...)
257
258		case protoreflect.EnumValueDescriptor:
259			rs.Append(rv, []methodAndName{
260				{rv.MethodByName("Number"), "Number"},
261			}...)
262
263		case protoreflect.ServiceDescriptor:
264			rs.Append(rv, []methodAndName{
265				{rv.MethodByName("Methods"), "Methods"},
266			}...)
267
268		case protoreflect.MethodDescriptor:
269			rs.Append(rv, []methodAndName{
270				{rv.MethodByName("Input"), "Input"},
271				{rv.MethodByName("Output"), "Output"},
272				{rv.MethodByName("IsStreamingClient"), "IsStreamingClient"},
273				{rv.MethodByName("IsStreamingServer"), "IsStreamingServer"},
274			}...)
275		}
276		if m := rv.MethodByName("GoType"); m.IsValid() {
277			rs.Append(rv, methodAndName{m, "GoType"})
278		}
279	}
280	return start + rs.Join() + end
281}
282
283type records struct {
284	recs       [][2]string
285	allowMulti bool
286
287	// record is a function that will be called for every Append() or
288	// AppendRecs() call, to be used for testing with the
289	// InternalFormatDescOptForTesting function.
290	record func(string)
291}
292
293func (rs *records) AppendRecs(fieldName string, newRecs [2]string) {
294	if rs.record != nil {
295		rs.record(fieldName)
296	}
297	rs.recs = append(rs.recs, newRecs)
298}
299
300func (rs *records) Append(v reflect.Value, accessors ...methodAndName) {
301	for _, a := range accessors {
302		if rs.record != nil {
303			rs.record(a.name)
304		}
305		var rv reflect.Value
306		if a.method.IsValid() {
307			rv = a.method.Call(nil)[0]
308		}
309		if v.Kind() == reflect.Struct && !rv.IsValid() {
310			rv = v.FieldByName(a.name)
311		}
312		if !rv.IsValid() {
313			panic(fmt.Sprintf("unknown accessor: %v.%s", v.Type(), a.name))
314		}
315		if _, ok := rv.Interface().(protoreflect.Value); ok {
316			rv = rv.MethodByName("Interface").Call(nil)[0]
317			if !rv.IsNil() {
318				rv = rv.Elem()
319			}
320		}
321
322		// Ignore zero values.
323		var isZero bool
324		switch rv.Kind() {
325		case reflect.Interface, reflect.Slice:
326			isZero = rv.IsNil()
327		case reflect.Bool:
328			isZero = rv.Bool() == false
329		case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
330			isZero = rv.Int() == 0
331		case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
332			isZero = rv.Uint() == 0
333		case reflect.String:
334			isZero = rv.String() == ""
335		}
336		if n, ok := rv.Interface().(list); ok {
337			isZero = n.Len() == 0
338		}
339		if isZero {
340			continue
341		}
342
343		// Format the value.
344		var s string
345		v := rv.Interface()
346		switch v := v.(type) {
347		case list:
348			s = formatListOpt(v, false, rs.allowMulti)
349		case protoreflect.FieldDescriptor, protoreflect.OneofDescriptor, protoreflect.EnumValueDescriptor, protoreflect.MethodDescriptor:
350			s = string(v.(protoreflect.Descriptor).Name())
351		case protoreflect.Descriptor:
352			s = string(v.FullName())
353		case string:
354			s = strconv.Quote(v)
355		case []byte:
356			s = fmt.Sprintf("%q", v)
357		default:
358			s = fmt.Sprint(v)
359		}
360		rs.recs = append(rs.recs, [2]string{a.name, s})
361	}
362}
363
364func (rs *records) Join() string {
365	var ss []string
366
367	// In single line mode, simply join all records with commas.
368	if !rs.allowMulti {
369		for _, r := range rs.recs {
370			ss = append(ss, r[0]+formatColon(0)+r[1])
371		}
372		return joinStrings(ss, false)
373	}
374
375	// In allowMulti line mode, align single line records for more readable output.
376	var maxLen int
377	flush := func(i int) {
378		for _, r := range rs.recs[len(ss):i] {
379			ss = append(ss, r[0]+formatColon(maxLen-len(r[0]))+r[1])
380		}
381		maxLen = 0
382	}
383	for i, r := range rs.recs {
384		if isMulti := strings.Contains(r[1], "\n"); isMulti {
385			flush(i)
386			ss = append(ss, r[0]+formatColon(0)+strings.Join(strings.Split(r[1], "\n"), "\n\t"))
387		} else if maxLen < len(r[0]) {
388			maxLen = len(r[0])
389		}
390	}
391	flush(len(rs.recs))
392	return joinStrings(ss, true)
393}
394
395func formatColon(padding int) string {
396	// Deliberately introduce instability into the debug output to
397	// discourage users from performing string comparisons.
398	// This provides us flexibility to change the output in the future.
399	if detrand.Bool() {
400		return ":" + strings.Repeat(" ", 1+padding) // use non-breaking spaces (U+00a0)
401	} else {
402		return ":" + strings.Repeat(" ", 1+padding) // use regular spaces (U+0020)
403	}
404}
405
406func joinStrings(ss []string, isMulti bool) string {
407	if len(ss) == 0 {
408		return ""
409	}
410	if isMulti {
411		return "\n\t" + strings.Join(ss, "\n\t") + "\n"
412	}
413	return strings.Join(ss, ", ")
414}