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  }