github.com/vanstinator/golangci-lint@v0.0.0-20240223191551-cc572f00d9d1/pkg/golinters/gofmt_common.go (about) 1 package golinters 2 3 import ( 4 "bytes" 5 "fmt" 6 "go/token" 7 "strings" 8 9 diffpkg "github.com/sourcegraph/go-diff/diff" 10 11 "github.com/vanstinator/golangci-lint/pkg/config" 12 "github.com/vanstinator/golangci-lint/pkg/lint/linter" 13 "github.com/vanstinator/golangci-lint/pkg/logutils" 14 "github.com/vanstinator/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 currentOriginalLineNumber := int(h.OrigStartLine) 53 var ret []diffLine 54 55 for i, line := range lines { 56 dl := diffLine{ 57 originalNumber: currentOriginalLineNumber, 58 } 59 60 lineStr := string(line) 61 62 if strings.HasPrefix(lineStr, "-") { 63 dl.typ = diffLineDeleted 64 dl.data = strings.TrimPrefix(lineStr, "-") 65 currentOriginalLineNumber++ 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 currentOriginalLineNumber++ 78 } 79 80 ret = append(ret, dl) 81 } 82 83 // if > 0, then the original file had a 'No newline at end of file' mark 84 if h.OrigNoNewlineAt > 0 { 85 dl := diffLine{ 86 originalNumber: currentOriginalLineNumber + 1, 87 typ: diffLineAdded, 88 data: "", 89 } 90 ret = append(ret, dl) 91 } 92 93 p.lines = ret 94 } 95 96 func (p *hunkChangesParser) handleOriginalLine(line diffLine, i *int) { 97 if len(p.replacementLinesToPrepend) == 0 { 98 p.lastOriginalLine = &line 99 *i++ 100 return 101 } 102 103 // check following added lines for the case: 104 // + added line 1 105 // original line 106 // + added line 2 107 108 *i++ 109 var followingAddedLines []string 110 for ; *i < len(p.lines) && p.lines[*i].typ == diffLineAdded; *i++ { 111 followingAddedLines = append(followingAddedLines, p.lines[*i].data) 112 } 113 114 p.ret = append(p.ret, Change{ 115 LineRange: result.Range{ 116 From: line.originalNumber, 117 To: line.originalNumber, 118 }, 119 Replacement: result.Replacement{ 120 NewLines: append(p.replacementLinesToPrepend, append([]string{line.data}, followingAddedLines...)...), 121 }, 122 }) 123 p.replacementLinesToPrepend = nil 124 p.lastOriginalLine = &line 125 } 126 127 func (p *hunkChangesParser) handleDeletedLines(deletedLines []diffLine, addedLines []string) { 128 change := Change{ 129 LineRange: result.Range{ 130 From: deletedLines[0].originalNumber, 131 To: deletedLines[len(deletedLines)-1].originalNumber, 132 }, 133 } 134 135 if len(addedLines) != 0 { 136 //nolint:gocritic 137 change.Replacement.NewLines = append(p.replacementLinesToPrepend, addedLines...) 138 if len(p.replacementLinesToPrepend) != 0 { 139 p.replacementLinesToPrepend = nil 140 } 141 142 p.ret = append(p.ret, change) 143 return 144 } 145 146 // delete-only change with possible prepending 147 if len(p.replacementLinesToPrepend) != 0 { 148 change.Replacement.NewLines = p.replacementLinesToPrepend 149 p.replacementLinesToPrepend = nil 150 } else { 151 change.Replacement.NeedOnlyDelete = true 152 } 153 154 p.ret = append(p.ret, change) 155 } 156 157 func (p *hunkChangesParser) handleAddedOnlyLines(addedLines []string) { 158 if p.lastOriginalLine == nil { 159 // the first line is added; the diff looks like: 160 // 1. + ... 161 // 2. - ... 162 // or 163 // 1. + ... 164 // 2. ... 165 166 p.replacementLinesToPrepend = addedLines 167 return 168 } 169 170 // add-only change merged into the last original line with possible prepending 171 p.ret = append(p.ret, Change{ 172 LineRange: result.Range{ 173 From: p.lastOriginalLine.originalNumber, 174 To: p.lastOriginalLine.originalNumber, 175 }, 176 Replacement: result.Replacement{ 177 NewLines: append(p.replacementLinesToPrepend, append([]string{p.lastOriginalLine.data}, addedLines...)...), 178 }, 179 }) 180 p.replacementLinesToPrepend = nil 181 } 182 183 func (p *hunkChangesParser) parse(h *diffpkg.Hunk) []Change { 184 p.parseDiffLines(h) 185 186 for i := 0; i < len(p.lines); { 187 line := p.lines[i] 188 if line.typ == diffLineOriginal { 189 p.handleOriginalLine(line, &i) 190 continue 191 } 192 193 var deletedLines []diffLine 194 for ; i < len(p.lines) && p.lines[i].typ == diffLineDeleted; i++ { 195 deletedLines = append(deletedLines, p.lines[i]) 196 } 197 198 var addedLines []string 199 for ; i < len(p.lines) && p.lines[i].typ == diffLineAdded; i++ { 200 addedLines = append(addedLines, p.lines[i].data) 201 } 202 203 if len(deletedLines) != 0 { 204 p.handleDeletedLines(deletedLines, addedLines) 205 continue 206 } 207 208 // no deletions, only additions 209 p.handleAddedOnlyLines(addedLines) 210 } 211 212 if len(p.replacementLinesToPrepend) != 0 { 213 p.log.Infof("The diff contains only additions: no original or deleted lines: %#v", p.lines) 214 return nil 215 } 216 217 return p.ret 218 } 219 220 func getErrorTextForLinter(settings *config.LintersSettings, linterName string) string { 221 text := "File is not formatted" 222 switch linterName { 223 case gciName: 224 text = getErrorTextForGci(settings.Gci) 225 case gofumptName: 226 text = "File is not `gofumpt`-ed" 227 if settings.Gofumpt.ExtraRules { 228 text += " with `-extra`" 229 } 230 case gofmtName: 231 text = "File is not `gofmt`-ed" 232 if settings.Gofmt.Simplify { 233 text += " with `-s`" 234 } 235 for _, rule := range settings.Gofmt.RewriteRules { 236 text += fmt.Sprintf(" `-r '%s -> %s'`", rule.Pattern, rule.Replacement) 237 } 238 case goimportsName: 239 text = "File is not `goimports`-ed" 240 if settings.Goimports.LocalPrefixes != "" { 241 text += " with -local " + settings.Goimports.LocalPrefixes 242 } 243 } 244 return text 245 } 246 247 func extractIssuesFromPatch(patch string, lintCtx *linter.Context, linterName string) ([]result.Issue, error) { 248 diffs, err := diffpkg.ParseMultiFileDiff([]byte(patch)) 249 if err != nil { 250 return nil, fmt.Errorf("can't parse patch: %w", err) 251 } 252 253 if len(diffs) == 0 { 254 return nil, fmt.Errorf("got no diffs from patch parser: %v", patch) 255 } 256 257 var issues []result.Issue 258 for _, d := range diffs { 259 if len(d.Hunks) == 0 { 260 lintCtx.Log.Warnf("Got no hunks in diff %+v", d) 261 continue 262 } 263 264 for _, hunk := range d.Hunks { 265 p := hunkChangesParser{log: lintCtx.Log} 266 267 changes := p.parse(hunk) 268 269 for _, change := range changes { 270 change := change // fix scope 271 i := result.Issue{ 272 FromLinter: linterName, 273 Pos: token.Position{ 274 Filename: d.NewName, 275 Line: change.LineRange.From, 276 }, 277 Text: getErrorTextForLinter(lintCtx.Settings(), linterName), 278 Replacement: &change.Replacement, 279 } 280 if change.LineRange.From != change.LineRange.To { 281 i.LineRange = &change.LineRange 282 } 283 284 issues = append(issues, i) 285 } 286 } 287 } 288 289 return issues, nil 290 }