xref: /aosp_15_r20/external/golang-protobuf/internal/descfmt/stringer.go (revision 1c12ee1efe575feb122dbf939ff15148a3b3e8f2)
1*1c12ee1eSDan Willemsen// Copyright 2018 The Go Authors. All rights reserved.
2*1c12ee1eSDan Willemsen// Use of this source code is governed by a BSD-style
3*1c12ee1eSDan Willemsen// license that can be found in the LICENSE file.
4*1c12ee1eSDan Willemsen
5*1c12ee1eSDan Willemsen// Package descfmt provides functionality to format descriptors.
6*1c12ee1eSDan Willemsenpackage descfmt
7*1c12ee1eSDan Willemsen
8*1c12ee1eSDan Willemsenimport (
9*1c12ee1eSDan Willemsen	"fmt"
10*1c12ee1eSDan Willemsen	"io"
11*1c12ee1eSDan Willemsen	"reflect"
12*1c12ee1eSDan Willemsen	"strconv"
13*1c12ee1eSDan Willemsen	"strings"
14*1c12ee1eSDan Willemsen
15*1c12ee1eSDan Willemsen	"google.golang.org/protobuf/internal/detrand"
16*1c12ee1eSDan Willemsen	"google.golang.org/protobuf/internal/pragma"
17*1c12ee1eSDan Willemsen	"google.golang.org/protobuf/reflect/protoreflect"
18*1c12ee1eSDan Willemsen)
19*1c12ee1eSDan Willemsen
20*1c12ee1eSDan Willemsentype list interface {
21*1c12ee1eSDan Willemsen	Len() int
22*1c12ee1eSDan Willemsen	pragma.DoNotImplement
23*1c12ee1eSDan Willemsen}
24*1c12ee1eSDan Willemsen
25*1c12ee1eSDan Willemsenfunc FormatList(s fmt.State, r rune, vs list) {
26*1c12ee1eSDan Willemsen	io.WriteString(s, formatListOpt(vs, true, r == 'v' && (s.Flag('+') || s.Flag('#'))))
27*1c12ee1eSDan Willemsen}
28*1c12ee1eSDan Willemsenfunc formatListOpt(vs list, isRoot, allowMulti bool) string {
29*1c12ee1eSDan Willemsen	start, end := "[", "]"
30*1c12ee1eSDan Willemsen	if isRoot {
31*1c12ee1eSDan Willemsen		var name string
32*1c12ee1eSDan Willemsen		switch vs.(type) {
33*1c12ee1eSDan Willemsen		case protoreflect.Names:
34*1c12ee1eSDan Willemsen			name = "Names"
35*1c12ee1eSDan Willemsen		case protoreflect.FieldNumbers:
36*1c12ee1eSDan Willemsen			name = "FieldNumbers"
37*1c12ee1eSDan Willemsen		case protoreflect.FieldRanges:
38*1c12ee1eSDan Willemsen			name = "FieldRanges"
39*1c12ee1eSDan Willemsen		case protoreflect.EnumRanges:
40*1c12ee1eSDan Willemsen			name = "EnumRanges"
41*1c12ee1eSDan Willemsen		case protoreflect.FileImports:
42*1c12ee1eSDan Willemsen			name = "FileImports"
43*1c12ee1eSDan Willemsen		case protoreflect.Descriptor:
44*1c12ee1eSDan Willemsen			name = reflect.ValueOf(vs).MethodByName("Get").Type().Out(0).Name() + "s"
45*1c12ee1eSDan Willemsen		default:
46*1c12ee1eSDan Willemsen			name = reflect.ValueOf(vs).Elem().Type().Name()
47*1c12ee1eSDan Willemsen		}
48*1c12ee1eSDan Willemsen		start, end = name+"{", "}"
49*1c12ee1eSDan Willemsen	}
50*1c12ee1eSDan Willemsen
51*1c12ee1eSDan Willemsen	var ss []string
52*1c12ee1eSDan Willemsen	switch vs := vs.(type) {
53*1c12ee1eSDan Willemsen	case protoreflect.Names:
54*1c12ee1eSDan Willemsen		for i := 0; i < vs.Len(); i++ {
55*1c12ee1eSDan Willemsen			ss = append(ss, fmt.Sprint(vs.Get(i)))
56*1c12ee1eSDan Willemsen		}
57*1c12ee1eSDan Willemsen		return start + joinStrings(ss, false) + end
58*1c12ee1eSDan Willemsen	case protoreflect.FieldNumbers:
59*1c12ee1eSDan Willemsen		for i := 0; i < vs.Len(); i++ {
60*1c12ee1eSDan Willemsen			ss = append(ss, fmt.Sprint(vs.Get(i)))
61*1c12ee1eSDan Willemsen		}
62*1c12ee1eSDan Willemsen		return start + joinStrings(ss, false) + end
63*1c12ee1eSDan Willemsen	case protoreflect.FieldRanges:
64*1c12ee1eSDan Willemsen		for i := 0; i < vs.Len(); i++ {
65*1c12ee1eSDan Willemsen			r := vs.Get(i)
66*1c12ee1eSDan Willemsen			if r[0]+1 == r[1] {
67*1c12ee1eSDan Willemsen				ss = append(ss, fmt.Sprintf("%d", r[0]))
68*1c12ee1eSDan Willemsen			} else {
69*1c12ee1eSDan Willemsen				ss = append(ss, fmt.Sprintf("%d:%d", r[0], r[1])) // enum ranges are end exclusive
70*1c12ee1eSDan Willemsen			}
71*1c12ee1eSDan Willemsen		}
72*1c12ee1eSDan Willemsen		return start + joinStrings(ss, false) + end
73*1c12ee1eSDan Willemsen	case protoreflect.EnumRanges:
74*1c12ee1eSDan Willemsen		for i := 0; i < vs.Len(); i++ {
75*1c12ee1eSDan Willemsen			r := vs.Get(i)
76*1c12ee1eSDan Willemsen			if r[0] == r[1] {
77*1c12ee1eSDan Willemsen				ss = append(ss, fmt.Sprintf("%d", r[0]))
78*1c12ee1eSDan Willemsen			} else {
79*1c12ee1eSDan Willemsen				ss = append(ss, fmt.Sprintf("%d:%d", r[0], int64(r[1])+1)) // enum ranges are end inclusive
80*1c12ee1eSDan Willemsen			}
81*1c12ee1eSDan Willemsen		}
82*1c12ee1eSDan Willemsen		return start + joinStrings(ss, false) + end
83*1c12ee1eSDan Willemsen	case protoreflect.FileImports:
84*1c12ee1eSDan Willemsen		for i := 0; i < vs.Len(); i++ {
85*1c12ee1eSDan Willemsen			var rs records
86*1c12ee1eSDan Willemsen			rs.Append(reflect.ValueOf(vs.Get(i)), "Path", "Package", "IsPublic", "IsWeak")
87*1c12ee1eSDan Willemsen			ss = append(ss, "{"+rs.Join()+"}")
88*1c12ee1eSDan Willemsen		}
89*1c12ee1eSDan Willemsen		return start + joinStrings(ss, allowMulti) + end
90*1c12ee1eSDan Willemsen	default:
91*1c12ee1eSDan Willemsen		_, isEnumValue := vs.(protoreflect.EnumValueDescriptors)
92*1c12ee1eSDan Willemsen		for i := 0; i < vs.Len(); i++ {
93*1c12ee1eSDan Willemsen			m := reflect.ValueOf(vs).MethodByName("Get")
94*1c12ee1eSDan Willemsen			v := m.Call([]reflect.Value{reflect.ValueOf(i)})[0].Interface()
95*1c12ee1eSDan Willemsen			ss = append(ss, formatDescOpt(v.(protoreflect.Descriptor), false, allowMulti && !isEnumValue))
96*1c12ee1eSDan Willemsen		}
97*1c12ee1eSDan Willemsen		return start + joinStrings(ss, allowMulti && isEnumValue) + end
98*1c12ee1eSDan Willemsen	}
99*1c12ee1eSDan Willemsen}
100*1c12ee1eSDan Willemsen
101*1c12ee1eSDan Willemsen// descriptorAccessors is a list of accessors to print for each descriptor.
102*1c12ee1eSDan Willemsen//
103*1c12ee1eSDan Willemsen// Do not print all accessors since some contain redundant information,
104*1c12ee1eSDan Willemsen// while others are pointers that we do not want to follow since the descriptor
105*1c12ee1eSDan Willemsen// is actually a cyclic graph.
106*1c12ee1eSDan Willemsen//
107*1c12ee1eSDan Willemsen// Using a list allows us to print the accessors in a sensible order.
108*1c12ee1eSDan Willemsenvar descriptorAccessors = map[reflect.Type][]string{
109*1c12ee1eSDan Willemsen	reflect.TypeOf((*protoreflect.FileDescriptor)(nil)).Elem():      {"Path", "Package", "Imports", "Messages", "Enums", "Extensions", "Services"},
110*1c12ee1eSDan Willemsen	reflect.TypeOf((*protoreflect.MessageDescriptor)(nil)).Elem():   {"IsMapEntry", "Fields", "Oneofs", "ReservedNames", "ReservedRanges", "RequiredNumbers", "ExtensionRanges", "Messages", "Enums", "Extensions"},
111*1c12ee1eSDan Willemsen	reflect.TypeOf((*protoreflect.FieldDescriptor)(nil)).Elem():     {"Number", "Cardinality", "Kind", "HasJSONName", "JSONName", "HasPresence", "IsExtension", "IsPacked", "IsWeak", "IsList", "IsMap", "MapKey", "MapValue", "HasDefault", "Default", "ContainingOneof", "ContainingMessage", "Message", "Enum"},
112*1c12ee1eSDan Willemsen	reflect.TypeOf((*protoreflect.OneofDescriptor)(nil)).Elem():     {"Fields"}, // not directly used; must keep in sync with formatDescOpt
113*1c12ee1eSDan Willemsen	reflect.TypeOf((*protoreflect.EnumDescriptor)(nil)).Elem():      {"Values", "ReservedNames", "ReservedRanges"},
114*1c12ee1eSDan Willemsen	reflect.TypeOf((*protoreflect.EnumValueDescriptor)(nil)).Elem(): {"Number"},
115*1c12ee1eSDan Willemsen	reflect.TypeOf((*protoreflect.ServiceDescriptor)(nil)).Elem():   {"Methods"},
116*1c12ee1eSDan Willemsen	reflect.TypeOf((*protoreflect.MethodDescriptor)(nil)).Elem():    {"Input", "Output", "IsStreamingClient", "IsStreamingServer"},
117*1c12ee1eSDan Willemsen}
118*1c12ee1eSDan Willemsen
119*1c12ee1eSDan Willemsenfunc FormatDesc(s fmt.State, r rune, t protoreflect.Descriptor) {
120*1c12ee1eSDan Willemsen	io.WriteString(s, formatDescOpt(t, true, r == 'v' && (s.Flag('+') || s.Flag('#'))))
121*1c12ee1eSDan Willemsen}
122*1c12ee1eSDan Willemsenfunc formatDescOpt(t protoreflect.Descriptor, isRoot, allowMulti bool) string {
123*1c12ee1eSDan Willemsen	rv := reflect.ValueOf(t)
124*1c12ee1eSDan Willemsen	rt := rv.MethodByName("ProtoType").Type().In(0)
125*1c12ee1eSDan Willemsen
126*1c12ee1eSDan Willemsen	start, end := "{", "}"
127*1c12ee1eSDan Willemsen	if isRoot {
128*1c12ee1eSDan Willemsen		start = rt.Name() + "{"
129*1c12ee1eSDan Willemsen	}
130*1c12ee1eSDan Willemsen
131*1c12ee1eSDan Willemsen	_, isFile := t.(protoreflect.FileDescriptor)
132*1c12ee1eSDan Willemsen	rs := records{allowMulti: allowMulti}
133*1c12ee1eSDan Willemsen	if t.IsPlaceholder() {
134*1c12ee1eSDan Willemsen		if isFile {
135*1c12ee1eSDan Willemsen			rs.Append(rv, "Path", "Package", "IsPlaceholder")
136*1c12ee1eSDan Willemsen		} else {
137*1c12ee1eSDan Willemsen			rs.Append(rv, "FullName", "IsPlaceholder")
138*1c12ee1eSDan Willemsen		}
139*1c12ee1eSDan Willemsen	} else {
140*1c12ee1eSDan Willemsen		switch {
141*1c12ee1eSDan Willemsen		case isFile:
142*1c12ee1eSDan Willemsen			rs.Append(rv, "Syntax")
143*1c12ee1eSDan Willemsen		case isRoot:
144*1c12ee1eSDan Willemsen			rs.Append(rv, "Syntax", "FullName")
145*1c12ee1eSDan Willemsen		default:
146*1c12ee1eSDan Willemsen			rs.Append(rv, "Name")
147*1c12ee1eSDan Willemsen		}
148*1c12ee1eSDan Willemsen		switch t := t.(type) {
149*1c12ee1eSDan Willemsen		case protoreflect.FieldDescriptor:
150*1c12ee1eSDan Willemsen			for _, s := range descriptorAccessors[rt] {
151*1c12ee1eSDan Willemsen				switch s {
152*1c12ee1eSDan Willemsen				case "MapKey":
153*1c12ee1eSDan Willemsen					if k := t.MapKey(); k != nil {
154*1c12ee1eSDan Willemsen						rs.recs = append(rs.recs, [2]string{"MapKey", k.Kind().String()})
155*1c12ee1eSDan Willemsen					}
156*1c12ee1eSDan Willemsen				case "MapValue":
157*1c12ee1eSDan Willemsen					if v := t.MapValue(); v != nil {
158*1c12ee1eSDan Willemsen						switch v.Kind() {
159*1c12ee1eSDan Willemsen						case protoreflect.EnumKind:
160*1c12ee1eSDan Willemsen							rs.recs = append(rs.recs, [2]string{"MapValue", string(v.Enum().FullName())})
161*1c12ee1eSDan Willemsen						case protoreflect.MessageKind, protoreflect.GroupKind:
162*1c12ee1eSDan Willemsen							rs.recs = append(rs.recs, [2]string{"MapValue", string(v.Message().FullName())})
163*1c12ee1eSDan Willemsen						default:
164*1c12ee1eSDan Willemsen							rs.recs = append(rs.recs, [2]string{"MapValue", v.Kind().String()})
165*1c12ee1eSDan Willemsen						}
166*1c12ee1eSDan Willemsen					}
167*1c12ee1eSDan Willemsen				case "ContainingOneof":
168*1c12ee1eSDan Willemsen					if od := t.ContainingOneof(); od != nil {
169*1c12ee1eSDan Willemsen						rs.recs = append(rs.recs, [2]string{"Oneof", string(od.Name())})
170*1c12ee1eSDan Willemsen					}
171*1c12ee1eSDan Willemsen				case "ContainingMessage":
172*1c12ee1eSDan Willemsen					if t.IsExtension() {
173*1c12ee1eSDan Willemsen						rs.recs = append(rs.recs, [2]string{"Extendee", string(t.ContainingMessage().FullName())})
174*1c12ee1eSDan Willemsen					}
175*1c12ee1eSDan Willemsen				case "Message":
176*1c12ee1eSDan Willemsen					if !t.IsMap() {
177*1c12ee1eSDan Willemsen						rs.Append(rv, s)
178*1c12ee1eSDan Willemsen					}
179*1c12ee1eSDan Willemsen				default:
180*1c12ee1eSDan Willemsen					rs.Append(rv, s)
181*1c12ee1eSDan Willemsen				}
182*1c12ee1eSDan Willemsen			}
183*1c12ee1eSDan Willemsen		case protoreflect.OneofDescriptor:
184*1c12ee1eSDan Willemsen			var ss []string
185*1c12ee1eSDan Willemsen			fs := t.Fields()
186*1c12ee1eSDan Willemsen			for i := 0; i < fs.Len(); i++ {
187*1c12ee1eSDan Willemsen				ss = append(ss, string(fs.Get(i).Name()))
188*1c12ee1eSDan Willemsen			}
189*1c12ee1eSDan Willemsen			if len(ss) > 0 {
190*1c12ee1eSDan Willemsen				rs.recs = append(rs.recs, [2]string{"Fields", "[" + joinStrings(ss, false) + "]"})
191*1c12ee1eSDan Willemsen			}
192*1c12ee1eSDan Willemsen		default:
193*1c12ee1eSDan Willemsen			rs.Append(rv, descriptorAccessors[rt]...)
194*1c12ee1eSDan Willemsen		}
195*1c12ee1eSDan Willemsen		if rv.MethodByName("GoType").IsValid() {
196*1c12ee1eSDan Willemsen			rs.Append(rv, "GoType")
197*1c12ee1eSDan Willemsen		}
198*1c12ee1eSDan Willemsen	}
199*1c12ee1eSDan Willemsen	return start + rs.Join() + end
200*1c12ee1eSDan Willemsen}
201*1c12ee1eSDan Willemsen
202*1c12ee1eSDan Willemsentype records struct {
203*1c12ee1eSDan Willemsen	recs       [][2]string
204*1c12ee1eSDan Willemsen	allowMulti bool
205*1c12ee1eSDan Willemsen}
206*1c12ee1eSDan Willemsen
207*1c12ee1eSDan Willemsenfunc (rs *records) Append(v reflect.Value, accessors ...string) {
208*1c12ee1eSDan Willemsen	for _, a := range accessors {
209*1c12ee1eSDan Willemsen		var rv reflect.Value
210*1c12ee1eSDan Willemsen		if m := v.MethodByName(a); m.IsValid() {
211*1c12ee1eSDan Willemsen			rv = m.Call(nil)[0]
212*1c12ee1eSDan Willemsen		}
213*1c12ee1eSDan Willemsen		if v.Kind() == reflect.Struct && !rv.IsValid() {
214*1c12ee1eSDan Willemsen			rv = v.FieldByName(a)
215*1c12ee1eSDan Willemsen		}
216*1c12ee1eSDan Willemsen		if !rv.IsValid() {
217*1c12ee1eSDan Willemsen			panic(fmt.Sprintf("unknown accessor: %v.%s", v.Type(), a))
218*1c12ee1eSDan Willemsen		}
219*1c12ee1eSDan Willemsen		if _, ok := rv.Interface().(protoreflect.Value); ok {
220*1c12ee1eSDan Willemsen			rv = rv.MethodByName("Interface").Call(nil)[0]
221*1c12ee1eSDan Willemsen			if !rv.IsNil() {
222*1c12ee1eSDan Willemsen				rv = rv.Elem()
223*1c12ee1eSDan Willemsen			}
224*1c12ee1eSDan Willemsen		}
225*1c12ee1eSDan Willemsen
226*1c12ee1eSDan Willemsen		// Ignore zero values.
227*1c12ee1eSDan Willemsen		var isZero bool
228*1c12ee1eSDan Willemsen		switch rv.Kind() {
229*1c12ee1eSDan Willemsen		case reflect.Interface, reflect.Slice:
230*1c12ee1eSDan Willemsen			isZero = rv.IsNil()
231*1c12ee1eSDan Willemsen		case reflect.Bool:
232*1c12ee1eSDan Willemsen			isZero = rv.Bool() == false
233*1c12ee1eSDan Willemsen		case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
234*1c12ee1eSDan Willemsen			isZero = rv.Int() == 0
235*1c12ee1eSDan Willemsen		case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
236*1c12ee1eSDan Willemsen			isZero = rv.Uint() == 0
237*1c12ee1eSDan Willemsen		case reflect.String:
238*1c12ee1eSDan Willemsen			isZero = rv.String() == ""
239*1c12ee1eSDan Willemsen		}
240*1c12ee1eSDan Willemsen		if n, ok := rv.Interface().(list); ok {
241*1c12ee1eSDan Willemsen			isZero = n.Len() == 0
242*1c12ee1eSDan Willemsen		}
243*1c12ee1eSDan Willemsen		if isZero {
244*1c12ee1eSDan Willemsen			continue
245*1c12ee1eSDan Willemsen		}
246*1c12ee1eSDan Willemsen
247*1c12ee1eSDan Willemsen		// Format the value.
248*1c12ee1eSDan Willemsen		var s string
249*1c12ee1eSDan Willemsen		v := rv.Interface()
250*1c12ee1eSDan Willemsen		switch v := v.(type) {
251*1c12ee1eSDan Willemsen		case list:
252*1c12ee1eSDan Willemsen			s = formatListOpt(v, false, rs.allowMulti)
253*1c12ee1eSDan Willemsen		case protoreflect.FieldDescriptor, protoreflect.OneofDescriptor, protoreflect.EnumValueDescriptor, protoreflect.MethodDescriptor:
254*1c12ee1eSDan Willemsen			s = string(v.(protoreflect.Descriptor).Name())
255*1c12ee1eSDan Willemsen		case protoreflect.Descriptor:
256*1c12ee1eSDan Willemsen			s = string(v.FullName())
257*1c12ee1eSDan Willemsen		case string:
258*1c12ee1eSDan Willemsen			s = strconv.Quote(v)
259*1c12ee1eSDan Willemsen		case []byte:
260*1c12ee1eSDan Willemsen			s = fmt.Sprintf("%q", v)
261*1c12ee1eSDan Willemsen		default:
262*1c12ee1eSDan Willemsen			s = fmt.Sprint(v)
263*1c12ee1eSDan Willemsen		}
264*1c12ee1eSDan Willemsen		rs.recs = append(rs.recs, [2]string{a, s})
265*1c12ee1eSDan Willemsen	}
266*1c12ee1eSDan Willemsen}
267*1c12ee1eSDan Willemsen
268*1c12ee1eSDan Willemsenfunc (rs *records) Join() string {
269*1c12ee1eSDan Willemsen	var ss []string
270*1c12ee1eSDan Willemsen
271*1c12ee1eSDan Willemsen	// In single line mode, simply join all records with commas.
272*1c12ee1eSDan Willemsen	if !rs.allowMulti {
273*1c12ee1eSDan Willemsen		for _, r := range rs.recs {
274*1c12ee1eSDan Willemsen			ss = append(ss, r[0]+formatColon(0)+r[1])
275*1c12ee1eSDan Willemsen		}
276*1c12ee1eSDan Willemsen		return joinStrings(ss, false)
277*1c12ee1eSDan Willemsen	}
278*1c12ee1eSDan Willemsen
279*1c12ee1eSDan Willemsen	// In allowMulti line mode, align single line records for more readable output.
280*1c12ee1eSDan Willemsen	var maxLen int
281*1c12ee1eSDan Willemsen	flush := func(i int) {
282*1c12ee1eSDan Willemsen		for _, r := range rs.recs[len(ss):i] {
283*1c12ee1eSDan Willemsen			ss = append(ss, r[0]+formatColon(maxLen-len(r[0]))+r[1])
284*1c12ee1eSDan Willemsen		}
285*1c12ee1eSDan Willemsen		maxLen = 0
286*1c12ee1eSDan Willemsen	}
287*1c12ee1eSDan Willemsen	for i, r := range rs.recs {
288*1c12ee1eSDan Willemsen		if isMulti := strings.Contains(r[1], "\n"); isMulti {
289*1c12ee1eSDan Willemsen			flush(i)
290*1c12ee1eSDan Willemsen			ss = append(ss, r[0]+formatColon(0)+strings.Join(strings.Split(r[1], "\n"), "\n\t"))
291*1c12ee1eSDan Willemsen		} else if maxLen < len(r[0]) {
292*1c12ee1eSDan Willemsen			maxLen = len(r[0])
293*1c12ee1eSDan Willemsen		}
294*1c12ee1eSDan Willemsen	}
295*1c12ee1eSDan Willemsen	flush(len(rs.recs))
296*1c12ee1eSDan Willemsen	return joinStrings(ss, true)
297*1c12ee1eSDan Willemsen}
298*1c12ee1eSDan Willemsen
299*1c12ee1eSDan Willemsenfunc formatColon(padding int) string {
300*1c12ee1eSDan Willemsen	// Deliberately introduce instability into the debug output to
301*1c12ee1eSDan Willemsen	// discourage users from performing string comparisons.
302*1c12ee1eSDan Willemsen	// This provides us flexibility to change the output in the future.
303*1c12ee1eSDan Willemsen	if detrand.Bool() {
304*1c12ee1eSDan Willemsen		return ":" + strings.Repeat(" ", 1+padding) // use non-breaking spaces (U+00a0)
305*1c12ee1eSDan Willemsen	} else {
306*1c12ee1eSDan Willemsen		return ":" + strings.Repeat(" ", 1+padding) // use regular spaces (U+0020)
307*1c12ee1eSDan Willemsen	}
308*1c12ee1eSDan Willemsen}
309*1c12ee1eSDan Willemsen
310*1c12ee1eSDan Willemsenfunc joinStrings(ss []string, isMulti bool) string {
311*1c12ee1eSDan Willemsen	if len(ss) == 0 {
312*1c12ee1eSDan Willemsen		return ""
313*1c12ee1eSDan Willemsen	}
314*1c12ee1eSDan Willemsen	if isMulti {
315*1c12ee1eSDan Willemsen		return "\n\t" + strings.Join(ss, "\n\t") + "\n"
316*1c12ee1eSDan Willemsen	}
317*1c12ee1eSDan Willemsen	return strings.Join(ss, ", ")
318*1c12ee1eSDan Willemsen}
319