12 | 12 |
"strings"
|
13 | 13 |
)
|
14 | 14 |
|
15 | |
var packageNameStripperRegexp = regexp.MustCompile("\\b[a-zA-Z_]+[a-zA-Z_0-9]+\\.")
|
16 | |
var compactTypeRegexp = regexp.MustCompile(`\s*([,;{}()])\s*`)
|
|
15 |
var (
|
|
16 |
packageNameStripperRegexp = regexp.MustCompile(`\b[a-zA-Z_]+[a-zA-Z_0-9]+\.`)
|
|
17 |
compactTypeRegexp = regexp.MustCompile(`\s*([,;{}()])\s*`)
|
|
18 |
)
|
17 | 19 |
|
18 | 20 |
// Dumper is the interface for implementing custom dumper for your types.
|
19 | 21 |
type Dumper interface {
|
|
30 | 32 |
FieldFilter func(reflect.StructField, reflect.Value) bool
|
31 | 33 |
HomePackage string
|
32 | 34 |
Separator string
|
|
35 |
StrictGo bool
|
|
36 |
|
|
37 |
// DisablePointerReplacement, if true, disables the replacing of pointer data with variable names
|
|
38 |
// when it's safe. This is useful for diffing two structures, where pointer variables would cause
|
|
39 |
// false changes. However, circular graphs are still detected and elided to avoid infinite output.
|
|
40 |
DisablePointerReplacement bool
|
33 | 41 |
}
|
34 | 42 |
|
35 | 43 |
// Config is the default config used when calling Dump
|
|
44 | 52 |
w io.Writer
|
45 | 53 |
depth int
|
46 | 54 |
config *Options
|
47 | |
pointers []uintptr
|
48 | |
visitedPointers []uintptr
|
|
55 |
pointers ptrmap
|
|
56 |
visitedPointers ptrmap
|
|
57 |
parentPointers ptrmap
|
49 | 58 |
currentPointerName string
|
50 | 59 |
homePackageRegexp *regexp.Regexp
|
51 | 60 |
}
|
52 | 61 |
|
|
62 |
func (s *dumpState) write(b []byte) {
|
|
63 |
if _, err := s.w.Write(b); err != nil {
|
|
64 |
panic(err)
|
|
65 |
}
|
|
66 |
}
|
|
67 |
|
|
68 |
func (s *dumpState) writeString(str string) {
|
|
69 |
s.write([]byte(str))
|
|
70 |
}
|
|
71 |
|
53 | 72 |
func (s *dumpState) indent() {
|
54 | 73 |
if !s.config.Compact {
|
55 | |
s.w.Write(bytes.Repeat([]byte(" "), s.depth))
|
|
74 |
s.write(bytes.Repeat([]byte(" "), s.depth))
|
56 | 75 |
}
|
57 | 76 |
}
|
58 | 77 |
|
59 | 78 |
func (s *dumpState) newlineWithPointerNameComment() {
|
60 | |
if s.currentPointerName != "" {
|
|
79 |
if name := s.currentPointerName; name != "" {
|
61 | 80 |
if s.config.Compact {
|
62 | |
s.w.Write([]byte(fmt.Sprintf("/*%s*/", s.currentPointerName)))
|
|
81 |
s.write([]byte(fmt.Sprintf("/*%s*/", name)))
|
63 | 82 |
} else {
|
64 | |
s.w.Write([]byte(fmt.Sprintf(" // %s\n", s.currentPointerName)))
|
|
83 |
s.write([]byte(fmt.Sprintf(" // %s\n", name)))
|
65 | 84 |
}
|
66 | 85 |
s.currentPointerName = ""
|
67 | 86 |
return
|
68 | 87 |
}
|
69 | 88 |
if !s.config.Compact {
|
70 | |
s.w.Write([]byte("\n"))
|
|
89 |
s.write([]byte("\n"))
|
71 | 90 |
}
|
72 | 91 |
}
|
73 | 92 |
|
|
81 | 100 |
if s.config.Compact {
|
82 | 101 |
typeName = compactTypeRegexp.ReplaceAllString(typeName, "$1")
|
83 | 102 |
}
|
84 | |
s.w.Write([]byte(typeName))
|
|
103 |
s.write([]byte(typeName))
|
85 | 104 |
}
|
86 | 105 |
|
87 | 106 |
func (s *dumpState) dumpSlice(v reflect.Value) {
|
88 | 107 |
s.dumpType(v)
|
89 | 108 |
numEntries := v.Len()
|
90 | 109 |
if numEntries == 0 {
|
91 | |
s.w.Write([]byte("{}"))
|
|
110 |
s.write([]byte("{}"))
|
92 | 111 |
if s.config.Compact {
|
93 | |
s.w.Write([]byte(";"))
|
|
112 |
s.write([]byte(";"))
|
94 | 113 |
}
|
95 | 114 |
s.newlineWithPointerNameComment()
|
96 | 115 |
return
|
97 | 116 |
}
|
98 | |
s.w.Write([]byte("{"))
|
|
117 |
s.write([]byte("{"))
|
99 | 118 |
s.newlineWithPointerNameComment()
|
100 | 119 |
s.depth++
|
101 | 120 |
for i := 0; i < numEntries; i++ {
|
102 | 121 |
s.indent()
|
103 | 122 |
s.dumpVal(v.Index(i))
|
104 | 123 |
if !s.config.Compact || i < numEntries-1 {
|
105 | |
s.w.Write([]byte(","))
|
|
124 |
s.write([]byte(","))
|
106 | 125 |
}
|
107 | 126 |
s.newlineWithPointerNameComment()
|
108 | 127 |
}
|
109 | 128 |
s.depth--
|
110 | 129 |
s.indent()
|
111 | |
s.w.Write([]byte("}"))
|
|
130 |
s.write([]byte("}"))
|
112 | 131 |
}
|
113 | 132 |
|
114 | 133 |
func (s *dumpState) dumpStruct(v reflect.Value) {
|
115 | 134 |
dumpPreamble := func() {
|
116 | 135 |
s.dumpType(v)
|
117 | |
s.w.Write([]byte("{"))
|
|
136 |
s.write([]byte("{"))
|
118 | 137 |
s.newlineWithPointerNameComment()
|
119 | 138 |
s.depth++
|
120 | 139 |
}
|
|
137 | 156 |
preambleDumped = true
|
138 | 157 |
}
|
139 | 158 |
s.indent()
|
140 | |
s.w.Write([]byte(vtf.Name))
|
|
159 |
s.write([]byte(vtf.Name))
|
141 | 160 |
if s.config.Compact {
|
142 | |
s.w.Write([]byte(":"))
|
|
161 |
s.write([]byte(":"))
|
143 | 162 |
} else {
|
144 | |
s.w.Write([]byte(": "))
|
|
163 |
s.write([]byte(": "))
|
145 | 164 |
}
|
146 | 165 |
s.dumpVal(v.Field(i))
|
147 | 166 |
if !s.config.Compact || i < numFields-1 {
|
148 | |
s.w.Write([]byte(","))
|
|
167 |
s.write([]byte(","))
|
149 | 168 |
}
|
150 | 169 |
s.newlineWithPointerNameComment()
|
151 | 170 |
}
|
152 | 171 |
if preambleDumped {
|
153 | 172 |
s.depth--
|
154 | 173 |
s.indent()
|
155 | |
s.w.Write([]byte("}"))
|
|
174 |
s.write([]byte("}"))
|
156 | 175 |
} else {
|
157 | 176 |
// There were no fields dumped
|
158 | 177 |
s.dumpType(v)
|
159 | |
s.w.Write([]byte("{}"))
|
|
178 |
s.write([]byte("{}"))
|
160 | 179 |
}
|
161 | 180 |
}
|
162 | 181 |
|
163 | 182 |
func (s *dumpState) dumpMap(v reflect.Value) {
|
164 | 183 |
s.dumpType(v)
|
165 | |
s.w.Write([]byte("{"))
|
|
184 |
s.write([]byte("{"))
|
166 | 185 |
s.newlineWithPointerNameComment()
|
167 | 186 |
s.depth++
|
168 | 187 |
keys := v.MapKeys()
|
|
175 | 194 |
s.indent()
|
176 | 195 |
s.dumpVal(key)
|
177 | 196 |
if s.config.Compact {
|
178 | |
s.w.Write([]byte(":"))
|
|
197 |
s.write([]byte(":"))
|
179 | 198 |
} else {
|
180 | |
s.w.Write([]byte(": "))
|
|
199 |
s.write([]byte(": "))
|
181 | 200 |
}
|
182 | 201 |
s.dumpVal(v.MapIndex(key))
|
183 | 202 |
if !s.config.Compact || i < numKeys-1 {
|
184 | |
s.w.Write([]byte(","))
|
|
203 |
s.write([]byte(","))
|
185 | 204 |
}
|
186 | 205 |
s.newlineWithPointerNameComment()
|
187 | 206 |
}
|
188 | 207 |
s.depth--
|
189 | 208 |
s.indent()
|
190 | |
s.w.Write([]byte("}"))
|
|
209 |
s.write([]byte("}"))
|
191 | 210 |
}
|
192 | 211 |
|
193 | 212 |
func (s *dumpState) dumpFunc(v reflect.Value) {
|
|
206 | 225 |
if s.config.Compact {
|
207 | 226 |
name = compactTypeRegexp.ReplaceAllString(name, "$1")
|
208 | 227 |
}
|
209 | |
s.w.Write([]byte(name))
|
|
228 |
s.write([]byte(name))
|
210 | 229 |
}
|
211 | 230 |
}
|
212 | 231 |
|
|
220 | 239 |
s.dumpType(v)
|
221 | 240 |
|
222 | 241 |
if s.config.Compact {
|
223 | |
s.w.Write(buf.Bytes())
|
|
242 |
s.write(buf.Bytes())
|
224 | 243 |
return
|
225 | 244 |
}
|
226 | 245 |
|
|
242 | 261 |
} else {
|
243 | 262 |
s.indent()
|
244 | 263 |
}
|
245 | |
s.w.Write([]byte(line))
|
|
264 |
s.write([]byte(line))
|
246 | 265 |
|
247 | 266 |
// At EOF we're done
|
248 | 267 |
if err == io.EOF {
|
|
262 | 281 |
s.dumpVal(v)
|
263 | 282 |
}
|
264 | 283 |
|
265 | |
func (s *dumpState) handlePointerAliasingAndCheckIfShouldDescend(value reflect.Value) bool {
|
|
284 |
func (s *dumpState) descendIntoPossiblePointer(value reflect.Value, f func()) {
|
|
285 |
canonicalize := true
|
|
286 |
if isPointerValue(value) {
|
|
287 |
ptr := value.Pointer()
|
|
288 |
|
|
289 |
// If elision disabled, and this is not a circular reference, don't canonicalize
|
|
290 |
if s.config.DisablePointerReplacement && s.parentPointers.add(ptr) {
|
|
291 |
canonicalize = false
|
|
292 |
}
|
|
293 |
|
|
294 |
// Add to stack of pointers we're recursively descending into
|
|
295 |
s.parentPointers.add(ptr)
|
|
296 |
defer s.parentPointers.remove(ptr)
|
|
297 |
}
|
|
298 |
|
|
299 |
if !canonicalize {
|
|
300 |
pointerName, _ := s.pointerNameFor(value)
|
|
301 |
s.currentPointerName = pointerName
|
|
302 |
f()
|
|
303 |
return
|
|
304 |
}
|
|
305 |
|
266 | 306 |
pointerName, firstVisit := s.pointerNameFor(value)
|
267 | 307 |
if pointerName == "" {
|
268 | |
return true
|
|
308 |
f()
|
|
309 |
return
|
269 | 310 |
}
|
270 | 311 |
if firstVisit {
|
271 | 312 |
s.currentPointerName = pointerName
|
272 | |
return true
|
273 | |
}
|
274 | |
s.w.Write([]byte(pointerName))
|
275 | |
return false
|
|
313 |
f()
|
|
314 |
return
|
|
315 |
}
|
|
316 |
s.write([]byte(pointerName))
|
276 | 317 |
}
|
277 | 318 |
|
278 | 319 |
func (s *dumpState) dumpVal(value reflect.Value) {
|
279 | 320 |
if value.Kind() == reflect.Ptr && value.IsNil() {
|
280 | |
s.w.Write([]byte("nil"))
|
|
321 |
s.write([]byte("nil"))
|
281 | 322 |
return
|
282 | 323 |
}
|
283 | 324 |
|
|
287 | 328 |
// Handle custom dumpers
|
288 | 329 |
dumperType := reflect.TypeOf((*Dumper)(nil)).Elem()
|
289 | 330 |
if v.Type().Implements(dumperType) {
|
290 | |
if s.handlePointerAliasingAndCheckIfShouldDescend(v) {
|
|
331 |
s.descendIntoPossiblePointer(v, func() {
|
291 | 332 |
s.dumpCustom(v)
|
292 | |
}
|
|
333 |
})
|
293 | 334 |
return
|
294 | 335 |
}
|
295 | 336 |
|
|
297 | 338 |
case reflect.Invalid:
|
298 | 339 |
// Do nothing. We should never get here since invalid has already
|
299 | 340 |
// been handled above.
|
300 | |
s.w.Write([]byte("<invalid>"))
|
|
341 |
s.write([]byte("<invalid>"))
|
301 | 342 |
|
302 | 343 |
case reflect.Bool:
|
303 | 344 |
printBool(s.w, v.Bool())
|
|
321 | 362 |
printComplex(s.w, v.Complex(), 64)
|
322 | 363 |
|
323 | 364 |
case reflect.String:
|
324 | |
s.w.Write([]byte(strconv.Quote(v.String())))
|
|
365 |
s.write([]byte(strconv.Quote(v.String())))
|
325 | 366 |
|
326 | 367 |
case reflect.Slice:
|
327 | 368 |
if v.IsNil() {
|
|
331 | 372 |
fallthrough
|
332 | 373 |
|
333 | 374 |
case reflect.Array:
|
334 | |
if s.handlePointerAliasingAndCheckIfShouldDescend(v) {
|
|
375 |
s.descendIntoPossiblePointer(v, func() {
|
335 | 376 |
s.dumpSlice(v)
|
336 | |
}
|
|
377 |
})
|
337 | 378 |
|
338 | 379 |
case reflect.Interface:
|
339 | 380 |
// The only time we should get here is for nil interfaces due to
|
|
343 | 384 |
}
|
344 | 385 |
|
345 | 386 |
case reflect.Ptr:
|
346 | |
if s.handlePointerAliasingAndCheckIfShouldDescend(v) {
|
347 | |
s.w.Write([]byte("&"))
|
348 | |
s.dumpVal(v.Elem())
|
349 | |
}
|
|
387 |
s.descendIntoPossiblePointer(v, func() {
|
|
388 |
if s.config.StrictGo {
|
|
389 |
s.writeString(fmt.Sprintf("(func(v %s) *%s { return &v })(", v.Elem().Type(), v.Elem().Type()))
|
|
390 |
s.dumpVal(v.Elem())
|
|
391 |
s.writeString(")")
|
|
392 |
} else {
|
|
393 |
s.writeString("&")
|
|
394 |
s.dumpVal(v.Elem())
|
|
395 |
}
|
|
396 |
})
|
350 | 397 |
|
351 | 398 |
case reflect.Map:
|
352 | |
if s.handlePointerAliasingAndCheckIfShouldDescend(v) {
|
|
399 |
s.descendIntoPossiblePointer(v, func() {
|
353 | 400 |
s.dumpMap(v)
|
354 | |
}
|
|
401 |
})
|
355 | 402 |
|
356 | 403 |
case reflect.Struct:
|
357 | 404 |
s.dumpStruct(v)
|
|
361 | 408 |
|
362 | 409 |
default:
|
363 | 410 |
if v.CanInterface() {
|
364 | |
fmt.Fprintf(s.w, "%v", v.Interface())
|
|
411 |
s.writeString(fmt.Sprintf("%v", v.Interface()))
|
365 | 412 |
} else {
|
366 | |
fmt.Fprintf(s.w, "%v", v.String())
|
367 | |
}
|
368 | |
}
|
369 | |
}
|
370 | |
|
371 | |
// call to signal that the pointer is being visited, returns true if this is the
|
372 | |
// first visit to that pointer. Used to detect when to output the entire contents
|
373 | |
// behind a pointer (the first time), and when to just emit a name (all other times)
|
374 | |
func (s *dumpState) visitPointerAndCheckIfItIsTheFirstTime(ptr uintptr) bool {
|
375 | |
for _, addr := range s.visitedPointers {
|
376 | |
if addr == ptr {
|
377 | |
return false
|
378 | |
}
|
379 | |
}
|
380 | |
s.visitedPointers = append(s.visitedPointers, ptr)
|
381 | |
return true
|
|
413 |
s.writeString(fmt.Sprintf("%v", v.String()))
|
|
414 |
}
|
|
415 |
}
|
382 | 416 |
}
|
383 | 417 |
|
384 | 418 |
// registers that the value has been visited and checks to see if it is one of the
|
|
389 | 423 |
func (s *dumpState) pointerNameFor(v reflect.Value) (string, bool) {
|
390 | 424 |
if isPointerValue(v) {
|
391 | 425 |
ptr := v.Pointer()
|
392 | |
for i, addr := range s.pointers {
|
393 | |
if ptr == addr {
|
394 | |
firstVisit := s.visitPointerAndCheckIfItIsTheFirstTime(ptr)
|
395 | |
return fmt.Sprintf("p%d", i), firstVisit
|
396 | |
}
|
|
426 |
if info, ok := s.pointers[ptr]; ok {
|
|
427 |
firstVisit := s.visitedPointers.add(ptr)
|
|
428 |
return fmt.Sprintf("p%d", info.order), firstVisit
|
397 | 429 |
}
|
398 | 430 |
}
|
399 | 431 |
return "", false
|
|
403 | 435 |
func newDumpState(value interface{}, options *Options, writer io.Writer) *dumpState {
|
404 | 436 |
result := &dumpState{
|
405 | 437 |
config: options,
|
406 | |
pointers: MapReusedPointers(reflect.ValueOf(value)),
|
|
438 |
pointers: mapReusedPointers(reflect.ValueOf(value)),
|
407 | 439 |
w: writer,
|
408 | 440 |
}
|
409 | 441 |
|
|
429 | 461 |
for i, value := range values {
|
430 | 462 |
state := newDumpState(value, &o, os.Stdout)
|
431 | 463 |
if i > 0 {
|
432 | |
state.w.Write([]byte(o.Separator))
|
|
464 |
state.write([]byte(o.Separator))
|
433 | 465 |
}
|
434 | 466 |
state.dump(value)
|
435 | 467 |
}
|
436 | |
os.Stdout.Write([]byte("\n"))
|
|
468 |
_, _ = os.Stdout.Write([]byte("\n"))
|
437 | 469 |
}
|
438 | 470 |
|
439 | 471 |
// Sdump dumps a value to a string according to the options
|
|
441 | 473 |
buf := new(bytes.Buffer)
|
442 | 474 |
for i, value := range values {
|
443 | 475 |
if i > 0 {
|
444 | |
buf.Write([]byte(o.Separator))
|
|
476 |
_, _ = buf.Write([]byte(o.Separator))
|
445 | 477 |
}
|
446 | 478 |
state := newDumpState(value, &o, buf)
|
447 | 479 |
state.dump(value)
|