github.com/nozzle/golangci-lint@v1.49.0-nz3/pkg/golinters/gofmt_common.go (about) 1 package golinters 2 3 import ( 4 "bytes" 5 "fmt" 6 "go/token" 7 "strings" 8 9 "github.com/pkg/errors" 10 diffpkg "github.com/sourcegraph/go-diff/diff" 11 12 "github.com/golangci/golangci-lint/pkg/config" 13 "github.com/golangci/golangci-lint/pkg/lint/linter" 14 "github.com/golangci/golangci-lint/pkg/logutils" 15 "github.com/golangci/golangci-lint/pkg/result" 16 ) 17 18 type Change struct { 19 LineRange result.Range 20 Replacement result.Replacement 21 } 22 23 type diffLineType string 24 25 const ( 26 diffLineAdded diffLineType = "added" 27 diffLineOriginal diffLineType = "original" 28 diffLineDeleted diffLineType = "deleted" 29 ) 30 31 type diffLine struct { 32 originalNumber int // 1-based original line number 33 typ diffLineType 34 data string // "+" or "-" stripped line 35 } 36 37 type hunkChangesParser struct { 38 // needed because we merge currently added lines with the last original line 39 lastOriginalLine *diffLine 40 41 // if the first line of diff is an adding we save all additions to replacementLinesToPrepend 42 replacementLinesToPrepend []string 43 44 log logutils.Log 45 46 lines []diffLine 47 48 ret []Change 49 } 50 51 func (p *hunkChangesParser) parseDiffLines(h *diffpkg.Hunk) { 52 lines := bytes.Split(h.Body, []byte{'\n'}) 53 currentOriginalLineNumber := int(h.OrigStartLine) 54 var ret []diffLine 55 56 for i, line := range lines { 57 dl := diffLine{ 58 originalNumber: currentOriginalLineNumber, 59 } 60 61 lineStr := string(line) 62 63 if strings.HasPrefix(lineStr, "-") { 64 dl.typ = diffLineDeleted 65 dl.data = strings.TrimPrefix(lineStr, "-") 66 currentOriginalLineNumber++ 67 } else if strings.HasPrefix(lineStr, "+") { 68 dl.typ = diffLineAdded 69 dl.data = strings.TrimPrefix(lineStr, "+") 70 } else { 71 if i == len(lines)-1 && lineStr == "" { 72 // handle last \n: don't add an empty original line 73 break 74 } 75 76 dl.typ = diffLineOriginal 77 dl.data = strings.TrimPrefix(lineStr, " ") 78 currentOriginalLineNumber++ 79 } 80 81 ret = append(ret, dl) 82 } 83 84 p.lines = ret 85 } 86 87 func (p *hunkChangesParser) handleOriginalLine(line diffLine, i *int) { 88 if len(p.replacementLinesToPrepend) == 0 { 89 p.lastOriginalLine = &line 90 *i++ 91 return 92 } 93 94 // check following added lines for the case: 95 // + added line 1 96 // original line 97 // + added line 2 98 99 *i++ 100 var followingAddedLines []string 101 for ; *i < len(p.lines) && p.lines[*i].typ == diffLineAdded; *i++ { 102 followingAddedLines = append(followingAddedLines, p.lines[*i].data) 103 } 104 105 p.ret = append(p.ret, Change{ 106 LineRange: result.Range{ 107 From: line.originalNumber, 108 To: line.originalNumber, 109 }, 110 Replacement: result.Replacement{ 111 NewLines: append(p.replacementLinesToPrepend, append([]string{line.data}, followingAddedLines...)...), 112 }, 113 }) 114 p.replacementLinesToPrepend = nil 115 p.lastOriginalLine = &line 116 } 117 118 func (p *hunkChangesParser) handleDeletedLines(deletedLines []diffLine, addedLines []string) { 119 change := Change{ 120 LineRange: result.Range{ 121 From: deletedLines[0].originalNumber, 122 To: deletedLines[len(deletedLines)-1].originalNumber, 123 }, 124 } 125 126 if len(addedLines) != 0 { 127 //nolint:gocritic 128 change.Replacement.NewLines = append(p.replacementLinesToPrepend, addedLines...) 129 if len(p.replacementLinesToPrepend) != 0 { 130 p.replacementLinesToPrepend = nil 131 } 132 133 p.ret = append(p.ret, change) 134 return 135 } 136 137 // delete-only change with possible prepending 138 if len(p.replacementLinesToPrepend) != 0 { 139 change.Replacement.NewLines = p.replacementLinesToPrepend 140 p.replacementLinesToPrepend = nil 141 } else { 142 change.Replacement.NeedOnlyDelete = true 143 } 144 145 p.ret = append(p.ret, change) 146 } 147 148 func (p *hunkChangesParser) handleAddedOnlyLines(addedLines []string) { 149 if p.lastOriginalLine == nil { 150 // the first line is added; the diff looks like: 151 // 1. + ... 152 // 2. - ... 153 // or 154 // 1. + ... 155 // 2. ... 156 157 p.replacementLinesToPrepend = addedLines 158 return 159 } 160 161 // add-only change merged into the last original line with possible prepending 162 p.ret = append(p.ret, Change{ 163 LineRange: result.Range{ 164 From: p.lastOriginalLine.originalNumber, 165 To: p.lastOriginalLine.originalNumber, 166 }, 167 Replacement: result.Replacement{ 168 NewLines: append(p.replacementLinesToPrepend, append([]string{p.lastOriginalLine.data}, addedLines...)...), 169 }, 170 }) 171 p.replacementLinesToPrepend = nil 172 } 173 174 func (p *hunkChangesParser) parse(h *diffpkg.Hunk) []Change { 175 p.parseDiffLines(h) 176 177 for i := 0; i < len(p.lines); { 178 line := p.lines[i] 179 if line.typ == diffLineOriginal { 180 p.handleOriginalLine(line, &i) 181 continue 182 } 183 184 var deletedLines []diffLine 185 for ; i < len(p.lines) && p.lines[i].typ == diffLineDeleted; i++ { 186 deletedLines = append(deletedLines, p.lines[i]) 187 } 188 189 var addedLines []string 190 for ; i < len(p.lines) && p.lines[i].typ == diffLineAdded; i++ { 191 addedLines = append(addedLines, p.lines[i].data) 192 } 193 194 if len(deletedLines) != 0 { 195 p.handleDeletedLines(deletedLines, addedLines) 196 continue 197 } 198 199 // no deletions, only additions 200 p.handleAddedOnlyLines(addedLines) 201 } 202 203 if len(p.replacementLinesToPrepend) != 0 { 204 p.log.Infof("The diff contains only additions: no original or deleted lines: %#v", p.lines) 205 return nil 206 } 207 208 return p.ret 209 } 210 211 func getErrorTextForLinter(settings *config.LintersSettings, linterName string) string { 212 text := "File is not formatted" 213 switch linterName { 214 case gciName: 215 text = getErrorTextForGci(settings.Gci) 216 case gofumptName: 217 text = "File is not `gofumpt`-ed" 218 if settings.Gofumpt.ExtraRules { 219 text += " with `-extra`" 220 } 221 case gofmtName: 222 text = "File is not `gofmt`-ed" 223 if settings.Gofmt.Simplify { 224 text += " with `-s`" 225 } 226 for _, rule := range settings.Gofmt.RewriteRules { 227 text += fmt.Sprintf(" `-r '%s -> %s'`", rule.Pattern, rule.Replacement) 228 } 229 case goimportsName: 230 text = "File is not `goimports`-ed" 231 if settings.Goimports.LocalPrefixes != "" { 232 text += " with -local " + settings.Goimports.LocalPrefixes 233 } 234 } 235 return text 236 } 237 238 func extractIssuesFromPatch(patch string, lintCtx *linter.Context, linterName string) ([]result.Issue, error) { 239 diffs, err := diffpkg.ParseMultiFileDiff([]byte(patch)) 240 if err != nil { 241 return nil, errors.Wrap(err, "can't parse patch") 242 } 243 244 if len(diffs) == 0 { 245 return nil, fmt.Errorf("got no diffs from patch parser: %v", diffs) 246 } 247 248 var issues []result.Issue 249 for _, d := range diffs { 250 if len(d.Hunks) == 0 { 251 lintCtx.Log.Warnf("Got no hunks in diff %+v", d) 252 continue 253 } 254 255 for _, hunk := range d.Hunks { 256 p := hunkChangesParser{log: lintCtx.Log} 257 258 changes := p.parse(hunk) 259 260 for _, change := range changes { 261 change := change // fix scope 262 i := result.Issue{ 263 FromLinter: linterName, 264 Pos: token.Position{ 265 Filename: d.NewName, 266 Line: change.LineRange.From, 267 }, 268 Text: getErrorTextForLinter(lintCtx.Settings(), linterName), 269 Replacement: &change.Replacement, 270 } 271 if change.LineRange.From != change.LineRange.To { 272 i.LineRange = &change.LineRange 273 } 274 275 issues = append(issues, i) 276 } 277 } 278 } 279 280 return issues, nil 281 }