forked from toolshed/abra
		
	
		
			
				
	
	
		
			433 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			433 lines
		
	
	
		
			12 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 file.
 | ||
| 
 | ||
| package cmp
 | ||
| 
 | ||
| import (
 | ||
| 	"bytes"
 | ||
| 	"fmt"
 | ||
| 	"math/rand"
 | ||
| 	"strings"
 | ||
| 	"time"
 | ||
| 	"unicode/utf8"
 | ||
| 
 | ||
| 	"github.com/google/go-cmp/cmp/internal/flags"
 | ||
| )
 | ||
| 
 | ||
| var randBool = rand.New(rand.NewSource(time.Now().Unix())).Intn(2) == 0
 | ||
| 
 | ||
| const maxColumnLength = 80
 | ||
| 
 | ||
| type indentMode int
 | ||
| 
 | ||
| func (n indentMode) appendIndent(b []byte, d diffMode) []byte {
 | ||
| 	// The output of Diff is documented as being unstable to provide future
 | ||
| 	// flexibility in changing the output for more humanly readable reports.
 | ||
| 	// This logic intentionally introduces instability to the exact output
 | ||
| 	// so that users can detect accidental reliance on stability early on,
 | ||
| 	// rather than much later when an actual change to the format occurs.
 | ||
| 	if flags.Deterministic || randBool {
 | ||
| 		// Use regular spaces (U+0020).
 | ||
| 		switch d {
 | ||
| 		case diffUnknown, diffIdentical:
 | ||
| 			b = append(b, "  "...)
 | ||
| 		case diffRemoved:
 | ||
| 			b = append(b, "- "...)
 | ||
| 		case diffInserted:
 | ||
| 			b = append(b, "+ "...)
 | ||
| 		}
 | ||
| 	} else {
 | ||
| 		// Use non-breaking spaces (U+00a0).
 | ||
| 		switch d {
 | ||
| 		case diffUnknown, diffIdentical:
 | ||
| 			b = append(b, "  "...)
 | ||
| 		case diffRemoved:
 | ||
| 			b = append(b, "- "...)
 | ||
| 		case diffInserted:
 | ||
| 			b = append(b, "+ "...)
 | ||
| 		}
 | ||
| 	}
 | ||
| 	return repeatCount(n).appendChar(b, '\t')
 | ||
| }
 | ||
| 
 | ||
| type repeatCount int
 | ||
| 
 | ||
| func (n repeatCount) appendChar(b []byte, c byte) []byte {
 | ||
| 	for ; n > 0; n-- {
 | ||
| 		b = append(b, c)
 | ||
| 	}
 | ||
| 	return b
 | ||
| }
 | ||
| 
 | ||
| // textNode is a simplified tree-based representation of structured text.
 | ||
| // Possible node types are textWrap, textList, or textLine.
 | ||
| type textNode interface {
 | ||
| 	// Len reports the length in bytes of a single-line version of the tree.
 | ||
| 	// Nested textRecord.Diff and textRecord.Comment fields are ignored.
 | ||
| 	Len() int
 | ||
| 	// Equal reports whether the two trees are structurally identical.
 | ||
| 	// Nested textRecord.Diff and textRecord.Comment fields are compared.
 | ||
| 	Equal(textNode) bool
 | ||
| 	// String returns the string representation of the text tree.
 | ||
| 	// It is not guaranteed that len(x.String()) == x.Len(),
 | ||
| 	// nor that x.String() == y.String() implies that x.Equal(y).
 | ||
| 	String() string
 | ||
| 
 | ||
| 	// formatCompactTo formats the contents of the tree as a single-line string
 | ||
| 	// to the provided buffer. Any nested textRecord.Diff and textRecord.Comment
 | ||
| 	// fields are ignored.
 | ||
| 	//
 | ||
| 	// However, not all nodes in the tree should be collapsed as a single-line.
 | ||
| 	// If a node can be collapsed as a single-line, it is replaced by a textLine
 | ||
| 	// node. Since the top-level node cannot replace itself, this also returns
 | ||
| 	// the current node itself.
 | ||
| 	//
 | ||
| 	// This does not mutate the receiver.
 | ||
| 	formatCompactTo([]byte, diffMode) ([]byte, textNode)
 | ||
| 	// formatExpandedTo formats the contents of the tree as a multi-line string
 | ||
| 	// to the provided buffer. In order for column alignment to operate well,
 | ||
| 	// formatCompactTo must be called before calling formatExpandedTo.
 | ||
| 	formatExpandedTo([]byte, diffMode, indentMode) []byte
 | ||
| }
 | ||
