Andrew Shannon Brown 306816ec85
Revert "Update nolintlint to fix nolint formatting and remove unused nolint statements ()" ()
This reverts commit aeb98303293570ba682ea933a4e9501a11a3aa99.

There are some cases that nolinter fixer wasn't handling properly or expectedly (, , ) so we'll fix those in a new attempt.
2020-12-27 11:49:58 -08:00

301 lines
7.6 KiB
Go

package processors
import (
"fmt"
"go/ast"
"go/parser"
"go/token"
"sort"
"strings"
"github.com/golangci/golangci-lint/pkg/golinters"
"github.com/golangci/golangci-lint/pkg/lint/linter"
"github.com/golangci/golangci-lint/pkg/lint/lintersdb"
"github.com/golangci/golangci-lint/pkg/logutils"
"github.com/golangci/golangci-lint/pkg/result"
)
var nolintDebugf = logutils.Debug("nolint")
type ignoredRange struct {
linters []string
matchedIssueFromLinter map[string]bool
result.Range
col int
}
func (i *ignoredRange) doesMatch(issue *result.Issue) bool {
if issue.Line() < i.From || issue.Line() > i.To {
return false
}
// handle possible unused nolint directives
// nolintlint generates potential issues for every nolint directive and they are filtered out here
if issue.ExpectNoLint {
if issue.ExpectedNoLintLinter != "" {
return i.matchedIssueFromLinter[issue.ExpectedNoLintLinter]
}
return len(i.matchedIssueFromLinter) > 0
}
if len(i.linters) == 0 {
return true
}
for _, linterName := range i.linters {
if linterName == issue.FromLinter {
return true
}
}
return false
}
type fileData struct {
ignoredRanges []ignoredRange
}
type filesCache map[string]*fileData
type Nolint struct {
cache filesCache
dbManager *lintersdb.Manager
enabledLinters map[string]*linter.Config
log logutils.Log
unknownLintersSet map[string]bool
}
func NewNolint(log logutils.Log, dbManager *lintersdb.Manager, enabledLinters map[string]*linter.Config) *Nolint {
return &Nolint{
cache: filesCache{},
dbManager: dbManager,
enabledLinters: enabledLinters,
log: log,
unknownLintersSet: map[string]bool{},
}
}
var _ Processor = &Nolint{}
func (p Nolint) Name() string {
return "nolint"
}
func (p *Nolint) Process(issues []result.Issue) ([]result.Issue, error) {
// put nolintlint issues last because we process other issues first to determine which nolint directives are unused
sort.Stable(sortWithNolintlintLast(issues))
return filterIssuesErr(issues, p.shouldPassIssue)
}
func (p *Nolint) getOrCreateFileData(i *result.Issue) (*fileData, error) {
fd := p.cache[i.FilePath()]
if fd != nil {
return fd, nil
}
fd = &fileData{}
p.cache[i.FilePath()] = fd
if i.FilePath() == "" {
return nil, fmt.Errorf("no file path for issue")
}
// TODO: migrate this parsing to go/analysis facts
// or cache them somehow per file.
// Don't use cached AST because they consume a lot of memory on large projects.
fset := token.NewFileSet()
f, err := parser.ParseFile(fset, i.FilePath(), nil, parser.ParseComments)
if err != nil {
// Don't report error because it's already must be reporter by typecheck or go/analysis.
return fd, nil
}
fd.ignoredRanges = p.buildIgnoredRangesForFile(f, fset, i.FilePath())
nolintDebugf("file %s: built nolint ranges are %+v", i.FilePath(), fd.ignoredRanges)
return fd, nil
}
func (p *Nolint) buildIgnoredRangesForFile(f *ast.File, fset *token.FileSet, filePath string) []ignoredRange {
inlineRanges := p.extractFileCommentsInlineRanges(fset, f.Comments...)
nolintDebugf("file %s: inline nolint ranges are %+v", filePath, inlineRanges)
if len(inlineRanges) == 0 {
return nil
}
e := rangeExpander{
fset: fset,
inlineRanges: inlineRanges,
}
ast.Walk(&e, f)
// TODO: merge all ranges: there are repeated ranges
allRanges := append([]ignoredRange{}, inlineRanges...)
allRanges = append(allRanges, e.expandedRanges...)
return allRanges
}
func (p *Nolint) shouldPassIssue(i *result.Issue) (bool, error) {
nolintDebugf("got issue: %v", *i)
if i.FromLinter == golinters.NolintlintName {
// always pass nolintlint issues except ones trying find unused nolint directives
if !i.ExpectNoLint {
return true, nil
}
if i.ExpectedNoLintLinter != "" {
// don't expect disabled linters to cover their nolint statements
nolintDebugf("enabled linters: %v", p.enabledLinters)
if p.enabledLinters[i.ExpectedNoLintLinter] == nil {
return false, nil
}
nolintDebugf("checking that lint issue was used for %s: %v", i.ExpectedNoLintLinter, i)
}
}
fd, err := p.getOrCreateFileData(i)
if err != nil {
return false, err
}
for _, ir := range fd.ignoredRanges {
if ir.doesMatch(i) {
ir.matchedIssueFromLinter[i.FromLinter] = true
return false, nil
}
}
return true, nil
}
type rangeExpander struct {
fset *token.FileSet
inlineRanges []ignoredRange
expandedRanges []ignoredRange
}
func (e *rangeExpander) Visit(node ast.Node) ast.Visitor {
if node == nil {
return e
}
nodeStartPos := e.fset.Position(node.Pos())
nodeStartLine := nodeStartPos.Line
nodeEndLine := e.fset.Position(node.End()).Line
var foundRange *ignoredRange
for _, r := range e.inlineRanges {
if r.To == nodeStartLine-1 && nodeStartPos.Column == r.col {
r := r
foundRange = &r
break
}
}
if foundRange == nil {
return e
}
expandedRange := *foundRange
if expandedRange.To < nodeEndLine {
expandedRange.To = nodeEndLine
}
nolintDebugf("found range is %v for node %#v [%d;%d], expanded range is %v",
*foundRange, node, nodeStartLine, nodeEndLine, expandedRange)
e.expandedRanges = append(e.expandedRanges, expandedRange)
return e
}
func (p *Nolint) extractFileCommentsInlineRanges(fset *token.FileSet, comments ...*ast.CommentGroup) []ignoredRange {
var ret []ignoredRange
for _, g := range comments {
for _, c := range g.List {
ir := p.extractInlineRangeFromComment(c.Text, g, fset)
if ir != nil {
ret = append(ret, *ir)
}
}
}
return ret
}
func (p *Nolint) extractInlineRangeFromComment(text string, g ast.Node, fset *token.FileSet) *ignoredRange {
text = strings.TrimLeft(text, "/ ")
if !strings.HasPrefix(text, "nolint") {
return nil
}
buildRange := func(linters []string) *ignoredRange {
pos := fset.Position(g.Pos())
return &ignoredRange{
Range: result.Range{
From: pos.Line,
To: fset.Position(g.End()).Line,
},
col: pos.Column,
linters: linters,
matchedIssueFromLinter: make(map[string]bool),
}
}
if !strings.HasPrefix(text, "nolint:") {
return buildRange(nil) // ignore all linters
}
// ignore specific linters
var linters []string
text = strings.Split(text, "//")[0] // allow another comment after this comment
linterItems := strings.Split(strings.TrimPrefix(text, "nolint:"), ",")
for _, linter := range linterItems {
linterName := strings.ToLower(strings.TrimSpace(linter))
lcs := p.dbManager.GetLinterConfigs(linterName)
if lcs == nil {
p.unknownLintersSet[linterName] = true
linters = append(linters, linterName)
nolintDebugf("unknown linter %s on line %d", linterName, fset.Position(g.Pos()).Line)
continue
}
for _, lc := range lcs {
linters = append(linters, lc.Name()) // normalize name to work with aliases
}
}
nolintDebugf("%d: linters are %s", fset.Position(g.Pos()).Line, linters)
return buildRange(linters)
}
func (p Nolint) Finish() {
if len(p.unknownLintersSet) == 0 {
return
}
unknownLinters := []string{}
for name := range p.unknownLintersSet {
unknownLinters = append(unknownLinters, name)
}
sort.Strings(unknownLinters)
p.log.Warnf("Found unknown linters in //nolint directives: %s", strings.Join(unknownLinters, ", "))
}
// put nolintlint last
type sortWithNolintlintLast []result.Issue
func (issues sortWithNolintlintLast) Len() int {
return len(issues)
}
func (issues sortWithNolintlintLast) Less(i, j int) bool {
return issues[i].FromLinter != golinters.NolintlintName && issues[j].FromLinter == golinters.NolintlintName
}
func (issues sortWithNolintlintLast) Swap(i, j int) {
issues[j], issues[i] = issues[i], issues[j]
}