| // 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 file. | |
| package cmp | |
| import ( | |
| "bytes" | |
| "fmt" | |
| "reflect" | |
| "strconv" | |
| "strings" | |
| "unicode" | |
| "unicode/utf8" | |
| "github.com/google/go-cmp/cmp/internal/value" | |
| ) | |
| var ( | |
| anyType = reflect.TypeOf((*interface{})(nil)).Elem() | |
| stringType = reflect.TypeOf((*string)(nil)).Elem() | |
| bytesType = reflect.TypeOf((*[]byte)(nil)).Elem() | |
| byteType = reflect.TypeOf((*byte)(nil)).Elem() | |
| ) | |
| 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 vv.IsZero() { | |
| continue // Elide fields with zero values | |
| } | |
| if len(list) == maxLen { | |
| list.AppendEllipsis(diffStats{}) | |
| break | |
| } | |
| sf := t.Field(i) | |
| if !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() == byteType { | |
| 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)) | |
| skipType = true | |
| return opts.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() | |
| // Skip the name only if this is an unnamed pointer type. | |
| // Otherwise taking the address of a value does not reproduce | |
| // the named pointer type. | |
| if v.Type().Name() == "" { | |
| 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. | |
| 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 | |
| opts.VerbosityLevel = maxVerbosityPreset | |
| opts.LimitVerbosity = true | |
| 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) | |
| } |