Skip to content

Commit 9680bfa

Browse files
authored
Use triple-quote formatting for multiline strings (#229)
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.
1 parent 1536a0c commit 9680bfa

File tree

3 files changed

+103
-16
lines changed

3 files changed

+103
-16
lines changed

cmp/compare_test.go

+13
Original file line numberDiff line numberDiff line change
@@ -1133,6 +1133,19 @@ func reporterTests() []test {
11331133
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",
11341134
wantEqual: false,
11351135
reason: "avoid triple-quote syntax due to visual equivalence of differences",
1136+
}, {
1137+
label: label + "/TripleQuoteStringer",
1138+
x: []fmt.Stringer{
1139+
bytes.NewBuffer([]byte("package main\n\nimport (\n\t\"fmt\"\n)\n\nfunc main() {\n\tfmt.Println(\"Hello, playground\")\n}\n")),
1140+
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")),
1141+
},
1142+
y: []fmt.Stringer{
1143+
bytes.NewBuffer([]byte("package main\n\nimport (\n\t\"fmt\"\n)\n\nfunc main() {\n\tfmt.Println(\"Hello, playground\")\n}\n")),
1144+
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")),
1145+
},
1146+
opts: []cmp.Option{cmp.Comparer(func(x, y fmt.Stringer) bool { return x.String() == y.String() })},
1147+
wantEqual: false,
1148+
reason: "multi-line String output should be formatted with triple quote",
11361149
}, {
11371150
label: label + "/LimitMaximumBytesDiffs",
11381151
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============="),

cmp/report_reflect.go

+57-16
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
package cmp
66

77
import (
8+
"bytes"
89
"fmt"
910
"reflect"
1011
"strconv"
@@ -138,14 +139,7 @@ func (opts formatOptions) FormatValue(v reflect.Value, parentKind reflect.Kind,
138139
}
139140
}()
140141
if prefix != "" {
141-
maxLen := len(strVal)
142-
if opts.LimitVerbosity {
143-
maxLen = (1 << opts.verbosity()) << 5 // 32, 64, 128, 256, etc...
144-
}
145-
if len(strVal) > maxLen+len(textEllipsis) {
146-
return textLine(prefix + formatString(strVal[:maxLen]) + string(textEllipsis))
147-
}
148-
return textLine(prefix + formatString(strVal))
142+
return opts.formatString(prefix, strVal)
149143
}
150144
}
151145
}
@@ -177,14 +171,7 @@ func (opts formatOptions) FormatValue(v reflect.Value, parentKind reflect.Kind,
177171
case reflect.Complex64, reflect.Complex128:
178172
return textLine(fmt.Sprint(v.Complex()))
179173
case reflect.String:
180-
maxLen := v.Len()
181-
if opts.LimitVerbosity {
182-
maxLen = (1 << opts.verbosity()) << 5 // 32, 64, 128, 256, etc...
183-
}
184-
if v.Len() > maxLen+len(textEllipsis) {
185-
return textLine(formatString(v.String()[:maxLen]) + string(textEllipsis))
186-
}
187-
return textLine(formatString(v.String()))
174+
return opts.formatString("", v.String())
188175
case reflect.UnsafePointer, reflect.Chan, reflect.Func:
189176
return textLine(formatPointer(value.PointerOf(v), true))
190177
case reflect.Struct:
@@ -216,6 +203,17 @@ func (opts formatOptions) FormatValue(v reflect.Value, parentKind reflect.Kind,
216203
if v.IsNil() {
217204
return textNil
218205
}
206+
207+
// Check whether this is a []byte of text data.
208+
if t.Elem() == reflect.TypeOf(byte(0)) {
209+
b := v.Bytes()
210+
isPrintSpace := func(r rune) bool { return unicode.IsPrint(r) && unicode.IsSpace(r) }
211+
if len(b) > 0 && utf8.Valid(b) && len(bytes.TrimFunc(b, isPrintSpace)) == 0 {
212+
out = opts.formatString("", string(b))
213+
return opts.WithTypeMode(emitType).FormatType(t, out)
214+
}
215+
}
216+
219217
fallthrough
220218
case reflect.Array:
221219
maxLen := v.Len()
@@ -301,6 +299,49 @@ func (opts formatOptions) FormatValue(v reflect.Value, parentKind reflect.Kind,
301299
}
302300
}
303301

302+
func (opts formatOptions) formatString(prefix, s string) textNode {
303+
maxLen := len(s)
304+
maxLines := strings.Count(s, "\n") + 1
305+
if opts.LimitVerbosity {
306+
maxLen = (1 << opts.verbosity()) << 5 // 32, 64, 128, 256, etc...
307+
maxLines = (1 << opts.verbosity()) << 2 // 4, 8, 16, 32, 64, etc...
308+
}
309+
310+
// For multiline strings, use the triple-quote syntax,
311+
// but only use it when printing removed or inserted nodes since
312+
// we only want the extra verbosity for those cases.
313+
lines := strings.Split(strings.TrimSuffix(s, "\n"), "\n")
314+
isTripleQuoted := len(lines) >= 4 && (opts.DiffMode == '-' || opts.DiffMode == '+')
315+
for i := 0; i < len(lines) && isTripleQuoted; i++ {
316+
lines[i] = strings.TrimPrefix(strings.TrimSuffix(lines[i], "\r"), "\r") // trim leading/trailing carriage returns for legacy Windows endline support
317+
isPrintable := func(r rune) bool {
318+
return unicode.IsPrint(r) || r == '\t' // specially treat tab as printable
319+
}
320+
line := lines[i]
321+
isTripleQuoted = !strings.HasPrefix(strings.TrimPrefix(line, prefix), `"""`) && !strings.HasPrefix(line, "...") && strings.TrimFunc(line, isPrintable) == "" && len(line) <= maxLen
322+
}
323+
if isTripleQuoted {
324+
var list textList
325+
list = append(list, textRecord{Diff: opts.DiffMode, Value: textLine(prefix + `"""`), ElideComma: true})
326+
for i, line := range lines {
327+
if numElided := len(lines) - i; i == maxLines-1 && numElided > 1 {
328+
comment := commentString(fmt.Sprintf("%d elided lines", numElided))
329+
list = append(list, textRecord{Diff: opts.DiffMode, Value: textEllipsis, ElideComma: true, Comment: comment})
330+
break
331+
}
332+
list = append(list, textRecord{Diff: opts.DiffMode, Value: textLine(line), ElideComma: true})
333+
}
334+
list = append(list, textRecord{Diff: opts.DiffMode, Value: textLine(prefix + `"""`), ElideComma: true})
335+
return &textWrap{Prefix: "(", Value: list, Suffix: ")"}
336+
}
337+
338+
// Format the string as a single-line quoted string.
339+
if len(s) > maxLen+len(textEllipsis) {
340+
return textLine(prefix + formatString(s[:maxLen]) + string(textEllipsis))
341+
}
342+
return textLine(prefix + formatString(s))
343+
}
344+
304345
// formatMapKey formats v as if it were a map key.
305346
// The result is guaranteed to be a single line.
306347
func formatMapKey(v reflect.Value, disambiguate bool, ptrs *pointerReferences) string {

cmp/testdata/diffs

+33
Original file line numberDiff line numberDiff line change
@@ -730,6 +730,39 @@
730730
... // 7 identical lines
731731
}, "\n")
732732
>>> TestDiff/Reporter/AvoidTripleQuoteIdenticalWhitespace
733+
<<< TestDiff/Reporter/TripleQuoteStringer
734+
[]fmt.Stringer{
735+
s"package main\n\nimport (\n\t\"fmt\"\n)\n\nfunc main() {\n\tfmt.Println(\"Hel"...,
736+
- (
737+
- s"""
738+
- package main
739+
-
740+
- import (
741+
- "fmt"
742+
- "math/rand"
743+
- )
744+
-
745+
- func main() {
746+
- fmt.Println("My favorite number is", rand.Intn(10))
747+
- }
748+
- s"""
749+
- ),
750+
+ (
751+
+ s"""
752+
+ package main
753+
+
754+
+ import (
755+
+ "fmt"
756+
+ "math"
757+
+ )
758+
+
759+
+ func main() {
760+
+ fmt.Printf("Now you have %g problems.\n", math.Sqrt(7))
761+
+ }
762+
+ s"""
763+
+ ),
764+
}
765+
>>> TestDiff/Reporter/TripleQuoteStringer
733766
<<< TestDiff/Reporter/LimitMaximumBytesDiffs
734767
[]uint8{
735768
- 0xcd, 0x3d, 0x3d, 0x3d, 0x3d, 0x06, 0x1f, 0xc2, 0xcc, 0xc2, 0x2d, 0x53, // -|.====.....-S|

0 commit comments

Comments
 (0)