233 lines
4.7 KiB
Go

package processors
import (
"bufio"
"bytes"
"fmt"
"go/ast"
"go/parser"
"go/token"
"io/ioutil"
"sort"
"strings"
"github.com/golangci/golangci-lint/pkg/result"
)
type ignoredRange struct {
linters []string
result.Range
col int
}
func (i *ignoredRange) isAdjacent(col, start int) bool {
return col == i.col && i.To == start-1
}
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 _, l := range i.linters {
if l == issue.FromLinter {
return true
}
}
return false
}
type fileData struct {
ignoredRanges []ignoredRange
isGenerated bool
}
type filesCache map[string]*fileData
type Nolint struct {
fset *token.FileSet
cache filesCache
}
func NewNolint(fset *token.FileSet) *Nolint {
return &Nolint{
fset: fset,
cache: filesCache{},
}
}
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)
}
var (
genHdr = []byte("// Code generated")
genFtr = []byte("DO NOT EDIT")
)
// isGenerated reports whether the source file is generated code.
// Using a bit laxer rules than https://golang.org/s/generatedcode to
// match more generated code.
func isGenerated(src []byte) bool {
sc := bufio.NewScanner(bytes.NewReader(src))
var hdr, ftr bool
for sc.Scan() {
b := sc.Bytes()
if bytes.HasPrefix(b, genHdr) {
hdr = true
}
if bytes.Contains(b, genFtr) {
ftr = true
}
}
return hdr && ftr
}
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
src, err := ioutil.ReadFile(i.FilePath())
if err != nil {
return nil, fmt.Errorf("can't read file %s: %s", i.FilePath(), err)
}
fd.isGenerated = isGenerated(src)
if fd.isGenerated { // don't report issues for autogenerated files
return fd, nil
}
file, err := parser.ParseFile(p.fset, i.FilePath(), src, parser.ParseComments)
if err != nil {
return nil, fmt.Errorf("can't parse file %s", i.FilePath())
}
fd.ignoredRanges = buildIgnoredRangesForFile(file, p.fset)
return fd, nil
}
func buildIgnoredRangesForFile(f *ast.File, fset *token.FileSet) []ignoredRange {
inlineRanges := extractFileCommentsInlineRanges(fset, f.Comments...)
if len(inlineRanges) == 0 {
return nil
}
e := rangeExpander{
fset: fset,
ranges: ignoredRanges(inlineRanges),
}
ast.Walk(&e, f)
return e.ranges
}
func (p *Nolint) shouldPassIssue(i *result.Issue) (bool, error) {
fd, err := p.getOrCreateFileData(i)
if err != nil {
return false, err
}
if fd.isGenerated { // don't report issues for autogenerated files
return false, nil
}
for _, ir := range fd.ignoredRanges {
if ir.doesMatch(i) {
return false, nil
}
}
return true, nil
}
type ignoredRanges []ignoredRange
func (ir ignoredRanges) Len() int { return len(ir) }
func (ir ignoredRanges) Swap(i, j int) { ir[i], ir[j] = ir[j], ir[i] }
func (ir ignoredRanges) Less(i, j int) bool { return ir[i].To < ir[j].To }
type rangeExpander struct {
fset *token.FileSet
ranges ignoredRanges
}
func (e *rangeExpander) Visit(node ast.Node) ast.Visitor {
if node == nil {
return e
}
startPos := e.fset.Position(node.Pos())
start := startPos.Line
end := e.fset.Position(node.End()).Line
found := sort.Search(len(e.ranges), func(i int) bool {
return e.ranges[i].To+1 >= start
})
if found < len(e.ranges) && e.ranges[found].isAdjacent(startPos.Column, start) {
r := &e.ranges[found]
if r.From > start {
r.From = start
}
if r.To < end {
r.To = end
}
}
return e
}
func extractFileCommentsInlineRanges(fset *token.FileSet, comments ...*ast.CommentGroup) []ignoredRange {
var ret []ignoredRange
for _, g := range comments {
for _, c := range g.List {
text := strings.TrimLeft(c.Text, "/ ")
if !strings.HasPrefix(text, "nolint") {
continue
}
var linters []string
if strings.HasPrefix(text, "nolint:") {
// ignore specific linters
text = strings.Split(text, "//")[0] // allow another comment after this comment
linterItems := strings.Split(strings.TrimPrefix(text, "nolint:"), ",")
for _, linter := range linterItems {
linterName := strings.TrimSpace(linter) // TODO: validate it here
linters = append(linters, linterName)
}
} // else ignore all linters
pos := fset.Position(g.Pos())
ret = append(ret, ignoredRange{
Range: result.Range{
From: pos.Line,
To: fset.Position(g.End()).Line,
},
col: pos.Column,
linters: linters,
})
}
}
return ret
}
func (p Nolint) Finish() {}