Skip to content

Commit e1a734e

Browse files
authored
nolintlint: allow to fix //nolint lines (#1583)
1 parent d28c666 commit e1a734e

File tree

10 files changed

+330
-117
lines changed

10 files changed

+330
-117
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

+122-71
Original file line numberDiff line numberDiff line change
@@ -8,18 +8,25 @@ 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

1922
func (b BaseIssue) Position() token.Position {
2023
return b.position
2124
}
2225

26+
func (b BaseIssue) Replacement() *result.Replacement {
27+
return b.replacement
28+
}
29+
2330
type ExtraLeadingSpace struct {
2431
BaseIssue
2532
}
@@ -85,7 +92,7 @@ type UnusedCandidate struct {
8592
func (i UnusedCandidate) Details() string {
8693
details := fmt.Sprintf("directive `%s` is unused", i.fullDirective)
8794
if i.ExpectedLinter != "" {
88-
details += fmt.Sprintf(" for linter %s", i.ExpectedLinter)
95+
details += fmt.Sprintf(" for linter %q", i.ExpectedLinter)
8996
}
9097
return details
9198
}
@@ -100,6 +107,7 @@ type Issue interface {
100107
Details() string
101108
Position() token.Position
102109
String() string
110+
Replacement() *result.Replacement
103111
}
104112

105113
type Needs uint
@@ -115,7 +123,7 @@ const (
115123
var commentPattern = regexp.MustCompile(`^//\s*(nolint)(:\s*[\w-]+\s*(?:,\s*[\w-]+\s*)*)?\b`)
116124

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

120128
type Linter struct {
121129
excludes []string // lists individual linters that don't require explanations
@@ -143,96 +151,139 @@ func (l Linter) Run(fset *token.FileSet, nodes ...ast.Node) ([]Issue, error) {
143151
var issues []Issue
144152

145153
for _, node := range nodes {
146-
if file, ok := node.(*ast.File); ok {
147-
for _, c := range file.Comments {
148-
for _, comment := range c.List {
149-
if !commentPattern.MatchString(comment.Text) {
150-
continue
151-
}
154+
file, ok := node.(*ast.File)
155+
if !ok {
156+
continue
157+
}
152158

153-
// check for a space between the "//" and the directive
154-
leadingSpaceMatches := leadingSpacePattern.FindStringSubmatch(comment.Text)
159+
for _, c := range file.Comments {
160+
for _, comment := range c.List {
161+
if !commentPattern.MatchString(comment.Text) {
162+
continue
163+
}
155164

156-
var leadingSpace string
157-
if len(leadingSpaceMatches) > 0 {
158-
leadingSpace = leadingSpaceMatches[1]
159-
}
165+
// check for a space between the "//" and the directive
166+
leadingSpaceMatches := leadingSpacePattern.FindStringSubmatch(comment.Text)
160167

161-
directiveWithOptionalLeadingSpace := comment.Text
162-
if len(leadingSpace) > 0 {
163-
split := strings.Split(strings.SplitN(comment.Text, ":", 2)[0], "//")
164-
directiveWithOptionalLeadingSpace = "// " + strings.TrimSpace(split[1])
165-
}
168+
var leadingSpace string
169+
if len(leadingSpaceMatches) > 0 {
170+
leadingSpace = leadingSpaceMatches[1]
171+
}
166172

167-
base := BaseIssue{
168-
fullDirective: comment.Text,
169-
directiveWithOptionalLeadingSpace: directiveWithOptionalLeadingSpace,
170-
position: fset.Position(comment.Pos()),
171-
}
173+
directiveWithOptionalLeadingSpace := comment.Text
174+
if len(leadingSpace) > 0 {
175+
split := strings.Split(strings.SplitN(comment.Text, ":", 2)[0], "//")
176+
directiveWithOptionalLeadingSpace = "// " + strings.TrimSpace(split[1])
177+
}
172178

173-
// 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-
}
179+
pos := fset.Position(comment.Pos())
180+
end := fset.Position(comment.End())
177181

178-
if (l.needs&NeedsMachineOnly) != 0 && len(leadingSpace) > 0 {
179-
issues = append(issues, NotMachine{BaseIssue: base})
180-
}
182+
base := BaseIssue{
183+
fullDirective: comment.Text,
184+
directiveWithOptionalLeadingSpace: directiveWithOptionalLeadingSpace,
185+
position: pos,
186+
}
181187

182-
fullMatches := fullDirectivePattern.FindStringSubmatch(comment.Text)
183-
if len(fullMatches) == 0 {
184-
issues = append(issues, ParseError{BaseIssue: base})
185-
continue
188+
// check for, report and eliminate leading spaces so we can check for other issues
189+
if len(leadingSpace) > 0 {
190+
removeWhitespace := &result.Replacement{
191+
Inline: &result.InlineFix{
192+
StartCol: pos.Column + 1,
193+
Length: len(leadingSpace),
194+
NewString: "",
195+
},
196+
}
197+
if (l.needs & NeedsMachineOnly) != 0 {
198+
issue := NotMachine{BaseIssue: base}
199+
issue.BaseIssue.replacement = removeWhitespace
200+
issues = append(issues, issue)
201+
} else if len(leadingSpace) > 1 {
202+
issue := ExtraLeadingSpace{BaseIssue: base}
203+
issue.BaseIssue.replacement = removeWhitespace
204+
issue.BaseIssue.replacement.Inline.NewString = " " // assume a single space was intended
205+
issues = append(issues, issue)
186206
}
207+
}
187208

188-
lintersText, explanation := fullMatches[1], fullMatches[2]
189-
var linters []string
190-
if len(lintersText) > 0 {
191-
lls := strings.Split(lintersText[1:], ",")
192-
linters = make([]string, 0, len(lls))
193-
for _, ll := range lls {
194-
ll = strings.TrimSpace(ll)
195-
if ll != "" {
196-
linters = append(linters, ll)
197-
}
209+
fullMatches := fullDirectivePattern.FindStringSubmatch(comment.Text)
210+
if len(fullMatches) == 0 {
211+
issues = append(issues, ParseError{BaseIssue: base})
212+
continue
213+
}
214+
215+
lintersText, explanation := fullMatches[1], fullMatches[2]
216+
var linters []string
217+
var linterRange []result.Range
218+
if len(lintersText) > 0 {
219+
lls := strings.Split(lintersText, ",")
220+
linters = make([]string, 0, len(lls))
221+
rangeStart := (pos.Column - 1) + len("//") + len(leadingSpace) + len("nolint:")
222+
for i, ll := range lls {
223+
rangeEnd := rangeStart + len(ll)
224+
if i < len(lls)-1 {
225+
rangeEnd++ // include trailing comma
198226
}
227+
trimmedLinterName := strings.TrimSpace(ll)
228+
if trimmedLinterName != "" {
229+
linters = append(linters, trimmedLinterName)
230+
linterRange = append(linterRange, result.Range{From: rangeStart, To: rangeEnd})
231+
}
232+
rangeStart = rangeEnd
199233
}
234+
}
200235

201-
if (l.needs & NeedsSpecific) != 0 {
202-
if len(linters) == 0 {
203-
issues = append(issues, NotSpecific{BaseIssue: base})
204-
}
236+
if (l.needs & NeedsSpecific) != 0 {
237+
if len(linters) == 0 {
238+
issues = append(issues, NotSpecific{BaseIssue: base})
205239
}
240+
}
206241

207-
// when detecting unused directives, we send all the directives through and filter them out in the nolint processor
208-
if (l.needs & NeedsUnused) != 0 {
209-
if len(linters) == 0 {
210-
issues = append(issues, UnusedCandidate{BaseIssue: base})
211-
} else {
212-
for _, linter := range linters {
213-
issues = append(issues, UnusedCandidate{BaseIssue: base, ExpectedLinter: linter})
214-
}
215-
}
242+
// when detecting unused directives, we send all the directives through and filter them out in the nolint processor
243+
if (l.needs & NeedsUnused) != 0 {
244+
removeNolintCompletely := &result.Replacement{
245+
Inline: &result.InlineFix{
246+
StartCol: pos.Column - 1,
247+
Length: end.Column - pos.Column,
248+
NewString: "",
249+
},
216250
}
217251

218-
if (l.needs&NeedsExplanation) != 0 && (explanation == "" || strings.TrimSpace(explanation) == "//") {
219-
needsExplanation := len(linters) == 0 // if no linters are mentioned, we must have explanation
220-
// otherwise, check if we are excluding all of the mentioned linters
221-
for _, ll := range linters {
222-
if !l.excludeByLinter[ll] { // if a linter does require explanation
223-
needsExplanation = true
224-
break
252+
if len(linters) == 0 {
253+
issue := UnusedCandidate{BaseIssue: base}
254+
issue.replacement = removeNolintCompletely
255+
issues = append(issues, issue)
256+
} else {
257+
for _, linter := range linters {
258+
issue := UnusedCandidate{BaseIssue: base, ExpectedLinter: linter}
259+
// only offer replacement if there is a single linter
260+
// because of issues around commas and the possibility of all
261+
// linters being removed
262+
if len(linters) == 1 {
263+
issue.replacement = removeNolintCompletely
225264
}
265+
issues = append(issues, issue)
226266
}
267+
}
268+
}
227269

228-
if needsExplanation {
229-
fullDirectiveWithoutExplanation := trailingBlankExplanation.ReplaceAllString(comment.Text, "")
230-
issues = append(issues, NoExplanation{
231-
BaseIssue: base,
232-
fullDirectiveWithoutExplanation: fullDirectiveWithoutExplanation,
233-
})
270+
if (l.needs&NeedsExplanation) != 0 && (explanation == "" || strings.TrimSpace(explanation) == "//") {
271+
needsExplanation := len(linters) == 0 // if no linters are mentioned, we must have explanation
272+
// otherwise, check if we are excluding all of the mentioned linters
273+
for _, ll := range linters {
274+
if !l.excludeByLinter[ll] { // if a linter does require explanation
275+
needsExplanation = true
276+
break
234277
}
235278
}
279+
280+
if needsExplanation {
281+
fullDirectiveWithoutExplanation := trailingBlankExplanation.ReplaceAllString(comment.Text, "")
282+
issues = append(issues, NoExplanation{
283+
BaseIssue: base,
284+
fullDirectiveWithoutExplanation: fullDirectiveWithoutExplanation,
285+
})
286+
}
236287
}
237288
}
238289
}

0 commit comments

Comments
 (0)