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  }