github.com/vanstinator/golangci-lint@v0.0.0-20240223191551-cc572f00d9d1/pkg/result/processors/nolint.go (about) 1 package processors 2 3 import ( 4 "errors" 5 "go/ast" 6 "go/parser" 7 "go/token" 8 "regexp" 9 "sort" 10 "strings" 11 12 "golang.org/x/exp/maps" 13 14 "github.com/vanstinator/golangci-lint/pkg/golinters" 15 "github.com/vanstinator/golangci-lint/pkg/lint/linter" 16 "github.com/vanstinator/golangci-lint/pkg/lint/lintersdb" 17 "github.com/vanstinator/golangci-lint/pkg/logutils" 18 "github.com/vanstinator/golangci-lint/pkg/result" 19 ) 20 21 var nolintDebugf = logutils.Debug(logutils.DebugKeyNolint) 22 var nolintRe = regexp.MustCompile(`^nolint( |:|$)`) 23 24 type ignoredRange struct { 25 linters []string 26 matchedIssueFromLinter map[string]bool 27 result.Range 28 col int 29 originalRange *ignoredRange // pre-expanded range (used to match nolintlint issues) 30 } 31 32 func (i *ignoredRange) doesMatch(issue *result.Issue) bool { 33 if issue.Line() < i.From || issue.Line() > i.To { 34 return false 35 } 36 37 // only allow selective nolinting of nolintlint 38 nolintFoundForLinter := len(i.linters) == 0 && issue.FromLinter != golinters.NoLintLintName 39 40 for _, linterName := range i.linters { 41 if linterName == issue.FromLinter { 42 nolintFoundForLinter = true 43 break 44 } 45 } 46 47 if nolintFoundForLinter { 48 return true 49 } 50 51 // handle possible unused nolint directives 52 // nolintlint generates potential issues for every nolint directive, and they are filtered out here 53 if issue.FromLinter == golinters.NoLintLintName && issue.ExpectNoLint { 54 if issue.ExpectedNoLintLinter != "" { 55 return i.matchedIssueFromLinter[issue.ExpectedNoLintLinter] 56 } 57 return len(i.matchedIssueFromLinter) > 0 58 } 59 60 return false 61 } 62 63 type fileData struct { 64 ignoredRanges []ignoredRange 65 } 66 67 type filesCache map[string]*fileData 68 69 type Nolint struct { 70 cache filesCache 71 dbManager *lintersdb.Manager 72 enabledLinters map[string]*linter.Config 73 log logutils.Log 74 75 unknownLintersSet map[string]bool 76 } 77 78 func NewNolint(log logutils.Log, dbManager *lintersdb.Manager, enabledLinters map[string]*linter.Config) *Nolint { 79 return &Nolint{ 80 cache: filesCache{}, 81 dbManager: dbManager, 82 enabledLinters: enabledLinters, 83 log: log, 84 unknownLintersSet: map[string]bool{}, 85 } 86 } 87 88 var _ Processor = &Nolint{} 89 90 func (p *Nolint) Name() string { 91 return "nolint" 92 } 93 94 func (p *Nolint) Process(issues []result.Issue) ([]result.Issue, error) { 95 // put nolintlint issues last because we process other issues first to determine which nolint directives are unused 96 sort.Stable(sortWithNolintlintLast(issues)) 97 return filterIssuesErr(issues, p.shouldPassIssue) 98 } 99 100 func (p *Nolint) getOrCreateFileData(i *result.Issue) (*fileData, error) { 101 fd := p.cache[i.FilePath()] 102 if fd != nil { 103 return fd, nil 104 } 105 106 fd = &fileData{} 107 p.cache[i.FilePath()] = fd 108 109 if i.FilePath() == "" { 110 return nil, errors.New("no file path for issue") 111 } 112 113 // TODO: migrate this parsing to go/analysis facts 114 // or cache them somehow per file. 115 116 // Don't use cached AST because they consume a lot of memory on large projects. 117 fset := token.NewFileSet() 118 f, err := parser.ParseFile(fset, i.FilePath(), nil, parser.ParseComments) 119 if err != nil { 120 // Don't report error because it's already must be reporter by typecheck or go/analysis. 121 return fd, nil 122 } 123 124 fd.ignoredRanges = p.buildIgnoredRangesForFile(f, fset, i.FilePath()) 125 nolintDebugf("file %s: built nolint ranges are %+v", i.FilePath(), fd.ignoredRanges) 126 return fd, nil 127 } 128 129 func (p *Nolint) buildIgnoredRangesForFile(f *ast.File, fset *token.FileSet, filePath string) []ignoredRange { 130 inlineRanges := p.extractFileCommentsInlineRanges(fset, f.Comments...) 131 nolintDebugf("file %s: inline nolint ranges are %+v", filePath, inlineRanges) 132 133 if len(inlineRanges) == 0 { 134 return nil 135 } 136 137 e := rangeExpander{ 138 fset: fset, 139 inlineRanges: inlineRanges, 140 } 141 142 ast.Walk(&e, f) 143 144 // TODO: merge all ranges: there are repeated ranges 145 allRanges := append([]ignoredRange{}, inlineRanges...) 146 allRanges = append(allRanges, e.expandedRanges...) 147 148 return allRanges 149 } 150 151 func (p *Nolint) shouldPassIssue(i *result.Issue) (bool, error) { 152 nolintDebugf("got issue: %v", *i) 153 if i.FromLinter == golinters.NoLintLintName && i.ExpectNoLint && i.ExpectedNoLintLinter != "" { 154 // don't expect disabled linters to cover their nolint statements 155 nolintDebugf("enabled linters: %v", p.enabledLinters) 156 if p.enabledLinters[i.ExpectedNoLintLinter] == nil { 157 return false, nil 158 } 159 nolintDebugf("checking that lint issue was used for %s: %v", i.ExpectedNoLintLinter, i) 160 } 161 162 fd, err := p.getOrCreateFileData(i) 163 if err != nil { 164 return false, err 165 } 166 167 for _, ir := range fd.ignoredRanges { 168 if ir.doesMatch(i) { 169 nolintDebugf("found ignored range for issue %v: %v", i, ir) 170 ir.matchedIssueFromLinter[i.FromLinter] = true 171 if ir.originalRange != nil { 172 ir.originalRange.matchedIssueFromLinter[i.FromLinter] = true 173 } 174 return false, nil 175 } 176 } 177 178 return true, nil 179 } 180 181 type rangeExpander struct { 182 fset *token.FileSet 183 inlineRanges []ignoredRange 184 expandedRanges []ignoredRange 185 } 186 187 func (e *rangeExpander) Visit(node ast.Node) ast.Visitor { 188 if node == nil { 189 return e 190 } 191 192 nodeStartPos := e.fset.Position(node.Pos()) 193 nodeStartLine := nodeStartPos.Line 194 nodeEndLine := e.fset.Position(node.End()).Line 195 196 var foundRange *ignoredRange 197 for _, r := range e.inlineRanges { 198 if r.To == nodeStartLine-1 && nodeStartPos.Column == r.col { 199 r := r 200 foundRange = &r 201 break 202 } 203 } 204 if foundRange == nil { 205 return e 206 } 207 208 expandedRange := *foundRange 209 // store the original unexpanded range for matching nolintlint issues 210 if expandedRange.originalRange == nil { 211 expandedRange.originalRange = foundRange 212 } 213 if expandedRange.To < nodeEndLine { 214 expandedRange.To = nodeEndLine 215 } 216 217 nolintDebugf("found range is %v for node %#v [%d;%d], expanded range is %v", 218 *foundRange, node, nodeStartLine, nodeEndLine, expandedRange) 219 e.expandedRanges = append(e.expandedRanges, expandedRange) 220 221 return e 222 } 223 224 func (p *Nolint) extractFileCommentsInlineRanges(fset *token.FileSet, comments ...*ast.CommentGroup) []ignoredRange { 225 var ret []ignoredRange 226 for _, g := range comments { 227 for _, c := range g.List { 228 ir := p.extractInlineRangeFromComment(c.Text, g, fset) 229 if ir != nil { 230 ret = append(ret, *ir) 231 } 232 } 233 } 234 235 return ret 236 } 237 238 func (p *Nolint) extractInlineRangeFromComment(text string, g ast.Node, fset *token.FileSet) *ignoredRange { 239 text = strings.TrimLeft(text, "/ ") 240 if !nolintRe.MatchString(text) { 241 return nil 242 } 243 244 buildRange := func(linters []string) *ignoredRange { 245 pos := fset.Position(g.Pos()) 246 return &ignoredRange{ 247 Range: result.Range{ 248 From: pos.Line, 249 To: fset.Position(g.End()).Line, 250 }, 251 col: pos.Column, 252 linters: linters, 253 matchedIssueFromLinter: make(map[string]bool), 254 } 255 } 256 257 if strings.HasPrefix(text, "nolint:all") || !strings.HasPrefix(text, "nolint:") { 258 return buildRange(nil) // ignore all linters 259 } 260 261 // ignore specific linters 262 var linters []string 263 text = strings.Split(text, "//")[0] // allow another comment after this comment 264 linterItems := strings.Split(strings.TrimPrefix(text, "nolint:"), ",") 265 for _, item := range linterItems { 266 linterName := strings.ToLower(strings.TrimSpace(item)) 267 if linterName == "all" { 268 p.unknownLintersSet = map[string]bool{} 269 return buildRange(nil) 270 } 271 272 lcs := p.dbManager.GetLinterConfigs(linterName) 273 if lcs == nil { 274 p.unknownLintersSet[linterName] = true 275 linters = append(linters, linterName) 276 nolintDebugf("unknown linter %s on line %d", linterName, fset.Position(g.Pos()).Line) 277 continue 278 } 279 280 for _, lc := range lcs { 281 linters = append(linters, lc.Name()) // normalize name to work with aliases 282 } 283 } 284 285 nolintDebugf("%d: linters are %s", fset.Position(g.Pos()).Line, linters) 286 return buildRange(linters) 287 } 288 289 func (p *Nolint) Finish() { 290 if len(p.unknownLintersSet) == 0 { 291 return 292 } 293 294 unknownLinters := maps.Keys(p.unknownLintersSet) 295 sort.Strings(unknownLinters) 296 297 p.log.Warnf("Found unknown linters in //nolint directives: %s", strings.Join(unknownLinters, ", ")) 298 } 299 300 // put nolintlint last 301 type sortWithNolintlintLast []result.Issue 302 303 func (issues sortWithNolintlintLast) Len() int { 304 return len(issues) 305 } 306 307 func (issues sortWithNolintlintLast) Less(i, j int) bool { 308 return issues[i].FromLinter != golinters.NoLintLintName && issues[j].FromLinter == golinters.NoLintLintName 309 } 310 311 func (issues sortWithNolintlintLast) Swap(i, j int) { 312 issues[j], issues[i] = issues[i], issues[j] 313 }