Skip to content

Commit 110f584

Browse files
authored
Merge pull request #71 from golangci/feature/full-support-of-nolint-directives
#65, #68: make //nolint processing like in gometalinter
2 parents af77076 + 8a9b3a5 commit 110f584

File tree

7 files changed

+261
-42
lines changed

7 files changed

+261
-42
lines changed

Diff for: pkg/result/processors/cgo.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ func (p Cgo) Process(issues []result.Issue) ([]result.Issue, error) {
2323
return filterIssues(issues, func(i *result.Issue) bool {
2424
// some linters (.e.g gas, deadcode) return incorrect filepaths for cgo issues,
2525
// it breaks next processing, so skip them
26-
return !strings.HasSuffix(i.FilePath(), "/C")
26+
return i.FilePath() != "C" && !strings.HasSuffix(i.FilePath(), "/C")
2727
}), nil
2828
}
2929

Diff for: pkg/result/processors/nolint.go

+103-39
Original file line numberDiff line numberDiff line change
@@ -8,20 +8,45 @@ import (
88
"go/parser"
99
"go/token"
1010
"io/ioutil"
11+
"sort"
1112
"strings"
1213

1314
"github.com/golangci/golangci-lint/pkg/result"
1415
)
1516

16-
type comment struct {
17+
type ignoredRange struct {
1718
linters []string
18-
line int
19+
result.Range
20+
col int
1921
}
20-
type fileComments []comment
22+
23+
func (i *ignoredRange) isAdjacent(col, start int) bool {
24+
return col == i.col && i.To == start-1
25+
}
26+
27+
func (i *ignoredRange) doesMatch(issue *result.Issue) bool {
28+
if issue.Line() < i.From || issue.Line() > i.To {
29+
return false
30+
}
31+
32+
if len(i.linters) == 0 {
33+
return true
34+
}
35+
36+
for _, l := range i.linters {
37+
if l == issue.FromLinter {
38+
return true
39+
}
40+
}
41+
42+
return false
43+
}
44+
2145
type fileData struct {
22-
comments fileComments
23-
isGenerated bool
46+
ignoredRanges []ignoredRange
47+
isGenerated bool
2448
}
49+
2550
type filesCache map[string]*fileData
2651

2752
type Nolint struct {
@@ -93,15 +118,28 @@ func (p *Nolint) getOrCreateFileData(i *result.Issue) (*fileData, error) {
93118
return nil, fmt.Errorf("can't parse file %s", i.FilePath())
94119
}
95120

96-
fd.comments = extractFileComments(p.fset, file.Comments...)
121+
fd.ignoredRanges = buildIgnoredRangesForFile(file, p.fset)
97122
return fd, nil
98123
}
99124

100-
func (p *Nolint) shouldPassIssue(i *result.Issue) (bool, error) {
101-
if i.FilePath() == "C" {
102-
return false, nil
125+
func buildIgnoredRangesForFile(f *ast.File, fset *token.FileSet) []ignoredRange {
126+
inlineRanges := extractFileCommentsInlineRanges(fset, f.Comments...)
127+
128+
if len(inlineRanges) == 0 {
129+
return nil
103130
}
104131

132+
e := rangeExpander{
133+
fset: fset,
134+
ranges: ignoredRanges(inlineRanges),
135+
}
136+
137+
ast.Walk(&e, f)
138+
139+
return e.ranges
140+
}
141+
142+
func (p *Nolint) shouldPassIssue(i *result.Issue) (bool, error) {
105143
fd, err := p.getOrCreateFileData(i)
106144
if err != nil {
107145
return false, err
@@ -111,53 +149,79 @@ func (p *Nolint) shouldPassIssue(i *result.Issue) (bool, error) {
111149
return false, nil
112150
}
113151

114-
for _, comment := range fd.comments {
115-
if comment.line != i.Line() {
116-
continue
152+
for _, ir := range fd.ignoredRanges {
153+
if ir.doesMatch(i) {
154+
return false, nil
117155
}
156+
}
118157

119-
if len(comment.linters) == 0 {
120-
return false, nil // skip all linters
121-
}
158+
return true, nil
159+
}
122160

123-
for _, linter := range comment.linters {
124-
if i.FromLinter == linter {
125-
return false, nil
126-
}
161+
type ignoredRanges []ignoredRange
162+
163+
func (ir ignoredRanges) Len() int { return len(ir) }
164+
func (ir ignoredRanges) Swap(i, j int) { ir[i], ir[j] = ir[j], ir[i] }
165+
func (ir ignoredRanges) Less(i, j int) bool { return ir[i].To < ir[j].To }
166+
167+
type rangeExpander struct {
168+
fset *token.FileSet
169+
ranges ignoredRanges
170+
}
171+
172+
func (e *rangeExpander) Visit(node ast.Node) ast.Visitor {
173+
if node == nil {
174+
return e
175+
}
176+
177+
startPos := e.fset.Position(node.Pos())
178+
start := startPos.Line
179+
end := e.fset.Position(node.End()).Line
180+
found := sort.Search(len(e.ranges), func(i int) bool {
181+
return e.ranges[i].To+1 >= start
182+
})
183+
184+
if found < len(e.ranges) && e.ranges[found].isAdjacent(startPos.Column, start) {
185+
r := &e.ranges[found]
186+
if r.From > start {
187+
r.From = start
188+
}
189+
if r.To < end {
190+
r.To = end
127191
}
128192
}
129193

130-
return true, nil
194+
return e
131195
}
132196

133-
func extractFileComments(fset *token.FileSet, comments ...*ast.CommentGroup) fileComments {
134-
ret := fileComments{}
197+
func extractFileCommentsInlineRanges(fset *token.FileSet, comments ...*ast.CommentGroup) []ignoredRange {
198+
var ret []ignoredRange
135199
for _, g := range comments {
136200
for _, c := range g.List {
137201
text := strings.TrimLeft(c.Text, "/ ")
138202
if !strings.HasPrefix(text, "nolint") {
139203
continue
140204
}
141205

142-
pos := fset.Position(g.Pos())
143-
if !strings.HasPrefix(text, "nolint:") { // ignore all linters
144-
ret = append(ret, comment{
145-
line: pos.Line,
146-
})
147-
continue
148-
}
149-
150-
// ignore specific linters
151206
var linters []string
152-
text = strings.Split(text, "//")[0] // allow another comment after this comment
153-
linterItems := strings.Split(strings.TrimPrefix(text, "nolint:"), ",")
154-
for _, linter := range linterItems {
155-
linterName := strings.TrimSpace(linter) // TODO: validate it here
156-
linters = append(linters, linterName)
157-
}
158-
ret = append(ret, comment{
207+
if strings.HasPrefix(text, "nolint:") {
208+
// ignore specific linters
209+
text = strings.Split(text, "//")[0] // allow another comment after this comment
210+
linterItems := strings.Split(strings.TrimPrefix(text, "nolint:"), ",")
211+
for _, linter := range linterItems {
212+
linterName := strings.TrimSpace(linter) // TODO: validate it here
213+
linters = append(linters, linterName)
214+
}
215+
} // else ignore all linters
216+
217+
pos := fset.Position(g.Pos())
218+
ret = append(ret, ignoredRange{
219+
Range: result.Range{
220+
From: pos.Line,
221+
To: fset.Position(g.End()).Line,
222+
},
223+
col: pos.Column,
159224
linters: linters,
160-
line: pos.Line,
161225
})
162226
}
163227
}

Diff for: pkg/result/processors/nolint_test.go

+98
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"testing"
77

88
"github.com/golangci/golangci-lint/pkg/result"
9+
"github.com/stretchr/testify/assert"
910
)
1011

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

2122
func TestNolint(t *testing.T) {
2223
p := NewNolint(token.NewFileSet())
24+
25+
// test inline comments
2326
processAssertEmpty(t, p, newNolintFileIssue(3, "gofmt"))
2427
processAssertEmpty(t, p, newNolintFileIssue(3, "gofmt")) // check cached is ok
2528
processAssertSame(t, p, newNolintFileIssue(3, "gofmtA")) // check different name
@@ -35,6 +38,42 @@ func TestNolint(t *testing.T) {
3538
processAssertEmpty(t, p, newNolintFileIssue(7, "any"))
3639

3740
processAssertSame(t, p, newNolintFileIssue(1, "golint")) // no directive
41+
42+
// test preceding comments
43+
processAssertEmpty(t, p, newNolintFileIssue(10, "any")) // preceding comment for var
44+
processAssertEmpty(t, p, newNolintFileIssue(9, "any")) // preceding comment for var itself
45+
46+
processAssertSame(t, p, newNolintFileIssue(14, "any")) // preceding comment with extra \n
47+
processAssertEmpty(t, p, newNolintFileIssue(12, "any")) // preceding comment with extra \n itself
48+
49+
processAssertSame(t, p, newNolintFileIssue(17, "any")) // preceding comment on different column
50+
processAssertEmpty(t, p, newNolintFileIssue(16, "any")) // preceding comment on different column itself
51+
52+
// preceding comment for func name and comment itself
53+
for i := 19; i <= 23; i++ {
54+
processAssertEmpty(t, p, newNolintFileIssue(i, "any"))
55+
}
56+
57+
processAssertSame(t, p, newNolintFileIssue(24, "any")) // right after func
58+
59+
// preceding multiline comment: last line
60+
for i := 25; i <= 30; i++ {
61+
processAssertEmpty(t, p, newNolintFileIssue(i, "any"))
62+
}
63+
64+
processAssertSame(t, p, newNolintFileIssue(31, "any")) // between funcs
65+
66+
// preceding multiline comment: first line
67+
for i := 32; i <= 37; i++ {
68+
processAssertEmpty(t, p, newNolintFileIssue(i, "any"))
69+
}
70+
71+
processAssertSame(t, p, newNolintFileIssue(38, "any")) // between funcs
72+
73+
// preceding multiline comment: medium line
74+
for i := 39; i <= 45; i++ {
75+
processAssertEmpty(t, p, newNolintFileIssue(i, "any"))
76+
}
3877
}
3978

4079
func TestNoIssuesInAutogeneratedFiles(t *testing.T) {
@@ -56,3 +95,62 @@ func TestNoIssuesInAutogeneratedFiles(t *testing.T) {
5695
})
5796
}
5897
}
98+
99+
func TestIgnoredRangeMatches(t *testing.T) {
100+
var testcases = []struct {
101+
doc string
102+
issue result.Issue
103+
linters []string
104+
expected bool
105+
}{
106+
{
107+
doc: "unmatched line",
108+
issue: result.Issue{
109+
Pos: token.Position{
110+
Line: 100,
111+
},
112+
},
113+
},
114+
{
115+
doc: "matched line, all linters",
116+
issue: result.Issue{
117+
Pos: token.Position{
118+
Line: 5,
119+
},
120+
},
121+
expected: true,
122+
},
123+
{
124+
doc: "matched line, unmatched linter",
125+
issue: result.Issue{
126+
Pos: token.Position{
127+
Line: 5,
128+
},
129+
},
130+
linters: []string{"vet"},
131+
},
132+
{
133+
doc: "matched line and linters",
134+
issue: result.Issue{
135+
Pos: token.Position{
136+
Line: 20,
137+
},
138+
FromLinter: "vet",
139+
},
140+
linters: []string{"vet"},
141+
expected: true,
142+
},
143+
}
144+
145+
for _, testcase := range testcases {
146+
ir := ignoredRange{
147+
col: 20,
148+
Range: result.Range{
149+
From: 5,
150+
To: 20,
151+
},
152+
linters: testcase.linters,
153+
}
154+
assert.Equal(t, testcase.expected, ir.doesMatch(&testcase.issue), testcase.doc)
155+
}
156+
}

Diff for: pkg/result/processors/testdata/nolint.go

+38
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,41 @@ var nolintSpace int // nolint: gofmt
55
var nolintSpaces int //nolint: gofmt, govet
66
var nolintAll int // nolint
77
var nolintAndAppendix int // nolint // another comment
8+
9+
//nolint
10+
var nolintVarByPrecedingComment int
11+
12+
//nolint
13+
14+
var dontNolintVarByPrecedingCommentBecauseOfNewLine int
15+
16+
var nolintPrecedingVar string //nolint
17+
var dontNolintVarByPrecedingCommentBecauseOfDifferentColumn int
18+
19+
//nolint
20+
func nolintFuncByPrecedingComment() *string {
21+
xv := "v"
22+
return &xv
23+
}
24+
25+
//nolint
26+
// second line
27+
func nolintFuncByPrecedingMultilineComment1() *string {
28+
xv := "v"
29+
return &xv
30+
}
31+
32+
// first line
33+
//nolint
34+
func nolintFuncByPrecedingMultilineComment2() *string {
35+
xv := "v"
36+
return &xv
37+
}
38+
39+
// first line
40+
//nolint
41+
// third line
42+
func nolintFuncByPrecedingMultilineComment3() *string {
43+
xv := "v"
44+
return &xv
45+
}

Diff for: pkg/result/processors/testdata/nolint_autogenerated.go

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Diff for: pkg/result/processors/testdata/nolint_autogenerated_alt_hdr2.go

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)