| 
 | ||
| // textWrap is a wrapper that concatenates a prefix and/or a suffix
 | ||
| // to the underlying node.
 | ||
| type textWrap struct {
 | ||
| 	Prefix   string      // e.g., "bytes.Buffer{"
 | ||
| 	Value    textNode    // textWrap | textList | textLine
 | ||
| 	Suffix   string      // e.g., "}"
 | ||
| 	Metadata interface{} // arbitrary metadata; has no effect on formatting
 | ||
| }
 | ||
| 
 | ||
| func (s *textWrap) Len() int {
 | ||
| 	return len(s.Prefix) + s.Value.Len() + len(s.Suffix)
 | ||
| }
 | ||
| func (s1 *textWrap) Equal(s2 textNode) bool {
 | ||
| 	if s2, ok := s2.(*textWrap); ok {
 | ||
| 		return s1.Prefix == s2.Prefix && s1.Value.Equal(s2.Value) && s1.Suffix == s2.Suffix
 | ||
| 	}
 | ||
| 	return false
 | ||
| }
 | ||
| func (s *textWrap) String() string {
 | ||
| 	var d diffMode
 | ||
| 	var n indentMode
 | ||
| 	_, s2 := s.formatCompactTo(nil, d)
 | ||
| 	b := n.appendIndent(nil, d)      // Leading indent
 | ||
| 	b = s2.formatExpandedTo(b, d, n) // Main body
 | ||
| 	b = append(b, '\n')              // Trailing newline
 | ||
| 	return string(b)
 | ||
| }
 | ||
| func (s *textWrap) formatCompactTo(b []byte, d diffMode) ([]byte, textNode) {
 | ||
| 	n0 := len(b) // Original buffer length
 | ||
| 	b = append(b, s.Prefix...)
 | ||
| 	b, s.Value = s.Value.formatCompactTo(b, d)
 | ||
| 	b = append(b, s.Suffix...)
 | ||
| 	if _, ok := s.Value.(textLine); ok {
 | ||
| 		return b, textLine(b[n0:])
 | ||
| 	}
 | ||
| 	return b, s
 | ||
| }
 | ||
| func (s *textWrap) formatExpandedTo(b []byte, d diffMode, n indentMode) []byte {
 | ||
| 	b = append(b, s.Prefix...)
 | ||
| 	b = s.Value.formatExpandedTo(b, d, n)
 | ||
| 	b = append(b, s.Suffix...)
 | ||
| 	return b
 | ||
| }
 | ||
| 
 | ||
| // textList is a comma-separated list of textWrap or textLine nodes.
 | ||
| // The list may be formatted as multi-lines or single-line at the discretion
 | ||
| // of the textList.formatCompactTo method.
 | ||
| type textList []textRecord
 | ||
| type textRecord struct {
 | ||
| 	Diff       diffMode     // e.g., 0 or '-' or '+'
 | ||
| 	Key        string       // e.g., "MyField"
 | ||
| 	Value      textNode     // textWrap | textLine
 | ||
| 	ElideComma bool         // avoid trailing comma
 | ||
| 	Comment    fmt.Stringer // e.g., "6 identical fields"
 | ||
| }
 | ||
| 
 | ||
| // AppendEllipsis appends a new ellipsis node to the list if none already
 | ||
| // exists at the end. If cs is non-zero it coalesces the statistics with the
 | ||
| // previous diffStats.
 | ||
| func (s *textList) AppendEllipsis(ds diffStats) {
 | ||
| 	hasStats := !ds.IsZero()
 | ||
| 	if len(*s) == 0 || !(*s)[len(*s)-1].Value.Equal(textEllipsis) {
 | ||
| 		if hasStats {
 | ||
| 			*s = append(*s, textRecord{Value: textEllipsis, ElideComma: true, Comment: ds})
 | ||
| 		} else {
 | ||
| 			*s = append(*s, textRecord{Value: textEllipsis, ElideComma: true})
 | ||
| 		}
 | ||
| 		return
 | ||
| 	}
 | ||
| 	if hasStats {
 | ||
| 		(*s)[len(*s)-1].Comment = (*s)[len(*s)-1].Comment.(diffStats).Append(ds)
 | ||
| 	}
 | ||
| }
 | ||
