401 lines
13 KiB
Go
401 lines
13 KiB
Go
|
// Copyright 2019, The Go Authors. All rights reserved.
|
||
|
// Use of this source code is governed by a BSD-style
|
||
|
// license that can be found in the LICENSE.md file.
|
||
|
|
||
|
package cmp
|
||
|
|
||
|
import (
|
||
|
"bytes"
|
||
|
"fmt"
|
||
|
"reflect"
|
||
|
"strconv"
|
||
|
"strings"
|
||
|
"unicode"
|
||
|
"unicode/utf8"
|
||
|
|
||
|
"github.com/google/go-cmp/cmp/internal/value"
|
||
|
)
|
||
|
|
||
|
type formatValueOptions struct {
|
||
|
// AvoidStringer controls whether to avoid calling custom stringer
|
||
|
// methods like error.Error or fmt.Stringer.String.
|
||
|
AvoidStringer bool
|
||
|
|
||
|
// PrintAddresses controls whether to print the address of all pointers,
|
||
|
// slice elements, and maps.
|
||
|
PrintAddresses bool
|
||
|
|
||
|
// QualifiedNames controls whether FormatType uses the fully qualified name
|
||
|
// (including the full package path as opposed to just the package name).
|
||
|
QualifiedNames bool
|
||
|
|
||
|
// VerbosityLevel controls the amount of output to produce.
|
||
|
// A higher value produces more output. A value of zero or lower produces
|
||
|
// no output (represented using an ellipsis).
|
||
|
// If LimitVerbosity is false, then the level is treated as infinite.
|
||
|
VerbosityLevel int
|
||
|
|
||
|
// LimitVerbosity specifies that formatting should respect VerbosityLevel.
|
||
|
LimitVerbosity bool
|
||
|
}
|
||
|
|
||
|
// FormatType prints the type as if it were wrapping s.
|
||
|
// This may return s as-is depending on the current type and TypeMode mode.
|
||
|
func (opts formatOptions) FormatType(t reflect.Type, s textNode) textNode {
|
||
|
// Check whether to emit the type or not.
|
||
|
switch opts.TypeMode {
|
||
|
case autoType:
|
||
|
switch t.Kind() {
|
||
|
case reflect.Struct, reflect.Slice, reflect.Array, reflect.Map:
|
||
|
if s.Equal(textNil) {
|
||
|
return s
|
||
|
}
|
||
|
default:
|
||
|
return s
|
||
|
}
|
||
|
if opts.DiffMode == diffIdentical {
|
||
|
return s // elide type for identical nodes
|
||
|
}
|
||
|
case elideType:
|
||
|
return s
|
||
|
}
|
||
|
|
||
|
// Determine the type label, applying special handling for unnamed types.
|
||
|
typeName := value.TypeString(t, opts.QualifiedNames)
|
||
|
if t.Name() == "" {
|
||
|
// According to Go grammar, certain type literals contain symbols that
|
||
|
// do not strongly bind to the next lexicographical token (e.g., *T).
|
||
|
switch t.Kind() {
|
||
|
case reflect.Chan, reflect.Func, reflect.Ptr:
|
||
|
typeName = "(" + typeName + ")"
|
||
|
}
|
||
|
}
|
||
|
return &textWrap{Prefix: typeName, Value: wrapParens(s)}
|
||
|
}
|
||
|
|
||
|
// wrapParens wraps s with a set of parenthesis, but avoids it if the
|
||
|
// wrapped node itself is already surrounded by a pair of parenthesis or braces.
|
||
|
// It handles unwrapping one level of pointer-reference nodes.
|
||
|
func wrapParens(s textNode) textNode {
|
||
|
var refNode *textWrap
|
||
|
if s2, ok := s.(*textWrap); ok {
|
||
|
// Unwrap a single pointer reference node.
|
||
|
switch s2.Metadata.(type) {
|
||
|
case leafReference, trunkReference, trunkReferences:
|
||
|
refNode = s2
|
||
|
if s3, ok := refNode.Value.(*textWrap); ok {
|
||
|
s2 = s3
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Already has delimiters that make parenthesis unnecessary.
|
||
|
hasParens := strings.HasPrefix(s2.Prefix, "(") && strings.HasSuffix(s2.Suffix, ")")
|
||
|
hasBraces := strings.HasPrefix(s2.Prefix, "{") && strings.HasSuffix(s2.Suffix, "}")
|
||
|
if hasParens || hasBraces {
|
||
|
return s
|
||
|
}
|
||
|
}
|
||
|
if refNode != nil {
|
||
|
refNode.Value = &textWrap{Prefix: "(", Value: refNode.Value, Suffix: ")"}
|
||
|
return s
|
||
|
}
|
||
|
return &textWrap{Prefix: "(", Value: s, Suffix: ")"}
|
||
|
}
|
||
|
|
||
|
// FormatValue prints the reflect.Value, taking extra care to avoid descending
|
||
|
// into pointers already in ptrs. As pointers are visited, ptrs is also updated.
|
||
|
func (opts formatOptions) FormatValue(v reflect.Value, parentKind reflect.Kind, ptrs *pointerReferences) (out textNode) {
|
||
|
if !v.IsValid() {
|
||
|
return nil
|
||
|
}
|
||
|
t := v.Type()
|
||
|
|
||
|
// Check slice element for cycles.
|
||
|
if parentKind == reflect.Slice {
|
||
|
ptrRef, visited := ptrs.Push(v.Addr())
|
||
|
if visited {
|
||
|
return makeLeafReference(ptrRef, false)
|
||
|
}
|
||
|
defer ptrs.Pop()
|
||
|
defer func() { out = wrapTrunkReference(ptrRef, false, out) }()
|
||
|
}
|
||
|
|
||
|
// Check whether there is an Error or String method to call.
|
||
|
if !opts.AvoidStringer && v.CanInterface() {
|
||
|
// Avoid calling Error or String methods on nil receivers since many
|
||
|
// implementations crash when doing so.
|
||
|
if (t.Kind() != reflect.Ptr && t.Kind() != reflect.Interface) || !v.IsNil() {
|
||
|
var prefix, strVal string
|
||
|
func() {
|
||
|
// Swallow and ignore any panics from String or Error.
|
||
|
defer func() { recover() }()
|
||
|
switch v := v.Interface().(type) {
|
||
|
case error:
|
||
|
strVal = v.Error()
|
||
|
prefix = "e"
|
||
|
case fmt.Stringer:
|
||
|
strVal = v.String()
|
||
|
prefix = "s"
|
||
|
}
|
||
|
}()
|
||
|
if prefix != "" {
|
||
|
return opts.formatString(prefix, strVal)
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Check whether to explicitly wrap the result with the type.
|
||
|
var skipType bool
|
||
|
defer func() {
|
||
|
if !skipType {
|
||
|
out = opts.FormatType(t, out)
|
||
|
}
|
||
|
}()
|
||
|
|
||
|
switch t.Kind() {
|
||
|
case reflect.Bool:
|
||
|
return textLine(fmt.Sprint(v.Bool()))
|
||
|
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||
|
return textLine(fmt.Sprint(v.Int()))
|
||
|
case reflect.Uint, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||
|
return textLine(fmt.Sprint(v.Uint()))
|
||
|
case reflect.Uint8:
|
||
|
if parentKind == reflect.Slice || parentKind == reflect.Array {
|
||
|
return textLine(formatHex(v.Uint()))
|
||
|
}
|
||
|
return textLine(fmt.Sprint(v.Uint()))
|
||
|
case reflect.Uintptr:
|
||
|
return textLine(formatHex(v.Uint()))
|
||
|
case reflect.Float32, reflect.Float64:
|
||
|
return textLine(fmt.Sprint(v.Float()))
|
||
|
case reflect.Complex64, reflect.Complex128:
|
||
|
return textLine(fmt.Sprint(v.Complex()))
|
||
|
case reflect.String:
|
||
|
return opts.formatString("", v.String())
|
||
|
case reflect.UnsafePointer, reflect.Chan, reflect.Func:
|
||
|
return textLine(formatPointer(value.PointerOf(v), true))
|
||
|
case reflect.Struct:
|
||
|
var list textList
|
||
|
v := makeAddressable(v) // needed for retrieveUnexportedField
|
||
|
maxLen := v.NumField()
|
||
|
if opts.LimitVerbosity {
|
||
|
maxLen = ((1 << opts.verbosity()) >> 1) << 2 // 0, 4, 8, 16, 32, etc...
|
||
|
opts.VerbosityLevel--
|
||
|
}
|
||
|
for i := 0; i < v.NumField(); i++ {
|
||
|
vv := v.Field(i)
|
||
|
if value.IsZero(vv) {
|
||
|
continue // Elide fields with zero values
|
||
|
}
|
||
|
if len(list) == maxLen {
|
||
|
list.AppendEllipsis(diffStats{})
|
||
|
break
|
||
|
}
|
||
|
sf := t.Field(i)
|
||
|
if supportExporters && !isExported(sf.Name) {
|
||
|
vv = retrieveUnexportedField(v, sf, true)
|
||
|
}
|
||
|
s := opts.WithTypeMode(autoType).FormatValue(vv, t.Kind(), ptrs)
|
||
|
list = append(list, textRecord{Key: sf.Name, Value: s})
|
||
|
}
|
||
|
return &textWrap{Prefix: "{", Value: list, Suffix: "}"}
|
||
|
case reflect.Slice:
|
||
|
if v.IsNil() {
|
||
|
return textNil
|
||
|
}
|
||
|
|
||
|
// Check whether this is a []byte of text data.
|
||
|
if t.Elem() == reflect.TypeOf(byte(0)) {
|
||
|
b := v.Bytes()
|
||
|
isPrintSpace := func(r rune) bool { return unicode.IsPrint(r) && unicode.IsSpace(r) }
|
||
|
if len(b) > 0 && utf8.Valid(b) && len(bytes.TrimFunc(b, isPrintSpace)) == 0 {
|
||
|
out = opts.formatString("", string(b))
|
||
|
return opts.WithTypeMode(emitType).FormatType(t, out)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
fallthrough
|
||
|
case reflect.Array:
|
||
|
maxLen := v.Len()
|
||
|
if opts.LimitVerbosity {
|
||
|
maxLen = ((1 << opts.verbosity()) >> 1) << 2 // 0, 4, 8, 16, 32, etc...
|
||
|
opts.VerbosityLevel--
|
||
|
}
|
||
|
var list textList
|
||
|
for i := 0; i < v.Len(); i++ {
|
||
|
if len(list) == maxLen {
|
||
|
list.AppendEllipsis(diffStats{})
|
||
|
break
|
||
|
}
|
||
|
s := opts.WithTypeMode(elideType).FormatValue(v.Index(i), t.Kind(), ptrs)
|
||
|
list = append(list, textRecord{Value: s})
|
||
|
}
|
||
|
|
||
|
out = &textWrap{Prefix: "{", Value: list, Suffix: "}"}
|
||
|
if t.Kind() == reflect.Slice && opts.PrintAddresses {
|
||
|
header := fmt.Sprintf("ptr:%v, len:%d, cap:%d", formatPointer(value.PointerOf(v), false), v.Len(), v.Cap())
|
||
|
out = &textWrap{Prefix: pointerDelimPrefix + header + pointerDelimSuffix, Value: out}
|
||
|
}
|
||
|
return out
|
||
|
case reflect.Map:
|
||
|
if v.IsNil() {
|
||
|
return textNil
|
||
|
}
|
||
|
|
||
|
// Check pointer for cycles.
|
||
|
ptrRef, visited := ptrs.Push(v)
|
||
|
if visited {
|
||
|
return makeLeafReference(ptrRef, opts.PrintAddresses)
|
||
|
}
|
||
|
defer ptrs.Pop()
|
||
|
|
||
|
maxLen := v.Len()
|
||
|
if opts.LimitVerbosity {
|
||
|
maxLen = ((1 << opts.verbosity()) >> 1) << 2 // 0, 4, 8, 16, 32, etc...
|
||
|
opts.VerbosityLevel--
|
||
|
}
|
||
|
var list textList
|
||
|
for _, k := range value.SortKeys(v.MapKeys()) {
|
||
|
if len(list) == maxLen {
|
||
|
list.AppendEllipsis(diffStats{})
|
||
|
break
|
||
|
}
|
||
|
sk := formatMapKey(k, false, ptrs)
|
||
|
sv := opts.WithTypeMode(elideType).FormatValue(v.MapIndex(k), t.Kind(), ptrs)
|
||
|
list = append(list, textRecord{Key: sk, Value: sv})
|
||
|
}
|
||
|
|
||
|
out = &textWrap{Prefix: "{", Value: list, Suffix: "}"}
|
||
|
out = wrapTrunkReference(ptrRef, opts.PrintAddresses, out)
|
||
|
return out
|
||
|
case reflect.Ptr:
|
||
|
if v.IsNil() {
|
||
|
return textNil
|
||
|
}
|
||
|
|
||
|
// Check pointer for cycles.
|
||
|
ptrRef, visited := ptrs.Push(v)
|
||
|
if visited {
|
||
|
out = makeLeafReference(ptrRef, opts.PrintAddresses)
|
||
|
return &textWrap{Prefix: "&", Value: out}
|
||
|
}
|
||
|
defer ptrs.Pop()
|
||
|
|
||
|
skipType = true // Let the underlying value print the type instead
|
||
|
out = opts.FormatValue(v.Elem(), t.Kind(), ptrs)
|
||
|
out = wrapTrunkReference(ptrRef, opts.PrintAddresses, out)
|
||
|
out = &textWrap{Prefix: "&", Value: out}
|
||
|
return out
|
||
|
case reflect.Interface:
|
||
|
if v.IsNil() {
|
||
|
return textNil
|
||
|
}
|
||
|
// Interfaces accept different concrete types,
|
||
|
// so configure the underlying value to explicitly print the type.
|
||
|
skipType = true // Print the concrete type instead
|
||
|
return opts.WithTypeMode(emitType).FormatValue(v.Elem(), t.Kind(), ptrs)
|
||
|
default:
|
||
|
panic(fmt.Sprintf("%v kind not handled", v.Kind()))
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func (opts formatOptions) formatString(prefix, s string) textNode {
|
||
|
maxLen := len(s)
|
||
|
maxLines := strings.Count(s, "\n") + 1
|
||
|
if opts.LimitVerbosity {
|
||
|
maxLen = (1 << opts.verbosity()) << 5 // 32, 64, 128, 256, etc...
|
||
|
maxLines = (1 << opts.verbosity()) << 2 // 4, 8, 16, 32, 64, etc...
|
||
|
}
|
||
|
|
||
|
// For multiline strings, use the triple-quote syntax,
|
||
|
// but only use it when printing removed or inserted nodes since
|
||
|
// we only want the extra verbosity for those cases.
|
||
|
lines := strings.Split(strings.TrimSuffix(s, "\n"), "\n")
|
||
|
isTripleQuoted := len(lines) >= 4 && (opts.DiffMode == '-' || opts.DiffMode == '+')
|
||
|
for i := 0; i < len(lines) && isTripleQuoted; i++ {
|
||
|
lines[i] = strings.TrimPrefix(strings.TrimSuffix(lines[i], "\r"), "\r") // trim leading/trailing carriage returns for legacy Windows endline support
|
||
|
isPrintable := func(r rune) bool {
|
||
|
return unicode.IsPrint(r) || r == '\t' // specially treat tab as printable
|
||
|
}
|
||
|
line := lines[i]
|
||
|
isTripleQuoted = !strings.HasPrefix(strings.TrimPrefix(line, prefix), `"""`) && !strings.HasPrefix(line, "...") && strings.TrimFunc(line, isPrintable) == "" && len(line) <= maxLen
|
||
|
}
|
||
|
if isTripleQuoted {
|
||
|
var list textList
|
||
|
list = append(list, textRecord{Diff: opts.DiffMode, Value: textLine(prefix + `"""`), ElideComma: true})
|
||
|
for i, line := range lines {
|
||
|
if numElided := len(lines) - i; i == maxLines-1 && numElided > 1 {
|
||
|
comment := commentString(fmt.Sprintf("%d elided lines", numElided))
|
||
|
list = append(list, textRecord{Diff: opts.DiffMode, Value: textEllipsis, ElideComma: true, Comment: comment})
|
||
|
break
|
||
|
}
|
||
|
list = append(list, textRecord{Diff: opts.DiffMode, Value: textLine(line), ElideComma: true})
|
||
|
}
|
||
|
list = append(list, textRecord{Diff: opts.DiffMode, Value: textLine(prefix + `"""`), ElideComma: true})
|
||
|
return &textWrap{Prefix: "(", Value: list, Suffix: ")"}
|
||
|
}
|
||
|
|
||
|
// Format the string as a single-line quoted string.
|
||
|
if len(s) > maxLen+len(textEllipsis) {
|
||
|
return textLine(prefix + formatString(s[:maxLen]) + string(textEllipsis))
|
||
|
}
|
||
|
return textLine(prefix + formatString(s))
|
||
|
}
|
||
|
|
||
|
// formatMapKey formats v as if it were a map key.
|
||
|
// The result is guaranteed to be a single line.
|
||
|
func formatMapKey(v reflect.Value, disambiguate bool, ptrs *pointerReferences) string {
|
||
|
var opts formatOptions
|
||
|
opts.DiffMode = diffIdentical
|
||
|
opts.TypeMode = elideType
|
||
|
opts.PrintAddresses = disambiguate
|
||
|
opts.AvoidStringer = disambiguate
|
||
|
opts.QualifiedNames = disambiguate
|
||
|
s := opts.FormatValue(v, reflect.Map, ptrs).String()
|
||
|
return strings.TrimSpace(s)
|
||
|
}
|
||
|
|
||
|
// formatString prints s as a double-quoted or backtick-quoted string.
|
||
|
func formatString(s string) string {
|
||
|
// Use quoted string if it the same length as a raw string literal.
|
||
|
// Otherwise, attempt to use the raw string form.
|
||
|
qs := strconv.Quote(s)
|
||
|
if len(qs) == 1+len(s)+1 {
|
||
|
return qs
|
||
|
}
|
||
|
|
||
|
// Disallow newlines to ensure output is a single line.
|
||
|
// Only allow printable runes for readability purposes.
|
||
|
rawInvalid := func(r rune) bool {
|
||
|
return r == '`' || r == '\n' || !(unicode.IsPrint(r) || r == '\t')
|
||
|
}
|
||
|
if utf8.ValidString(s) && strings.IndexFunc(s, rawInvalid) < 0 {
|
||
|
return "`" + s + "`"
|
||
|
}
|
||
|
return qs
|
||
|
}
|
||
|
|
||
|
// formatHex prints u as a hexadecimal integer in Go notation.
|
||
|
func formatHex(u uint64) string {
|
||
|
var f string
|
||
|
switch {
|
||
|
case u <= 0xff:
|
||
|
f = "0x%02x"
|
||
|
case u <= 0xffff:
|
||
|
f = "0x%04x"
|
||
|
case u <= 0xffffff:
|
||
|
f = "0x%06x"
|
||
|
case u <= 0xffffffff:
|
||
|
f = "0x%08x"
|
||
|
case u <= 0xffffffffff:
|
||
|
f = "0x%010x"
|
||
|
case u <= 0xffffffffffff:
|
||
|
f = "0x%012x"
|
||
|
case u <= 0xffffffffffffff:
|
||
|
f = "0x%014x"
|
||
|
case u <= 0xffffffffffffffff:
|
||
|
f = "0x%016x"
|
||
|
}
|
||
|
return fmt.Sprintf(f, u)
|
||
|
}
|