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:
Isaev Denis 2018-06-06 23:59:28 +03:00 committed by GitHub
commit 110f584ad9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 263 additions and 44 deletions

View File

@ -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
} }

View File

@ -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 return false, nil
}
if len(comment.linters) == 0 {
return false, nil // skip all linters
}
for _, linter := range comment.linters {
if i.FromLinter == linter {
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
text = strings.Split(text, "//")[0] // allow another comment after this comment if strings.HasPrefix(text, "nolint:") {
linterItems := strings.Split(strings.TrimPrefix(text, "nolint:"), ",") // ignore specific linters
for _, linter := range linterItems { text = strings.Split(text, "//")[0] // allow another comment after this comment
linterName := strings.TrimSpace(linter) // TODO: validate it here linterItems := strings.Split(strings.TrimPrefix(text, "nolint:"), ",")
linters = append(linters, linterName) for _, linter := range linterItems {
} linterName := strings.TrimSpace(linter) // TODO: validate it here
ret = append(ret, comment{ 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, linters: linters,
line: pos.Line,
}) })
} }
} }

View File

@ -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)
}
}

View File

@ -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
}

View File

@ -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

View File

@ -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
View 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.