Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

#65, #68: make //nolint processing like in gometalinter #71

Merged
merged 1 commit into from
Jun 6, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pkg/result/processors/cgo.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ func (p Cgo) Process(issues []result.Issue) ([]result.Issue, error) {
return filterIssues(issues, func(i *result.Issue) bool {
// some linters (.e.g gas, deadcode) return incorrect filepaths for cgo issues,
// it breaks next processing, so skip them
return !strings.HasSuffix(i.FilePath(), "/C")
return i.FilePath() != "C" && !strings.HasSuffix(i.FilePath(), "/C")
}), nil
}

Expand Down
142 changes: 103 additions & 39 deletions pkg/result/processors/nolint.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,20 +8,45 @@ import (
"go/parser"
"go/token"
"io/ioutil"
"sort"
"strings"

"github.com/golangci/golangci-lint/pkg/result"
)

type comment struct {
type ignoredRange struct {
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 {
comments fileComments
isGenerated bool
ignoredRanges []ignoredRange
isGenerated bool
}

type filesCache map[string]*fileData

type Nolint struct {
Expand Down Expand Up @@ -93,15 +118,28 @@ func (p *Nolint) getOrCreateFileData(i *result.Issue) (*fileData, error) {
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
}

func (p *Nolint) shouldPassIssue(i *result.Issue) (bool, error) {
if i.FilePath() == "C" {
return false, 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
Expand All @@ -111,53 +149,79 @@ func (p *Nolint) shouldPassIssue(i *result.Issue) (bool, error) {
return false, nil
}

for _, comment := range fd.comments {
if comment.line != i.Line() {
continue
for _, ir := range fd.ignoredRanges {
if ir.doesMatch(i) {
return false, nil
}
}

if len(comment.linters) == 0 {
return false, nil // skip all linters
}
return true, nil
}

for _, linter := range comment.linters {
if i.FromLinter == linter {
return false, 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 true, nil
return e
}

func extractFileComments(fset *token.FileSet, comments ...*ast.CommentGroup) fileComments {
ret := fileComments{}
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
}

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
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)
}
ret = append(ret, comment{
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,
line: pos.Line,
})
}
}
Expand Down
98 changes: 98 additions & 0 deletions pkg/result/processors/nolint_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"testing"

"github.com/golangci/golangci-lint/pkg/result"
"github.com/stretchr/testify/assert"
)

func newNolintFileIssue(line int, fromLinter string) result.Issue {
Expand All @@ -20,6 +21,8 @@ func newNolintFileIssue(line int, fromLinter string) result.Issue {

func TestNolint(t *testing.T) {
p := NewNolint(token.NewFileSet())

// test inline comments
processAssertEmpty(t, p, newNolintFileIssue(3, "gofmt"))
processAssertEmpty(t, p, newNolintFileIssue(3, "gofmt")) // check cached is ok
processAssertSame(t, p, newNolintFileIssue(3, "gofmtA")) // check different name
Expand All @@ -35,6 +38,42 @@ func TestNolint(t *testing.T) {
processAssertEmpty(t, p, newNolintFileIssue(7, "any"))

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) {
Expand All @@ -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 changes: 38 additions & 0 deletions pkg/result/processors/testdata/nolint.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,41 @@ var nolintSpace int // nolint: gofmt
var nolintSpaces int //nolint: gofmt, govet
var nolintAll int // nolint
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
}
2 changes: 1 addition & 1 deletion pkg/result/processors/testdata/nolint_autogenerated.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading