Update nolintlint to fix nolint formatting and remove unused nolint statements (#1573)

Also allow multiple ranges to satisfy a nolint statement as having been used.
This commit is contained in:
Andrew Shannon Brown 2020-12-27 06:18:02 -08:00 committed by GitHub
parent df9278efd2
commit aeb9830329
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 240 additions and 38 deletions

View File

@ -72,6 +72,7 @@ func NewNoLintLint() *goanalysis.Linter {
Pos: i.Position(), Pos: i.Position(),
ExpectNoLint: expectNoLint, ExpectNoLint: expectNoLint,
ExpectedNoLintLinter: expectedNolintLinter, ExpectedNoLintLinter: expectedNolintLinter,
Replacement: i.Replacement(),
} }
res = append(res, goanalysis.NewIssue(issue, pass)) res = append(res, goanalysis.NewIssue(issue, pass))
} }

View File

@ -8,16 +8,21 @@ import (
"regexp" "regexp"
"strings" "strings"
"unicode" "unicode"
"github.com/golangci/golangci-lint/pkg/result"
) )
type BaseIssue struct { type BaseIssue struct {
fullDirective string fullDirective string
directiveWithOptionalLeadingSpace string directiveWithOptionalLeadingSpace string
position token.Position position token.Position
replacement *result.Replacement
} }
func (b BaseIssue) Position() token.Position { func (b BaseIssue) Position() token.Position { return b.position }
return b.position
func (b BaseIssue) Replacement() *result.Replacement {
return b.replacement
} }
type ExtraLeadingSpace struct { type ExtraLeadingSpace struct {
@ -85,7 +90,7 @@ type UnusedCandidate struct {
func (i UnusedCandidate) Details() string { func (i UnusedCandidate) Details() string {
details := fmt.Sprintf("directive `%s` is unused", i.fullDirective) details := fmt.Sprintf("directive `%s` is unused", i.fullDirective)
if i.ExpectedLinter != "" { if i.ExpectedLinter != "" {
details += fmt.Sprintf(" for linter %s", i.ExpectedLinter) details += fmt.Sprintf(" for linter %q", i.ExpectedLinter)
} }
return details return details
} }
@ -100,6 +105,7 @@ type Issue interface {
Details() string Details() string
Position() token.Position Position() token.Position
String() string String() string
Replacement() *result.Replacement
} }
type Needs uint type Needs uint
@ -115,7 +121,7 @@ const (
var commentPattern = regexp.MustCompile(`^//\s*(nolint)(:\s*[\w-]+\s*(?:,\s*[\w-]+\s*)*)?\b`) var commentPattern = regexp.MustCompile(`^//\s*(nolint)(:\s*[\w-]+\s*(?:,\s*[\w-]+\s*)*)?\b`)
// matches a complete nolint directive // matches a complete nolint directive
var fullDirectivePattern = regexp.MustCompile(`^//\s*nolint(:\s*[\w-]+\s*(?:,\s*[\w-]+\s*)*)?\s*(//.*)?\s*\n?$`) var fullDirectivePattern = regexp.MustCompile(`^//\s*nolint(?::(\s*[\w-]+\s*(?:,\s*[\w-]+\s*)*))?\s*(//.*)?\s*\n?$`)
type Linter struct { type Linter struct {
excludes []string // lists individual linters that don't require explanations 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) {
directiveWithOptionalLeadingSpace = "// " + strings.TrimSpace(split[1]) directiveWithOptionalLeadingSpace = "// " + strings.TrimSpace(split[1])
} }
pos := fset.Position(comment.Pos())
end := fset.Position(comment.End())
base := BaseIssue{ base := BaseIssue{
fullDirective: comment.Text, fullDirective: comment.Text,
directiveWithOptionalLeadingSpace: directiveWithOptionalLeadingSpace, directiveWithOptionalLeadingSpace: directiveWithOptionalLeadingSpace,
position: fset.Position(comment.Pos()), position: pos,
} }
// check for, report and eliminate leading spaces so we can check for other issues // check for, report and eliminate leading spaces so we can check for other issues
if len(leadingSpace) > 1 { if len(leadingSpace) > 0 {
issues = append(issues, ExtraLeadingSpace{BaseIssue: base}) machineReadableReplacement := &result.Replacement{
Inline: &result.InlineFix{
StartCol: pos.Column - 1,
Length: len(leadingSpace) + 2,
NewString: "//",
},
} }
if (l.needs&NeedsMachineOnly) != 0 && len(leadingSpace) > 0 { if (l.needs & NeedsMachineOnly) != 0 {
issues = append(issues, NotMachine{BaseIssue: base}) issue := NotMachine{BaseIssue: base}
issue.BaseIssue.replacement = machineReadableReplacement
issues = append(issues, issue)
} else if len(leadingSpace) > 1 {
issue := ExtraLeadingSpace{BaseIssue: base}
issue.BaseIssue.replacement = machineReadableReplacement
issues = append(issues, issue)
}
} }
fullMatches := fullDirectivePattern.FindStringSubmatch(comment.Text) fullMatches := fullDirectivePattern.FindStringSubmatch(comment.Text)
@ -188,7 +209,7 @@ func (l Linter) Run(fset *token.FileSet, nodes ...ast.Node) ([]Issue, error) {
lintersText, explanation := fullMatches[1], fullMatches[2] lintersText, explanation := fullMatches[1], fullMatches[2]
var linters []string var linters []string
if len(lintersText) > 0 { if len(lintersText) > 0 {
lls := strings.Split(lintersText[1:], ",") lls := strings.Split(lintersText, ",")
linters = make([]string, 0, len(lls)) linters = make([]string, 0, len(lls))
for _, ll := range lls { for _, ll := range lls {
ll = strings.TrimSpace(ll) ll = strings.TrimSpace(ll)
@ -206,11 +227,34 @@ func (l Linter) Run(fset *token.FileSet, nodes ...ast.Node) ([]Issue, error) {
// when detecting unused directives, we send all the directives through and filter them out in the nolint processor // when detecting unused directives, we send all the directives through and filter them out in the nolint processor
if (l.needs & NeedsUnused) != 0 { if (l.needs & NeedsUnused) != 0 {
removeNolintCompletely := &result.Replacement{
Inline: &result.InlineFix{
StartCol: pos.Column - 1,
Length: end.Column - pos.Column,
NewString: "",
},
}
if len(linters) == 0 { if len(linters) == 0 {
issues = append(issues, UnusedCandidate{BaseIssue: base}) issue := UnusedCandidate{BaseIssue: base}
issue.replacement = removeNolintCompletely
issues = append(issues, issue)
} else { } else {
for _, linter := range linters { for i, linter := range linters {
issues = append(issues, UnusedCandidate{BaseIssue: base, ExpectedLinter: linter}) issue := UnusedCandidate{BaseIssue: base, ExpectedLinter: linter}
replacement := removeNolintCompletely
if len(linters) > 1 {
otherLinters := append(append([]string(nil), linters[0:i]...), linters[i+1:]...)
replacement = &result.Replacement{
Inline: &result.InlineFix{
StartCol: (pos.Column - 1) + len("//") + len(leadingSpace) + len("nolint:"),
Length: len(lintersText) - 1,
NewString: strings.Join(otherLinters, ","),
},
}
}
issue.replacement = replacement
issues = append(issues, issue)
} }
} }
} }

View File

@ -7,16 +7,22 @@ import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/golangci/golangci-lint/pkg/result"
) )
//nolint:funlen //nolint:funlen
func TestNoLintLint(t *testing.T) { func TestNoLintLint(t *testing.T) {
type issueWithReplacement struct {
issue string
replacement *result.Replacement
}
testCases := []struct { testCases := []struct {
desc string desc string
needs Needs needs Needs
excludes []string excludes []string
contents string contents string
expected []string expected []issueWithReplacement
}{ }{
{ {
desc: "when no explanation is provided", desc: "when no explanation is provided",
@ -33,11 +39,11 @@ func foo() {
good() //nolint // this is ok good() //nolint // this is ok
other() //nolintother other() //nolintother
}`, }`,
expected: []string{ expected: []issueWithReplacement{
"directive `//nolint` should provide explanation such as `//nolint // this is why` at testing.go:5:1", {"directive `//nolint` should provide explanation such as `//nolint // this is why` at testing.go:5:1", nil},
"directive `//nolint` should provide explanation such as `//nolint // this is why` at testing.go:7:9", {"directive `//nolint` should provide explanation such as `//nolint // this is why` at testing.go:7:9", nil},
"directive `//nolint //` should provide explanation such as `//nolint // this is why` at testing.go:8:9", {"directive `//nolint //` should provide explanation such as `//nolint // this is why` at testing.go:8:9", nil},
"directive `//nolint // ` should provide explanation such as `//nolint // this is why` at testing.go:9:9", {"directive `//nolint // ` should provide explanation such as `//nolint // this is why` at testing.go:9:9", nil},
}, },
}, },
{ {
@ -50,8 +56,8 @@ package bar
//nolint // this is ok //nolint // this is ok
//nolint:dupl //nolint:dupl
func foo() {}`, func foo() {}`,
expected: []string{ expected: []issueWithReplacement{
"directive `//nolint:dupl` should provide explanation such as `//nolint:dupl // this is why` at testing.go:6:1", {"directive `//nolint:dupl` should provide explanation such as `//nolint:dupl // this is why` at testing.go:6:1", nil},
}, },
}, },
{ {
@ -76,9 +82,9 @@ func foo() {
bad() //nolint bad() //nolint
bad() // nolint // because bad() // nolint // because
}`, }`,
expected: []string{ expected: []issueWithReplacement{
"directive `//nolint` should mention specific linter such as `//nolint:my-linter` at testing.go:6:9", {"directive `//nolint` should mention specific linter such as `//nolint:my-linter` at testing.go:6:9", nil},
"directive `// nolint // because` should mention specific linter such as `// nolint:my-linter` at testing.go:7:9", {"directive `// nolint // because` should mention specific linter such as `// nolint:my-linter` at testing.go:7:9", nil},
}, },
}, },
{ {
@ -91,8 +97,17 @@ func foo() {
bad() // nolint bad() // nolint
good() //nolint good() //nolint
}`, }`,
expected: []string{ expected: []issueWithReplacement{
{
"directive `// nolint` should be written without leading space as `//nolint` at testing.go:5:9", "directive `// nolint` should be written without leading space as `//nolint` at testing.go:5:9",
&result.Replacement{
Inline: &result.InlineFix{
StartCol: 8,
Length: 3,
NewString: "//",
},
},
},
}, },
}, },
{ {
@ -104,8 +119,17 @@ func foo() {
bad() // nolint bad() // nolint
good() // nolint good() // nolint
}`, }`,
expected: []string{ expected: []issueWithReplacement{
{
"directive `// nolint` should not have more than one leading space at testing.go:5:9", "directive `// nolint` should not have more than one leading space at testing.go:5:9",
&result.Replacement{
Inline: &result.InlineFix{
StartCol: 8,
Length: 4,
NewString: "//",
},
},
},
}, },
}, },
{ {
@ -119,8 +143,8 @@ func foo() {
good() // nolint: linter1,linter2 good() // nolint: linter1,linter2
good() // nolint: linter1, linter2 good() // nolint: linter1, linter2
}`, }`,
expected: []string{ expected: []issueWithReplacement{
"directive `// nolint:linter1 linter2` should match `// nolint[:<comma-separated-linters>] [// <explanation>]` at testing.go:6:9", //nolint:lll // this is a string {"directive `// nolint:linter1 linter2` should match `// nolint[:<comma-separated-linters>] [// <explanation>]` at testing.go:6:9", nil}, //nolint:lll // this is a string
}, },
}, },
{ {
@ -133,6 +157,92 @@ func foo() {
// something else // something else
}`, }`,
}, },
{
desc: "needs unused without specific linter generates replacement",
needs: NeedsUnused,
contents: `
package bar
func foo() {
bad() //nolint
}`,
expected: []issueWithReplacement{
{
"directive `//nolint` is unused at testing.go:5:9",
&result.Replacement{
Inline: &result.InlineFix{
StartCol: 8,
Length: 8,
NewString: "",
},
},
},
},
},
{
desc: "needs unused with one specific linter generates replacement",
needs: NeedsUnused,
contents: `
package bar
func foo() {
bad() //nolint:somelinter
}`,
expected: []issueWithReplacement{
{
"directive `//nolint:somelinter` is unused for linter \"somelinter\" at testing.go:5:9",
&result.Replacement{
Inline: &result.InlineFix{
StartCol: 8,
Length: 19,
NewString: "",
},
},
},
},
},
{
desc: "needs unused with multiple specific linter generates replacement for each linter",
needs: NeedsUnused,
contents: `
package bar
func foo() {
bad() //nolint:linter1,linter2,linter3
}`,
expected: []issueWithReplacement{
{
"directive `//nolint:linter1,linter2,linter3` is unused for linter \"linter1\" at testing.go:5:9",
&result.Replacement{
Inline: &result.InlineFix{
StartCol: 17,
Length: 22,
NewString: "linter2,linter3",
},
},
},
{
"directive `//nolint:linter1,linter2,linter3` is unused for linter \"linter2\" at testing.go:5:9",
&result.Replacement{
Inline: &result.InlineFix{
StartCol: 17,
Length: 22,
NewString: "linter1,linter3",
},
},
},
{
"directive `//nolint:linter1,linter2,linter3` is unused for linter \"linter3\" at testing.go:5:9",
&result.Replacement{
Inline: &result.InlineFix{
StartCol: 17,
Length: 22,
NewString: "linter1,linter2",
},
},
},
},
},
} }
for _, test := range testCases { for _, test := range testCases {
@ -149,12 +259,16 @@ func foo() {
actualIssues, err := linter.Run(fset, expr) actualIssues, err := linter.Run(fset, expr)
require.NoError(t, err) require.NoError(t, err)
actualIssueStrs := make([]string, 0, len(actualIssues)) actualIssuesWithReplacements := make([]issueWithReplacement, 0, len(actualIssues))
for _, i := range actualIssues { for _, i := range actualIssues {
actualIssueStrs = append(actualIssueStrs, i.String()) actualIssuesWithReplacements = append(actualIssuesWithReplacements, issueWithReplacement{
issue: i.String(),
replacement: i.Replacement(),
})
} }
assert.ElementsMatch(t, test.expected, actualIssueStrs, "expected %s \nbut got %s", test.expected, actualIssues) assert.ElementsMatch(t, test.expected, actualIssuesWithReplacements,
"expected %s \nbut got %s", test.expected, actualIssuesWithReplacements)
}) })
} }
} }

View File

@ -14,7 +14,7 @@ type Range struct {
type Replacement struct { type Replacement struct {
NeedOnlyDelete bool // need to delete all lines of the issue without replacement with new lines NeedOnlyDelete bool // need to delete all lines of the issue without replacement with new lines
NewLines []string // is NeedDelete is false it's the replacement lines NewLines []string // if NeedDelete is false it's the replacement lines
Inline *InlineFix Inline *InlineFix
} }

View File

@ -22,6 +22,7 @@ type ignoredRange struct {
matchedIssueFromLinter map[string]bool matchedIssueFromLinter map[string]bool
result.Range result.Range
col int col int
originalRange *ignoredRange // pre-expanded range (used to match nolintlint issues)
} }
func (i *ignoredRange) doesMatch(issue *result.Issue) bool { func (i *ignoredRange) doesMatch(issue *result.Issue) bool {
@ -163,7 +164,11 @@ func (p *Nolint) shouldPassIssue(i *result.Issue) (bool, error) {
for _, ir := range fd.ignoredRanges { for _, ir := range fd.ignoredRanges {
if ir.doesMatch(i) { if ir.doesMatch(i) {
nolintDebugf("found ignored range for issue %v: %v", i, ir)
ir.matchedIssueFromLinter[i.FromLinter] = true ir.matchedIssueFromLinter[i.FromLinter] = true
if ir.originalRange != nil {
ir.originalRange.matchedIssueFromLinter[i.FromLinter] = true
}
return false, nil return false, nil
} }
} }
@ -199,9 +204,14 @@ func (e *rangeExpander) Visit(node ast.Node) ast.Visitor {
} }
expandedRange := *foundRange expandedRange := *foundRange
// store the original unexpanded range for matching nolintlint issues
if expandedRange.originalRange == nil {
expandedRange.originalRange = foundRange
}
if expandedRange.To < nodeEndLine { if expandedRange.To < nodeEndLine {
expandedRange.To = nodeEndLine expandedRange.To = nodeEndLine
} }
nolintDebugf("found range is %v for node %#v [%d;%d], expanded range is %v", nolintDebugf("found range is %v for node %#v [%d;%d], expanded range is %v",
*foundRange, node, nodeStartLine, nodeEndLine, expandedRange) *foundRange, node, nodeStartLine, nodeEndLine, expandedRange)
e.expandedRanges = append(e.expandedRanges, expandedRange) e.expandedRanges = append(e.expandedRanges, expandedRange)

13
test/testdata/fix/in/nolintlint.go vendored Normal file
View File

@ -0,0 +1,13 @@
//args: -Enolintlint -Elll
//config: linters-settings.nolintlint.allow-leading-space=false
package p
func nolintlint() {
run() // nolint:bob // leading space should be dropped
run() // nolint:bob // leading spaces should be dropped
// note that the next lines will retain trailing whitespace when fixed
run() //nolint // nolint should be dropped
run() //nolint:lll // nolint should be dropped
run() //nolint:alice,lll,bob // enabled linter should be dropped
run() //nolint:alice,lll,bob,nolintlint // enabled linter should not be dropped when nolintlint is nolinted
}

13
test/testdata/fix/out/nolintlint.go vendored Normal file
View File

@ -0,0 +1,13 @@
//args: -Enolintlint -Elll
//config: linters-settings.nolintlint.allow-leading-space=false
package p
func nolintlint() {
run() //nolint:bob // leading space should be dropped
run() //nolint:bob // leading spaces should be dropped
// note that the next lines will retain trailing whitespace when fixed
run()
run()
run() //nolint:alice,bob // enabled linter should be dropped
run() //nolint:alice,lll,bob,nolintlint // enabled linter should not be dropped when nolintlint is nolinted
}

View File

@ -1,7 +1,7 @@
//args: -Enolintlint //args: -Enolintlint -Emisspell
//config: linters-settings.nolintlint.require-explanation=true //config: linters-settings.nolintlint.require-explanation=true
//config: linters-settings.nolintlint.require-specific=true //config: linters-settings.nolintlint.require-specific=true
//config: linters-settings.nolintlint.allowing-leading-space=false //config: linters-settings.nolintlint.allow-leading-space=false
package testdata package testdata
import "fmt" import "fmt"
@ -10,4 +10,11 @@ func Foo() {
fmt.Println("not specific") //nolint // ERROR "directive `.*` should mention specific linter such as `//nolint:my-linter`" fmt.Println("not specific") //nolint // ERROR "directive `.*` should mention specific linter such as `//nolint:my-linter`"
fmt.Println("not machine readable") // nolint // ERROR "directive `.*` should be written as `//nolint`" fmt.Println("not machine readable") // nolint // ERROR "directive `.*` should be written as `//nolint`"
fmt.Println("extra spaces") // nolint:deadcode // because // ERROR "directive `.*` should not have more than one leading space" fmt.Println("extra spaces") // nolint:deadcode // because // ERROR "directive `.*` should not have more than one leading space"
// test expanded range
//nolint:misspell // deliberate misspelling to trigger nolintlint
func() {
mispell := true
fmt.Println(mispell)
}()
} }