github.com/elek/golangci-lint@v1.42.2-0.20211208090441-c05b7fcb3a9a/pkg/result/processors/fixer.go (about) 1 package processors 2 3 import ( 4 "bytes" 5 "fmt" 6 "os" 7 "path/filepath" 8 "sort" 9 "strings" 10 11 "github.com/pkg/errors" 12 13 "github.com/elek/golangci-lint/pkg/config" 14 "github.com/elek/golangci-lint/pkg/fsutils" 15 "github.com/elek/golangci-lint/pkg/logutils" 16 "github.com/elek/golangci-lint/pkg/result" 17 "github.com/elek/golangci-lint/pkg/timeutils" 18 ) 19 20 type Fixer struct { 21 cfg *config.Config 22 log logutils.Log 23 fileCache *fsutils.FileCache 24 sw *timeutils.Stopwatch 25 } 26 27 func NewFixer(cfg *config.Config, log logutils.Log, fileCache *fsutils.FileCache) *Fixer { 28 return &Fixer{ 29 cfg: cfg, 30 log: log, 31 fileCache: fileCache, 32 sw: timeutils.NewStopwatch("fixer", log), 33 } 34 } 35 36 func (f Fixer) printStat() { 37 f.sw.PrintStages() 38 } 39 40 func (f Fixer) Process(issues []result.Issue) []result.Issue { 41 if !f.cfg.Issues.NeedFix { 42 return issues 43 } 44 45 outIssues := make([]result.Issue, 0, len(issues)) 46 issuesToFixPerFile := map[string][]result.Issue{} 47 for i := range issues { 48 issue := &issues[i] 49 if issue.Replacement == nil { 50 outIssues = append(outIssues, *issue) 51 continue 52 } 53 54 issuesToFixPerFile[issue.FilePath()] = append(issuesToFixPerFile[issue.FilePath()], *issue) 55 } 56 57 for file, issuesToFix := range issuesToFixPerFile { 58 var err error 59 f.sw.TrackStage("all", func() { 60 err = f.fixIssuesInFile(file, issuesToFix) 61 }) 62 if err != nil { 63 f.log.Errorf("Failed to fix issues in file %s: %s", file, err) 64 65 // show issues only if can't fix them 66 outIssues = append(outIssues, issuesToFix...) 67 } 68 } 69 70 f.printStat() 71 return outIssues 72 } 73 74 func (f Fixer) fixIssuesInFile(filePath string, issues []result.Issue) error { 75 // TODO: don't read the whole file into memory: read line by line; 76 // can't just use bufio.scanner: it has a line length limit 77 origFileData, err := f.fileCache.GetFileBytes(filePath) 78 if err != nil { 79 return errors.Wrapf(err, "failed to get file bytes for %s", filePath) 80 } 81 origFileLines := bytes.Split(origFileData, []byte("\n")) 82 83 tmpFileName := filepath.Join(filepath.Dir(filePath), fmt.Sprintf(".%s.golangci_fix", filepath.Base(filePath))) 84 tmpOutFile, err := os.Create(tmpFileName) 85 if err != nil { 86 return errors.Wrapf(err, "failed to make file %s", tmpFileName) 87 } 88 89 // merge multiple issues per line into one issue 90 issuesPerLine := map[int][]result.Issue{} 91 for i := range issues { 92 issue := &issues[i] 93 issuesPerLine[issue.Line()] = append(issuesPerLine[issue.Line()], *issue) 94 } 95 96 issues = issues[:0] // reuse the same memory 97 for line, lineIssues := range issuesPerLine { 98 if mergedIssue := f.mergeLineIssues(line, lineIssues, origFileLines); mergedIssue != nil { 99 issues = append(issues, *mergedIssue) 100 } 101 } 102 103 issues = f.findNotIntersectingIssues(issues) 104 105 if err = f.writeFixedFile(origFileLines, issues, tmpOutFile); err != nil { 106 tmpOutFile.Close() 107 os.Remove(tmpOutFile.Name()) 108 return err 109 } 110 111 tmpOutFile.Close() 112 if err = os.Rename(tmpOutFile.Name(), filePath); err != nil { 113 os.Remove(tmpOutFile.Name()) 114 return errors.Wrapf(err, "failed to rename %s -> %s", tmpOutFile.Name(), filePath) 115 } 116 117 return nil 118 } 119 120 func (f Fixer) mergeLineIssues(lineNum int, lineIssues []result.Issue, origFileLines [][]byte) *result.Issue { 121 origLine := origFileLines[lineNum-1] // lineNum is 1-based 122 123 if len(lineIssues) == 1 && lineIssues[0].Replacement.Inline == nil { 124 return &lineIssues[0] 125 } 126 127 // check issues first 128 for ind := range lineIssues { 129 i := &lineIssues[ind] 130 if i.LineRange != nil { 131 f.log.Infof("Line %d has multiple issues but at least one of them is ranged: %#v", lineNum, lineIssues) 132 return &lineIssues[0] 133 } 134 135 r := i.Replacement 136 if r.Inline == nil || len(r.NewLines) != 0 || r.NeedOnlyDelete { 137 f.log.Infof("Line %d has multiple issues but at least one of them isn't inline: %#v", lineNum, lineIssues) 138 return &lineIssues[0] 139 } 140 141 if r.Inline.StartCol < 0 || r.Inline.Length <= 0 || r.Inline.StartCol+r.Inline.Length > len(origLine) { 142 f.log.Warnf("Line %d (%q) has invalid inline fix: %#v, %#v", lineNum, origLine, i, r.Inline) 143 return nil 144 } 145 } 146 147 return f.applyInlineFixes(lineIssues, origLine, lineNum) 148 } 149 150 func (f Fixer) applyInlineFixes(lineIssues []result.Issue, origLine []byte, lineNum int) *result.Issue { 151 sort.Slice(lineIssues, func(i, j int) bool { 152 return lineIssues[i].Replacement.Inline.StartCol < lineIssues[j].Replacement.Inline.StartCol 153 }) 154 155 var newLineBuf bytes.Buffer 156 newLineBuf.Grow(len(origLine)) 157 158 //nolint:misspell 159 // example: origLine="it's becouse of them", StartCol=5, Length=7, NewString="because" 160 161 curOrigLinePos := 0 162 for i := range lineIssues { 163 fix := lineIssues[i].Replacement.Inline 164 if fix.StartCol < curOrigLinePos { 165 f.log.Warnf("Line %d has multiple intersecting issues: %#v", lineNum, lineIssues) 166 return nil 167 } 168 169 if curOrigLinePos != fix.StartCol { 170 newLineBuf.Write(origLine[curOrigLinePos:fix.StartCol]) 171 } 172 newLineBuf.WriteString(fix.NewString) 173 curOrigLinePos = fix.StartCol + fix.Length 174 } 175 if curOrigLinePos != len(origLine) { 176 newLineBuf.Write(origLine[curOrigLinePos:]) 177 } 178 179 mergedIssue := lineIssues[0] // use text from the first issue (it's not really used) 180 mergedIssue.Replacement = &result.Replacement{ 181 NewLines: []string{newLineBuf.String()}, 182 } 183 return &mergedIssue 184 } 185 186 func (f Fixer) findNotIntersectingIssues(issues []result.Issue) []result.Issue { 187 sort.SliceStable(issues, func(i, j int) bool { 188 a, b := issues[i], issues[j] 189 return a.Line() < b.Line() 190 }) 191 192 var ret []result.Issue 193 var currentEnd int 194 for i := range issues { 195 issue := &issues[i] 196 rng := issue.GetLineRange() 197 if rng.From <= currentEnd { 198 f.log.Infof("Skip issue %#v: intersects with end %d", issue, currentEnd) 199 continue // skip intersecting issue 200 } 201 f.log.Infof("Fix issue %#v with range %v", issue, issue.GetLineRange()) 202 ret = append(ret, *issue) 203 currentEnd = rng.To 204 } 205 206 return ret 207 } 208 209 func (f Fixer) writeFixedFile(origFileLines [][]byte, issues []result.Issue, tmpOutFile *os.File) error { 210 // issues aren't intersecting 211 212 nextIssueIndex := 0 213 for i := 0; i < len(origFileLines); i++ { 214 var outLine string 215 var nextIssue *result.Issue 216 if nextIssueIndex != len(issues) { 217 nextIssue = &issues[nextIssueIndex] 218 } 219 220 origFileLineNumber := i + 1 221 if nextIssue == nil || origFileLineNumber != nextIssue.GetLineRange().From { 222 outLine = string(origFileLines[i]) 223 } else { 224 nextIssueIndex++ 225 rng := nextIssue.GetLineRange() 226 if rng.From > rng.To { 227 // Maybe better decision is to skip such issues, re-evaluate if regressed. 228 f.log.Warnf("[fixer]: issue line range is probably invalid, fix can be incorrect (from=%d, to=%d, linter=%s)", 229 rng.From, rng.To, nextIssue.FromLinter, 230 ) 231 } 232 i += rng.To - rng.From 233 if nextIssue.Replacement.NeedOnlyDelete { 234 continue 235 } 236 outLine = strings.Join(nextIssue.Replacement.NewLines, "\n") 237 } 238 239 if i < len(origFileLines)-1 { 240 outLine += "\n" 241 } 242 if _, err := tmpOutFile.WriteString(outLine); err != nil { 243 return errors.Wrap(err, "failed to write output line") 244 } 245 } 246 247 return nil 248 }