github.com/mistwind/reviewdog@v0.0.0-20230322024206-9cfa11856d58/parser/diff.go (about)

     1  package parser
     2  
     3  import (
     4  	"fmt"
     5  	"io"
     6  	"strings"
     7  
     8  	"github.com/mistwind/reviewdog/diff"
     9  	"github.com/mistwind/reviewdog/filter"
    10  	"github.com/mistwind/reviewdog/proto/rdf"
    11  )
    12  
    13  var _ Parser = &DiffParser{}
    14  
    15  // DiffParser is a unified diff parser.
    16  type DiffParser struct {
    17  	strip int
    18  }
    19  
    20  // NewDiffParser creates a new DiffParser.
    21  func NewDiffParser(strip int) *DiffParser {
    22  	return &DiffParser{strip: strip}
    23  }
    24  
    25  // state data for a diagnostic.
    26  type dstate struct {
    27  	startLine     int
    28  	isInsert      bool
    29  	newLines      []string
    30  	originalLines []string // For Diagnostic.original_output
    31  }
    32  
    33  func (d dstate) build(path string, currentLine int) *rdf.Diagnostic {
    34  	drange := &rdf.Range{ // Diagnostic Range
    35  		Start: &rdf.Position{Line: int32(d.startLine)},
    36  		End:   &rdf.Position{Line: int32(currentLine)},
    37  	}
    38  	text := strings.Join(d.newLines, "\n")
    39  	if d.isInsert {
    40  		text += "\n" // Need line-break at the end if it's insertion,
    41  		drange.GetEnd().Line = int32(d.startLine)
    42  		drange.GetEnd().Column = 1
    43  		drange.GetStart().Column = 1
    44  	}
    45  	return &rdf.Diagnostic{
    46  		Location:       &rdf.Location{Path: path, Range: drange},
    47  		Suggestions:    []*rdf.Suggestion{{Range: drange, Text: text}},
    48  		OriginalOutput: strings.Join(d.originalLines, "\n"),
    49  	}
    50  }
    51  
    52  // Parse parses input as unified diff format and return it as diagnostics.
    53  func (p *DiffParser) Parse(r io.Reader) ([]*rdf.Diagnostic, error) {
    54  	filediffs, err := diff.ParseMultiFile(r)
    55  	if err != nil {
    56  		return nil, fmt.Errorf("fail to parse diff: %w", err)
    57  	}
    58  	var diagnostics []*rdf.Diagnostic
    59  	for _, fdiff := range filediffs {
    60  		path := filter.NormalizeDiffPath(fdiff.PathNew, p.strip)
    61  		for _, hunk := range fdiff.Hunks {
    62  			lnum := hunk.StartLineOld - 1
    63  			prevState := diff.LineUnchanged
    64  			state := dstate{}
    65  			emit := func() {
    66  				diagnostics = append(diagnostics, state.build(path, lnum))
    67  				state = dstate{}
    68  			}
    69  			for i, diffLine := range hunk.Lines {
    70  				switch diffLine.Type {
    71  				case diff.LineAdded:
    72  					if i == 0 {
    73  						lnum++ // Increment line number only when it's at head.
    74  					}
    75  					state.newLines = append(state.newLines, diffLine.Content)
    76  					state.originalLines = append(state.originalLines, buildOriginalLine(path, diffLine))
    77  					switch prevState {
    78  					case diff.LineUnchanged:
    79  						// Insert.
    80  						state.startLine = lnum + 1
    81  						state.isInsert = true
    82  					case diff.LineDeleted, diff.LineAdded:
    83  						// Do nothing in particular.
    84  					}
    85  				case diff.LineDeleted:
    86  					lnum++
    87  					state.originalLines = append(state.originalLines, buildOriginalLine(path, diffLine))
    88  					switch prevState {
    89  					case diff.LineUnchanged:
    90  						state.startLine = lnum
    91  					case diff.LineAdded:
    92  						state.isInsert = false
    93  					case diff.LineDeleted:
    94  						// Do nothing in particular.
    95  					}
    96  				case diff.LineUnchanged:
    97  					switch prevState {
    98  					case diff.LineUnchanged:
    99  						// Do nothing in particular.
   100  					case diff.LineAdded, diff.LineDeleted:
   101  						emit() // Output a diagnostic.
   102  					}
   103  					lnum++
   104  				}
   105  				prevState = diffLine.Type
   106  			}
   107  			if state.startLine > 0 {
   108  				emit() // Output a diagnostic at the end of hunk.
   109  			}
   110  		}
   111  	}
   112  	return diagnostics, nil
   113  }
   114  
   115  func buildOriginalLine(path string, line *diff.Line) string {
   116  	var (
   117  		lnum int
   118  		mark rune
   119  	)
   120  	switch line.Type {
   121  	case diff.LineAdded:
   122  		mark = '+'
   123  		lnum = line.LnumNew
   124  	case diff.LineDeleted:
   125  		mark = '-'
   126  		lnum = line.LnumOld
   127  	case diff.LineUnchanged:
   128  		mark = ' '
   129  		lnum = line.LnumOld
   130  	}
   131  	return fmt.Sprintf("%s:%d:%s%s", path, lnum, string(mark), line.Content)
   132  }