| 
 | ||
| func (s textList) Len() (n int) {
 | ||
| 	for i, r := range s {
 | ||
| 		n += len(r.Key)
 | ||
| 		if r.Key != "" {
 | ||
| 			n += len(": ")
 | ||
| 		}
 | ||
| 		n += r.Value.Len()
 | ||
| 		if i < len(s)-1 {
 | ||
| 			n += len(", ")
 | ||
| 		}
 | ||
| 	}
 | ||
| 	return n
 | ||
| }
 | ||
| 
 | ||
| func (s1 textList) Equal(s2 textNode) bool {
 | ||
| 	if s2, ok := s2.(textList); ok {
 | ||
| 		if len(s1) != len(s2) {
 | ||
| 			return false
 | ||
| 		}
 | ||
| 		for i := range s1 {
 | ||
| 			r1, r2 := s1[i], s2[i]
 | ||
| 			if !(r1.Diff == r2.Diff && r1.Key == r2.Key && r1.Value.Equal(r2.Value) && r1.Comment == r2.Comment) {
 | ||
| 				return false
 | ||
| 			}
 | ||
| 		}
 | ||
| 		return true
 | ||
| 	}
 | ||
| 	return false
 | ||
| }
 | ||
| 
 | ||
| func (s textList) String() string {
 | ||
| 	return (&textWrap{Prefix: "{", Value: s, Suffix: "}"}).String()
 | ||
| }
 | ||
| 
 | ||
| func (s textList) formatCompactTo(b []byte, d diffMode) ([]byte, textNode) {
 | ||
| 	s = append(textList(nil), s...) // Avoid mutating original
 | ||
| 
 | ||
| 	// Determine whether we can collapse this list as a single line.
 | ||
| 	n0 := len(b) // Original buffer length
 | ||
| 	var multiLine bool
 | ||
| 	for i, r := range s {
 | ||
| 		if r.Diff == diffInserted || r.Diff == diffRemoved {
 | ||
| 			multiLine = true
 | ||
| 		}
 | ||
| 		b = append(b, r.Key...)
 | ||
| 		if r.Key != "" {
 | ||
| 			b = append(b, ": "...)
 | ||
| 		}
 | ||
| 		b, s[i].Value = r.Value.formatCompactTo(b, d|r.Diff)
 | ||
| 		if _, ok := s[i].Value.(textLine); !ok {
 | ||
| 			multiLine = true
 | ||
| 		}
 | ||
| 		if r.Comment != nil {
 | ||
| 			multiLine = true
 | ||
| 		}
 | ||
| 		if i < len(s)-1 {
 | ||
| 			b = append(b, ", "...)
 | ||
| 		}
 | ||
| 	}
 | ||
| 	// Force multi-lined output when printing a removed/inserted node that
 | ||
| 	// is sufficiently long.
 | ||
| 	if (d == diffInserted || d == diffRemoved) && len(b[n0:]) > maxColumnLength {
 | ||
| 		multiLine = true
 | ||
| 	}
 | ||
| 	if !multiLine {
 | ||
| 		return b, textLine(b[n0:])
 | ||
| 	}
 | ||
| 	return b, s
 | ||
| }
 | ||
| 
 | ||
