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 }