Denis Isaev ed0b551070 Fix linting of preprocessed files
Preprocessed files like .qtpl.go quicktemplate Go files can have
//line directives. They map to a source .qtpl file.
This commit fixes linting of such files:
1. don't fail on AST cache loading
2. output Go filename not .qtpl or similar

Also, here we update golint to the upstream version.

Relates: #316, #466, #467, #468
2019-04-20 21:50:26 +03:00

263 lines
6.1 KiB
Go

package processors
import (
"fmt"
"go/ast"
"go/token"
"sort"
"strings"
"github.com/pkg/errors"
"github.com/golangci/golangci-lint/pkg/lint/astcache"
"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
result.Range
col int
}
func (i *ignoredRange) doesMatch(issue *result.Issue) bool {
if issue.Line() < i.From || issue.Line() > i.To {
return false
}
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
astCache *astcache.Cache
dbManager *lintersdb.Manager
log logutils.Log
unknownLintersSet map[string]bool
}
func NewNolint(astCache *astcache.Cache, log logutils.Log, dbManager *lintersdb.Manager) *Nolint {
return &Nolint{
cache: filesCache{},
astCache: astCache,
dbManager: dbManager,
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) {
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")
}
file := p.astCache.Get(i.FilePath())
if file == nil {
return nil, fmt.Errorf("no file %s in ast cache %v",
i.FilePath(), p.astCache.ParsedFilenames())
}
if file.Err != nil {
return nil, errors.Wrapf(file.Err, "can't parse file %s", i.FilePath())
}
fd.ignoredRanges = p.buildIgnoredRangesForFile(file.F, file.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) {
fd, err := p.getOrCreateFileData(i)
if err != nil {
return false, err
}
for _, ir := range fd.ignoredRanges {
if ir.doesMatch(i) {
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,
}
}
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:"), ",")
var gotUnknownLinters bool
for _, linter := range linterItems {
linterName := strings.ToLower(strings.TrimSpace(linter))
metaLinter := p.dbManager.GetMetaLinter(linterName)
if metaLinter != nil {
// user can set metalinter name in nolint directive (e.g. megacheck), then
// we should add to nolint all the metalinter's default children
linters = append(linters, metaLinter.DefaultChildLinterNames()...)
continue
}
lc := p.dbManager.GetLinterConfig(linterName)
if lc == nil {
p.unknownLintersSet[linterName] = true
gotUnknownLinters = true
continue
}
linters = append(linters, lc.Name()) // normalize name to work with aliases
}
if gotUnknownLinters {
return buildRange(nil) // ignore all linters to not annoy user
}
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, ", "))
}