• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Copyright 2019, The Go Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style
3// license that can be found in the LICENSE file.
4
5package cmp
6
7import (
8	"fmt"
9	"reflect"
10
11	"github.com/google/go-cmp/cmp/internal/value"
12)
13
14// numContextRecords is the number of surrounding equal records to print.
15const numContextRecords = 2
16
17type diffMode byte
18
19const (
20	diffUnknown   diffMode = 0
21	diffIdentical diffMode = ' '
22	diffRemoved   diffMode = '-'
23	diffInserted  diffMode = '+'
24)
25
26type typeMode int
27
28const (
29	// emitType always prints the type.
30	emitType typeMode = iota
31	// elideType never prints the type.
32	elideType
33	// autoType prints the type only for composite kinds
34	// (i.e., structs, slices, arrays, and maps).
35	autoType
36)
37
38type formatOptions struct {
39	// DiffMode controls the output mode of FormatDiff.
40	//
41	// If diffUnknown,   then produce a diff of the x and y values.
42	// If diffIdentical, then emit values as if they were equal.
43	// If diffRemoved,   then only emit x values (ignoring y values).
44	// If diffInserted,  then only emit y values (ignoring x values).
45	DiffMode diffMode
46
47	// TypeMode controls whether to print the type for the current node.
48	//
49	// As a general rule of thumb, we always print the type of the next node
50	// after an interface, and always elide the type of the next node after
51	// a slice or map node.
52	TypeMode typeMode
53
54	// formatValueOptions are options specific to printing reflect.Values.
55	formatValueOptions
56}
57
58func (opts formatOptions) WithDiffMode(d diffMode) formatOptions {
59	opts.DiffMode = d
60	return opts
61}
62func (opts formatOptions) WithTypeMode(t typeMode) formatOptions {
63	opts.TypeMode = t
64	return opts
65}
66func (opts formatOptions) WithVerbosity(level int) formatOptions {
67	opts.VerbosityLevel = level
68	opts.LimitVerbosity = true
69	return opts
70}
71func (opts formatOptions) verbosity() uint {
72	switch {
73	case opts.VerbosityLevel < 0:
74		return 0
75	case opts.VerbosityLevel > 16:
76		return 16 // some reasonable maximum to avoid shift overflow
77	default:
78		return uint(opts.VerbosityLevel)
79	}
80}
81
82const maxVerbosityPreset = 6
83
84// verbosityPreset modifies the verbosity settings given an index
85// between 0 and maxVerbosityPreset, inclusive.
86func verbosityPreset(opts formatOptions, i int) formatOptions {
87	opts.VerbosityLevel = int(opts.verbosity()) + 2*i
88	if i > 0 {
89		opts.AvoidStringer = true
90	}
91	if i >= maxVerbosityPreset {
92		opts.PrintAddresses = true
93		opts.QualifiedNames = true
94	}
95	return opts
96}
97
98// FormatDiff converts a valueNode tree into a textNode tree, where the later
99// is a textual representation of the differences detected in the former.
100func (opts formatOptions) FormatDiff(v *valueNode, ptrs *pointerReferences) (out textNode) {
101	if opts.DiffMode == diffIdentical {
102		opts = opts.WithVerbosity(1)
103	} else if opts.verbosity() < 3 {
104		opts = opts.WithVerbosity(3)
105	}
106
107	// Check whether we have specialized formatting for this node.
108	// This is not necessary, but helpful for producing more readable outputs.
109	if opts.CanFormatDiffSlice(v) {
110		return opts.FormatDiffSlice(v)
111	}
112
113	var parentKind reflect.Kind
114	if v.parent != nil && v.parent.TransformerName == "" {
115		parentKind = v.parent.Type.Kind()
116	}
117
118	// For leaf nodes, format the value based on the reflect.Values alone.
119	if v.MaxDepth == 0 {
120		switch opts.DiffMode {
121		case diffUnknown, diffIdentical:
122			// Format Equal.
123			if v.NumDiff == 0 {
124				outx := opts.FormatValue(v.ValueX, parentKind, ptrs)
125				outy := opts.FormatValue(v.ValueY, parentKind, ptrs)
126				if v.NumIgnored > 0 && v.NumSame == 0 {
127					return textEllipsis
128				} else if outx.Len() < outy.Len() {
129					return outx
130				} else {
131					return outy
132				}
133			}
134
135			// Format unequal.
136			assert(opts.DiffMode == diffUnknown)
137			var list textList
138			outx := opts.WithTypeMode(elideType).FormatValue(v.ValueX, parentKind, ptrs)
139			outy := opts.WithTypeMode(elideType).FormatValue(v.ValueY, parentKind, ptrs)
140			for i := 0; i <= maxVerbosityPreset && outx != nil && outy != nil && outx.Equal(outy); i++ {
141				opts2 := verbosityPreset(opts, i).WithTypeMode(elideType)
142				outx = opts2.FormatValue(v.ValueX, parentKind, ptrs)
143				outy = opts2.FormatValue(v.ValueY, parentKind, ptrs)
144			}
145			if outx != nil {
146				list = append(list, textRecord{Diff: '-', Value: outx})
147			}
148			if outy != nil {
149				list = append(list, textRecord{Diff: '+', Value: outy})
150			}
151			return opts.WithTypeMode(emitType).FormatType(v.Type, list)
152		case diffRemoved:
153			return opts.FormatValue(v.ValueX, parentKind, ptrs)
154		case diffInserted:
155			return opts.FormatValue(v.ValueY, parentKind, ptrs)
156		default:
157			panic("invalid diff mode")
158		}
159	}
160
161	// Register slice element to support cycle detection.
162	if parentKind == reflect.Slice {
163		ptrRefs := ptrs.PushPair(v.ValueX, v.ValueY, opts.DiffMode, true)
164		defer ptrs.Pop()
165		defer func() { out = wrapTrunkReferences(ptrRefs, out) }()
166	}
167
168	// Descend into the child value node.
169	if v.TransformerName != "" {
170		out := opts.WithTypeMode(emitType).FormatDiff(v.Value, ptrs)
171		out = &textWrap{Prefix: "Inverse(" + v.TransformerName + ", ", Value: out, Suffix: ")"}
172		return opts.FormatType(v.Type, out)
173	} else {
174		switch k := v.Type.Kind(); k {
175		case reflect.Struct, reflect.Array, reflect.Slice:
176			out = opts.formatDiffList(v.Records, k, ptrs)
177			out = opts.FormatType(v.Type, out)
178		case reflect.Map:
179			// Register map to support cycle detection.
180			ptrRefs := ptrs.PushPair(v.ValueX, v.ValueY, opts.DiffMode, false)
181			defer ptrs.Pop()
182
183			out = opts.formatDiffList(v.Records, k, ptrs)
184			out = wrapTrunkReferences(ptrRefs, out)
185			out = opts.FormatType(v.Type, out)
186		case reflect.Ptr:
187			// Register pointer to support cycle detection.
188			ptrRefs := ptrs.PushPair(v.ValueX, v.ValueY, opts.DiffMode, false)
189			defer ptrs.Pop()
190
191			out = opts.FormatDiff(v.Value, ptrs)
192			out = wrapTrunkReferences(ptrRefs, out)
193			out = &textWrap{Prefix: "&", Value: out}
194		case reflect.Interface:
195			out = opts.WithTypeMode(emitType).FormatDiff(v.Value, ptrs)
196		default:
197			panic(fmt.Sprintf("%v cannot have children", k))
198		}
199		return out
200	}
201}
202
203func (opts formatOptions) formatDiffList(recs []reportRecord, k reflect.Kind, ptrs *pointerReferences) textNode {
204	// Derive record name based on the data structure kind.
205	var name string
206	var formatKey func(reflect.Value) string
207	switch k {
208	case reflect.Struct:
209		name = "field"
210		opts = opts.WithTypeMode(autoType)
211		formatKey = func(v reflect.Value) string { return v.String() }
212	case reflect.Slice, reflect.Array:
213		name = "element"
214		opts = opts.WithTypeMode(elideType)
215		formatKey = func(reflect.Value) string { return "" }
216	case reflect.Map:
217		name = "entry"
218		opts = opts.WithTypeMode(elideType)
219		formatKey = func(v reflect.Value) string { return formatMapKey(v, false, ptrs) }
220	}
221
222	maxLen := -1
223	if opts.LimitVerbosity {
224		if opts.DiffMode == diffIdentical {
225			maxLen = ((1 << opts.verbosity()) >> 1) << 2 // 0, 4, 8, 16, 32, etc...
226		} else {
227			maxLen = (1 << opts.verbosity()) << 1 // 2, 4, 8, 16, 32, 64, etc...
228		}
229		opts.VerbosityLevel--
230	}
231
232	// Handle unification.
233	switch opts.DiffMode {
234	case diffIdentical, diffRemoved, diffInserted:
235		var list textList
236		var deferredEllipsis bool // Add final "..." to indicate records were dropped
237		for _, r := range recs {
238			if len(list) == maxLen {
239				deferredEllipsis = true
240				break
241			}
242
243			// Elide struct fields that are zero value.
244			if k == reflect.Struct {
245				var isZero bool
246				switch opts.DiffMode {
247				case diffIdentical:
248					isZero = value.IsZero(r.Value.ValueX) || value.IsZero(r.Value.ValueY)
249				case diffRemoved:
250					isZero = value.IsZero(r.Value.ValueX)
251				case diffInserted:
252					isZero = value.IsZero(r.Value.ValueY)
253				}
254				if isZero {
255					continue
256				}
257			}
258			// Elide ignored nodes.
259			if r.Value.NumIgnored > 0 && r.Value.NumSame+r.Value.NumDiff == 0 {
260				deferredEllipsis = !(k == reflect.Slice || k == reflect.Array)
261				if !deferredEllipsis {
262					list.AppendEllipsis(diffStats{})
263				}
264				continue
265			}
266			if out := opts.FormatDiff(r.Value, ptrs); out != nil {
267				list = append(list, textRecord{Key: formatKey(r.Key), Value: out})
268			}
269		}
270		if deferredEllipsis {
271			list.AppendEllipsis(diffStats{})
272		}
273		return &textWrap{Prefix: "{", Value: list, Suffix: "}"}
274	case diffUnknown:
275	default:
276		panic("invalid diff mode")
277	}
278
279	// Handle differencing.
280	var numDiffs int
281	var list textList
282	var keys []reflect.Value // invariant: len(list) == len(keys)
283	groups := coalesceAdjacentRecords(name, recs)
284	maxGroup := diffStats{Name: name}
285	for i, ds := range groups {
286		if maxLen >= 0 && numDiffs >= maxLen {
287			maxGroup = maxGroup.Append(ds)
288			continue
289		}
290
291		// Handle equal records.
292		if ds.NumDiff() == 0 {
293			// Compute the number of leading and trailing records to print.
294			var numLo, numHi int
295			numEqual := ds.NumIgnored + ds.NumIdentical
296			for numLo < numContextRecords && numLo+numHi < numEqual && i != 0 {
297				if r := recs[numLo].Value; r.NumIgnored > 0 && r.NumSame+r.NumDiff == 0 {
298					break
299				}
300				numLo++
301			}
302			for numHi < numContextRecords && numLo+numHi < numEqual && i != len(groups)-1 {
303				if r := recs[numEqual-numHi-1].Value; r.NumIgnored > 0 && r.NumSame+r.NumDiff == 0 {
304					break
305				}
306				numHi++
307			}
308			if numEqual-(numLo+numHi) == 1 && ds.NumIgnored == 0 {
309				numHi++ // Avoid pointless coalescing of a single equal record
310			}
311
312			// Format the equal values.
313			for _, r := range recs[:numLo] {
314				out := opts.WithDiffMode(diffIdentical).FormatDiff(r.Value, ptrs)
315				list = append(list, textRecord{Key: formatKey(r.Key), Value: out})
316				keys = append(keys, r.Key)
317			}
318			if numEqual > numLo+numHi {
319				ds.NumIdentical -= numLo + numHi
320				list.AppendEllipsis(ds)
321				for len(keys) < len(list) {
322					keys = append(keys, reflect.Value{})
323				}
324			}
325			for _, r := range recs[numEqual-numHi : numEqual] {
326				out := opts.WithDiffMode(diffIdentical).FormatDiff(r.Value, ptrs)
327				list = append(list, textRecord{Key: formatKey(r.Key), Value: out})
328				keys = append(keys, r.Key)
329			}
330			recs = recs[numEqual:]
331			continue
332		}
333
334		// Handle unequal records.
335		for _, r := range recs[:ds.NumDiff()] {
336			switch {
337			case opts.CanFormatDiffSlice(r.Value):
338				out := opts.FormatDiffSlice(r.Value)
339				list = append(list, textRecord{Key: formatKey(r.Key), Value: out})
340				keys = append(keys, r.Key)
341			case r.Value.NumChildren == r.Value.MaxDepth:
342				outx := opts.WithDiffMode(diffRemoved).FormatDiff(r.Value, ptrs)
343				outy := opts.WithDiffMode(diffInserted).FormatDiff(r.Value, ptrs)
344				for i := 0; i <= maxVerbosityPreset && outx != nil && outy != nil && outx.Equal(outy); i++ {
345					opts2 := verbosityPreset(opts, i)
346					outx = opts2.WithDiffMode(diffRemoved).FormatDiff(r.Value, ptrs)
347					outy = opts2.WithDiffMode(diffInserted).FormatDiff(r.Value, ptrs)
348				}
349				if outx != nil {
350					list = append(list, textRecord{Diff: diffRemoved, Key: formatKey(r.Key), Value: outx})
351					keys = append(keys, r.Key)
352				}
353				if outy != nil {
354					list = append(list, textRecord{Diff: diffInserted, Key: formatKey(r.Key), Value: outy})
355					keys = append(keys, r.Key)
356				}
357			default:
358				out := opts.FormatDiff(r.Value, ptrs)
359				list = append(list, textRecord{Key: formatKey(r.Key), Value: out})
360				keys = append(keys, r.Key)
361			}
362		}
363		recs = recs[ds.NumDiff():]
364		numDiffs += ds.NumDiff()
365	}
366	if maxGroup.IsZero() {
367		assert(len(recs) == 0)
368	} else {
369		list.AppendEllipsis(maxGroup)
370		for len(keys) < len(list) {
371			keys = append(keys, reflect.Value{})
372		}
373	}
374	assert(len(list) == len(keys))
375
376	// For maps, the default formatting logic uses fmt.Stringer which may
377	// produce ambiguous output. Avoid calling String to disambiguate.
378	if k == reflect.Map {
379		var ambiguous bool
380		seenKeys := map[string]reflect.Value{}
381		for i, currKey := range keys {
382			if currKey.IsValid() {
383				strKey := list[i].Key
384				prevKey, seen := seenKeys[strKey]
385				if seen && prevKey.CanInterface() && currKey.CanInterface() {
386					ambiguous = prevKey.Interface() != currKey.Interface()
387					if ambiguous {
388						break
389					}
390				}
391				seenKeys[strKey] = currKey
392			}
393		}
394		if ambiguous {
395			for i, k := range keys {
396				if k.IsValid() {
397					list[i].Key = formatMapKey(k, true, ptrs)
398				}
399			}
400		}
401	}
402
403	return &textWrap{Prefix: "{", Value: list, Suffix: "}"}
404}
405
406// coalesceAdjacentRecords coalesces the list of records into groups of
407// adjacent equal, or unequal counts.
408func coalesceAdjacentRecords(name string, recs []reportRecord) (groups []diffStats) {
409	var prevCase int // Arbitrary index into which case last occurred
410	lastStats := func(i int) *diffStats {
411		if prevCase != i {
412			groups = append(groups, diffStats{Name: name})
413			prevCase = i
414		}
415		return &groups[len(groups)-1]
416	}
417	for _, r := range recs {
418		switch rv := r.Value; {
419		case rv.NumIgnored > 0 && rv.NumSame+rv.NumDiff == 0:
420			lastStats(1).NumIgnored++
421		case rv.NumDiff == 0:
422			lastStats(1).NumIdentical++
423		case rv.NumDiff > 0 && !rv.ValueY.IsValid():
424			lastStats(2).NumRemoved++
425		case rv.NumDiff > 0 && !rv.ValueX.IsValid():
426			lastStats(2).NumInserted++
427		default:
428			lastStats(2).NumModified++
429		}
430	}
431	return groups
432}
433