// 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, ", ") }