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  }