├── .github └── workflows │ └── test.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── dump.go ├── dump_test.go ├── go.mod ├── go.sum ├── pointers.go ├── print.go ├── testdata ├── config_Compact.dump ├── config_DisablePointerReplacement_circular.dump ├── config_DisablePointerReplacement_simpleReusedStruct.dump ├── config_DumpFunc.dump ├── config_FieldFilter.dump ├── config_FormatTime.dump ├── config_HidePrivateFields.dump ├── config_HideZeroValues.dump ├── config_HomePackage.dump ├── config_StrictGo.dump ├── config_StripPackageNames.dump ├── customDumper.dump ├── maps.dump ├── multipleArgs_lineBreak.dump ├── multipleArgs_noSeparator.dump ├── multipleArgs_separator.dump ├── nilIntefacesInStructs.dump ├── pointerAliasing.dump ├── primitives.dump ├── recursive_maps.dump └── unexported.dump └── util.go /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | push: 5 | 6 | jobs: 7 | scheduled: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Check out this repo 11 | uses: actions/checkout@v2 12 | - name: Set up Go 13 | uses: actions/setup-go@v2 14 | with: {go-version: '^1.16'} 15 | - name: Download dependencies 16 | run: go mod download 17 | - name: Build 18 | run: go build ./... 19 | - name: Test 20 | run: go test ./... 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 1.1.0 (2017-11-1) 2 | 3 | A slight breaking change. The dump-method of the `Dumper` interface has changed from `Dump` to `LitterDump` to mitigate potential collisions. 4 | 5 | # 1.0.0 (2017-10-29) 6 | 7 | Tagged 1.0.0. 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016-2017 Sanity.io. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![!Build Status](https://travis-ci.org/sanity-io/litter.svg?branch=master)](https://travis-ci.org/sanity-io/litter) 2 | 3 | # Litter 4 | 5 | **Litter is a pretty printer library for Go data structures to aid in debugging and testing.** 6 | 7 | --- 8 | 9 | Litter is provided by 10 | 11 | 12 |
13 | Sanity: The Headless CMS Construction Kit 14 |
15 | 16 | --- 17 | 18 | Litter named for the fact that it outputs *literals*, which you *litter* your output with. As a side benefit, all Litter output is syntactically correct Go. You can use Litter to emit data during debug, and it's also really nice for "snapshot data" in unit tests, since it produces consistent, sorted output. Litter was inspired by [Spew](https://github.com/davecgh/go-spew), but focuses on terseness and readability. 19 | 20 | ### Basic example 21 | 22 | This: 23 | 24 | ```go 25 | type Person struct { 26 | Name string 27 | Age int 28 | Parent *Person 29 | } 30 | 31 | litter.Dump(Person{ 32 | Name: "Bob", 33 | Age: 20, 34 | Parent: &Person{ 35 | Name: "Jane", 36 | Age: 50, 37 | }, 38 | }) 39 | ``` 40 | 41 | will output: 42 | 43 | ``` 44 | Person{ 45 | Name: "Bob", 46 | Age: 20, 47 | Parent: &Person{ 48 | Name: "Jane", 49 | Age: 50, 50 | }, 51 | } 52 | ``` 53 | 54 | ### Use in tests 55 | 56 | Litter is a great alternative to JSON or YAML for providing "snapshots" or example data. For example: 57 | 58 | ```go 59 | func TestSearch(t *testing.T) { 60 | result := DoSearch() 61 | 62 | actual := litterOpts.Sdump(result) 63 | expected, err := ioutil.ReadFile("testdata.txt") 64 | if err != nil { 65 | // First run, write test data since it doesn't exist 66 | if !os.IsNotExist(err) { 67 | t.Error(err) 68 | } 69 | ioutil.Write("testdata.txt", actual, 0644) 70 | actual = expected 71 | } 72 | if expected != actual { 73 | t.Errorf("Expected %s, got %s", expected, actual) 74 | } 75 | } 76 | ``` 77 | 78 | The first run will use Litter to write the data to `testdata.txt`. On subsequent runs, the test will compare the data. Since Litter always provides a consistent view of a value, you can compare the strings directly. 79 | 80 | ### Circular references 81 | 82 | Litter detects circular references or aliasing, and will replace additional references to the same object with aliases. For example: 83 | 84 | ```go 85 | type Circular struct { 86 | Self *Circular 87 | } 88 | 89 | selfref := Circular{} 90 | selfref.Self = &selfref 91 | 92 | litter.Dump(selfref) 93 | ``` 94 | 95 | will output: 96 | 97 | ``` 98 | Circular { // p0 99 | Self: p0, 100 | } 101 | ``` 102 | 103 | ## Installation 104 | 105 | ```bash 106 | $ go get -u github.com/sanity-io/litter 107 | ``` 108 | 109 | ## Quick start 110 | 111 | Add this import line to the file you're working in: 112 | 113 | ```go 114 | import "github.com/sanity-io/litter" 115 | ``` 116 | 117 | To dump a variable with full newlines, indentation, type, and aliasing information, use `Dump` or `Sdump`: 118 | 119 | ```go 120 | litter.Dump(myVar1) 121 | str := litter.Sdump(myVar1) 122 | ``` 123 | 124 | ### `litter.Dump(value, ...)` 125 | 126 | Dumps the data structure to STDOUT. 127 | 128 | ### `litter.Sdump(value, ...)` 129 | 130 | Returns the dump as a string 131 | 132 | ## Configuration 133 | 134 | You can configure litter globally by modifying the default `litter.Config` 135 | 136 | ```go 137 | // Strip all package names from types 138 | litter.Config.StripPackageNames = true 139 | 140 | // Hide private struct fields from dumped structs 141 | litter.Config.HidePrivateFields = true 142 | 143 | // Hide fields matched with given regexp if it is not nil. It is set up to hide fields generate with protoc-gen-go 144 | litter.Config.FieldExclusions = regexp.MustCompile(`^(XXX_.*)$`) 145 | 146 | // Sets a "home" package. The package name will be stripped from all its types 147 | litter.Config.HomePackage = "mypackage" 148 | 149 | // Sets separator used when multiple arguments are passed to Dump() or Sdump(). 150 | litter.Config.Separator = "\n" 151 | 152 | // Use compact output: strip newlines and other unnecessary whitespace 153 | litter.Config.Compact = true 154 | 155 | // Prevents duplicate pointers from being replaced by placeholder variable names (except in necessary, in the case 156 | // of circular references) 157 | litter.Config.DisablePointerReplacement = true 158 | ``` 159 | 160 | ### `litter.Options` 161 | 162 | Allows you to configure a local configuration of litter to allow for proper compartmentalization of state at the expense of some comfort: 163 | 164 | ``` go 165 | sq := litter.Options { 166 | HidePrivateFields: true, 167 | HomePackage: "thispack", 168 | Separator: " ", 169 | } 170 | 171 | sq.Dump("dumped", "with", "local", "settings") 172 | ``` 173 | 174 | ## Custom dumpers 175 | 176 | Implement the interface Dumper on your types to take control of how your type is dumped. 177 | 178 | ``` go 179 | type Dumper interface { 180 | LitterDump(w io.Writer) 181 | } 182 | ``` 183 | 184 | Just write your custom dump to the provided stream, using multiple lines divided by `"\n"` if you need. Litter 185 | might indent your output according to context, and optionally decorate your first line with a pointer comment 186 | where appropriate. 187 | 188 | A couple of examples from the test suite: 189 | 190 | ``` go 191 | type CustomMultiLineDumper struct {} 192 | 193 | func (cmld *CustomMultiLineDumper) LitterDump(w io.Writer) { 194 | w.Write([]byte("{\n multi\n line\n}")) 195 | } 196 | 197 | type CustomSingleLineDumper int 198 | 199 | func (csld CustomSingleLineDumper) LitterDump(w io.Writer) { 200 | w.Write([]byte("")) 201 | } 202 | ```` 203 | -------------------------------------------------------------------------------- /dump.go: -------------------------------------------------------------------------------- 1 | package litter 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io" 7 | "os" 8 | "reflect" 9 | "regexp" 10 | "runtime" 11 | "sort" 12 | "strconv" 13 | "strings" 14 | "time" 15 | ) 16 | 17 | var ( 18 | packageNameStripperRegexp = regexp.MustCompile(`\b[a-zA-Z_]+[a-zA-Z_0-9]+\.`) 19 | compactTypeRegexp = regexp.MustCompile(`\s*([,;{}()])\s*`) 20 | ) 21 | 22 | // Dumper is the interface for implementing custom dumper for your types. 23 | type Dumper interface { 24 | LitterDump(w io.Writer) 25 | } 26 | 27 | // Options represents configuration options for litter 28 | type Options struct { 29 | Compact bool 30 | StripPackageNames bool 31 | HidePrivateFields bool 32 | HideZeroValues bool 33 | FieldExclusions *regexp.Regexp 34 | FieldFilter func(reflect.StructField, reflect.Value) bool 35 | HomePackage string 36 | Separator string 37 | StrictGo bool 38 | DumpFunc func(reflect.Value, io.Writer) bool 39 | 40 | // DisablePointerReplacement, if true, disables the replacing of pointer data with variable names 41 | // when it's safe. This is useful for diffing two structures, where pointer variables would cause 42 | // false changes. However, circular graphs are still detected and elided to avoid infinite output. 43 | DisablePointerReplacement bool 44 | 45 | // FormatTime, if true, will format [time.Time] values. 46 | FormatTime bool 47 | } 48 | 49 | // Config is the default config used when calling Dump 50 | var Config = Options{ 51 | StripPackageNames: false, 52 | HidePrivateFields: true, 53 | FieldExclusions: regexp.MustCompile(`^(XXX_.*)$`), // XXX_ is a prefix of fields generated by protoc-gen-go 54 | Separator: " ", 55 | } 56 | 57 | type dumpState struct { 58 | w io.Writer 59 | depth int 60 | config *Options 61 | pointers ptrmap 62 | visitedPointers ptrmap 63 | parentPointers ptrmap 64 | currentPointer *ptrinfo 65 | homePackageRegexp *regexp.Regexp 66 | timeFormatter func(t time.Time) string 67 | } 68 | 69 | func (s *dumpState) write(b []byte) { 70 | if _, err := s.w.Write(b); err != nil { 71 | panic(err) 72 | } 73 | } 74 | 75 | func (s *dumpState) writeString(str string) { 76 | s.write([]byte(str)) 77 | } 78 | 79 | func (s *dumpState) indent() { 80 | if !s.config.Compact { 81 | s.write(bytes.Repeat([]byte(" "), s.depth)) 82 | } 83 | } 84 | 85 | func (s *dumpState) newlineWithPointerNameComment() { 86 | if ptr := s.currentPointer; ptr != nil { 87 | if s.config.Compact { 88 | s.write([]byte(fmt.Sprintf("/*%s*/", ptr.label()))) 89 | } else { 90 | s.write([]byte(fmt.Sprintf(" // %s\n", ptr.label()))) 91 | } 92 | s.currentPointer = nil 93 | return 94 | } 95 | if !s.config.Compact { 96 | s.write([]byte("\n")) 97 | } 98 | } 99 | 100 | func (s *dumpState) dumpType(v reflect.Value) { 101 | typeName := v.Type().String() 102 | if s.config.StripPackageNames { 103 | typeName = packageNameStripperRegexp.ReplaceAllLiteralString(typeName, "") 104 | } else if s.homePackageRegexp != nil { 105 | typeName = s.homePackageRegexp.ReplaceAllLiteralString(typeName, "") 106 | } 107 | if s.config.Compact { 108 | typeName = compactTypeRegexp.ReplaceAllString(typeName, "$1") 109 | } 110 | s.write([]byte(typeName)) 111 | } 112 | 113 | func (s *dumpState) dumpSlice(v reflect.Value) { 114 | s.dumpType(v) 115 | numEntries := v.Len() 116 | if numEntries == 0 { 117 | s.write([]byte("{}")) 118 | return 119 | } 120 | s.write([]byte("{")) 121 | s.newlineWithPointerNameComment() 122 | s.depth++ 123 | for i := 0; i < numEntries; i++ { 124 | s.indent() 125 | s.dumpVal(v.Index(i)) 126 | if !s.config.Compact || i < numEntries-1 { 127 | s.write([]byte(",")) 128 | } 129 | s.newlineWithPointerNameComment() 130 | } 131 | s.depth-- 132 | s.indent() 133 | s.write([]byte("}")) 134 | } 135 | 136 | func (s *dumpState) dumpStruct(v reflect.Value) { 137 | if v.CanInterface() { 138 | val := v.Interface() 139 | if t, ok := val.(time.Time); ok && s.timeFormatter != nil { 140 | s.writeString(s.timeFormatter(t)) 141 | return 142 | } 143 | } 144 | 145 | dumpPreamble := func() { 146 | s.dumpType(v) 147 | s.write([]byte("{")) 148 | s.newlineWithPointerNameComment() 149 | s.depth++ 150 | } 151 | preambleDumped := false 152 | vt := v.Type() 153 | numFields := v.NumField() 154 | for i := 0; i < numFields; i++ { 155 | vtf := vt.Field(i) 156 | if s.config.HidePrivateFields && vtf.PkgPath != "" || s.config.FieldExclusions != nil && s.config.FieldExclusions.MatchString(vtf.Name) { 157 | continue 158 | } 159 | if s.config.FieldFilter != nil && !s.config.FieldFilter(vtf, v.Field(i)) { 160 | continue 161 | } 162 | if s.config.HideZeroValues && isZeroValue(v.Field(i)) { 163 | continue 164 | } 165 | if !preambleDumped { 166 | dumpPreamble() 167 | preambleDumped = true 168 | } 169 | s.indent() 170 | s.write([]byte(vtf.Name)) 171 | if s.config.Compact { 172 | s.write([]byte(":")) 173 | } else { 174 | s.write([]byte(": ")) 175 | } 176 | s.dumpVal(v.Field(i)) 177 | if !s.config.Compact || i < numFields-1 { 178 | s.write([]byte(",")) 179 | } 180 | s.newlineWithPointerNameComment() 181 | } 182 | if preambleDumped { 183 | s.depth-- 184 | s.indent() 185 | s.write([]byte("}")) 186 | } else { 187 | // There were no fields dumped 188 | s.dumpType(v) 189 | s.write([]byte("{}")) 190 | } 191 | } 192 | 193 | func (s *dumpState) dumpMap(v reflect.Value) { 194 | if v.IsNil() { 195 | s.dumpType(v) 196 | s.writeString("(nil)") 197 | return 198 | } 199 | 200 | s.dumpType(v) 201 | 202 | keys := v.MapKeys() 203 | if len(keys) == 0 { 204 | s.write([]byte("{}")) 205 | return 206 | } 207 | 208 | s.write([]byte("{")) 209 | s.newlineWithPointerNameComment() 210 | s.depth++ 211 | sort.Sort(mapKeySorter{ 212 | keys: keys, 213 | options: s.config, 214 | }) 215 | numKeys := len(keys) 216 | for i, key := range keys { 217 | s.indent() 218 | s.dumpVal(key) 219 | if s.config.Compact { 220 | s.write([]byte(":")) 221 | } else { 222 | s.write([]byte(": ")) 223 | } 224 | s.dumpVal(v.MapIndex(key)) 225 | if !s.config.Compact || i < numKeys-1 { 226 | s.write([]byte(",")) 227 | } 228 | s.newlineWithPointerNameComment() 229 | } 230 | s.depth-- 231 | s.indent() 232 | s.write([]byte("}")) 233 | } 234 | 235 | func (s *dumpState) dumpFunc(v reflect.Value) { 236 | parts := strings.Split(runtime.FuncForPC(v.Pointer()).Name(), "/") 237 | name := parts[len(parts)-1] 238 | 239 | // Anonymous function 240 | if strings.Count(name, ".") > 1 { 241 | s.dumpType(v) 242 | } else { 243 | if s.config.StripPackageNames { 244 | name = packageNameStripperRegexp.ReplaceAllLiteralString(name, "") 245 | } else if s.homePackageRegexp != nil { 246 | name = s.homePackageRegexp.ReplaceAllLiteralString(name, "") 247 | } 248 | if s.config.Compact { 249 | name = compactTypeRegexp.ReplaceAllString(name, "$1") 250 | } 251 | s.write([]byte(name)) 252 | } 253 | } 254 | 255 | func (s *dumpState) dumpChan(v reflect.Value) { 256 | vType := v.Type() 257 | res := []byte(vType.String()) 258 | s.write(res) 259 | } 260 | 261 | func (s *dumpState) dumpCustom(v reflect.Value, buf *bytes.Buffer) { 262 | // Dump the type 263 | s.dumpType(v) 264 | 265 | if s.config.Compact { 266 | s.write(buf.Bytes()) 267 | return 268 | } 269 | 270 | // Now output the dump taking care to apply the current indentation-level 271 | // and pointer name comments. 272 | var err error 273 | firstLine := true 274 | for err == nil { 275 | var lineBytes []byte 276 | lineBytes, err = buf.ReadBytes('\n') 277 | line := strings.TrimRight(string(lineBytes), " \n") 278 | 279 | if err != nil && err != io.EOF { 280 | break 281 | } 282 | // Do not indent first line 283 | if firstLine { 284 | firstLine = false 285 | } else { 286 | s.indent() 287 | } 288 | s.write([]byte(line)) 289 | 290 | // At EOF we're done 291 | if err == io.EOF { 292 | return 293 | } 294 | s.newlineWithPointerNameComment() 295 | } 296 | panic(err) 297 | } 298 | 299 | func (s *dumpState) dump(value interface{}) { 300 | if value == nil { 301 | printNil(s.w) 302 | return 303 | } 304 | v := reflect.ValueOf(value) 305 | s.dumpVal(v) 306 | } 307 | 308 | var dumperType = reflect.TypeOf((*Dumper)(nil)).Elem() 309 | 310 | func (s *dumpState) descendIntoPossiblePointer(value reflect.Value, f func()) { 311 | canonicalize := true 312 | if isPointerValue(value) { 313 | // If elision disabled, and this is not a circular reference, don't canonicalize 314 | if s.config.DisablePointerReplacement && s.parentPointers.add(value) { 315 | canonicalize = false 316 | } 317 | 318 | // Add to stack of pointers we're recursively descending into 319 | s.parentPointers.add(value) 320 | defer s.parentPointers.remove(value) 321 | } 322 | 323 | if !canonicalize { 324 | ptr, _ := s.pointerFor(value) 325 | s.currentPointer = ptr 326 | f() 327 | return 328 | } 329 | 330 | ptr, firstVisit := s.pointerFor(value) 331 | if ptr == nil { 332 | f() 333 | return 334 | } 335 | if firstVisit { 336 | s.currentPointer = ptr 337 | f() 338 | return 339 | } 340 | s.write([]byte(ptr.label())) 341 | } 342 | 343 | func (s *dumpState) dumpVal(value reflect.Value) { 344 | if value.Kind() == reflect.Ptr && value.IsNil() { 345 | s.write([]byte("nil")) 346 | return 347 | } 348 | 349 | v := deInterface(value) 350 | kind := v.Kind() 351 | 352 | // Try to handle with dump func 353 | if s.config.DumpFunc != nil { 354 | buf := new(bytes.Buffer) 355 | if s.config.DumpFunc(v, buf) { 356 | s.dumpCustom(v, buf) 357 | return 358 | } 359 | } 360 | 361 | // Handle custom dumpers 362 | if v.Type().Implements(dumperType) { 363 | s.descendIntoPossiblePointer(v, func() { 364 | // Run the custom dumper buffering the output 365 | buf := new(bytes.Buffer) 366 | dumpFunc := v.MethodByName("LitterDump") 367 | dumpFunc.Call([]reflect.Value{reflect.ValueOf(buf)}) 368 | s.dumpCustom(v, buf) 369 | }) 370 | return 371 | } 372 | 373 | switch kind { 374 | case reflect.Invalid: 375 | // Do nothing. We should never get here since invalid has already 376 | // been handled above. 377 | s.write([]byte("")) 378 | 379 | case reflect.Bool: 380 | printBool(s.w, v.Bool()) 381 | 382 | case reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Int: 383 | printInt(s.w, v.Int(), 10) 384 | 385 | case reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uint: 386 | printUint(s.w, v.Uint(), 10) 387 | 388 | case reflect.Float32: 389 | printFloat(s.w, v.Float(), 32) 390 | 391 | case reflect.Float64: 392 | printFloat(s.w, v.Float(), 64) 393 | 394 | case reflect.Complex64: 395 | printComplex(s.w, v.Complex(), 32) 396 | 397 | case reflect.Complex128: 398 | printComplex(s.w, v.Complex(), 64) 399 | 400 | case reflect.String: 401 | s.write([]byte(strconv.Quote(v.String()))) 402 | 403 | case reflect.Slice: 404 | if v.IsNil() { 405 | printNil(s.w) 406 | break 407 | } 408 | fallthrough 409 | 410 | case reflect.Array: 411 | s.descendIntoPossiblePointer(v, func() { 412 | s.dumpSlice(v) 413 | }) 414 | 415 | case reflect.Interface: 416 | // The only time we should get here is for nil interfaces due to 417 | // unpackValue calls. 418 | if v.IsNil() { 419 | printNil(s.w) 420 | } 421 | 422 | case reflect.Ptr: 423 | s.descendIntoPossiblePointer(v, func() { 424 | if s.config.StrictGo { 425 | s.writeString(fmt.Sprintf("(func(v %s) *%s { return &v })(", v.Elem().Type(), v.Elem().Type())) 426 | s.dumpVal(v.Elem()) 427 | s.writeString(")") 428 | } else { 429 | s.writeString("&") 430 | s.dumpVal(v.Elem()) 431 | } 432 | }) 433 | 434 | case reflect.Map: 435 | s.descendIntoPossiblePointer(v, func() { 436 | s.dumpMap(v) 437 | }) 438 | 439 | case reflect.Struct: 440 | s.dumpStruct(v) 441 | 442 | case reflect.Func: 443 | s.dumpFunc(v) 444 | 445 | case reflect.Chan: 446 | s.dumpChan(v) 447 | 448 | default: 449 | if v.CanInterface() { 450 | s.writeString(fmt.Sprintf("%v", v.Interface())) 451 | } else { 452 | s.writeString(fmt.Sprintf("%v", v.String())) 453 | } 454 | } 455 | } 456 | 457 | // registers that the value has been visited and checks to see if it is one of the 458 | // pointers we will see multiple times. If it is, it returns a temporary name for this 459 | // pointer. It also returns a boolean value indicating whether this is the first time 460 | // this name is returned so the caller can decide whether the contents of the pointer 461 | // has been dumped before or not. 462 | func (s *dumpState) pointerFor(v reflect.Value) (*ptrinfo, bool) { 463 | if isPointerValue(v) { 464 | if info, ok := s.pointers.get(v); ok { 465 | firstVisit := s.visitedPointers.add(v) 466 | return info, firstVisit 467 | } 468 | } 469 | return nil, false 470 | } 471 | 472 | // prepares a new state object for dumping the provided value 473 | func newDumpState(value reflect.Value, options *Options, writer io.Writer) *dumpState { 474 | result := &dumpState{ 475 | config: options, 476 | pointers: mapReusedPointers(value), 477 | w: writer, 478 | } 479 | 480 | if options.FormatTime { 481 | result.timeFormatter = func(t time.Time) string { 482 | t = t.In(time.UTC) 483 | return fmt.Sprintf( 484 | `time.Date(%d, %d, %d, %d, %d, %d, %d, time.UTC)`, 485 | t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute(), t.Second(), t.Nanosecond(), 486 | ) 487 | } 488 | } 489 | 490 | if options.HomePackage != "" { 491 | result.homePackageRegexp = regexp.MustCompile(fmt.Sprintf("\\b%s\\.", options.HomePackage)) 492 | } 493 | 494 | return result 495 | } 496 | 497 | // Dump a value to stdout. 498 | func Dump(value ...interface{}) { 499 | (&Config).Dump(value...) 500 | } 501 | 502 | // D dumps a value to stdout, and is a shorthand for [Dump]. 503 | func D(value ...interface{}) { 504 | Dump(value...) 505 | } 506 | 507 | // Sdump dumps a value to a string. 508 | func Sdump(value ...interface{}) string { 509 | return (&Config).Sdump(value...) 510 | } 511 | 512 | // Dump a value to stdout according to the options 513 | func (o Options) Dump(values ...interface{}) { 514 | for i, value := range values { 515 | state := newDumpState(reflect.ValueOf(value), &o, os.Stdout) 516 | if i > 0 { 517 | state.write([]byte(o.Separator)) 518 | } 519 | state.dump(value) 520 | } 521 | _, _ = os.Stdout.Write([]byte("\n")) 522 | } 523 | 524 | // Sdump dumps a value to a string according to the options 525 | func (o Options) Sdump(values ...interface{}) string { 526 | buf := new(bytes.Buffer) 527 | for i, value := range values { 528 | if i > 0 { 529 | _, _ = buf.Write([]byte(o.Separator)) 530 | } 531 | state := newDumpState(reflect.ValueOf(value), &o, buf) 532 | state.dump(value) 533 | } 534 | return buf.String() 535 | } 536 | 537 | type mapKeySorter struct { 538 | keys []reflect.Value 539 | options *Options 540 | } 541 | 542 | func (s mapKeySorter) Len() int { 543 | return len(s.keys) 544 | } 545 | 546 | func (s mapKeySorter) Swap(i, j int) { 547 | s.keys[i], s.keys[j] = s.keys[j], s.keys[i] 548 | } 549 | 550 | func (s mapKeySorter) Less(i, j int) bool { 551 | ibuf := new(bytes.Buffer) 552 | jbuf := new(bytes.Buffer) 553 | newDumpState(s.keys[i], s.options, ibuf).dumpVal(s.keys[i]) 554 | newDumpState(s.keys[j], s.options, jbuf).dumpVal(s.keys[j]) 555 | return ibuf.String() < jbuf.String() 556 | } 557 | -------------------------------------------------------------------------------- /dump_test.go: -------------------------------------------------------------------------------- 1 | package litter_test 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "io" 7 | "os" 8 | "os/exec" 9 | "reflect" 10 | "testing" 11 | "time" 12 | 13 | "github.com/stretchr/testify/assert" 14 | "github.com/stretchr/testify/require" 15 | 16 | "github.com/sanity-io/litter" 17 | ) 18 | 19 | func Function(string, int) (string, error) { 20 | return "", nil 21 | } 22 | 23 | type BlankStruct struct{} 24 | 25 | type BasicStruct struct { 26 | Public int 27 | private int 28 | } 29 | 30 | type IntAlias int 31 | 32 | type InterfaceStruct struct { 33 | Ifc interface{} 34 | } 35 | 36 | type RecursiveStruct struct { 37 | Ptr *RecursiveStruct 38 | } 39 | 40 | type CustomMap map[string]int 41 | 42 | type CustomMultiLineDumper struct { 43 | Dummy int 44 | } 45 | 46 | func (cmld *CustomMultiLineDumper) LitterDump(w io.Writer) { 47 | _, _ = w.Write([]byte("{\n multi\n line\n}")) 48 | } 49 | 50 | type CustomSingleLineDumper int 51 | 52 | func (csld CustomSingleLineDumper) LitterDump(w io.Writer) { 53 | _, _ = w.Write([]byte("")) 54 | } 55 | 56 | func TestSdump_primitives(t *testing.T) { 57 | messages := make(chan string, 3) 58 | sends := make(chan<- int64, 1) 59 | receives := make(<-chan uint64) 60 | 61 | runTests(t, "primitives", []interface{}{ 62 | false, 63 | true, 64 | 7, 65 | int8(10), 66 | int16(10), 67 | int32(10), 68 | int64(10), 69 | uint8(10), 70 | uint16(10), 71 | uint32(10), 72 | uint64(10), 73 | uint(10), 74 | float32(12.3), 75 | float64(12.3), 76 | float32(1.0), 77 | float64(1.0), 78 | complex64(12 + 10.5i), 79 | complex128(-1.2 - 0.1i), 80 | (func(v int) *int { return &v })(10), 81 | "string with \"quote\"", 82 | []int{1, 2, 3}, 83 | interface{}("hello from interface"), 84 | BlankStruct{}, 85 | &BlankStruct{}, 86 | BasicStruct{1, 2}, 87 | IntAlias(10), 88 | (func(v IntAlias) *IntAlias { return &v })(10), 89 | Function, 90 | func(arg string) (bool, error) { return false, nil }, 91 | nil, 92 | interface{}(nil), 93 | CustomMap{}, 94 | CustomMap(nil), 95 | messages, 96 | sends, 97 | receives, 98 | }) 99 | } 100 | 101 | func TestSdump_customDumper(t *testing.T) { 102 | cmld := CustomMultiLineDumper{Dummy: 1} 103 | cmld2 := CustomMultiLineDumper{Dummy: 2} 104 | csld := CustomSingleLineDumper(42) 105 | csld2 := CustomSingleLineDumper(43) 106 | runTests(t, "customDumper", map[string]interface{}{ 107 | "v1": &cmld, 108 | "v2": &cmld, 109 | "v2x": &cmld2, 110 | "v3": csld, 111 | "v4": &csld, 112 | "v5": &csld, 113 | "v6": &csld2, 114 | }) 115 | } 116 | 117 | func TestSdump_pointerAliasing(t *testing.T) { 118 | p0 := &RecursiveStruct{Ptr: nil} 119 | p1 := &RecursiveStruct{Ptr: p0} 120 | p2 := &RecursiveStruct{} 121 | p2.Ptr = p2 122 | 123 | runTests(t, "pointerAliasing", []*RecursiveStruct{ 124 | p0, 125 | p0, 126 | p1, 127 | p2, 128 | }) 129 | } 130 | 131 | func TestSdump_nilIntefacesInStructs(t *testing.T) { 132 | p0 := &InterfaceStruct{nil} 133 | p1 := &InterfaceStruct{p0} 134 | 135 | runTests(t, "nilIntefacesInStructs", []*InterfaceStruct{ 136 | p0, 137 | p1, 138 | p0, 139 | nil, 140 | }) 141 | } 142 | 143 | func TestSdump_config(t *testing.T) { 144 | type options struct { 145 | Compact bool 146 | StripPackageNames bool 147 | HidePrivateFields bool 148 | HomePackage string 149 | Separator string 150 | StrictGo bool 151 | } 152 | 153 | opts := options{ 154 | StripPackageNames: false, 155 | HidePrivateFields: true, 156 | Separator: " ", 157 | } 158 | 159 | data := []interface{}{ 160 | opts, 161 | &BasicStruct{1, 2}, 162 | Function, 163 | (func(v int) *int { return &v })(20), 164 | (func(v IntAlias) *IntAlias { return &v })(20), 165 | litter.Dump, 166 | func(s string, i int) (bool, error) { return false, nil }, 167 | time.Date(2018, 1, 1, 0, 0, 0, 0, time.UTC), 168 | } 169 | 170 | runTestWithCfg(t, "config_Compact", &litter.Options{ 171 | Compact: true, 172 | }, data) 173 | runTestWithCfg(t, "config_HidePrivateFields", &litter.Options{ 174 | HidePrivateFields: true, 175 | }, data) 176 | runTestWithCfg(t, "config_HideZeroValues", &litter.Options{ 177 | HideZeroValues: true, 178 | }, data) 179 | runTestWithCfg(t, "config_StripPackageNames", &litter.Options{ 180 | StripPackageNames: true, 181 | }, data) 182 | runTestWithCfg(t, "config_HomePackage", &litter.Options{ 183 | HomePackage: "litter_test", 184 | }, data) 185 | runTestWithCfg(t, "config_FieldFilter", &litter.Options{ 186 | FieldFilter: func(f reflect.StructField, v reflect.Value) bool { 187 | return f.Type.Kind() == reflect.String 188 | }, 189 | }, data) 190 | runTestWithCfg(t, "config_StrictGo", &litter.Options{ 191 | StrictGo: true, 192 | }, data) 193 | runTestWithCfg(t, "config_DumpFunc", &litter.Options{ 194 | DumpFunc: func(v reflect.Value, w io.Writer) bool { 195 | if !v.CanInterface() { 196 | return false 197 | } 198 | if b, ok := v.Interface().(bool); ok { 199 | if b { 200 | io.WriteString(w, `"on"`) 201 | } else { 202 | io.WriteString(w, `"off"`) 203 | } 204 | return true 205 | } 206 | return false 207 | }, 208 | }, data) 209 | runTestWithCfg(t, "config_FormatTime", &litter.Options{ 210 | FormatTime: true, 211 | }, data) 212 | 213 | basic := &BasicStruct{1, 2} 214 | runTestWithCfg(t, "config_DisablePointerReplacement_simpleReusedStruct", &litter.Options{ 215 | DisablePointerReplacement: true, 216 | }, []interface{}{basic, basic}) 217 | circular := &RecursiveStruct{} 218 | circular.Ptr = circular 219 | runTestWithCfg(t, "config_DisablePointerReplacement_circular", &litter.Options{ 220 | DisablePointerReplacement: true, 221 | }, circular) 222 | } 223 | 224 | func TestSdump_multipleArgs(t *testing.T) { 225 | value1 := []string{"x", "y"} 226 | value2 := int32(42) 227 | 228 | runTestWithCfg(t, "multipleArgs_noSeparator", &litter.Options{}, value1, value2) 229 | runTestWithCfg(t, "multipleArgs_lineBreak", &litter.Options{Separator: "\n"}, value1, value2) 230 | runTestWithCfg(t, "multipleArgs_separator", &litter.Options{Separator: "***"}, value1, value2) 231 | } 232 | 233 | func TestSdump_maps(t *testing.T) { 234 | runTests(t, "maps", []interface{}{ 235 | map[string]string{ 236 | "hello": "there", 237 | "something": "something something", 238 | "another string": "indeed", 239 | }, 240 | map[int]string{ 241 | 3: "three", 242 | 1: "one", 243 | 2: "two", 244 | }, 245 | map[int]*BlankStruct{ 246 | 2: {}, 247 | }, 248 | }) 249 | } 250 | 251 | func TestSdump_RecursiveMaps(t *testing.T) { 252 | mp := make(map[*RecursiveStruct]*RecursiveStruct) 253 | k1 := &RecursiveStruct{} 254 | k1.Ptr = k1 255 | v1 := &RecursiveStruct{} 256 | v1.Ptr = v1 257 | k2 := &RecursiveStruct{} 258 | k2.Ptr = k2 259 | v2 := &RecursiveStruct{} 260 | v2.Ptr = v2 261 | mp[k1] = v1 262 | mp[k2] = v2 263 | runTests(t, "recursive_maps", mp) 264 | } 265 | 266 | type unexportedStruct struct { 267 | x int 268 | } 269 | type StructWithUnexportedType struct { 270 | unexported unexportedStruct 271 | } 272 | 273 | func TestSdump_unexported(t *testing.T) { 274 | runTests(t, "unexported", StructWithUnexportedType{ 275 | unexported: unexportedStruct{}, 276 | }) 277 | } 278 | 279 | var standardCfg = litter.Options{} 280 | 281 | func runTestWithCfg(t *testing.T, name string, cfg *litter.Options, cases ...interface{}) { 282 | t.Run(name, func(t *testing.T) { 283 | fileName := fmt.Sprintf("testdata/%s.dump", name) 284 | 285 | dump := cfg.Sdump(cases...) 286 | 287 | reference, err := os.ReadFile(fileName) 288 | if os.IsNotExist(err) { 289 | t.Logf("Note: Test data file %s does not exist, writing it; verify contents!", fileName) 290 | err := os.WriteFile(fileName, []byte(dump), 0644) 291 | if err != nil { 292 | t.Error(err) 293 | } 294 | return 295 | } 296 | 297 | assertEqualStringsWithDiff(t, string(reference), dump) 298 | }) 299 | } 300 | 301 | func runTests(t *testing.T, name string, cases ...interface{}) { 302 | runTestWithCfg(t, name, &standardCfg, cases...) 303 | } 304 | 305 | func diffStrings(t *testing.T, expected, actual string) (*string, bool) { 306 | if actual == expected { 307 | return nil, true 308 | } 309 | 310 | dir, err := os.MkdirTemp("", "test") 311 | require.NoError(t, err) 312 | defer os.RemoveAll(dir) 313 | 314 | require.NoError(t, os.WriteFile(fmt.Sprintf("%s/expected", dir), []byte(expected), 0644)) 315 | require.NoError(t, os.WriteFile(fmt.Sprintf("%s/actual", dir), []byte(actual), 0644)) 316 | 317 | out, err := exec.Command("diff", "--side-by-side", 318 | fmt.Sprintf("%s/expected", dir), 319 | fmt.Sprintf("%s/actual", dir)).Output() 320 | 321 | var exitErr *exec.ExitError 322 | if !errors.As(err, &exitErr) { 323 | require.NoError(t, err) 324 | } 325 | 326 | diff := string(out) 327 | return &diff, false 328 | } 329 | 330 | func assertEqualStringsWithDiff(t *testing.T, expected, actual string, 331 | msgAndArgs ...interface{}) bool { 332 | diff, ok := diffStrings(t, expected, actual) 333 | if ok { 334 | return true 335 | } 336 | 337 | message := messageFromMsgAndArgs(msgAndArgs...) 338 | if message == "" { 339 | message = "Strings are different" 340 | } 341 | assert.Fail(t, fmt.Sprintf("%s (left is expected, right is actual):\n%s", message, *diff)) 342 | return false 343 | } 344 | 345 | func messageFromMsgAndArgs(msgAndArgs ...interface{}) string { 346 | if len(msgAndArgs) == 0 || msgAndArgs == nil { 347 | return "" 348 | } 349 | if len(msgAndArgs) == 1 { 350 | return msgAndArgs[0].(string) 351 | } 352 | if len(msgAndArgs) > 1 { 353 | return fmt.Sprintf(msgAndArgs[0].(string), msgAndArgs[1:]...) 354 | } 355 | return "" 356 | } 357 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/sanity-io/litter 2 | 3 | go 1.16 4 | 5 | require ( 6 | github.com/davecgh/go-spew v0.0.0-20161028175848-04cdfd42973b // indirect 7 | github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0 // indirect 8 | github.com/stretchr/testify v0.0.0-20161117074351-18a02ba4a312 9 | ) 10 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v0.0.0-20161028175848-04cdfd42973b h1:XxMZvQZtTXpWMNWK82vdjCLCe7uGMFXdTsJH0v3Hkvw= 2 | github.com/davecgh/go-spew v0.0.0-20161028175848-04cdfd42973b/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0 h1:GD+A8+e+wFkqje55/2fOVnZPkoDIu1VooBWfNrnY8Uo= 4 | github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 5 | github.com/stretchr/testify v0.0.0-20161117074351-18a02ba4a312 h1:UsFdQ3ZmlzS0BqZYGxvYaXvFGUbCmPGy8DM7qWJJiIQ= 6 | github.com/stretchr/testify v0.0.0-20161117074351-18a02ba4a312/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 7 | -------------------------------------------------------------------------------- /pointers.go: -------------------------------------------------------------------------------- 1 | package litter 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | "sort" 7 | ) 8 | 9 | // mapReusedPointers takes a structure, and recursively maps all pointers mentioned in the tree, 10 | // detecting circular references, and providing a list of all pointers that was referenced at 11 | // least twice by the provided structure. 12 | func mapReusedPointers(v reflect.Value) ptrmap { 13 | pm := &pointerVisitor{} 14 | pm.consider(v) 15 | return pm.reused 16 | } 17 | 18 | // A map of pointers. 19 | type ptrinfo struct { 20 | id int 21 | parent *ptrmap 22 | } 23 | 24 | func (p *ptrinfo) label() string { 25 | if p.id == -1 { 26 | p.id = p.parent.count 27 | p.parent.count++ 28 | } 29 | return fmt.Sprintf("p%d", p.id) 30 | } 31 | 32 | type ptrkey struct { 33 | p uintptr 34 | t reflect.Type 35 | } 36 | 37 | func ptrkeyFor(v reflect.Value) (k ptrkey) { 38 | k.p = v.Pointer() 39 | for v.Kind() == reflect.Ptr { 40 | v = v.Elem() 41 | } 42 | if v.IsValid() { 43 | k.t = v.Type() 44 | } 45 | return 46 | } 47 | 48 | type ptrmap struct { 49 | m map[ptrkey]*ptrinfo 50 | count int 51 | } 52 | 53 | // Returns true if contains a pointer. 54 | func (pm *ptrmap) contains(v reflect.Value) bool { 55 | if pm.m != nil { 56 | _, ok := pm.m[ptrkeyFor(v)] 57 | return ok 58 | } 59 | return false 60 | } 61 | 62 | // Gets a pointer. 63 | func (pm *ptrmap) get(v reflect.Value) (*ptrinfo, bool) { 64 | if pm.m != nil { 65 | p, ok := pm.m[ptrkeyFor(v)] 66 | return p, ok 67 | } 68 | return nil, false 69 | } 70 | 71 | // Removes a pointer. 72 | func (pm *ptrmap) remove(v reflect.Value) { 73 | if pm.m != nil { 74 | delete(pm.m, ptrkeyFor(v)) 75 | } 76 | } 77 | 78 | // Adds a pointer. 79 | func (pm *ptrmap) add(p reflect.Value) bool { 80 | if pm.contains(p) { 81 | return false 82 | } 83 | pm.put(p) 84 | return true 85 | } 86 | 87 | // Adds a pointer (slow path). 88 | func (pm *ptrmap) put(v reflect.Value) { 89 | if pm.m == nil { 90 | pm.m = make(map[ptrkey]*ptrinfo, 31) 91 | } 92 | 93 | key := ptrkeyFor(v) 94 | if _, ok := pm.m[key]; !ok { 95 | pm.m[key] = &ptrinfo{id: -1, parent: pm} 96 | } 97 | } 98 | 99 | type pointerVisitor struct { 100 | pointers ptrmap 101 | reused ptrmap 102 | } 103 | 104 | // Recursively consider v and each of its children, updating the map according to the 105 | // semantics of MapReusedPointers 106 | func (pv *pointerVisitor) consider(v reflect.Value) { 107 | if v.Kind() == reflect.Invalid { 108 | return 109 | } 110 | if isPointerValue(v) { // pointer is 0 for unexported fields 111 | if pv.tryAddPointer(v) { 112 | // No use descending inside this value, since it have been seen before and all its descendants 113 | // have been considered 114 | return 115 | } 116 | } 117 | 118 | // Now descend into any children of this value 119 | switch v.Kind() { 120 | case reflect.Slice, reflect.Array: 121 | for i := 0; i < v.Len(); i++ { 122 | pv.consider(v.Index(i)) 123 | } 124 | 125 | case reflect.Interface: 126 | pv.consider(v.Elem()) 127 | 128 | case reflect.Ptr: 129 | pv.consider(v.Elem()) 130 | 131 | case reflect.Map: 132 | keys := v.MapKeys() 133 | sort.Sort(mapKeySorter{ 134 | keys: keys, 135 | options: &Config, 136 | }) 137 | for _, key := range keys { 138 | pv.consider(key) 139 | pv.consider(v.MapIndex(key)) 140 | } 141 | 142 | case reflect.Struct: 143 | numFields := v.NumField() 144 | for i := 0; i < numFields; i++ { 145 | pv.consider(v.Field(i)) 146 | } 147 | } 148 | } 149 | 150 | // addPointer to the pointerMap, update reusedPointers. Returns true if pointer was reused 151 | func (pv *pointerVisitor) tryAddPointer(v reflect.Value) bool { 152 | // Is this allready known to be reused? 153 | if pv.reused.contains(v) { 154 | return true 155 | } 156 | 157 | // Have we seen it once before? 158 | if pv.pointers.contains(v) { 159 | // Add it to the register of pointers we have seen more than once 160 | pv.reused.add(v) 161 | return true 162 | } 163 | 164 | // This pointer was new to us 165 | pv.pointers.add(v) 166 | return false 167 | } 168 | -------------------------------------------------------------------------------- /print.go: -------------------------------------------------------------------------------- 1 | package litter 2 | 3 | import ( 4 | "io" 5 | "math" 6 | "strconv" 7 | ) 8 | 9 | func printBool(w io.Writer, value bool) { 10 | if value { 11 | w.Write([]byte("true")) 12 | return 13 | } 14 | w.Write([]byte("false")) 15 | } 16 | 17 | func printInt(w io.Writer, val int64, base int) { 18 | w.Write([]byte(strconv.FormatInt(val, base))) 19 | } 20 | 21 | func printUint(w io.Writer, val uint64, base int) { 22 | w.Write([]byte(strconv.FormatUint(val, base))) 23 | } 24 | 25 | func printFloat(w io.Writer, val float64, precision int) { 26 | if math.Trunc(val) == val { 27 | // Ensure that floats like 1.0 are always printed with a decimal point 28 | w.Write([]byte(strconv.FormatFloat(val, 'f', 1, precision))) 29 | } else { 30 | w.Write([]byte(strconv.FormatFloat(val, 'g', -1, precision))) 31 | } 32 | } 33 | 34 | func printComplex(w io.Writer, c complex128, floatPrecision int) { 35 | w.Write([]byte("complex")) 36 | printInt(w, int64(floatPrecision*2), 10) 37 | r := real(c) 38 | w.Write([]byte("(")) 39 | w.Write([]byte(strconv.FormatFloat(r, 'g', -1, floatPrecision))) 40 | i := imag(c) 41 | if i >= 0 { 42 | w.Write([]byte("+")) 43 | } 44 | w.Write([]byte(strconv.FormatFloat(i, 'g', -1, floatPrecision))) 45 | w.Write([]byte("i)")) 46 | } 47 | 48 | func printNil(w io.Writer) { 49 | w.Write([]byte("nil")) 50 | } 51 | -------------------------------------------------------------------------------- /testdata/config_Compact.dump: -------------------------------------------------------------------------------- 1 | []interface{}{litter_test.options{Compact:false,StripPackageNames:false,HidePrivateFields:true,HomePackage:"",Separator:" ",StrictGo:false},&litter_test.BasicStruct{Public:1,private:2},litter_test.Function,&20,&20,litter.Dump,func(string,int)(bool,error),time.Time{wall:0,ext:63650361600,loc:nil}} -------------------------------------------------------------------------------- /testdata/config_DisablePointerReplacement_circular.dump: -------------------------------------------------------------------------------- 1 | &litter_test.RecursiveStruct{ // p0 2 | Ptr: p0, 3 | } -------------------------------------------------------------------------------- /testdata/config_DisablePointerReplacement_simpleReusedStruct.dump: -------------------------------------------------------------------------------- 1 | []interface {}{ 2 | &litter_test.BasicStruct{ // p0 3 | Public: 1, 4 | private: 2, 5 | }, 6 | &litter_test.BasicStruct{ // p0 7 | Public: 1, 8 | private: 2, 9 | }, 10 | } -------------------------------------------------------------------------------- /testdata/config_DumpFunc.dump: -------------------------------------------------------------------------------- 1 | []interface {}{ 2 | litter_test.options{ 3 | Compact: bool"off", 4 | StripPackageNames: bool"off", 5 | HidePrivateFields: bool"on", 6 | HomePackage: "", 7 | Separator: " ", 8 | StrictGo: bool"off", 9 | }, 10 | &litter_test.BasicStruct{ 11 | Public: 1, 12 | private: 2, 13 | }, 14 | litter_test.Function, 15 | &20, 16 | &20, 17 | litter.Dump, 18 | func(string, int) (bool, error), 19 | time.Time{ 20 | wall: 0, 21 | ext: 63650361600, 22 | loc: nil, 23 | }, 24 | } -------------------------------------------------------------------------------- /testdata/config_FieldFilter.dump: -------------------------------------------------------------------------------- 1 | []interface {}{ 2 | litter_test.options{ 3 | HomePackage: "", 4 | Separator: " ", 5 | }, 6 | &litter_test.BasicStruct{}, 7 | litter_test.Function, 8 | &20, 9 | &20, 10 | litter.Dump, 11 | func(string, int) (bool, error), 12 | time.Time{}, 13 | } -------------------------------------------------------------------------------- /testdata/config_FormatTime.dump: -------------------------------------------------------------------------------- 1 | []interface {}{ 2 | litter_test.options{ 3 | Compact: false, 4 | StripPackageNames: false, 5 | HidePrivateFields: true, 6 | HomePackage: "", 7 | Separator: " ", 8 | StrictGo: false, 9 | }, 10 | &litter_test.BasicStruct{ 11 | Public: 1, 12 | private: 2, 13 | }, 14 | litter_test.Function, 15 | &20, 16 | &20, 17 | litter.Dump, 18 | func(string, int) (bool, error), 19 | time.Date(2018, 1, 1, 0, 0, 0, 0, time.UTC), 20 | } -------------------------------------------------------------------------------- /testdata/config_HidePrivateFields.dump: -------------------------------------------------------------------------------- 1 | []interface {}{ 2 | litter_test.options{ 3 | Compact: false, 4 | StripPackageNames: false, 5 | HidePrivateFields: true, 6 | HomePackage: "", 7 | Separator: " ", 8 | StrictGo: false, 9 | }, 10 | &litter_test.BasicStruct{ 11 | Public: 1, 12 | }, 13 | litter_test.Function, 14 | &20, 15 | &20, 16 | litter.Dump, 17 | func(string, int) (bool, error), 18 | time.Time{}, 19 | } -------------------------------------------------------------------------------- /testdata/config_HideZeroValues.dump: -------------------------------------------------------------------------------- 1 | []interface {}{ 2 | litter_test.options{ 3 | HidePrivateFields: true, 4 | Separator: " ", 5 | }, 6 | &litter_test.BasicStruct{ 7 | Public: 1, 8 | private: 2, 9 | }, 10 | litter_test.Function, 11 | &20, 12 | &20, 13 | litter.Dump, 14 | func(string, int) (bool, error), 15 | time.Time{ 16 | wall: 0, 17 | ext: 63650361600, 18 | }, 19 | } -------------------------------------------------------------------------------- /testdata/config_HomePackage.dump: -------------------------------------------------------------------------------- 1 | []interface {}{ 2 | options{ 3 | Compact: false, 4 | StripPackageNames: false, 5 | HidePrivateFields: true, 6 | HomePackage: "", 7 | Separator: " ", 8 | StrictGo: false, 9 | }, 10 | &BasicStruct{ 11 | Public: 1, 12 | private: 2, 13 | }, 14 | Function, 15 | &20, 16 | &20, 17 | litter.Dump, 18 | func(string, int) (bool, error), 19 | time.Time{ 20 | wall: 0, 21 | ext: 63650361600, 22 | loc: nil, 23 | }, 24 | } -------------------------------------------------------------------------------- /testdata/config_StrictGo.dump: -------------------------------------------------------------------------------- 1 | []interface {}{ 2 | litter_test.options{ 3 | Compact: false, 4 | StripPackageNames: false, 5 | HidePrivateFields: true, 6 | HomePackage: "", 7 | Separator: " ", 8 | StrictGo: false, 9 | }, 10 | (func(v litter_test.BasicStruct) *litter_test.BasicStruct { return &v })(litter_test.BasicStruct{ 11 | Public: 1, 12 | private: 2, 13 | }), 14 | litter_test.Function, 15 | (func(v int) *int { return &v })(20), 16 | (func(v litter_test.IntAlias) *litter_test.IntAlias { return &v })(20), 17 | litter.Dump, 18 | func(string, int) (bool, error), 19 | time.Time{ 20 | wall: 0, 21 | ext: 63650361600, 22 | loc: nil, 23 | }, 24 | } -------------------------------------------------------------------------------- /testdata/config_StripPackageNames.dump: -------------------------------------------------------------------------------- 1 | []interface {}{ 2 | options{ 3 | Compact: false, 4 | StripPackageNames: false, 5 | HidePrivateFields: true, 6 | HomePackage: "", 7 | Separator: " ", 8 | StrictGo: false, 9 | }, 10 | &BasicStruct{ 11 | Public: 1, 12 | private: 2, 13 | }, 14 | Function, 15 | &20, 16 | &20, 17 | Dump, 18 | func(string, int) (bool, error), 19 | Time{ 20 | wall: 0, 21 | ext: 63650361600, 22 | loc: nil, 23 | }, 24 | } -------------------------------------------------------------------------------- /testdata/customDumper.dump: -------------------------------------------------------------------------------- 1 | map[string]interface {}{ 2 | "v1": *litter_test.CustomMultiLineDumper{ // p0 3 | multi 4 | line 5 | }, 6 | "v2": p0, 7 | "v2x": *litter_test.CustomMultiLineDumper{ 8 | multi 9 | line 10 | }, 11 | "v3": litter_test.CustomSingleLineDumper, 12 | "v4": *litter_test.CustomSingleLineDumper, // p1 13 | "v5": p1, 14 | "v6": *litter_test.CustomSingleLineDumper, 15 | } -------------------------------------------------------------------------------- /testdata/maps.dump: -------------------------------------------------------------------------------- 1 | []interface {}{ 2 | map[string]string{ 3 | "another string": "indeed", 4 | "hello": "there", 5 | "something": "something something", 6 | }, 7 | map[int]string{ 8 | 1: "one", 9 | 2: "two", 10 | 3: "three", 11 | }, 12 | map[int]*litter_test.BlankStruct{ 13 | 2: &litter_test.BlankStruct{}, 14 | }, 15 | } -------------------------------------------------------------------------------- /testdata/multipleArgs_lineBreak.dump: -------------------------------------------------------------------------------- 1 | []string{ 2 | "x", 3 | "y", 4 | } 5 | 42 -------------------------------------------------------------------------------- /testdata/multipleArgs_noSeparator.dump: -------------------------------------------------------------------------------- 1 | []string{ 2 | "x", 3 | "y", 4 | }42 -------------------------------------------------------------------------------- /testdata/multipleArgs_separator.dump: -------------------------------------------------------------------------------- 1 | []string{ 2 | "x", 3 | "y", 4 | }***42 -------------------------------------------------------------------------------- /testdata/nilIntefacesInStructs.dump: -------------------------------------------------------------------------------- 1 | []*litter_test.InterfaceStruct{ 2 | &litter_test.InterfaceStruct{ // p0 3 | Ifc: nil, 4 | }, 5 | &litter_test.InterfaceStruct{ 6 | Ifc: p0, 7 | }, 8 | p0, 9 | nil, 10 | } -------------------------------------------------------------------------------- /testdata/pointerAliasing.dump: -------------------------------------------------------------------------------- 1 | []*litter_test.RecursiveStruct{ 2 | &litter_test.RecursiveStruct{ // p0 3 | Ptr: nil, 4 | }, 5 | p0, 6 | &litter_test.RecursiveStruct{ 7 | Ptr: p0, 8 | }, 9 | &litter_test.RecursiveStruct{ // p1 10 | Ptr: p1, 11 | }, 12 | } -------------------------------------------------------------------------------- /testdata/primitives.dump: -------------------------------------------------------------------------------- 1 | []interface {}{ 2 | false, 3 | true, 4 | 7, 5 | 10, 6 | 10, 7 | 10, 8 | 10, 9 | 10, 10 | 10, 11 | 10, 12 | 10, 13 | 10, 14 | 12.3, 15 | 12.3, 16 | 1.0, 17 | 1.0, 18 | complex64(12+10.5i), 19 | complex128(-1.2-0.1i), 20 | &10, 21 | "string with \"quote\"", 22 | []int{ 23 | 1, 24 | 2, 25 | 3, 26 | }, 27 | "hello from interface", 28 | litter_test.BlankStruct{}, 29 | &litter_test.BlankStruct{}, 30 | litter_test.BasicStruct{ 31 | Public: 1, 32 | private: 2, 33 | }, 34 | 10, 35 | &10, 36 | litter_test.Function, 37 | func(string) (bool, error), 38 | nil, 39 | nil, 40 | litter_test.CustomMap{}, 41 | litter_test.CustomMap(nil), 42 | chan string, 43 | chan<- int64, 44 | <-chan uint64, 45 | } -------------------------------------------------------------------------------- /testdata/recursive_maps.dump: -------------------------------------------------------------------------------- 1 | map[*litter_test.RecursiveStruct]*litter_test.RecursiveStruct{ 2 | &litter_test.RecursiveStruct{ // p0 3 | Ptr: p0, 4 | }: &litter_test.RecursiveStruct{ // p1 5 | Ptr: p1, 6 | }, 7 | &litter_test.RecursiveStruct{ // p2 8 | Ptr: p2, 9 | }: &litter_test.RecursiveStruct{ // p3 10 | Ptr: p3, 11 | }, 12 | } -------------------------------------------------------------------------------- /testdata/unexported.dump: -------------------------------------------------------------------------------- 1 | litter_test.StructWithUnexportedType{ 2 | unexported: litter_test.unexportedStruct{ 3 | x: 0, 4 | }, 5 | } -------------------------------------------------------------------------------- /util.go: -------------------------------------------------------------------------------- 1 | package litter 2 | 3 | import ( 4 | "reflect" 5 | ) 6 | 7 | // deInterface returns values inside of non-nil interfaces when possible. 8 | // This is useful for data types like structs, arrays, slices, and maps which 9 | // can contain varying types packed inside an interface. 10 | func deInterface(v reflect.Value) reflect.Value { 11 | if v.Kind() == reflect.Interface && !v.IsNil() { 12 | v = v.Elem() 13 | } 14 | return v 15 | } 16 | 17 | func isPointerValue(v reflect.Value) bool { 18 | switch v.Kind() { 19 | case reflect.Chan, reflect.Func, reflect.Map, reflect.Ptr, reflect.Slice, reflect.UnsafePointer: 20 | return true 21 | } 22 | return false 23 | } 24 | 25 | func isZeroValue(v reflect.Value) bool { 26 | return (isPointerValue(v) && v.IsNil()) || 27 | (v.IsValid() && v.CanInterface() && reflect.DeepEqual(v.Interface(), reflect.Zero(v.Type()).Interface())) 28 | } 29 | --------------------------------------------------------------------------------