| func (s textList) formatExpandedTo(b []byte, d diffMode, n indentMode) []byte {
 | ||
| 	alignKeyLens := s.alignLens(
 | ||
| 		func(r textRecord) bool {
 | ||
| 			_, isLine := r.Value.(textLine)
 | ||
| 			return r.Key == "" || !isLine
 | ||
| 		},
 | ||
| 		func(r textRecord) int { return utf8.RuneCountInString(r.Key) },
 | ||
| 	)
 | ||
| 	alignValueLens := s.alignLens(
 | ||
| 		func(r textRecord) bool {
 | ||
| 			_, isLine := r.Value.(textLine)
 | ||
| 			return !isLine || r.Value.Equal(textEllipsis) || r.Comment == nil
 | ||
| 		},
 | ||
| 		func(r textRecord) int { return utf8.RuneCount(r.Value.(textLine)) },
 | ||
| 	)
 | ||
| 
 | ||
| 	// Format lists of simple lists in a batched form.
 | ||
| 	// If the list is sequence of only textLine values,
 | ||
| 	// then batch multiple values on a single line.
 | ||
| 	var isSimple bool
 | ||
| 	for _, r := range s {
 | ||
| 		_, isLine := r.Value.(textLine)
 | ||
| 		isSimple = r.Diff == 0 && r.Key == "" && isLine && r.Comment == nil
 | ||
| 		if !isSimple {
 | ||
| 			break
 | ||
| 		}
 | ||
| 	}
 | ||
| 	if isSimple {
 | ||
| 		n++
 | ||
| 		var batch []byte
 | ||
| 		emitBatch := func() {
 | ||
| 			if len(batch) > 0 {
 | ||
| 				b = n.appendIndent(append(b, '\n'), d)
 | ||
| 				b = append(b, bytes.TrimRight(batch, " ")...)
 | ||
| 				batch = batch[:0]
 | ||
| 			}
 | ||
| 		}
 | ||
| 		for _, r := range s {
 | ||
| 			line := r.Value.(textLine)
 | ||
| 			if len(batch)+len(line)+len(", ") > maxColumnLength {
 | ||
| 				emitBatch()
 | ||
| 			}
 | ||
| 			batch = append(batch, line...)
 | ||
| 			batch = append(batch, ", "...)
 | ||
| 		}
 | ||
| 		emitBatch()
 | ||
| 		n--
 | ||
| 		return n.appendIndent(append(b, '\n'), d)
 | ||
| 	}
 | ||
| 
 | ||
| 	// Format the list as a multi-lined output.
 | ||
| 	n++
 | ||
| 	for i, r := range s {
 | ||
| 		b = n.appendIndent(append(b, '\n'), d|r.Diff)
 | ||
| 		if r.Key != "" {
 | ||
| 			b = append(b, r.Key+": "...)
 | ||
| 		}
 | ||
| 		b = alignKeyLens[i].appendChar(b, ' ')
 | ||
| 
 | ||
| 		b = r.Value.formatExpandedTo(b, d|r.Diff, n)
 | ||
| 		if !r.ElideComma {
 | ||
| 			b = append(b, ',')
 | ||
| 		}
 | ||
| 		b = alignValueLens[i].appendChar(b, ' ')
 | ||
| 
 | ||
| 		if r.Comment != nil {
 | ||
| 			b = append(b, " // "+r.Comment.String()...)
 | ||
| 		}
 | ||
| 	}
 | ||
| 	n--
 | ||
| 
 | ||
| 	return n.appendIndent(append(b, '\n'), d)
 | ||
| }
 | ||
| 
 | ||
| func (s textList) alignLens(
 | ||
| 	skipFunc func(textRecord) bool,
 | ||
| 	lenFunc func(textRecord) int,
 | ||
| ) []repeatCount {
 | ||
| 	var startIdx, endIdx, maxLen int
 | ||
| 	lens := make([]repeatCount, len(s))
 | ||
| 	for i, r := range s {
 | ||
| 		if skipFunc(r) {
 | ||
| 			for j := startIdx; j < endIdx && j < len(s); j++ {
 | ||
| 				lens[j] = repeatCount(maxLen - lenFunc(s[j]))
 | ||
| 			}
 | ||
| 			startIdx, endIdx, maxLen = i+1, i+1, 0
 | ||
| 		} else {
 | ||
| 			if maxLen < lenFunc(r) {
 | ||
| 				maxLen = lenFunc(r)
 | ||
| 			}
 | ||
| 			endIdx = i + 1
 | ||
| 		}
 | ||
| 	}
 | ||
| 	for j := startIdx; j < endIdx && j < len(s); j++ {
 | ||
| 		lens[j] = repeatCount(maxLen - lenFunc(s[j]))
 | ||
| 	}
 | ||
| 	return lens
 | ||
| }
 | ||
| 
 | ||
| // textLine is a single-line segment of text and is always a leaf node
 | ||
| // in the textNode tree.
 | ||
