Merge pull request #71 from golangci/feature/full-support-of-nolint-directives
#65, #68: make //nolint processing like in gometalinter
This commit is contained in:
commit
110f584ad9
@ -23,7 +23,7 @@ func (p Cgo) Process(issues []result.Issue) ([]result.Issue, error) {
|
|||||||
return filterIssues(issues, func(i *result.Issue) bool {
|
return filterIssues(issues, func(i *result.Issue) bool {
|
||||||
// some linters (.e.g gas, deadcode) return incorrect filepaths for cgo issues,
|
// some linters (.e.g gas, deadcode) return incorrect filepaths for cgo issues,
|
||||||
// it breaks next processing, so skip them
|
// it breaks next processing, so skip them
|
||||||
return !strings.HasSuffix(i.FilePath(), "/C")
|
return i.FilePath() != "C" && !strings.HasSuffix(i.FilePath(), "/C")
|
||||||
}), nil
|
}), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -8,20 +8,45 @@ import (
|
|||||||
"go/parser"
|
"go/parser"
|
||||||
"go/token"
|
"go/token"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/golangci/golangci-lint/pkg/result"
|
"github.com/golangci/golangci-lint/pkg/result"
|
||||||
)
|
)
|
||||||
|
|
||||||
type comment struct {
|
type ignoredRange struct {
|
||||||
linters []string
|
linters []string
|
||||||
line int
|
result.Range
|
||||||
|
col int
|
||||||
}
|
}
|
||||||
type fileComments []comment
|
|
||||||
|
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 {
|
type fileData struct {
|
||||||
comments fileComments
|
ignoredRanges []ignoredRange
|
||||||
isGenerated bool
|
isGenerated bool
|
||||||
}
|
}
|
||||||
|
|
||||||
type filesCache map[string]*fileData
|
type filesCache map[string]*fileData
|
||||||
|
|
||||||
type Nolint struct {
|
type Nolint struct {
|
||||||
@ -93,15 +118,28 @@ func (p *Nolint) getOrCreateFileData(i *result.Issue) (*fileData, error) {
|
|||||||
return nil, fmt.Errorf("can't parse file %s", i.FilePath())
|
return nil, fmt.Errorf("can't parse file %s", i.FilePath())
|
||||||
}
|
}
|
||||||
|
|
||||||
fd.comments = extractFileComments(p.fset, file.Comments...)
|
fd.ignoredRanges = buildIgnoredRangesForFile(file, p.fset)
|
||||||
return fd, nil
|
return fd, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Nolint) shouldPassIssue(i *result.Issue) (bool, error) {
|
func buildIgnoredRangesForFile(f *ast.File, fset *token.FileSet) []ignoredRange {
|
||||||
if i.FilePath() == "C" {
|
inlineRanges := extractFileCommentsInlineRanges(fset, f.Comments...)
|
||||||
return false, nil
|
|
||||||
|
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)
|
fd, err := p.getOrCreateFileData(i)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
@ -111,27 +149,53 @@ func (p *Nolint) shouldPassIssue(i *result.Issue) (bool, error) {
|
|||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, comment := range fd.comments {
|
for _, ir := range fd.ignoredRanges {
|
||||||
if comment.line != i.Line() {
|
if ir.doesMatch(i) {
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(comment.linters) == 0 {
|
|
||||||
return false, nil // skip all linters
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, linter := range comment.linters {
|
|
||||||
if i.FromLinter == linter {
|
|
||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func extractFileComments(fset *token.FileSet, comments ...*ast.CommentGroup) fileComments {
|
type ignoredRanges []ignoredRange
|
||||||
ret := fileComments{}
|
|
||||||
|
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 _, g := range comments {
|
||||||
for _, c := range g.List {
|
for _, c := range g.List {
|
||||||
text := strings.TrimLeft(c.Text, "/ ")
|
text := strings.TrimLeft(c.Text, "/ ")
|
||||||
@ -139,25 +203,25 @@ func extractFileComments(fset *token.FileSet, comments ...*ast.CommentGroup) fil
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
pos := fset.Position(g.Pos())
|
|
||||||
if !strings.HasPrefix(text, "nolint:") { // ignore all linters
|
|
||||||
ret = append(ret, comment{
|
|
||||||
line: pos.Line,
|
|
||||||
})
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// ignore specific linters
|
|
||||||
var linters []string
|
var linters []string
|
||||||
|
if strings.HasPrefix(text, "nolint:") {
|
||||||
|
// ignore specific linters
|
||||||
text = strings.Split(text, "//")[0] // allow another comment after this comment
|
text = strings.Split(text, "//")[0] // allow another comment after this comment
|
||||||
linterItems := strings.Split(strings.TrimPrefix(text, "nolint:"), ",")
|
linterItems := strings.Split(strings.TrimPrefix(text, "nolint:"), ",")
|
||||||
for _, linter := range linterItems {
|
for _, linter := range linterItems {
|
||||||
linterName := strings.TrimSpace(linter) // TODO: validate it here
|
linterName := strings.TrimSpace(linter) // TODO: validate it here
|
||||||
linters = append(linters, linterName)
|
linters = append(linters, linterName)
|
||||||
}
|
}
|
||||||
ret = append(ret, comment{
|
} // 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,
|
linters: linters,
|
||||||
line: pos.Line,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,6 +6,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/golangci/golangci-lint/pkg/result"
|
"github.com/golangci/golangci-lint/pkg/result"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
func newNolintFileIssue(line int, fromLinter string) result.Issue {
|
func newNolintFileIssue(line int, fromLinter string) result.Issue {
|
||||||
@ -20,6 +21,8 @@ func newNolintFileIssue(line int, fromLinter string) result.Issue {
|
|||||||
|
|
||||||
func TestNolint(t *testing.T) {
|
func TestNolint(t *testing.T) {
|
||||||
p := NewNolint(token.NewFileSet())
|
p := NewNolint(token.NewFileSet())
|
||||||
|
|
||||||
|
// test inline comments
|
||||||
processAssertEmpty(t, p, newNolintFileIssue(3, "gofmt"))
|
processAssertEmpty(t, p, newNolintFileIssue(3, "gofmt"))
|
||||||
processAssertEmpty(t, p, newNolintFileIssue(3, "gofmt")) // check cached is ok
|
processAssertEmpty(t, p, newNolintFileIssue(3, "gofmt")) // check cached is ok
|
||||||
processAssertSame(t, p, newNolintFileIssue(3, "gofmtA")) // check different name
|
processAssertSame(t, p, newNolintFileIssue(3, "gofmtA")) // check different name
|
||||||
@ -35,6 +38,42 @@ func TestNolint(t *testing.T) {
|
|||||||
processAssertEmpty(t, p, newNolintFileIssue(7, "any"))
|
processAssertEmpty(t, p, newNolintFileIssue(7, "any"))
|
||||||
|
|
||||||
processAssertSame(t, p, newNolintFileIssue(1, "golint")) // no directive
|
processAssertSame(t, p, newNolintFileIssue(1, "golint")) // no directive
|
||||||
|
|
||||||
|
// test preceding comments
|
||||||
|
processAssertEmpty(t, p, newNolintFileIssue(10, "any")) // preceding comment for var
|
||||||
|
processAssertEmpty(t, p, newNolintFileIssue(9, "any")) // preceding comment for var itself
|
||||||
|
|
||||||
|
processAssertSame(t, p, newNolintFileIssue(14, "any")) // preceding comment with extra \n
|
||||||
|
processAssertEmpty(t, p, newNolintFileIssue(12, "any")) // preceding comment with extra \n itself
|
||||||
|
|
||||||
|
processAssertSame(t, p, newNolintFileIssue(17, "any")) // preceding comment on different column
|
||||||
|
processAssertEmpty(t, p, newNolintFileIssue(16, "any")) // preceding comment on different column itself
|
||||||
|
|
||||||
|
// preceding comment for func name and comment itself
|
||||||
|
for i := 19; i <= 23; i++ {
|
||||||
|
processAssertEmpty(t, p, newNolintFileIssue(i, "any"))
|
||||||
|
}
|
||||||
|
|
||||||
|
processAssertSame(t, p, newNolintFileIssue(24, "any")) // right after func
|
||||||
|
|
||||||
|
// preceding multiline comment: last line
|
||||||
|
for i := 25; i <= 30; i++ {
|
||||||
|
processAssertEmpty(t, p, newNolintFileIssue(i, "any"))
|
||||||
|
}
|
||||||
|
|
||||||
|
processAssertSame(t, p, newNolintFileIssue(31, "any")) // between funcs
|
||||||
|
|
||||||
|
// preceding multiline comment: first line
|
||||||
|
for i := 32; i <= 37; i++ {
|
||||||
|
processAssertEmpty(t, p, newNolintFileIssue(i, "any"))
|
||||||
|
}
|
||||||
|
|
||||||
|
processAssertSame(t, p, newNolintFileIssue(38, "any")) // between funcs
|
||||||
|
|
||||||
|
// preceding multiline comment: medium line
|
||||||
|
for i := 39; i <= 45; i++ {
|
||||||
|
processAssertEmpty(t, p, newNolintFileIssue(i, "any"))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestNoIssuesInAutogeneratedFiles(t *testing.T) {
|
func TestNoIssuesInAutogeneratedFiles(t *testing.T) {
|
||||||
@ -56,3 +95,62 @@ func TestNoIssuesInAutogeneratedFiles(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestIgnoredRangeMatches(t *testing.T) {
|
||||||
|
var testcases = []struct {
|
||||||
|
doc string
|
||||||
|
issue result.Issue
|
||||||
|
linters []string
|
||||||
|
expected bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
doc: "unmatched line",
|
||||||
|
issue: result.Issue{
|
||||||
|
Pos: token.Position{
|
||||||
|
Line: 100,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
doc: "matched line, all linters",
|
||||||
|
issue: result.Issue{
|
||||||
|
Pos: token.Position{
|
||||||
|
Line: 5,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
doc: "matched line, unmatched linter",
|
||||||
|
issue: result.Issue{
|
||||||
|
Pos: token.Position{
|
||||||
|
Line: 5,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
linters: []string{"vet"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
doc: "matched line and linters",
|
||||||
|
issue: result.Issue{
|
||||||
|
Pos: token.Position{
|
||||||
|
Line: 20,
|
||||||
|
},
|
||||||
|
FromLinter: "vet",
|
||||||
|
},
|
||||||
|
linters: []string{"vet"},
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, testcase := range testcases {
|
||||||
|
ir := ignoredRange{
|
||||||
|
col: 20,
|
||||||
|
Range: result.Range{
|
||||||
|
From: 5,
|
||||||
|
To: 20,
|
||||||
|
},
|
||||||
|
linters: testcase.linters,
|
||||||
|
}
|
||||||
|
assert.Equal(t, testcase.expected, ir.doesMatch(&testcase.issue), testcase.doc)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
38
pkg/result/processors/testdata/nolint.go
vendored
38
pkg/result/processors/testdata/nolint.go
vendored
@ -5,3 +5,41 @@ var nolintSpace int // nolint: gofmt
|
|||||||
var nolintSpaces int //nolint: gofmt, govet
|
var nolintSpaces int //nolint: gofmt, govet
|
||||||
var nolintAll int // nolint
|
var nolintAll int // nolint
|
||||||
var nolintAndAppendix int // nolint // another comment
|
var nolintAndAppendix int // nolint // another comment
|
||||||
|
|
||||||
|
//nolint
|
||||||
|
var nolintVarByPrecedingComment int
|
||||||
|
|
||||||
|
//nolint
|
||||||
|
|
||||||
|
var dontNolintVarByPrecedingCommentBecauseOfNewLine int
|
||||||
|
|
||||||
|
var nolintPrecedingVar string //nolint
|
||||||
|
var dontNolintVarByPrecedingCommentBecauseOfDifferentColumn int
|
||||||
|
|
||||||
|
//nolint
|
||||||
|
func nolintFuncByPrecedingComment() *string {
|
||||||
|
xv := "v"
|
||||||
|
return &xv
|
||||||
|
}
|
||||||
|
|
||||||
|
//nolint
|
||||||
|
// second line
|
||||||
|
func nolintFuncByPrecedingMultilineComment1() *string {
|
||||||
|
xv := "v"
|
||||||
|
return &xv
|
||||||
|
}
|
||||||
|
|
||||||
|
// first line
|
||||||
|
//nolint
|
||||||
|
func nolintFuncByPrecedingMultilineComment2() *string {
|
||||||
|
xv := "v"
|
||||||
|
return &xv
|
||||||
|
}
|
||||||
|
|
||||||
|
// first line
|
||||||
|
//nolint
|
||||||
|
// third line
|
||||||
|
func nolintFuncByPrecedingMultilineComment3() *string {
|
||||||
|
xv := "v"
|
||||||
|
return &xv
|
||||||
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
// Code generated by ... DO NOT EDIT.
|
// Code generated by ... DO NOT EDIT.
|
||||||
package testdata
|
package testdata
|
||||||
|
|
||||||
var v int
|
var vvv int
|
||||||
|
@ -5,4 +5,4 @@
|
|||||||
// DO NOT EDIT!
|
// DO NOT EDIT!
|
||||||
package testdata
|
package testdata
|
||||||
|
|
||||||
var v int
|
var vv int
|
||||||
|
19
third_party/gometalinter/LICENSE
vendored
Normal file
19
third_party/gometalinter/LICENSE
vendored
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
Copyright (C) 2012 Alec Thomas
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
this software and associated documentation files (the "Software"), to deal in
|
||||||
|
the Software without restriction, including without limitation the rights to
|
||||||
|
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||||
|
of the Software, and to permit persons to whom the Software is furnished to do
|
||||||
|
so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
Loading…
x
Reference in New Issue
Block a user