mirror of
https://github.com/jesseduffield/lazygit.git
synced 2025-01-12 04:23:03 +02:00
529 lines
12 KiB
Go
529 lines
12 KiB
Go
package litter
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"reflect"
|
|
"regexp"
|
|
"runtime"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
var (
|
|
packageNameStripperRegexp = regexp.MustCompile(`\b[a-zA-Z_]+[a-zA-Z_0-9]+\.`)
|
|
compactTypeRegexp = regexp.MustCompile(`\s*([,;{}()])\s*`)
|
|
)
|
|
|
|
// Dumper is the interface for implementing custom dumper for your types.
|
|
type Dumper interface {
|
|
LitterDump(w io.Writer)
|
|
}
|
|
|
|
// Options represents configuration options for litter
|
|
type Options struct {
|
|
Compact bool
|
|
StripPackageNames bool
|
|
HidePrivateFields bool
|
|
HideZeroValues bool
|
|
FieldExclusions *regexp.Regexp
|
|
FieldFilter func(reflect.StructField, reflect.Value) bool
|
|
HomePackage string
|
|
Separator string
|
|
StrictGo bool
|
|
DumpFunc func(reflect.Value, io.Writer) bool
|
|
|
|
// DisablePointerReplacement, if true, disables the replacing of pointer data with variable names
|
|
// when it's safe. This is useful for diffing two structures, where pointer variables would cause
|
|
// false changes. However, circular graphs are still detected and elided to avoid infinite output.
|
|
DisablePointerReplacement bool
|
|
}
|
|
|
|
// Config is the default config used when calling Dump
|
|
var Config = Options{
|
|
StripPackageNames: false,
|
|
HidePrivateFields: true,
|
|
FieldExclusions: regexp.MustCompile(`^(XXX_.*)$`), // XXX_ is a prefix of fields generated by protoc-gen-go
|
|
Separator: " ",
|
|
}
|
|
|
|
type dumpState struct {
|
|
w io.Writer
|
|
depth int
|
|
config *Options
|
|
pointers ptrmap
|
|
visitedPointers ptrmap
|
|
parentPointers ptrmap
|
|
currentPointer *ptrinfo
|
|
homePackageRegexp *regexp.Regexp
|
|
}
|
|
|
|
func (s *dumpState) write(b []byte) {
|
|
if _, err := s.w.Write(b); err != nil {
|
|
panic(err)
|
|
}
|
|
}
|
|
|
|
func (s *dumpState) writeString(str string) {
|
|
s.write([]byte(str))
|
|
}
|
|
|
|
func (s *dumpState) indent() {
|
|
if !s.config.Compact {
|
|
s.write(bytes.Repeat([]byte(" "), s.depth))
|
|
}
|
|
}
|
|
|
|
func (s *dumpState) newlineWithPointerNameComment() {
|
|
if ptr := s.currentPointer; ptr != nil {
|
|
if s.config.Compact {
|
|
s.write([]byte(fmt.Sprintf("/*%s*/", ptr.label())))
|
|
} else {
|
|
s.write([]byte(fmt.Sprintf(" // %s\n", ptr.label())))
|
|
}
|
|
s.currentPointer = nil
|
|
return
|
|
}
|
|
if !s.config.Compact {
|
|
s.write([]byte("\n"))
|
|
}
|
|
}
|
|
|
|
func (s *dumpState) dumpType(v reflect.Value) {
|
|
typeName := v.Type().String()
|
|
if s.config.StripPackageNames {
|
|
typeName = packageNameStripperRegexp.ReplaceAllLiteralString(typeName, "")
|
|
} else if s.homePackageRegexp != nil {
|
|
typeName = s.homePackageRegexp.ReplaceAllLiteralString(typeName, "")
|
|
}
|
|
if s.config.Compact {
|
|
typeName = compactTypeRegexp.ReplaceAllString(typeName, "$1")
|
|
}
|
|
s.write([]byte(typeName))
|
|
}
|
|
|
|
func (s *dumpState) dumpSlice(v reflect.Value) {
|
|
s.dumpType(v)
|
|
numEntries := v.Len()
|
|
if numEntries == 0 {
|
|
s.write([]byte("{}"))
|
|
return
|
|
}
|
|
s.write([]byte("{"))
|
|
s.newlineWithPointerNameComment()
|
|
s.depth++
|
|
for i := 0; i < numEntries; i++ {
|
|
s.indent()
|
|
s.dumpVal(v.Index(i))
|
|
if !s.config.Compact || i < numEntries-1 {
|
|
s.write([]byte(","))
|
|
}
|
|
s.newlineWithPointerNameComment()
|
|
}
|
|
s.depth--
|
|
s.indent()
|
|
s.write([]byte("}"))
|
|
}
|
|
|
|
func (s *dumpState) dumpStruct(v reflect.Value) {
|
|
dumpPreamble := func() {
|
|
s.dumpType(v)
|
|
s.write([]byte("{"))
|
|
s.newlineWithPointerNameComment()
|
|
s.depth++
|
|
}
|
|
preambleDumped := false
|
|
vt := v.Type()
|
|
numFields := v.NumField()
|
|
for i := 0; i < numFields; i++ {
|
|
vtf := vt.Field(i)
|
|
if s.config.HidePrivateFields && vtf.PkgPath != "" || s.config.FieldExclusions != nil && s.config.FieldExclusions.MatchString(vtf.Name) {
|
|
continue
|
|
}
|
|
if s.config.FieldFilter != nil && !s.config.FieldFilter(vtf, v.Field(i)) {
|
|
continue
|
|
}
|
|
if s.config.HideZeroValues && isZeroValue(v.Field(i)) {
|
|
continue
|
|
}
|
|
if !preambleDumped {
|
|
dumpPreamble()
|
|
preambleDumped = true
|
|
}
|
|
s.indent()
|
|
s.write([]byte(vtf.Name))
|
|
if s.config.Compact {
|
|
s.write([]byte(":"))
|
|
} else {
|
|
s.write([]byte(": "))
|
|
}
|
|
s.dumpVal(v.Field(i))
|
|
if !s.config.Compact || i < numFields-1 {
|
|
s.write([]byte(","))
|
|
}
|
|
s.newlineWithPointerNameComment()
|
|
}
|
|
if preambleDumped {
|
|
s.depth--
|
|
s.indent()
|
|
s.write([]byte("}"))
|
|
} else {
|
|
// There were no fields dumped
|
|
s.dumpType(v)
|
|
s.write([]byte("{}"))
|
|
}
|
|
}
|
|
|
|
func (s *dumpState) dumpMap(v reflect.Value) {
|
|
if v.IsNil() {
|
|
s.dumpType(v)
|
|
s.writeString("(nil)")
|
|
return
|
|
}
|
|
|
|
s.dumpType(v)
|
|
|
|
keys := v.MapKeys()
|
|
if len(keys) == 0 {
|
|
s.write([]byte("{}"))
|
|
return
|
|
}
|
|
|
|
s.write([]byte("{"))
|
|
s.newlineWithPointerNameComment()
|
|
s.depth++
|
|
sort.Sort(mapKeySorter{
|
|
keys: keys,
|
|
options: s.config,
|
|
})
|
|
numKeys := len(keys)
|
|
for i, key := range keys {
|
|
s.indent()
|
|
s.dumpVal(key)
|
|
if s.config.Compact {
|
|
s.write([]byte(":"))
|
|
} else {
|
|
s.write([]byte(": "))
|
|
}
|
|
s.dumpVal(v.MapIndex(key))
|
|
if !s.config.Compact || i < numKeys-1 {
|
|
s.write([]byte(","))
|
|
}
|
|
s.newlineWithPointerNameComment()
|
|
}
|
|
s.depth--
|
|
s.indent()
|
|
s.write([]byte("}"))
|
|
}
|
|
|
|
func (s *dumpState) dumpFunc(v reflect.Value) {
|
|
parts := strings.Split(runtime.FuncForPC(v.Pointer()).Name(), "/")
|
|
name := parts[len(parts)-1]
|
|
|
|
// Anonymous function
|
|
if strings.Count(name, ".") > 1 {
|
|
s.dumpType(v)
|
|
} else {
|
|
if s.config.StripPackageNames {
|
|
name = packageNameStripperRegexp.ReplaceAllLiteralString(name, "")
|
|
} else if s.homePackageRegexp != nil {
|
|
name = s.homePackageRegexp.ReplaceAllLiteralString(name, "")
|
|
}
|
|
if s.config.Compact {
|
|
name = compactTypeRegexp.ReplaceAllString(name, "$1")
|
|
}
|
|
s.write([]byte(name))
|
|
}
|
|
}
|
|
|
|
func (s *dumpState) dumpChan(v reflect.Value) {
|
|
vType := v.Type()
|
|
res := []byte(vType.String())
|
|
s.write(res)
|
|
}
|
|
|
|
func (s *dumpState) dumpCustom(v reflect.Value, buf *bytes.Buffer) {
|
|
|
|
// Dump the type
|
|
s.dumpType(v)
|
|
|
|
if s.config.Compact {
|
|
s.write(buf.Bytes())
|
|
return
|
|
}
|
|
|
|
// Now output the dump taking care to apply the current indentation-level
|
|
// and pointer name comments.
|
|
var err error
|
|
firstLine := true
|
|
for err == nil {
|
|
var lineBytes []byte
|
|
lineBytes, err = buf.ReadBytes('\n')
|
|
line := strings.TrimRight(string(lineBytes), " \n")
|
|
|
|
if err != nil && err != io.EOF {
|
|
break
|
|
}
|
|
// Do not indent first line
|
|
if firstLine {
|
|
firstLine = false
|
|
} else {
|
|
s.indent()
|
|
}
|
|
s.write([]byte(line))
|
|
|
|
// At EOF we're done
|
|
if err == io.EOF {
|
|
return
|
|
}
|
|
s.newlineWithPointerNameComment()
|
|
}
|
|
panic(err)
|
|
}
|
|
|
|
func (s *dumpState) dump(value interface{}) {
|
|
if value == nil {
|
|
printNil(s.w)
|
|
return
|
|
}
|
|
v := reflect.ValueOf(value)
|
|
s.dumpVal(v)
|
|
}
|
|
|
|
func (s *dumpState) descendIntoPossiblePointer(value reflect.Value, f func()) {
|
|
canonicalize := true
|
|
if isPointerValue(value) {
|
|
// If elision disabled, and this is not a circular reference, don't canonicalize
|
|
if s.config.DisablePointerReplacement && s.parentPointers.add(value) {
|
|
canonicalize = false
|
|
}
|
|
|
|
// Add to stack of pointers we're recursively descending into
|
|
s.parentPointers.add(value)
|
|
defer s.parentPointers.remove(value)
|
|
}
|
|
|
|
if !canonicalize {
|
|
ptr, _ := s.pointerFor(value)
|
|
s.currentPointer = ptr
|
|
f()
|
|
return
|
|
}
|
|
|
|
ptr, firstVisit := s.pointerFor(value)
|
|
if ptr == nil {
|
|
f()
|
|
return
|
|
}
|
|
if firstVisit {
|
|
s.currentPointer = ptr
|
|
f()
|
|
return
|
|
}
|
|
s.write([]byte(ptr.label()))
|
|
}
|
|
|
|
func (s *dumpState) dumpVal(value reflect.Value) {
|
|
if value.Kind() == reflect.Ptr && value.IsNil() {
|
|
s.write([]byte("nil"))
|
|
return
|
|
}
|
|
|
|
v := deInterface(value)
|
|
kind := v.Kind()
|
|
|
|
// Try to handle with dump func
|
|
if s.config.DumpFunc != nil {
|
|
buf := new(bytes.Buffer)
|
|
if s.config.DumpFunc(v, buf) {
|
|
s.dumpCustom(v, buf)
|
|
return
|
|
}
|
|
}
|
|
|
|
// Handle custom dumpers
|
|
dumperType := reflect.TypeOf((*Dumper)(nil)).Elem()
|
|
if v.Type().Implements(dumperType) {
|
|
s.descendIntoPossiblePointer(v, func() {
|
|
// Run the custom dumper buffering the output
|
|
buf := new(bytes.Buffer)
|
|
dumpFunc := v.MethodByName("LitterDump")
|
|
dumpFunc.Call([]reflect.Value{reflect.ValueOf(buf)})
|
|
s.dumpCustom(v, buf)
|
|
})
|
|
return
|
|
}
|
|
|
|
switch kind {
|
|
case reflect.Invalid:
|
|
// Do nothing. We should never get here since invalid has already
|
|
// been handled above.
|
|
s.write([]byte("<invalid>"))
|
|
|
|
case reflect.Bool:
|
|
printBool(s.w, v.Bool())
|
|
|
|
case reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Int:
|
|
printInt(s.w, v.Int(), 10)
|
|
|
|
case reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uint:
|
|
printUint(s.w, v.Uint(), 10)
|
|
|
|
case reflect.Float32:
|
|
printFloat(s.w, v.Float(), 32)
|
|
|
|
case reflect.Float64:
|
|
printFloat(s.w, v.Float(), 64)
|
|
|
|
case reflect.Complex64:
|
|
printComplex(s.w, v.Complex(), 32)
|
|
|
|
case reflect.Complex128:
|
|
printComplex(s.w, v.Complex(), 64)
|
|
|
|
case reflect.String:
|
|
s.write([]byte(strconv.Quote(v.String())))
|
|
|
|
case reflect.Slice:
|
|
if v.IsNil() {
|
|
printNil(s.w)
|
|
break
|
|
}
|
|
fallthrough
|
|
|
|
case reflect.Array:
|
|
s.descendIntoPossiblePointer(v, func() {
|
|
s.dumpSlice(v)
|
|
})
|
|
|
|
case reflect.Interface:
|
|
// The only time we should get here is for nil interfaces due to
|
|
// unpackValue calls.
|
|
if v.IsNil() {
|
|
printNil(s.w)
|
|
}
|
|
|
|
case reflect.Ptr:
|
|
s.descendIntoPossiblePointer(v, func() {
|
|
if s.config.StrictGo {
|
|
s.writeString(fmt.Sprintf("(func(v %s) *%s { return &v })(", v.Elem().Type(), v.Elem().Type()))
|
|
s.dumpVal(v.Elem())
|
|
s.writeString(")")
|
|
} else {
|
|
s.writeString("&")
|
|
s.dumpVal(v.Elem())
|
|
}
|
|
})
|
|
|
|
case reflect.Map:
|
|
s.descendIntoPossiblePointer(v, func() {
|
|
s.dumpMap(v)
|
|
})
|
|
|
|
case reflect.Struct:
|
|
s.dumpStruct(v)
|
|
|
|
case reflect.Func:
|
|
s.dumpFunc(v)
|
|
|
|
case reflect.Chan:
|
|
s.dumpChan(v)
|
|
|
|
default:
|
|
if v.CanInterface() {
|
|
s.writeString(fmt.Sprintf("%v", v.Interface()))
|
|
} else {
|
|
s.writeString(fmt.Sprintf("%v", v.String()))
|
|
}
|
|
}
|
|
}
|
|
|
|
// registers that the value has been visited and checks to see if it is one of the
|
|
// pointers we will see multiple times. If it is, it returns a temporary name for this
|
|
// pointer. It also returns a boolean value indicating whether this is the first time
|
|
// this name is returned so the caller can decide whether the contents of the pointer
|
|
// has been dumped before or not.
|
|
func (s *dumpState) pointerFor(v reflect.Value) (*ptrinfo, bool) {
|
|
if isPointerValue(v) {
|
|
if info, ok := s.pointers.get(v); ok {
|
|
firstVisit := s.visitedPointers.add(v)
|
|
return info, firstVisit
|
|
}
|
|
}
|
|
return nil, false
|
|
}
|
|
|
|
// prepares a new state object for dumping the provided value
|
|
func newDumpState(value interface{}, options *Options, writer io.Writer) *dumpState {
|
|
result := &dumpState{
|
|
config: options,
|
|
pointers: mapReusedPointers(reflect.ValueOf(value)),
|
|
w: writer,
|
|
}
|
|
|
|
if options.HomePackage != "" {
|
|
result.homePackageRegexp = regexp.MustCompile(fmt.Sprintf("\\b%s\\.", options.HomePackage))
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
// Dump a value to stdout
|
|
func Dump(value ...interface{}) {
|
|
(&Config).Dump(value...)
|
|
}
|
|
|
|
// Sdump dumps a value to a string
|
|
func Sdump(value ...interface{}) string {
|
|
return (&Config).Sdump(value...)
|
|
}
|
|
|
|
// Dump a value to stdout according to the options
|
|
func (o Options) Dump(values ...interface{}) {
|
|
for i, value := range values {
|
|
state := newDumpState(value, &o, os.Stdout)
|
|
if i > 0 {
|
|
state.write([]byte(o.Separator))
|
|
}
|
|
state.dump(value)
|
|
}
|
|
_, _ = os.Stdout.Write([]byte("\n"))
|
|
}
|
|
|
|
// Sdump dumps a value to a string according to the options
|
|
func (o Options) Sdump(values ...interface{}) string {
|
|
buf := new(bytes.Buffer)
|
|
for i, value := range values {
|
|
if i > 0 {
|
|
_, _ = buf.Write([]byte(o.Separator))
|
|
}
|
|
state := newDumpState(value, &o, buf)
|
|
state.dump(value)
|
|
}
|
|
return buf.String()
|
|
}
|
|
|
|
type mapKeySorter struct {
|
|
keys []reflect.Value
|
|
options *Options
|
|
}
|
|
|
|
func (s mapKeySorter) Len() int {
|
|
return len(s.keys)
|
|
}
|
|
|
|
func (s mapKeySorter) Swap(i, j int) {
|
|
s.keys[i], s.keys[j] = s.keys[j], s.keys[i]
|
|
}
|
|
|
|
func (s mapKeySorter) Less(i, j int) bool {
|
|
ibuf := new(bytes.Buffer)
|
|
jbuf := new(bytes.Buffer)
|
|
newDumpState(s.keys[i], s.options, ibuf).dumpVal(s.keys[i])
|
|
newDumpState(s.keys[j], s.options, jbuf).dumpVal(s.keys[j])
|
|
return ibuf.String() < jbuf.String()
|
|
}
|