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