| // 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 descfmt provides functionality to format descriptors. |
| package descfmt |
| |
| import ( |
| "fmt" |
| "io" |
| "reflect" |
| "strconv" |
| "strings" |
| |
| "google.golang.org/protobuf/internal/detrand" |
| "google.golang.org/protobuf/internal/pragma" |
| "google.golang.org/protobuf/reflect/protoreflect" |
| ) |
| |
| type list interface { |
| Len() int |
| pragma.DoNotImplement |
| } |
| |
| 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 protoreflect.Names: |
| name = "Names" |
| case protoreflect.FieldNumbers: |
| name = "FieldNumbers" |
| case protoreflect.FieldRanges: |
| name = "FieldRanges" |
| case protoreflect.EnumRanges: |
| name = "EnumRanges" |
| case protoreflect.FileImports: |
| name = "FileImports" |
| case protoreflect.Descriptor: |
| name = reflect.ValueOf(vs).MethodByName("Get").Type().Out(0).Name() + "s" |
| default: |
| name = reflect.ValueOf(vs).Elem().Type().Name() |
| } |
| start, end = name+"{", "}" |
| } |
| |
| var ss []string |
| switch vs := vs.(type) { |
| case protoreflect.Names: |
| for i := 0; i < vs.Len(); i++ { |
| ss = append(ss, fmt.Sprint(vs.Get(i))) |
| } |
| return start + joinStrings(ss, false) + end |
| case protoreflect.FieldNumbers: |
| for i := 0; i < vs.Len(); i++ { |
| ss = append(ss, fmt.Sprint(vs.Get(i))) |
| } |
| return start + joinStrings(ss, false) + end |
| case protoreflect.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])) // enum ranges are end exclusive |
| } |
| } |
| return start + joinStrings(ss, false) + end |
| case protoreflect.EnumRanges: |
| for i := 0; i < vs.Len(); i++ { |
| r := vs.Get(i) |
| if r[0] == r[1] { |
| ss = append(ss, fmt.Sprintf("%d", r[0])) |
| } else { |
| ss = append(ss, fmt.Sprintf("%d:%d", r[0], int64(r[1])+1)) // enum ranges are end inclusive |
| } |
| } |
| return start + joinStrings(ss, false) + end |
| case protoreflect.FileImports: |
| for i := 0; i < vs.Len(); i++ { |
| var rs records |
| rv := reflect.ValueOf(vs.Get(i)) |
| rs.Append(rv, []methodAndName{ |
| {rv.MethodByName("Path"), "Path"}, |
| {rv.MethodByName("Package"), "Package"}, |
| {rv.MethodByName("IsPublic"), "IsPublic"}, |
| {rv.MethodByName("IsWeak"), "IsWeak"}, |
| }...) |
| ss = append(ss, "{"+rs.Join()+"}") |
| } |
| return start + joinStrings(ss, allowMulti) + end |
| default: |
| _, isEnumValue := vs.(protoreflect.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.(protoreflect.Descriptor), false, allowMulti && !isEnumValue, nil)) |
| } |
| return start + joinStrings(ss, allowMulti && isEnumValue) + end |
| } |
| } |
| |
| type methodAndName struct { |
| method reflect.Value |
| name string |
| } |
| |
| func FormatDesc(s fmt.State, r rune, t protoreflect.Descriptor) { |
| io.WriteString(s, formatDescOpt(t, true, r == 'v' && (s.Flag('+') || s.Flag('#')), nil)) |
| } |
| |
| func InternalFormatDescOptForTesting(t protoreflect.Descriptor, isRoot, allowMulti bool, record func(string)) string { |
| return formatDescOpt(t, isRoot, allowMulti, record) |
| } |
| |
| func formatDescOpt(t protoreflect.Descriptor, isRoot, allowMulti bool, record func(string)) string { |
| rv := reflect.ValueOf(t) |
| rt := rv.MethodByName("ProtoType").Type().In(0) |
| |
| start, end := "{", "}" |
| if isRoot { |
| start = rt.Name() + "{" |
| } |
| |
| _, isFile := t.(protoreflect.FileDescriptor) |
| rs := records{ |
| allowMulti: allowMulti, |
| record: record, |
| } |
| if t.IsPlaceholder() { |
| if isFile { |
| rs.Append(rv, []methodAndName{ |
| {rv.MethodByName("Path"), "Path"}, |
| {rv.MethodByName("Package"), "Package"}, |
| {rv.MethodByName("IsPlaceholder"), "IsPlaceholder"}, |
| }...) |
| } else { |
| rs.Append(rv, []methodAndName{ |
| {rv.MethodByName("FullName"), "FullName"}, |
| {rv.MethodByName("IsPlaceholder"), "IsPlaceholder"}, |
| }...) |
| } |
| } else { |
| switch { |
| case isFile: |
| rs.Append(rv, methodAndName{rv.MethodByName("Syntax"), "Syntax"}) |
| case isRoot: |
| rs.Append(rv, []methodAndName{ |
| {rv.MethodByName("Syntax"), "Syntax"}, |
| {rv.MethodByName("FullName"), "FullName"}, |
| }...) |
| default: |
| rs.Append(rv, methodAndName{rv.MethodByName("Name"), "Name"}) |
| } |
| switch t := t.(type) { |
| case protoreflect.FieldDescriptor: |
| accessors := []methodAndName{ |
| {rv.MethodByName("Number"), "Number"}, |
| {rv.MethodByName("Cardinality"), "Cardinality"}, |
| {rv.MethodByName("Kind"), "Kind"}, |
| {rv.MethodByName("HasJSONName"), "HasJSONName"}, |
| {rv.MethodByName("JSONName"), "JSONName"}, |
| {rv.MethodByName("HasPresence"), "HasPresence"}, |
| {rv.MethodByName("IsExtension"), "IsExtension"}, |
| {rv.MethodByName("IsPacked"), "IsPacked"}, |
| {rv.MethodByName("IsWeak"), "IsWeak"}, |
| {rv.MethodByName("IsList"), "IsList"}, |
| {rv.MethodByName("IsMap"), "IsMap"}, |
| {rv.MethodByName("MapKey"), "MapKey"}, |
| {rv.MethodByName("MapValue"), "MapValue"}, |
| {rv.MethodByName("HasDefault"), "HasDefault"}, |
| {rv.MethodByName("Default"), "Default"}, |
| {rv.MethodByName("ContainingOneof"), "ContainingOneof"}, |
| {rv.MethodByName("ContainingMessage"), "ContainingMessage"}, |
| {rv.MethodByName("Message"), "Message"}, |
| {rv.MethodByName("Enum"), "Enum"}, |
| } |
| for _, s := range accessors { |
| switch s.name { |
| case "MapKey": |
| if k := t.MapKey(); k != nil { |
| rs.recs = append(rs.recs, [2]string{"MapKey", k.Kind().String()}) |
| } |
| case "MapValue": |
| if v := t.MapValue(); v != nil { |
| switch v.Kind() { |
| case protoreflect.EnumKind: |
| rs.AppendRecs("MapValue", [2]string{"MapValue", string(v.Enum().FullName())}) |
| case protoreflect.MessageKind, protoreflect.GroupKind: |
| rs.AppendRecs("MapValue", [2]string{"MapValue", string(v.Message().FullName())}) |
| default: |
| rs.AppendRecs("MapValue", [2]string{"MapValue", v.Kind().String()}) |
| } |
| } |
| case "ContainingOneof": |
| if od := t.ContainingOneof(); od != nil { |
| rs.AppendRecs("ContainingOneof", [2]string{"Oneof", string(od.Name())}) |
| } |
| case "ContainingMessage": |
| if t.IsExtension() { |
| rs.AppendRecs("ContainingMessage", [2]string{"Extendee", string(t.ContainingMessage().FullName())}) |
| } |
| case "Message": |
| if !t.IsMap() { |
| rs.Append(rv, s) |
| } |
| default: |
| rs.Append(rv, s) |
| } |
| } |
| case protoreflect.OneofDescriptor: |
| 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.AppendRecs("Fields", [2]string{"Fields", "[" + joinStrings(ss, false) + "]"}) |
| } |
| |
| case protoreflect.FileDescriptor: |
| rs.Append(rv, []methodAndName{ |
| {rv.MethodByName("Path"), "Path"}, |
| {rv.MethodByName("Package"), "Package"}, |
| {rv.MethodByName("Imports"), "Imports"}, |
| {rv.MethodByName("Messages"), "Messages"}, |
| {rv.MethodByName("Enums"), "Enums"}, |
| {rv.MethodByName("Extensions"), "Extensions"}, |
| {rv.MethodByName("Services"), "Services"}, |
| }...) |
| |
| case protoreflect.MessageDescriptor: |
| rs.Append(rv, []methodAndName{ |
| {rv.MethodByName("IsMapEntry"), "IsMapEntry"}, |
| {rv.MethodByName("Fields"), "Fields"}, |
| {rv.MethodByName("Oneofs"), "Oneofs"}, |
| {rv.MethodByName("ReservedNames"), "ReservedNames"}, |
| {rv.MethodByName("ReservedRanges"), "ReservedRanges"}, |
| {rv.MethodByName("RequiredNumbers"), "RequiredNumbers"}, |
| {rv.MethodByName("ExtensionRanges"), "ExtensionRanges"}, |
| {rv.MethodByName("Messages"), "Messages"}, |
| {rv.MethodByName("Enums"), "Enums"}, |
| {rv.MethodByName("Extensions"), "Extensions"}, |
| }...) |
| |
| case protoreflect.EnumDescriptor: |
| rs.Append(rv, []methodAndName{ |
| {rv.MethodByName("Values"), "Values"}, |
| {rv.MethodByName("ReservedNames"), "ReservedNames"}, |
| {rv.MethodByName("ReservedRanges"), "ReservedRanges"}, |
| {rv.MethodByName("IsClosed"), "IsClosed"}, |
| }...) |
| |
| case protoreflect.EnumValueDescriptor: |
| rs.Append(rv, []methodAndName{ |
| {rv.MethodByName("Number"), "Number"}, |
| }...) |
| |
| case protoreflect.ServiceDescriptor: |
| rs.Append(rv, []methodAndName{ |
| {rv.MethodByName("Methods"), "Methods"}, |
| }...) |
| |
| case protoreflect.MethodDescriptor: |
| rs.Append(rv, []methodAndName{ |
| {rv.MethodByName("Input"), "Input"}, |
| {rv.MethodByName("Output"), "Output"}, |
| {rv.MethodByName("IsStreamingClient"), "IsStreamingClient"}, |
| {rv.MethodByName("IsStreamingServer"), "IsStreamingServer"}, |
| }...) |
| } |
| if m := rv.MethodByName("GoType"); m.IsValid() { |
| rs.Append(rv, methodAndName{m, "GoType"}) |
| } |
| } |
| return start + rs.Join() + end |
| } |
| |
| type records struct { |
| recs [][2]string |
| allowMulti bool |
| |
| // record is a function that will be called for every Append() or |
| // AppendRecs() call, to be used for testing with the |
| // InternalFormatDescOptForTesting function. |
| record func(string) |
| } |
| |
| func (rs *records) AppendRecs(fieldName string, newRecs [2]string) { |
| if rs.record != nil { |
| rs.record(fieldName) |
| } |
| rs.recs = append(rs.recs, newRecs) |
| } |
| |
| func (rs *records) Append(v reflect.Value, accessors ...methodAndName) { |
| for _, a := range accessors { |
| if rs.record != nil { |
| rs.record(a.name) |
| } |
| var rv reflect.Value |
| if a.method.IsValid() { |
| rv = a.method.Call(nil)[0] |
| } |
| if v.Kind() == reflect.Struct && !rv.IsValid() { |
| rv = v.FieldByName(a.name) |
| } |
| if !rv.IsValid() { |
| panic(fmt.Sprintf("unknown accessor: %v.%s", v.Type(), a.name)) |
| } |
| if _, ok := rv.Interface().(protoreflect.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 protoreflect.FieldDescriptor, protoreflect.OneofDescriptor, protoreflect.EnumValueDescriptor, protoreflect.MethodDescriptor: |
| s = string(v.(protoreflect.Descriptor).Name()) |
| case protoreflect.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.name, 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]+formatColon(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] { |
| ss = append(ss, r[0]+formatColon(maxLen-len(r[0]))+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]+formatColon(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 formatColon(padding int) string { |
| // Deliberately introduce instability into the debug output to |
| // discourage users from performing string comparisons. |
| // This provides us flexibility to change the output in the future. |
| if detrand.Bool() { |
| return ":" + strings.Repeat(" ", 1+padding) // use non-breaking spaces (U+00a0) |
| } else { |
| return ":" + strings.Repeat(" ", 1+padding) // use regular spaces (U+0020) |
| } |
| } |
| |
| 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, ", ") |
| } |