diff --git a/CHANGELOG.md b/CHANGELOG.md index ab34ac3..2cf2982 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,17 @@ The format is based on [Keep a Changelog], and this project adheres to [Keep a Changelog]: https://keepachangelog.com/en/1.0.0/ [Semantic Versioning]: https://semver.org/spec/v2.0.0.html +## [Unreleased] + +### Added + +- Added support for filters, allowing custom rendering on a per-value basis +- Added a built-in filter for `reflect.Type` + +### Fixed + +- Fixed a bug whereby IO errors were not propagated to the caller + ## [0.2.0] - 2019-01-19 ### Added diff --git a/README.md b/README.md index 9036b28..f5bd52d 100644 --- a/README.md +++ b/README.md @@ -6,9 +6,17 @@ [![GoDoc](https://godoc.org/github.com/dogmatiq/dapper?status.svg)](https://godoc.org/github.com/dogmatiq/dapper) [![Go Report Card](https://goreportcard.com/badge/github.com/dogmatiq/dapper)](https://goreportcard.com/report/github.com/dogmatiq/dapper) -Dapper is a pretty-printer for Go values that aims to produce the shortest -possible output without ambiguity. Additionally, the output is deterministic, -which allows for the generation of human-readable diffs using standard tools. +Dapper is a pretty-printer for Go values. + +It is not intended to be used directly as a debugging tool, but as a library +for applications that need to describe Go values to humans, such as testing +frameworks. + +Some features include: + +- Concise formatting, without type ambiguity +- Deterministic output, useful for generating diffs using standard tools +- A filtering system for producing customized output on a per-value basis ## Example diff --git a/array.go b/array.go index 8f9596f..9b62db6 100644 --- a/array.go +++ b/array.go @@ -8,8 +8,8 @@ import ( ) // visitArray formats values with a kind of reflect.Array or Slice. -func (vis *visitor) visitArray(w io.Writer, v value) { - if v.IsAmbiguousType { +func (vis *visitor) visitArray(w io.Writer, v Value) { + if v.IsAmbiguousType() { vis.write(w, v.TypeName()) } @@ -23,11 +23,31 @@ func (vis *visitor) visitArray(w io.Writer, v value) { vis.write(w, "}") } -func (vis *visitor) visitArrayValues(w io.Writer, v value) { - ambiguous := v.Type.Elem().Kind() == reflect.Interface +func (vis *visitor) visitArrayValues(w io.Writer, v Value) { + staticType := v.DynamicType.Elem() + isInterface := staticType.Kind() == reflect.Interface for i := 0; i < v.Value.Len(); i++ { - vis.visit(w, v.Value.Index(i), ambiguous) + elem := v.Value.Index(i) + + // unwrap interface values so that elem has it's actual type/kind, and not + // that of reflect.Interface. + if isInterface && !elem.IsNil() { + elem = elem.Elem() + } + + vis.visit( + w, + Value{ + Value: elem, + DynamicType: elem.Type(), + StaticType: staticType, + IsAmbiguousDynamicType: isInterface, + IsAmbiguousStaticType: false, + IsUnexported: v.IsUnexported, + }, + ) + vis.write(w, "\n") } } diff --git a/filter.go b/filter.go new file mode 100644 index 0000000..1a83f72 --- /dev/null +++ b/filter.go @@ -0,0 +1,82 @@ +package dapper + +import ( + "io" + "reflect" + + "github.com/dogmatiq/iago" +) + +// Filter is a function that provides custom formatting logic for specific +// values. +// +// It writes a formatted representation of v to w, and returns the number of +// bytes written. +// +// If the number of bytes written is non-zero, the default formatting logic is +// skipped. +// +// Particular attention should be paid to the v.IsUnexported field. If this flag +// is true, many operations on v.Value are unavailable. +type Filter func(w io.Writer, v Value) (int, error) + +// reflectTypeType is the reflect.Type for reflect.Type itself. +var reflectTypeType = reflect.TypeOf((*reflect.Type)(nil)).Elem() + +// ReflectTypeFilter is a filter that formats reflect.Type values. +func ReflectTypeFilter(w io.Writer, v Value) (n int, err error) { + defer iago.Recover(&err) + + if !v.DynamicType.Implements(reflectTypeType) { + return 0, nil + } + + if v.DynamicType.Kind() == reflect.Interface && v.Value.IsNil() { + return 0, nil + } + + ambiguous := false + + if v.IsAmbiguousStaticType { + // always render the type if the static type is ambiguous + ambiguous = true + } else if v.IsAmbiguousDynamicType { + // only consider the dynamic type to be ambiguous if the static type isn't reflect.Type + // we're not really concerned with rendering the underlying implementation's type. + ambiguous = v.StaticType != reflectTypeType + } + + if ambiguous { + n += iago.MustWriteString(w, "reflect.Type(") + } + + if v.IsUnexported { + n += iago.MustWriteString(w, "") + } else { + t := v.Value.Interface().(reflect.Type) + + if s := t.PkgPath(); s != "" { + n += iago.MustWriteString(w, s) + n += iago.MustWriteString(w, ".") + } + + if s := t.Name(); s != "" { + n += iago.MustWriteString(w, s) + } else { + n += iago.MustWriteString(w, t.String()) + } + + } + + // always render the pointer value for the type, this way when the field is + // unexported we still get something we can compare to known types instead of a + // rendering of the reflect.rtype struct. + n += iago.MustWriteString(w, " ") + n += iago.MustWriteString(w, formatPointerHex(v.Value.Pointer(), false)) + + if ambiguous { + n += iago.MustWriteString(w, ")") + } + + return +} diff --git a/filter_test.go b/filter_test.go new file mode 100644 index 0000000..a94b8d4 --- /dev/null +++ b/filter_test.go @@ -0,0 +1,101 @@ +package dapper_test + +import ( + "fmt" + "reflect" + "testing" +) + +type reflectType struct { + Exported reflect.Type + unexported reflect.Type +} + +var ( + intType = reflect.TypeOf(0) + intTypePointer = formatReflectTypePointer(intType) + mapType = reflect.TypeOf(map[string]string{}) + mapTypePointer = formatReflectTypePointer(mapType) + namedType = reflect.TypeOf(named{}) + namedTypePointer = formatReflectTypePointer(namedType) +) + +func formatReflectTypePointer(t reflect.Type) string { + return fmt.Sprintf("0x%x", reflect.ValueOf(t).Pointer()) +} + +func TestPrinter_ReflectTypeFilter(t *testing.T) { + test( + t, + "built-in type", + intType, + "reflect.Type(int "+intTypePointer+")", + ) + + test( + t, + "built-in parameterized type", + mapType, + "reflect.Type(map[string]string "+mapTypePointer+")", + ) + + test( + t, + "named type", + reflect.TypeOf(named{}), + "reflect.Type(github.com/dogmatiq/dapper_test.named "+namedTypePointer+")", + ) + + typ := reflect.TypeOf(struct{ Int int }{}) + test( + t, + "anonymous struct", + typ, + "reflect.Type(struct { Int int } "+formatReflectTypePointer(typ)+")", + ) + + typ = reflect.TypeOf((*interface{ Int() int })(nil)).Elem() + test( + t, + "anonymous interface", + typ, + "reflect.Type(interface { Int() int } "+formatReflectTypePointer(typ)+")", + ) + + test( + t, + "includes type when in an anonymous struct", + struct { + Type reflect.Type + }{ + reflect.TypeOf(0), + }, + "{", + " Type: reflect.Type(int "+intTypePointer+")", + "}", + ) + + test( + t, + "does not include type if static type is also reflect.Type", + reflectType{ + Exported: reflect.TypeOf(0), + }, + "dapper_test.reflectType{", + " Exported: int "+intTypePointer, + " unexported: nil", + "}", + ) + + test( + t, + "still renders the pointer address when the value is unexported", + reflectType{ + unexported: reflect.TypeOf(0), + }, + "dapper_test.reflectType{", + " Exported: nil", + " unexported: "+intTypePointer, + "}", + ) +} diff --git a/interface.go b/interface.go index edfdab2..6f63943 100644 --- a/interface.go +++ b/interface.go @@ -5,17 +5,20 @@ import ( ) // visitInterface formats values with a kind of reflect.Interface. -func (vis *visitor) visitInterface(w io.Writer, v value) { - if v.Value.IsNil() { - if v.IsAmbiguousType { - vis.write(w, v.TypeName()) - vis.write(w, "(nil)") - } else { - vis.write(w, "nil") - } - - return +func (vis *visitor) visitInterface(w io.Writer, v Value) { + if !v.Value.IsNil() { + // this should never happen, a more appropraite visit method should have been + // chosen based on the value's dynamic type. + panic("unexpectedly called visitInterface() with non-nil interface") } - vis.visit(w, v.Value.Elem(), v.IsAmbiguousType) + // for a nil interface, we only want to render the type if the STATIC type is + // ambigious, since the only information we have available is the interface + // type itself, not the actual implementation's type. + if v.IsAmbiguousStaticType { + vis.write(w, v.TypeName()) + vis.write(w, "(nil)") + } else { + vis.write(w, "nil") + } } diff --git a/interface_test.go b/interface_test.go index b813493..f5064a6 100644 --- a/interface_test.go +++ b/interface_test.go @@ -6,10 +6,13 @@ type interfaces struct { Iface interface{} } +type iface interface{} + func TestPrinter_Interface(t *testing.T) { // note that capturing a reflect.Value of a nil interface does NOT produces a // value with a "kind" of reflect.Invalid, NOT reflect.Interface. test(t, "nil interface", interface{}(nil), "interface{}(nil)") + test(t, "nil named interface", iface(nil), "interface{}(nil)") // interface information is shed when passed to Printer.Write(). test( t, diff --git a/map.go b/map.go index 2f7fdce..25193cd 100644 --- a/map.go +++ b/map.go @@ -12,13 +12,13 @@ import ( // visitMap formats values with a kind of reflect.Map. // // TODO(jmalloc): sort numerically-keyed maps numerically -func (vis *visitor) visitMap(w io.Writer, v value) { +func (vis *visitor) visitMap(w io.Writer, v Value) { if vis.enter(w, v) { return } defer vis.leave(v) - if v.IsAmbiguousType { + if v.IsAmbiguousType() { vis.write(w, v.TypeName()) } @@ -32,16 +32,34 @@ func (vis *visitor) visitMap(w io.Writer, v value) { vis.write(w, "}") } -func (vis *visitor) visitMapElements(w io.Writer, v value) { - ambiguous := v.Type.Elem().Kind() == reflect.Interface +func (vis *visitor) visitMapElements(w io.Writer, v Value) { + staticType := v.DynamicType.Elem() + isInterface := staticType.Kind() == reflect.Interface keys, alignment := vis.formatMapKeys(v) for _, mk := range keys { mv := v.Value.MapIndex(mk.Value) + + // unwrap interface values so that elem has it's actual type/kind, and not + // that of reflect.Interface. + if isInterface && !mv.IsNil() { + mv = mv.Elem() + } + vis.write(w, mk.String) vis.write(w, ": ") vis.write(w, strings.Repeat(" ", alignment-mk.Width)) - vis.visit(w, mv, ambiguous) + vis.visit( + w, + Value{ + Value: mv, + DynamicType: mv.Type(), + StaticType: staticType, + IsAmbiguousDynamicType: isInterface, + IsAmbiguousStaticType: false, + IsUnexported: v.IsUnexported, + }, + ) vis.write(w, "\n") } } @@ -56,14 +74,32 @@ type mapKey struct { // sorted by their string representation. // // padding is the number of padding characters to add to the shortest key. -func (vis *visitor) formatMapKeys(v value) (keys []mapKey, alignment int) { +func (vis *visitor) formatMapKeys(v Value) (keys []mapKey, alignment int) { var w strings.Builder - isInterface := v.Type.Key().Kind() == reflect.Interface + staticType := v.DynamicType.Key() + isInterface := staticType.Kind() == reflect.Interface keys = make([]mapKey, v.Value.Len()) alignToLastLine := false - for i, k := range v.Value.MapKeys() { - vis.visit(&w, k, isInterface) + for i, mk := range v.Value.MapKeys() { + + // unwrap interface values so that elem has it's actual type/kind, and not + // that of reflect.Interface. + if isInterface && !mk.IsNil() { + mk = mk.Elem() + } + + vis.visit( + &w, + Value{ + Value: mk, + DynamicType: mk.Type(), + StaticType: staticType, + IsAmbiguousDynamicType: isInterface, + IsAmbiguousStaticType: false, + IsUnexported: v.IsUnexported, + }, + ) s := w.String() w.Reset() @@ -74,7 +110,7 @@ func (vis *visitor) formatMapKeys(v value) (keys []mapKey, alignment int) { alignToLastLine = max == last } - keys[i] = mapKey{k, s, last} + keys[i] = mapKey{mk, s, last} } sort.Slice( diff --git a/printer.go b/printer.go index 12efb9e..ac0d43a 100644 --- a/printer.go +++ b/printer.go @@ -5,6 +5,8 @@ import ( "os" "reflect" "strings" + + "github.com/dogmatiq/iago" ) const ( @@ -16,15 +18,15 @@ const ( DefaultRecursionMarker = "" ) -// defaultPrinter is a Printer instance with default settings. -var defaultPrinter Printer - // Printer generates human-readable representations of Go values. // // The output format is intended to be as minimal as possible, without being // ambigious. To that end, type information is only included where it can not be // reliably inferred from the structure of the value. type Printer struct { + // Filters is the set of filters to apply when formatting values. + Filters []Filter + // Indent is the string used to indent nested values. // If it is empty, DefaultIndent is used. Indent string @@ -35,11 +37,17 @@ type Printer struct { RecursionMarker string } +// emptyInterfaceType is the reflect.Type for interface{}. +var emptyInterfaceType = reflect.TypeOf((*interface{})(nil)).Elem() + // Write writes a pretty-printed representation of v to w. // // It returns the number of bytes written. -func (p *Printer) Write(w io.Writer, v interface{}) (int, error) { +func (p *Printer) Write(w io.Writer, v interface{}) (n int, err error) { + defer iago.Recover(&err) + vis := visitor{ + filters: p.Filters, indent: []byte(p.Indent), recursionMarker: p.RecursionMarker, } @@ -52,9 +60,27 @@ func (p *Printer) Write(w io.Writer, v interface{}) (int, error) { vis.recursionMarker = DefaultRecursionMarker } - err := vis.visit(w, reflect.ValueOf(v), true) + rv := reflect.ValueOf(v) + var rt reflect.Type - return vis.bytes, err + if rv.Kind() != reflect.Invalid { + rt = rv.Type() + } + + vis.visit( + w, + Value{ + Value: rv, + DynamicType: rt, + StaticType: emptyInterfaceType, + IsAmbiguousDynamicType: true, + IsAmbiguousStaticType: true, + IsUnexported: false, + }, + ) + + n = vis.bytes + return } // Format returns a pretty-printed representation of v. @@ -68,6 +94,13 @@ func (p *Printer) Format(v interface{}) string { return b.String() } +// defaultPrinter is a Printer instance with default settings. +var defaultPrinter = Printer{ + Filters: []Filter{ + ReflectTypeFilter, + }, +} + // Write writes a pretty-printed representation of v to w using the default // printer settings. // diff --git a/printer_test.go b/printer_test.go index be7529c..7d4f76b 100644 --- a/printer_test.go +++ b/printer_test.go @@ -1,10 +1,14 @@ package dapper_test import ( + "bytes" + "fmt" + "os" + . "github.com/dogmatiq/dapper" ) -func ExamplePrint() { +func ExamplePrinter() { type TreeNode struct { Name string Value interface{} @@ -27,7 +31,9 @@ func ExamplePrint() { }, } - Print(v) + p := Printer{} + s := p.Format(v) + fmt.Println(s) // output: dapper_test.TreeNode{ // Name: "root" @@ -46,3 +52,28 @@ func ExamplePrint() { // } // } } + +func ExamplePrint() { + Print(123) + + // output: int(123) +} + +func ExampleFormat() { + s := Format(123) + fmt.Println(s) + + // output: int(123) +} + +func ExampleWrite() { + w := &bytes.Buffer{} + + if _, err := Write(w, 123); err != nil { + panic(err) + } + + w.WriteTo(os.Stdout) + + // output: int(123) +} diff --git a/ptr.go b/ptr.go index 66c2bc3..661d140 100644 --- a/ptr.go +++ b/ptr.go @@ -5,15 +5,27 @@ import ( ) // visitPtr formats values with a kind of reflect.Ptr. -func (vis *visitor) visitPtr(w io.Writer, v value) { +func (vis *visitor) visitPtr(w io.Writer, v Value) { if vis.enter(w, v) { return } defer vis.leave(v) - if v.IsAmbiguousType { + if v.IsAmbiguousType() { vis.write(w, "*") } - vis.visit(w, v.Value.Elem(), v.IsAmbiguousType) + elem := v.Value.Elem() + + vis.visit( + w, + Value{ + Value: elem, + DynamicType: elem.Type(), + StaticType: v.StaticType, + IsAmbiguousDynamicType: v.IsAmbiguousDynamicType, + IsAmbiguousStaticType: v.IsAmbiguousStaticType, + IsUnexported: v.IsUnexported, + }, + ) } diff --git a/ptr_test.go b/ptr_test.go index d604c29..ff7171a 100644 --- a/ptr_test.go +++ b/ptr_test.go @@ -2,10 +2,36 @@ package dapper_test import "testing" +type ptr struct { + Value interface{} +} + func TestPrinter_Ptr(t *testing.T) { value := 100 test(t, "nil pointer", (*int)(nil), "*int(nil)") test(t, "non-nil pointer", &value, "*int(100)") + + test( + t, + "nil pointer inside interface includes element type", + ptr{ + (*int)(nil), + }, + "dapper_test.ptr{", + " Value: *int(nil)", + "}", + ) + + test( + t, + "non-nil pointer inside interface includes element type", + ptr{ + &value, + }, + "dapper_test.ptr{", + " Value: *int(100)", + "}", + ) } type recursive struct { diff --git a/shallow.go b/shallow.go index 57393c4..1317095 100644 --- a/shallow.go +++ b/shallow.go @@ -3,12 +3,13 @@ package dapper import ( "fmt" "io" + "strconv" ) // visitInt formats values with a kind of reflect.Int, and the related // fixed-sized types. -func (vis *visitor) visitInt(w io.Writer, v value) { - if v.IsAmbiguousType { +func (vis *visitor) visitInt(w io.Writer, v Value) { + if v.IsAmbiguousType() { vis.write(w, v.TypeName()) vis.writef(w, "(%v)", v.Value.Int()) } else { @@ -18,8 +19,8 @@ func (vis *visitor) visitInt(w io.Writer, v value) { // visitUint formats values with a kind of reflect.Uint, and the related // fixed-sized types. -func (vis *visitor) visitUint(w io.Writer, v value) { - if v.IsAmbiguousType { +func (vis *visitor) visitUint(w io.Writer, v Value) { + if v.IsAmbiguousType() { vis.write(w, v.TypeName()) vis.writef(w, "(%v)", v.Value.Uint()) } else { @@ -28,8 +29,8 @@ func (vis *visitor) visitUint(w io.Writer, v value) { } // visitFloat formats values with a kind of reflect.Float32 and Float64. -func (vis *visitor) visitFloat(w io.Writer, v value) { - if v.IsAmbiguousType { +func (vis *visitor) visitFloat(w io.Writer, v Value) { + if v.IsAmbiguousType() { vis.write(w, v.TypeName()) vis.writef(w, "(%v)", v.Value.Float()) } else { @@ -38,11 +39,11 @@ func (vis *visitor) visitFloat(w io.Writer, v value) { } // visitComplex formats values with a kind of reflect.Complex64 and Complex128. -func (vis *visitor) visitComplex(w io.Writer, v value) { +func (vis *visitor) visitComplex(w io.Writer, v Value) { // note that %v formats a complex number already surrounded in parenthesis s := fmt.Sprintf("%v", v.Value.Complex()) - if v.IsAmbiguousType { + if v.IsAmbiguousType() { vis.write(w, v.TypeName()) vis.write(w, s) } else { @@ -51,10 +52,10 @@ func (vis *visitor) visitComplex(w io.Writer, v value) { } // visitUintptr formats values with a kind of reflect.Uintptr. -func (vis *visitor) visitUintptr(w io.Writer, v value) { - s := formatPointerHex(v.Value.Uint(), false) +func (vis *visitor) visitUintptr(w io.Writer, v Value) { + s := formatPointerHex(uintptr(v.Value.Uint()), false) - if v.IsAmbiguousType { + if v.IsAmbiguousType() { vis.write(w, v.TypeName()) vis.writef(w, "(%s)", s) } else { @@ -63,10 +64,10 @@ func (vis *visitor) visitUintptr(w io.Writer, v value) { } // visitUnsafePointer formats values with a kind of reflect.UnsafePointer. -func (vis *visitor) visitUnsafePointer(w io.Writer, v value) { +func (vis *visitor) visitUnsafePointer(w io.Writer, v Value) { s := formatPointerHex(v.Value.Pointer(), true) - if v.IsAmbiguousType { + if v.IsAmbiguousType() { vis.write(w, v.TypeName()) vis.writef(w, "(%s)", s) } else { @@ -75,8 +76,8 @@ func (vis *visitor) visitUnsafePointer(w io.Writer, v value) { } // visitChan formats values with a kind of reflect.Chan. -func (vis *visitor) visitChan(w io.Writer, v value) { - if v.IsAmbiguousType { +func (vis *visitor) visitChan(w io.Writer, v Value) { + if v.IsAmbiguousType() { vis.write(w, v.TypeName()) vis.write(w, "(") } @@ -95,16 +96,16 @@ func (vis *visitor) visitChan(w io.Writer, v value) { ) } - if v.IsAmbiguousType { + if v.IsAmbiguousType() { vis.write(w, ")") } } // visitFunc formats values with a kind of reflect.Func. -func (vis *visitor) visitFunc(w io.Writer, v value) { +func (vis *visitor) visitFunc(w io.Writer, v Value) { s := formatPointerHex(v.Value.Pointer(), true) - if v.IsAmbiguousType { + if v.IsAmbiguousType() { vis.write(w, v.TypeName()) vis.writef(w, "(%s)", s) } else { @@ -113,16 +114,14 @@ func (vis *visitor) visitFunc(w io.Writer, v value) { } // formatPointerHex returns a minimal hexadecimal represenation of v. -func formatPointerHex(v interface{}, zeroIsNil bool) string { - s := fmt.Sprintf("%x", v) - - if s == "0" { +func formatPointerHex(v uintptr, zeroIsNil bool) string { + if v == 0 { if zeroIsNil { return "nil" } - return s + return "0" } - return "0x" + s + return "0x" + strconv.FormatUint(uint64(v), 16) } diff --git a/slice.go b/slice.go index f0e9801..5ff2613 100644 --- a/slice.go +++ b/slice.go @@ -5,7 +5,7 @@ import ( ) // visitSlice formats values with a kind of reflect.Slice. -func (vis *visitor) visitSlice(w io.Writer, v value) { +func (vis *visitor) visitSlice(w io.Writer, v Value) { if vis.enter(w, v) { return } diff --git a/struct.go b/struct.go index 44eab3d..4d53fbc 100644 --- a/struct.go +++ b/struct.go @@ -9,12 +9,15 @@ import ( ) // visitStruct formats values with a kind of reflect.Struct. -func (vis *visitor) visitStruct(w io.Writer, v value) { - if v.IsAmbiguousType && !v.IsAnonymous() { +func (vis *visitor) visitStruct(w io.Writer, v Value) { + // even if the type is ambiguous, we only render it if it's not anonymous this + // is to avoid rendering the full type with field definitions. instead we mark + // each field's value as ambiguous and render their types inline. + if v.IsAmbiguousType() && !v.IsAnonymousType() { vis.write(w, v.TypeName()) } - if v.Type.NumField() == 0 { + if v.DynamicType.NumField() == 0 { vis.write(w, "{}") return } @@ -24,31 +27,44 @@ func (vis *visitor) visitStruct(w io.Writer, v value) { vis.write(w, "}") } -func (vis *visitor) visitStructFields(w io.Writer, v value) { - alignment := longestFieldName(v.Type) - anon := v.IsAnonymous() - var ambiguous bool +func (vis *visitor) visitStructFields(w io.Writer, v Value) { + alignment := longestFieldName(v.DynamicType) - for i := 0; i < v.Type.NumField(); i++ { - f := v.Type.Field(i) + for i := 0; i < v.DynamicType.NumField(); i++ { + f := v.DynamicType.Field(i) fv := v.Value.Field(i) - if anon { - ambiguous = v.IsAmbiguousType - } else if f.Type.Kind() == reflect.Interface { - ambiguous = !fv.IsNil() - } else { - ambiguous = false + isInterface := f.Type.Kind() == reflect.Interface + + // unwrap interface values so that elem has it's actual type/kind, and not + // that of reflect.Interface. + if isInterface && !fv.IsNil() { + fv = fv.Elem() } vis.write(w, f.Name) vis.write(w, ": ") vis.write(w, strings.Repeat(" ", alignment-len(f.Name))) - vis.visit(w, fv, ambiguous) + vis.visit( + w, + Value{ + Value: fv, + DynamicType: fv.Type(), + StaticType: f.Type, + IsAmbiguousDynamicType: isInterface, + IsAmbiguousStaticType: v.IsAmbiguousStaticType && v.IsAnonymousType(), + IsUnexported: v.IsUnexported || isUnexportedField(f), + }, + ) vis.write(w, "\n") } } +// isUnxportedField returns true if f is an unexported field. +func isUnexportedField(f reflect.StructField) bool { + return f.PkgPath != "" +} + // longestFieldName returns the length of the longest field name in a struct. func longestFieldName(rt reflect.Type) int { width := 0 diff --git a/value.go b/value.go index 7338f4c..5a6d527 100644 --- a/value.go +++ b/value.go @@ -5,25 +5,40 @@ import ( "strings" ) -// value is a container for a value that can be formatted. -type value struct { +// Value contains information about a Go value that is to be formatted. +type Value struct { // Value is the value to be formatted. Value reflect.Value - // Type is the value's type. - Type reflect.Type + // DynamicType is the value's type. + DynamicType reflect.Type - // Kind is the value's kind. - Kind reflect.Kind + // StaticType is the type of the "variable" that the value is stored in, which + // may not be the same as its dynamic type. + // + // For example, when formatting the values within a slice of interface{} + // containing integers, such as []interface{}{1, 2, 3}, the DynamicType will be + // "int", but the static type will be "interface{}". + StaticType reflect.Type - // IsAmbiguousType is true if the type of v.Value is not clear from what - // has already been rendered. - IsAmbiguousType bool + // IsAmbiguousDynamicType is true if the value's dynamic type is not clear from + // the context of what has already been rendered. + IsAmbiguousDynamicType bool + + // IsAmbiguousStaticType is true if the value's static type is not clear from + // the context of what has already been rendered. + IsAmbiguousStaticType bool + + // IsUnexported is true if this value was obtained from an unexported struct + // field. If so, it is not possible to extract the underlying value. + IsUnexported bool } -func (v value) TypeName() string { - n := v.Type.String() +// TypeName returns the name of the value's type formatted for display. +func (v *Value) TypeName() string { + n := v.DynamicType.String() n = strings.Replace(n, "interface {", "interface{", -1) + n = strings.Replace(n, "struct {", "struct{", -1) if strings.ContainsAny(n, "() \t\n") { return "(" + n + ")" @@ -32,7 +47,13 @@ func (v value) TypeName() string { return n } -// IsAnonymous returns true if the value has an anonymous type. -func (v value) IsAnonymous() bool { - return v.Type.Name() == "" +// IsAnonymousType returns true if the value has an anonymous type. +func (v *Value) IsAnonymousType() bool { + return v.DynamicType.Name() == "" +} + +// IsAmbiguousType returns true if either the dynamic type or the static type is +// ambiguous. +func (v *Value) IsAmbiguousType() bool { + return v.IsAmbiguousDynamicType || v.IsAmbiguousStaticType } diff --git a/visitor.go b/visitor.go index 4b79448..8e096f7 100644 --- a/visitor.go +++ b/visitor.go @@ -9,6 +9,9 @@ import ( // visitor walks a Go value in order to render it. type visitor struct { + // filters is the set of filters to apply. + filters []Filter + // indent is the string used to indent nested values. indent []byte @@ -23,22 +26,21 @@ type visitor struct { bytes int } -func (vis *visitor) visit(w io.Writer, rv reflect.Value, ambiguous bool) (err error) { - defer iago.Recover(&err) - - if rv.Kind() == reflect.Invalid { +// TODO: don't return err or, let propagate and use iago.Recover() in Printer instead. +func (vis *visitor) visit(w io.Writer, v Value) { + if v.Value.Kind() == reflect.Invalid { vis.write(w, "interface{}(nil)") return } - v := value{ - Value: rv, - Type: rv.Type(), - Kind: rv.Kind(), - IsAmbiguousType: ambiguous, + for _, f := range vis.filters { + if n := iago.Must(f(w, v)); n > 0 { + vis.bytes += n + return + } } - switch v.Kind { + switch v.DynamicType.Kind() { // type name is not rendered for these types, as the literals are unambiguous. case reflect.String: vis.writef(w, "%#v", v.Value.String()) @@ -83,7 +85,7 @@ func (vis *visitor) visit(w io.Writer, rv reflect.Value, ambiguous bool) (err er // // It returns true if the value is nil, or recursion has occurred, indicating // that the value should not be rendered. -func (vis *visitor) enter(w io.Writer, v value) bool { +func (vis *visitor) enter(w io.Writer, v Value) bool { marker := "nil" if !v.Value.IsNil() { @@ -102,7 +104,7 @@ func (vis *visitor) enter(w io.Writer, v value) bool { marker = vis.recursionMarker } - if v.IsAmbiguousType { + if v.IsAmbiguousType() { vis.write(w, v.TypeName()) vis.write(w, "(") vis.write(w, marker) @@ -117,7 +119,7 @@ func (vis *visitor) enter(w io.Writer, v value) bool { // leave indicates that a potentially recursive value has finished rendering. // // It must be called after enter(v) returns true. -func (vis *visitor) leave(v value) { +func (vis *visitor) leave(v Value) { if !v.Value.IsNil() { delete(vis.recursionSet, v.Value.Pointer()) } diff --git a/visitor_test.go b/visitor_test.go index 3f1a018..ffd39dc 100644 --- a/visitor_test.go +++ b/visitor_test.go @@ -19,12 +19,18 @@ func test( t.Run( n, func(t *testing.T) { - p := Format(v) + var w strings.Builder + _, err := Write(&w, v) + + if err != nil { + t.Fatal(err) + } t.Log("expected:\n\n" + x + "\n") - if p != x { - t.Fatal("actual:\n\n" + p + "\n") + s := w.String() + if s != x { + t.Fatal("actual:\n\n" + s + "\n") } }, )