@@ -8,18 +8,25 @@ import (
88 "regexp"
99 "strings"
1010 "unicode"
11+
12+ "github.com/golangci/golangci-lint/pkg/result"
1113)
1214
1315type BaseIssue struct {
1416 fullDirective string
1517 directiveWithOptionalLeadingSpace string
1618 position token.Position
19+ replacement * result.Replacement
1720}
1821
1922func (b BaseIssue ) Position () token.Position {
2023 return b .position
2124}
2225
26+ func (b BaseIssue ) Replacement () * result.Replacement {
27+ return b .replacement
28+ }
29+
2330type ExtraLeadingSpace struct {
2431 BaseIssue
2532}
@@ -85,7 +92,7 @@ type UnusedCandidate struct {
8592func (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
105113type Needs uint
@@ -115,7 +123,7 @@ const (
115123var 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
120128type 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