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