Skip to content

Commit c69dde5

Browse files
committed
Add fix option for nolintlint
Repair whitespace issues and remove nolint statements that aren't being triggered
1 parent eace6a1 commit c69dde5

File tree

10 files changed

+271
-64
lines changed

10 files changed

+271
-64
lines changed

pkg/golinters/nolintlint.go

+1
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ func NewNoLintLint() *goanalysis.Linter {
7272
Pos: i.Position(),
7373
ExpectNoLint: expectNoLint,
7474
ExpectedNoLintLinter: expectedNolintLinter,
75+
Replacement: i.Replacement(),
7576
}
7677
res = append(res, goanalysis.NewIssue(issue, pass))
7778
}

pkg/golinters/nolintlint/nolintlint.go

+64-18
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,21 @@ import (
88
"regexp"
99
"strings"
1010
"unicode"
11+
12+
"github.com/golangci/golangci-lint/pkg/result"
1113
)
1214

1315
type BaseIssue struct {
1416
fullDirective string
1517
directiveWithOptionalLeadingSpace string
1618
position token.Position
19+
replacement *result.Replacement
1720
}
1821

19-
func (b BaseIssue) Position() token.Position {
20-
return b.position
22+
func (b BaseIssue) Position() token.Position { return b.position }
23+
24+
func (b BaseIssue) Replacement() *result.Replacement {
25+
return b.replacement
2126
}
2227

2328
type ExtraLeadingSpace struct {
@@ -85,7 +90,7 @@ type UnusedCandidate struct {
8590
func (i UnusedCandidate) Details() string {
8691
details := fmt.Sprintf("directive `%s` is unused", i.fullDirective)
8792
if i.ExpectedLinter != "" {
88-
details += fmt.Sprintf(" for linter %s", i.ExpectedLinter)
93+
details += fmt.Sprintf(" for linter %q", i.ExpectedLinter)
8994
}
9095
return details
9196
}
@@ -100,6 +105,7 @@ type Issue interface {
100105
Details() string
101106
Position() token.Position
102107
String() string
108+
Replacement() *result.Replacement
103109
}
104110

105111
type Needs uint
@@ -115,7 +121,7 @@ const (
115121
var commentPattern = regexp.MustCompile(`^//\s*(nolint)(:\s*[\w-]+\s*(?:,\s*[\w-]+\s*)*)?\b`)
116122

117123
// matches a complete nolint directive
118-
var fullDirectivePattern = regexp.MustCompile(`^//\s*nolint(:\s*[\w-]+\s*(?:,\s*[\w-]+\s*)*)?\s*(//.*)?\s*\n?$`)
124+
var fullDirectivePattern = regexp.MustCompile(`^//\s*nolint(?::(\s*[\w-]+\s*(?:,\s*[\w-]+\s*)*))?\s*(//.*)?\s*\n?$`)
119125

120126
type Linter struct {
121127
excludes []string // lists individual linters that don't require explanations
@@ -164,19 +170,34 @@ func (l Linter) Run(fset *token.FileSet, nodes ...ast.Node) ([]Issue, error) {
164170
directiveWithOptionalLeadingSpace = "// " + strings.TrimSpace(split[1])
165171
}
166172

173+
pos := fset.Position(comment.Pos())
174+
end := fset.Position(comment.End())
175+
167176
base := BaseIssue{
168177
fullDirective: comment.Text,
169178
directiveWithOptionalLeadingSpace: directiveWithOptionalLeadingSpace,
170-
position: fset.Position(comment.Pos()),
179+
position: pos,
171180
}
172181

173182
// check for, report and eliminate leading spaces so we can check for other issues
174-
if len(leadingSpace) > 1 {
175-
issues = append(issues, ExtraLeadingSpace{BaseIssue: base})
176-
}
177-
178-
if (l.needs&NeedsMachineOnly) != 0 && len(leadingSpace) > 0 {
179-
issues = append(issues, NotMachine{BaseIssue: base})
183+
if len(leadingSpace) > 0 {
184+
removeWhitespace := &result.Replacement{
185+
Inline: &result.InlineFix{
186+
StartCol: pos.Column + 1,
187+
Length: len(leadingSpace),
188+
NewString: "",
189+
},
190+
}
191+
if (l.needs & NeedsMachineOnly) != 0 {
192+
issue := NotMachine{BaseIssue: base}
193+
issue.BaseIssue.replacement = removeWhitespace
194+
issues = append(issues, issue)
195+
} else if len(leadingSpace) > 1 {
196+
issue := ExtraLeadingSpace{BaseIssue: base}
197+
issue.BaseIssue.replacement = removeWhitespace
198+
issue.BaseIssue.replacement.Inline.NewString = " " // assume a single space was intended
199+
issues = append(issues, issue)
200+
}
180201
}
181202

182203
fullMatches := fullDirectivePattern.FindStringSubmatch(comment.Text)
@@ -187,14 +208,22 @@ func (l Linter) Run(fset *token.FileSet, nodes ...ast.Node) ([]Issue, error) {
187208

188209
lintersText, explanation := fullMatches[1], fullMatches[2]
189210
var linters []string
211+
var linterRange []result.Range
190212
if len(lintersText) > 0 {
191-
lls := strings.Split(lintersText[1:], ",")
213+
lls := strings.Split(lintersText, ",")
192214
linters = make([]string, 0, len(lls))
193-
for _, ll := range lls {
194-
ll = strings.TrimSpace(ll)
195-
if ll != "" {
196-
linters = append(linters, ll)
215+
rangeStart := (pos.Column - 1) + len("//") + len(leadingSpace) + len("nolint:")
216+
for i, ll := range lls {
217+
rangeEnd := rangeStart + len(ll)
218+
if i < len(lls)-1 {
219+
rangeEnd++ // include trailing comma
220+
}
221+
trimmedLinterName := strings.TrimSpace(ll)
222+
if trimmedLinterName != "" {
223+
linters = append(linters, trimmedLinterName)
224+
linterRange = append(linterRange, result.Range{From: rangeStart, To: rangeEnd})
197225
}
226+
rangeStart = rangeEnd
198227
}
199228
}
200229

@@ -206,11 +235,28 @@ func (l Linter) Run(fset *token.FileSet, nodes ...ast.Node) ([]Issue, error) {
206235

207236
// when detecting unused directives, we send all the directives through and filter them out in the nolint processor
208237
if (l.needs & NeedsUnused) != 0 {
238+
removeNolintCompletely := &result.Replacement{
239+
Inline: &result.InlineFix{
240+
StartCol: pos.Column - 1,
241+
Length: end.Column - pos.Column,
242+
NewString: "",
243+
},
244+
}
245+
209246
if len(linters) == 0 {
210-
issues = append(issues, UnusedCandidate{BaseIssue: base})
247+
issue := UnusedCandidate{BaseIssue: base}
248+
issue.replacement = removeNolintCompletely
249+
issues = append(issues, issue)
211250
} else {
212251
for _, linter := range linters {
213-
issues = append(issues, UnusedCandidate{BaseIssue: base, ExpectedLinter: linter})
252+
issue := UnusedCandidate{BaseIssue: base, ExpectedLinter: linter}
253+
// only offer replacement if there is a single linter
254+
// because of issues around commas and the possibility of all
255+
// linters being removed
256+
if len(linters) == 1 {
257+
issue.replacement = removeNolintCompletely
258+
}
259+
issues = append(issues, issue)
214260
}
215261
}
216262
}

pkg/golinters/nolintlint/nolintlint_test.go

+112-20
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,22 @@ import (
77

88
"github.com/stretchr/testify/assert"
99
"github.com/stretchr/testify/require"
10+
11+
"github.com/golangci/golangci-lint/pkg/result"
1012
)
1113

1214
//nolint:funlen
1315
func TestNoLintLint(t *testing.T) {
16+
type issueWithReplacement struct {
17+
issue string
18+
replacement *result.Replacement
19+
}
1420
testCases := []struct {
1521
desc string
1622
needs Needs
1723
excludes []string
1824
contents string
19-
expected []string
25+
expected []issueWithReplacement
2026
}{
2127
{
2228
desc: "when no explanation is provided",
@@ -33,11 +39,11 @@ func foo() {
3339
good() //nolint // this is ok
3440
other() //nolintother
3541
}`,
36-
expected: []string{
37-
"directive `//nolint` should provide explanation such as `//nolint // this is why` at testing.go:5:1",
38-
"directive `//nolint` should provide explanation such as `//nolint // this is why` at testing.go:7:9",
39-
"directive `//nolint //` should provide explanation such as `//nolint // this is why` at testing.go:8:9",
40-
"directive `//nolint // ` should provide explanation such as `//nolint // this is why` at testing.go:9:9",
42+
expected: []issueWithReplacement{
43+
{"directive `//nolint` should provide explanation such as `//nolint // this is why` at testing.go:5:1", nil},
44+
{"directive `//nolint` should provide explanation such as `//nolint // this is why` at testing.go:7:9", nil},
45+
{"directive `//nolint //` should provide explanation such as `//nolint // this is why` at testing.go:8:9", nil},
46+
{"directive `//nolint // ` should provide explanation such as `//nolint // this is why` at testing.go:9:9", nil},
4147
},
4248
},
4349
{
@@ -50,8 +56,8 @@ package bar
5056
//nolint // this is ok
5157
//nolint:dupl
5258
func foo() {}`,
53-
expected: []string{
54-
"directive `//nolint:dupl` should provide explanation such as `//nolint:dupl // this is why` at testing.go:6:1",
59+
expected: []issueWithReplacement{
60+
{"directive `//nolint:dupl` should provide explanation such as `//nolint:dupl // this is why` at testing.go:6:1", nil},
5561
},
5662
},
5763
{
@@ -76,9 +82,9 @@ func foo() {
7682
bad() //nolint
7783
bad() // nolint // because
7884
}`,
79-
expected: []string{
80-
"directive `//nolint` should mention specific linter such as `//nolint:my-linter` at testing.go:6:9",
81-
"directive `// nolint // because` should mention specific linter such as `// nolint:my-linter` at testing.go:7:9",
85+
expected: []issueWithReplacement{
86+
{"directive `//nolint` should mention specific linter such as `//nolint:my-linter` at testing.go:6:9", nil},
87+
{"directive `// nolint // because` should mention specific linter such as `// nolint:my-linter` at testing.go:7:9", nil},
8288
},
8389
},
8490
{
@@ -91,8 +97,17 @@ func foo() {
9197
bad() // nolint
9298
good() //nolint
9399
}`,
94-
expected: []string{
95-
"directive `// nolint` should be written without leading space as `//nolint` at testing.go:5:9",
100+
expected: []issueWithReplacement{
101+
{
102+
"directive `// nolint` should be written without leading space as `//nolint` at testing.go:5:9",
103+
&result.Replacement{
104+
Inline: &result.InlineFix{
105+
StartCol: 10,
106+
Length: 1,
107+
NewString: "",
108+
},
109+
},
110+
},
96111
},
97112
},
98113
{
@@ -104,8 +119,17 @@ func foo() {
104119
bad() // nolint
105120
good() // nolint
106121
}`,
107-
expected: []string{
108-
"directive `// nolint` should not have more than one leading space at testing.go:5:9",
122+
expected: []issueWithReplacement{
123+
{
124+
"directive `// nolint` should not have more than one leading space at testing.go:5:9",
125+
&result.Replacement{
126+
Inline: &result.InlineFix{
127+
StartCol: 10,
128+
Length: 2,
129+
NewString: " ",
130+
},
131+
},
132+
},
109133
},
110134
},
111135
{
@@ -119,8 +143,8 @@ func foo() {
119143
good() // nolint: linter1,linter2
120144
good() // nolint: linter1, linter2
121145
}`,
122-
expected: []string{
123-
"directive `// nolint:linter1 linter2` should match `// nolint[:<comma-separated-linters>] [// <explanation>]` at testing.go:6:9", //nolint:lll // this is a string
146+
expected: []issueWithReplacement{
147+
{"directive `// nolint:linter1 linter2` should match `// nolint[:<comma-separated-linters>] [// <explanation>]` at testing.go:6:9", nil}, //nolint:lll // this is a string
124148
},
125149
},
126150
{
@@ -133,6 +157,70 @@ func foo() {
133157
// something else
134158
}`,
135159
},
160+
{
161+
desc: "needs unused without specific linter generates replacement",
162+
needs: NeedsUnused,
163+
contents: `
164+
package bar
165+
166+
func foo() {
167+
bad() //nolint
168+
}`,
169+
expected: []issueWithReplacement{
170+
{
171+
"directive `//nolint` is unused at testing.go:5:9",
172+
&result.Replacement{
173+
Inline: &result.InlineFix{
174+
StartCol: 8,
175+
Length: 8,
176+
NewString: "",
177+
},
178+
},
179+
},
180+
},
181+
},
182+
{
183+
desc: "needs unused with one specific linter generates replacement",
184+
needs: NeedsUnused,
185+
contents: `
186+
package bar
187+
188+
func foo() {
189+
bad() //nolint:somelinter
190+
}`,
191+
expected: []issueWithReplacement{
192+
{
193+
"directive `//nolint:somelinter` is unused for linter \"somelinter\" at testing.go:5:9",
194+
&result.Replacement{
195+
Inline: &result.InlineFix{
196+
StartCol: 8,
197+
Length: 19,
198+
NewString: "",
199+
},
200+
},
201+
},
202+
},
203+
},
204+
{
205+
desc: "needs unused with multiple specific linters does not generate replacements",
206+
needs: NeedsUnused,
207+
contents: `
208+
package bar
209+
210+
func foo() {
211+
bad() //nolint:linter1,linter2
212+
}`,
213+
expected: []issueWithReplacement{
214+
{
215+
"directive `//nolint:linter1,linter2` is unused for linter \"linter1\" at testing.go:5:9",
216+
nil,
217+
},
218+
{
219+
"directive `//nolint:linter1,linter2` is unused for linter \"linter2\" at testing.go:5:9",
220+
nil,
221+
},
222+
},
223+
},
136224
}
137225

138226
for _, test := range testCases {
@@ -149,12 +237,16 @@ func foo() {
149237
actualIssues, err := linter.Run(fset, expr)
150238
require.NoError(t, err)
151239

152-
actualIssueStrs := make([]string, 0, len(actualIssues))
240+
actualIssuesWithReplacements := make([]issueWithReplacement, 0, len(actualIssues))
153241
for _, i := range actualIssues {
154-
actualIssueStrs = append(actualIssueStrs, i.String())
242+
actualIssuesWithReplacements = append(actualIssuesWithReplacements, issueWithReplacement{
243+
issue: i.String(),
244+
replacement: i.Replacement(),
245+
})
155246
}
156247

157-
assert.ElementsMatch(t, test.expected, actualIssueStrs, "expected %s \nbut got %s", test.expected, actualIssues)
248+
assert.ElementsMatch(t, test.expected, actualIssuesWithReplacements,
249+
"expected %s \nbut got %s", test.expected, actualIssuesWithReplacements)
158250
})
159251
}
160252
}

pkg/result/issue.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ type Range struct {
1414

1515
type Replacement struct {
1616
NeedOnlyDelete bool // need to delete all lines of the issue without replacement with new lines
17-
NewLines []string // is NeedDelete is false it's the replacement lines
17+
NewLines []string // if NeedDelete is false it's the replacement lines
1818
Inline *InlineFix
1919
}
2020

0 commit comments

Comments
 (0)