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