golang.org/x/tools@v0.21.1-0.20240520172518-788d39e776b1/internal/diff/unified.go (about)

     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  
     5  package diff
     6  
     7  import (
     8  	"fmt"
     9  	"log"
    10  	"strings"
    11  )
    12  
    13  // DefaultContextLines is the number of unchanged lines of surrounding
    14  // context displayed by Unified. Use ToUnified to specify a different value.
    15  const DefaultContextLines = 3
    16  
    17  // Unified returns a unified diff of the old and new strings.
    18  // The old and new labels are the names of the old and new files.
    19  // If the strings are equal, it returns the empty string.
    20  func Unified(oldLabel, newLabel, old, new string) string {
    21  	edits := Strings(old, new)
    22  	unified, err := ToUnified(oldLabel, newLabel, old, edits, DefaultContextLines)
    23  	if err != nil {
    24  		// Can't happen: edits are consistent.
    25  		log.Fatalf("internal error in diff.Unified: %v", err)
    26  	}
    27  	return unified
    28  }
    29  
    30  // ToUnified applies the edits to content and returns a unified diff,
    31  // with contextLines lines of (unchanged) context around each diff hunk.
    32  // The old and new labels are the names of the content and result files.
    33  // It returns an error if the edits are inconsistent; see ApplyEdits.
    34  func ToUnified(oldLabel, newLabel, content string, edits []Edit, contextLines int) (string, error) {
    35  	u, err := toUnified(oldLabel, newLabel, content, edits, contextLines)
    36  	if err != nil {
    37  		return "", err
    38  	}
    39  	return u.String(), nil
    40  }
    41  
    42  // unified represents a set of edits as a unified diff.
    43  type unified struct {
    44  	// from is the name of the original file.
    45  	from string
    46  	// to is the name of the modified file.
    47  	to string
    48  	// hunks is the set of edit hunks needed to transform the file content.
    49  	hunks []*hunk
    50  }
    51  
    52  // Hunk represents a contiguous set of line edits to apply.
    53  type hunk struct {
    54  	// The line in the original source where the hunk starts.
    55  	fromLine int
    56  	// The line in the original source where the hunk finishes.
    57  	toLine int
    58  	// The set of line based edits to apply.
    59  	lines []line
    60  }
    61  
    62  // Line represents a single line operation to apply as part of a Hunk.
    63  type line struct {
    64  	// kind is the type of line this represents, deletion, insertion or copy.
    65  	kind opKind
    66  	// content is the content of this line.
    67  	// For deletion it is the line being removed, for all others it is the line
    68  	// to put in the output.
    69  	content string
    70  }
    71  
    72  // opKind is used to denote the type of operation a line represents.
    73  type opKind int
    74  
    75  const (
    76  	// opDelete is the operation kind for a line that is present in the input
    77  	// but not in the output.
    78  	opDelete opKind = iota
    79  	// opInsert is the operation kind for a line that is new in the output.
    80  	opInsert
    81  	// opEqual is the operation kind for a line that is the same in the input and
    82  	// output, often used to provide context around edited lines.
    83  	opEqual
    84  )
    85  
    86  // String returns a human readable representation of an OpKind. It is not
    87  // intended for machine processing.
    88  func (k opKind) String() string {
    89  	switch k {
    90  	case opDelete:
    91  		return "delete"
    92  	case opInsert:
    93  		return "insert"
    94  	case opEqual:
    95  		return "equal"
    96  	default:
    97  		panic("unknown operation kind")
    98  	}
    99  }
   100  
   101  // toUnified takes a file contents and a sequence of edits, and calculates
   102  // a unified diff that represents those edits.
   103  func toUnified(fromName, toName string, content string, edits []Edit, contextLines int) (unified, error) {
   104  	gap := contextLines * 2
   105  	u := unified{
   106  		from: fromName,
   107  		to:   toName,
   108  	}
   109  	if len(edits) == 0 {
   110  		return u, nil
   111  	}
   112  	var err error
   113  	edits, err = lineEdits(content, edits) // expand to whole lines
   114  	if err != nil {
   115  		return u, err
   116  	}
   117  	lines := splitLines(content)
   118  	var h *hunk
   119  	last := 0
   120  	toLine := 0
   121  	for _, edit := range edits {
   122  		// Compute the zero-based line numbers of the edit start and end.
   123  		// TODO(adonovan): opt: compute incrementally, avoid O(n^2).
   124  		start := strings.Count(content[:edit.Start], "\n")
   125  		end := strings.Count(content[:edit.End], "\n")
   126  		if edit.End == len(content) && len(content) > 0 && content[len(content)-1] != '\n' {
   127  			end++ // EOF counts as an implicit newline
   128  		}
   129  
   130  		switch {
   131  		case h != nil && start == last:
   132  			//direct extension
   133  		case h != nil && start <= last+gap:
   134  			//within range of previous lines, add the joiners
   135  			addEqualLines(h, lines, last, start)
   136  		default:
   137  			//need to start a new hunk
   138  			if h != nil {
   139  				// add the edge to the previous hunk
   140  				addEqualLines(h, lines, last, last+contextLines)
   141  				u.hunks = append(u.hunks, h)
   142  			}
   143  			toLine += start - last
   144  			h = &hunk{
   145  				fromLine: start + 1,
   146  				toLine:   toLine + 1,
   147  			}
   148  			// add the edge to the new hunk
   149  			delta := addEqualLines(h, lines, start-contextLines, start)
   150  			h.fromLine -= delta
   151  			h.toLine -= delta
   152  		}
   153  		last = start
   154  		for i := start; i < end; i++ {
   155  			h.lines = append(h.lines, line{kind: opDelete, content: lines[i]})
   156  			last++
   157  		}
   158  		if edit.New != "" {
   159  			for _, content := range splitLines(edit.New) {
   160  				h.lines = append(h.lines, line{kind: opInsert, content: content})
   161  				toLine++
   162  			}
   163  		}
   164  	}
   165  	if h != nil {
   166  		// add the edge to the final hunk
   167  		addEqualLines(h, lines, last, last+contextLines)
   168  		u.hunks = append(u.hunks, h)
   169  	}
   170  	return u, nil
   171  }
   172  
   173  func splitLines(text string) []string {
   174  	lines := strings.SplitAfter(text, "\n")
   175  	if lines[len(lines)-1] == "" {
   176  		lines = lines[:len(lines)-1]
   177  	}
   178  	return lines
   179  }
   180  
   181  func addEqualLines(h *hunk, lines []string, start, end int) int {
   182  	delta := 0
   183  	for i := start; i < end; i++ {
   184  		if i < 0 {
   185  			continue
   186  		}
   187  		if i >= len(lines) {
   188  			return delta
   189  		}
   190  		h.lines = append(h.lines, line{kind: opEqual, content: lines[i]})
   191  		delta++
   192  	}
   193  	return delta
   194  }
   195  
   196  // String converts a unified diff to the standard textual form for that diff.
   197  // The output of this function can be passed to tools like patch.
   198  func (u unified) String() string {
   199  	if len(u.hunks) == 0 {
   200  		return ""
   201  	}
   202  	b := new(strings.Builder)
   203  	fmt.Fprintf(b, "--- %s\n", u.from)
   204  	fmt.Fprintf(b, "+++ %s\n", u.to)
   205  	for _, hunk := range u.hunks {
   206  		fromCount, toCount := 0, 0
   207  		for _, l := range hunk.lines {
   208  			switch l.kind {
   209  			case opDelete:
   210  				fromCount++
   211  			case opInsert:
   212  				toCount++
   213  			default:
   214  				fromCount++
   215  				toCount++
   216  			}
   217  		}
   218  		fmt.Fprint(b, "@@")
   219  		if fromCount > 1 {
   220  			fmt.Fprintf(b, " -%d,%d", hunk.fromLine, fromCount)
   221  		} else if hunk.fromLine == 1 && fromCount == 0 {
   222  			// Match odd GNU diff -u behavior adding to empty file.
   223  			fmt.Fprintf(b, " -0,0")
   224  		} else {
   225  			fmt.Fprintf(b, " -%d", hunk.fromLine)
   226  		}
   227  		if toCount > 1 {
   228  			fmt.Fprintf(b, " +%d,%d", hunk.toLine, toCount)
   229  		} else if hunk.toLine == 1 && toCount == 0 {
   230  			// Match odd GNU diff -u behavior adding to empty file.
   231  			fmt.Fprintf(b, " +0,0")
   232  		} else {
   233  			fmt.Fprintf(b, " +%d", hunk.toLine)
   234  		}
   235  		fmt.Fprint(b, " @@\n")
   236  		for _, l := range hunk.lines {
   237  			switch l.kind {
   238  			case opDelete:
   239  				fmt.Fprintf(b, "-%s", l.content)
   240  			case opInsert:
   241  				fmt.Fprintf(b, "+%s", l.content)
   242  			default:
   243  				fmt.Fprintf(b, " %s", l.content)
   244  			}
   245  			if !strings.HasSuffix(l.content, "\n") {
   246  				fmt.Fprintf(b, "\n\\ No newline at end of file\n")
   247  			}
   248  		}
   249  	}
   250  	return b.String()
   251  }