Skip to content

Commit

Permalink
Use triple-quote formatting for multiline strings (#229)
Browse files Browse the repository at this point in the history
For strings, []bytes containing text data, Error method output, and
String method output, use the triple-quoted syntax.
This improves readability by presenting the data more naturally
compared to a single-line quoted string with many escaped characters.
  • Loading branch information
dsnet authored Jul 21, 2020
1 parent 1536a0c commit 9680bfa
Show file tree
Hide file tree
Showing 3 changed files with 103 additions and 16 deletions.
13 changes: 13 additions & 0 deletions cmp/compare_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1133,6 +1133,19 @@ func reporterTests() []test {
y: "aaa\nbbb\nccc \nddd\neee\nfff\nggg\nhhh\niii\njjj\nkkk\nlll\nmmm\nnnn\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu\nvvv\nwww\nxxx\nyyy\nzzz\n",
wantEqual: false,
reason: "avoid triple-quote syntax due to visual equivalence of differences",
}, {
label: label + "/TripleQuoteStringer",
x: []fmt.Stringer{
bytes.NewBuffer([]byte("package main\n\nimport (\n\t\"fmt\"\n)\n\nfunc main() {\n\tfmt.Println(\"Hello, playground\")\n}\n")),
bytes.NewBuffer([]byte("package main\n\nimport (\n\t\"fmt\"\n\t\"math/rand\"\n)\n\nfunc main() {\n\tfmt.Println(\"My favorite number is\", rand.Intn(10))\n}\n")),
},
y: []fmt.Stringer{
bytes.NewBuffer([]byte("package main\n\nimport (\n\t\"fmt\"\n)\n\nfunc main() {\n\tfmt.Println(\"Hello, playground\")\n}\n")),
bytes.NewBuffer([]byte("package main\n\nimport (\n\t\"fmt\"\n\t\"math\"\n)\n\nfunc main() {\n\tfmt.Printf(\"Now you have %g problems.\\n\", math.Sqrt(7))\n}\n")),
},
opts: []cmp.Option{cmp.Comparer(func(x, y fmt.Stringer) bool { return x.String() == y.String() })},
wantEqual: false,
reason: "multi-line String output should be formatted with triple quote",
}, {
label: label + "/LimitMaximumBytesDiffs",
x: []byte("\xcd====\x06\x1f\xc2\xcc\xc2-S=====\x1d\xdfa\xae\x98\x9fH======ǰ\xb7=======\xef====:\\\x94\xe6J\xc7=====\xb4======\n\n\xf7\x94===========\xf2\x9c\xc0f=====4\xf6\xf1\xc3\x17\x82======n\x16`\x91D\xc6\x06=======\x1cE====.===========\xc4\x18=======\x8a\x8d\x0e====\x87\xb1\xa5\x8e\xc3=====z\x0f1\xaeU======G,=======5\xe75\xee\x82\xf4\xce====\x11r===========\xaf]=======z\x05\xb3\x91\x88%\xd2====\n1\x89=====i\xb7\x055\xe6\x81\xd2=============\x883=@̾====\x14\x05\x96%^t\x04=====\xe7Ȉ\x90\x1d============="),
Expand Down
73 changes: 57 additions & 16 deletions cmp/report_reflect.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
package cmp

import (
"bytes"
"fmt"
"reflect"
"strconv"
Expand Down Expand Up @@ -138,14 +139,7 @@ func (opts formatOptions) FormatValue(v reflect.Value, parentKind reflect.Kind,
}
}()
if prefix != "" {
maxLen := len(strVal)
if opts.LimitVerbosity {
maxLen = (1 << opts.verbosity()) << 5 // 32, 64, 128, 256, etc...
}
if len(strVal) > maxLen+len(textEllipsis) {
return textLine(prefix + formatString(strVal[:maxLen]) + string(textEllipsis))
}
return textLine(prefix + formatString(strVal))
return opts.formatString(prefix, strVal)
}
}
}
Expand Down Expand Up @@ -177,14 +171,7 @@ func (opts formatOptions) FormatValue(v reflect.Value, parentKind reflect.Kind,
case reflect.Complex64, reflect.Complex128:
return textLine(fmt.Sprint(v.Complex()))
case reflect.String:
maxLen := v.Len()
if opts.LimitVerbosity {
maxLen = (1 << opts.verbosity()) << 5 // 32, 64, 128, 256, etc...
}
if v.Len() > maxLen+len(textEllipsis) {
return textLine(formatString(v.String()[:maxLen]) + string(textEllipsis))
}
return textLine(formatString(v.String()))
return opts.formatString("", v.String())
case reflect.UnsafePointer, reflect.Chan, reflect.Func:
return textLine(formatPointer(value.PointerOf(v), true))
case reflect.Struct:
Expand Down Expand Up @@ -216,6 +203,17 @@ func (opts formatOptions) FormatValue(v reflect.Value, parentKind reflect.Kind,
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()
Expand Down Expand Up @@ -301,6 +299,49 @@ func (opts formatOptions) FormatValue(v reflect.Value, parentKind reflect.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 {
Expand Down
33 changes: 33 additions & 0 deletions cmp/testdata/diffs
Original file line number Diff line number Diff line change
Expand Up @@ -730,6 +730,39 @@
... // 7 identical lines
}, "\n")
>>> TestDiff/Reporter/AvoidTripleQuoteIdenticalWhitespace
<<< TestDiff/Reporter/TripleQuoteStringer
[]fmt.Stringer{
s"package main\n\nimport (\n\t\"fmt\"\n)\n\nfunc main() {\n\tfmt.Println(\"Hel"...,
- (
- s"""
- package main
-
- import (
- "fmt"
- "math/rand"
- )
-
- func main() {
- fmt.Println("My favorite number is", rand.Intn(10))
- }
- s"""
- ),
+ (
+ s"""
+ package main
+
+ import (
+ "fmt"
+ "math"
+ )
+
+ func main() {
+ fmt.Printf("Now you have %g problems.\n", math.Sqrt(7))
+ }
+ s"""
+ ),
}
>>> TestDiff/Reporter/TripleQuoteStringer
<<< TestDiff/Reporter/LimitMaximumBytesDiffs
[]uint8{
- 0xcd, 0x3d, 0x3d, 0x3d, 0x3d, 0x06, 0x1f, 0xc2, 0xcc, 0xc2, 0x2d, 0x53, // -|.====.....-S|
Expand Down

0 comments on commit 9680bfa

Please sign in to comment.