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