| type textLine []byte
 | ||
| 
 | ||
| var (
 | ||
| 	textNil      = textLine("nil")
 | ||
| 	textEllipsis = textLine("...")
 | ||
| )
 | ||
| 
 | ||
| func (s textLine) Len() int {
 | ||
| 	return len(s)
 | ||
| }
 | ||
| func (s1 textLine) Equal(s2 textNode) bool {
 | ||
| 	if s2, ok := s2.(textLine); ok {
 | ||
| 		return bytes.Equal([]byte(s1), []byte(s2))
 | ||
| 	}
 | ||
| 	return false
 | ||
| }
 | ||
| func (s textLine) String() string {
 | ||
| 	return string(s)
 | ||
| }
 | ||
| func (s textLine) formatCompactTo(b []byte, d diffMode) ([]byte, textNode) {
 | ||
| 	return append(b, s...), s
 | ||
| }
 | ||
| func (s textLine) formatExpandedTo(b []byte, _ diffMode, _ indentMode) []byte {
 | ||
| 	return append(b, s...)
 | ||
| }
 | ||
| 
 | ||
| type diffStats struct {
 | ||
| 	Name         string
 | ||
| 	NumIgnored   int
 | ||
| 	NumIdentical int
 | ||
| 	NumRemoved   int
 | ||
| 	NumInserted  int
 | ||
| 	NumModified  int
 | ||
| }
 | ||
| 
 | ||
| func (s diffStats) IsZero() bool {
 | ||
| 	s.Name = ""
 | ||
| 	return s == diffStats{}
 | ||
| }
 | ||
| 
 | ||
| func (s diffStats) NumDiff() int {
 | ||
| 	return s.NumRemoved + s.NumInserted + s.NumModified
 | ||
| }
 | ||
| 
 | ||
| func (s diffStats) Append(ds diffStats) diffStats {
 | ||
| 	assert(s.Name == ds.Name)
 | ||
| 	s.NumIgnored += ds.NumIgnored
 | ||
| 	s.NumIdentical += ds.NumIdentical
 | ||
| 	s.NumRemoved += ds.NumRemoved
 | ||
| 	s.NumInserted += ds.NumInserted
 | ||
| 	s.NumModified += ds.NumModified
 | ||
| 	return s
 | ||
| }
 | ||
| 
 | ||
| // String prints a humanly-readable summary of coalesced records.
 | ||
| //
 | ||
| // Example:
 | ||
| //
 | ||
| //	diffStats{Name: "Field", NumIgnored: 5}.String() => "5 ignored fields"
 | ||
| func (s diffStats) String() string {
 | ||
| 	var ss []string
 | ||
| 	var sum int
 | ||
| 	labels := [...]string{"ignored", "identical", "removed", "inserted", "modified"}
 | ||
| 	counts := [...]int{s.NumIgnored, s.NumIdentical, s.NumRemoved, s.NumInserted, s.NumModified}
 | ||
| 	for i, n := range counts {
 | ||
| 		if n > 0 {
 | ||
| 			ss = append(ss, fmt.Sprintf("%d %v", n, labels[i]))
 | ||
| 		}
 | ||
| 		sum += n
 | ||
| 	}
 | ||
| 
 | ||
| 	// Pluralize the name (adjusting for some obscure English grammar rules).
 | ||
| 	name := s.Name
 | ||
| 	if sum > 1 {
 | ||
| 		name += "s"
 | ||
| 		if strings.HasSuffix(name, "ys") {
 | ||
| 			name = name[:len(name)-2] + "ies" // e.g., "entrys" => "entries"
 | ||
| 		}
 | ||
| 	}
 | ||
| 
 | ||
| 	// Format the list according to English grammar (with Oxford comma).
 | ||
| 	switch n := len(ss); n {
 | ||
| 	case 0:
 | ||
| 		return ""
 | ||
| 	case 1, 2:
 | ||
| 		return strings.Join(ss, " and ") + " " + name
 | ||
| 	default:
 | ||
| 		return strings.Join(ss[:n-1], ", ") + ", and " + ss[n-1] + " " + name
 | ||
| 	}
 | ||
| }
 | ||
| 
 | ||
| type commentString string
 | ||
| 
 | ||
| func (s commentString) String() string { return string(s) }
 |