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