306 lines
9.0 KiB
Go
306 lines
9.0 KiB
Go
// Package nolintlint provides a linter to ensure that all //nolint directives are followed by explanations
|
|
package nolintlint
|
|
|
|
import (
|
|
"fmt"
|
|
"go/ast"
|
|
"go/token"
|
|
"regexp"
|
|
"strings"
|
|
"unicode"
|
|
|
|
"github.com/golangci/golangci-lint/pkg/result"
|
|
)
|
|
|
|
type BaseIssue struct {
|
|
fullDirective string
|
|
directiveWithOptionalLeadingSpace string
|
|
position token.Position
|
|
replacement *result.Replacement
|
|
}
|
|
|
|
//nolint:gocritic // TODO must be change in the future.
|
|
func (b BaseIssue) Position() token.Position {
|
|
return b.position
|
|
}
|
|
|
|
//nolint:gocritic // TODO must be change in the future.
|
|
func (b BaseIssue) Replacement() *result.Replacement {
|
|
return b.replacement
|
|
}
|
|
|
|
type ExtraLeadingSpace struct {
|
|
BaseIssue
|
|
}
|
|
|
|
//nolint:gocritic // TODO must be change in the future.
|
|
func (i ExtraLeadingSpace) Details() string {
|
|
return fmt.Sprintf("directive `%s` should not have more than one leading space", i.fullDirective)
|
|
}
|
|
|
|
//nolint:gocritic // TODO must be change in the future.
|
|
func (i ExtraLeadingSpace) String() string { return toString(i) }
|
|
|
|
type NotMachine struct {
|
|
BaseIssue
|
|
}
|
|
|
|
//nolint:gocritic // TODO must be change in the future.
|
|
func (i NotMachine) Details() string {
|
|
expected := i.fullDirective[:2] + strings.TrimLeftFunc(i.fullDirective[2:], unicode.IsSpace)
|
|
return fmt.Sprintf("directive `%s` should be written without leading space as `%s`",
|
|
i.fullDirective, expected)
|
|
}
|
|
|
|
//nolint:gocritic // TODO must be change in the future.
|
|
func (i NotMachine) String() string { return toString(i) }
|
|
|
|
type NotSpecific struct {
|
|
BaseIssue
|
|
}
|
|
|
|
//nolint:gocritic // TODO must be change in the future.
|
|
func (i NotSpecific) Details() string {
|
|
return fmt.Sprintf("directive `%s` should mention specific linter such as `%s:my-linter`",
|
|
i.fullDirective, i.directiveWithOptionalLeadingSpace)
|
|
}
|
|
|
|
//nolint:gocritic // TODO must be change in the future.
|
|
func (i NotSpecific) String() string { return toString(i) }
|
|
|
|
type ParseError struct {
|
|
BaseIssue
|
|
}
|
|
|
|
//nolint:gocritic // TODO must be change in the future.
|
|
func (i ParseError) Details() string {
|
|
return fmt.Sprintf("directive `%s` should match `%s[:<comma-separated-linters>] [// <explanation>]`",
|
|
i.fullDirective,
|
|
i.directiveWithOptionalLeadingSpace)
|
|
}
|
|
|
|
//nolint:gocritic // TODO must be change in the future.
|
|
func (i ParseError) String() string { return toString(i) }
|
|
|
|
type NoExplanation struct {
|
|
BaseIssue
|
|
fullDirectiveWithoutExplanation string
|
|
}
|
|
|
|
//nolint:gocritic // TODO must be change in the future.
|
|
func (i NoExplanation) Details() string {
|
|
return fmt.Sprintf("directive `%s` should provide explanation such as `%s // this is why`",
|
|
i.fullDirective, i.fullDirectiveWithoutExplanation)
|
|
}
|
|
|
|
//nolint:gocritic // TODO must be change in the future.
|
|
func (i NoExplanation) String() string { return toString(i) }
|
|
|
|
type UnusedCandidate struct {
|
|
BaseIssue
|
|
ExpectedLinter string
|
|
}
|
|
|
|
//nolint:gocritic // TODO must be change in the future.
|
|
func (i UnusedCandidate) Details() string {
|
|
details := fmt.Sprintf("directive `%s` is unused", i.fullDirective)
|
|
if i.ExpectedLinter != "" {
|
|
details += fmt.Sprintf(" for linter %q", i.ExpectedLinter)
|
|
}
|
|
return details
|
|
}
|
|
|
|
//nolint:gocritic // TODO must be change in the future.
|
|
func (i UnusedCandidate) String() string { return toString(i) }
|
|
|
|
func toString(i Issue) string {
|
|
return fmt.Sprintf("%s at %s", i.Details(), i.Position())
|
|
}
|
|
|
|
type Issue interface {
|
|
Details() string
|
|
Position() token.Position
|
|
String() string
|
|
Replacement() *result.Replacement
|
|
}
|
|
|
|
type Needs uint
|
|
|
|
const (
|
|
NeedsMachineOnly Needs = 1 << iota
|
|
NeedsSpecific
|
|
NeedsExplanation
|
|
NeedsUnused
|
|
NeedsAll = NeedsMachineOnly | NeedsSpecific | NeedsExplanation
|
|
)
|
|
|
|
var commentPattern = regexp.MustCompile(`^//\s*(nolint)(:\s*[\w-]+\s*(?:,\s*[\w-]+\s*)*)?\b`)
|
|
|
|
// matches a complete nolint directive
|
|
var fullDirectivePattern = regexp.MustCompile(`^//\s*nolint(?::(\s*[\w-]+\s*(?:,\s*[\w-]+\s*)*))?\s*(//.*)?\s*\n?$`)
|
|
|
|
type Linter struct {
|
|
needs Needs // indicates which linter checks to perform
|
|
excludeByLinter map[string]bool
|
|
}
|
|
|
|
// NewLinter creates a linter that enforces that the provided directives fulfill the provided requirements
|
|
func NewLinter(needs Needs, excludes []string) (*Linter, error) {
|
|
excludeByName := make(map[string]bool)
|
|
for _, e := range excludes {
|
|
excludeByName[e] = true
|
|
}
|
|
|
|
return &Linter{
|
|
needs: needs,
|
|
excludeByLinter: excludeByName,
|
|
}, nil
|
|
}
|
|
|
|
var leadingSpacePattern = regexp.MustCompile(`^//(\s*)`)
|
|
var trailingBlankExplanation = regexp.MustCompile(`\s*(//\s*)?$`)
|
|
|
|
//nolint:funlen,gocyclo
|
|
func (l Linter) Run(fset *token.FileSet, nodes ...ast.Node) ([]Issue, error) {
|
|
var issues []Issue
|
|
|
|
for _, node := range nodes {
|
|
file, ok := node.(*ast.File)
|
|
if !ok {
|
|
continue
|
|
}
|
|
|
|
for _, c := range file.Comments {
|
|
for _, comment := range c.List {
|
|
if !commentPattern.MatchString(comment.Text) {
|
|
continue
|
|
}
|
|
|
|
// check for a space between the "//" and the directive
|
|
leadingSpaceMatches := leadingSpacePattern.FindStringSubmatch(comment.Text)
|
|
|
|
var leadingSpace string
|
|
if len(leadingSpaceMatches) > 0 {
|
|
leadingSpace = leadingSpaceMatches[1]
|
|
}
|
|
|
|
directiveWithOptionalLeadingSpace := comment.Text
|
|
if len(leadingSpace) > 0 {
|
|
split := strings.Split(strings.SplitN(comment.Text, ":", 2)[0], "//")
|
|
directiveWithOptionalLeadingSpace = "// " + strings.TrimSpace(split[1])
|
|
}
|
|
|
|
pos := fset.Position(comment.Pos())
|
|
end := fset.Position(comment.End())
|
|
|
|
base := BaseIssue{
|
|
fullDirective: comment.Text,
|
|
directiveWithOptionalLeadingSpace: directiveWithOptionalLeadingSpace,
|
|
position: pos,
|
|
}
|
|
|
|
// check for, report and eliminate leading spaces, so we can check for other issues
|
|
if len(leadingSpace) > 0 {
|
|
removeWhitespace := &result.Replacement{
|
|
Inline: &result.InlineFix{
|
|
StartCol: pos.Column + 1,
|
|
Length: len(leadingSpace),
|
|
NewString: "",
|
|
},
|
|
}
|
|
if (l.needs & NeedsMachineOnly) != 0 {
|
|
issue := NotMachine{BaseIssue: base}
|
|
issue.BaseIssue.replacement = removeWhitespace
|
|
issues = append(issues, issue)
|
|
} else if len(leadingSpace) > 1 {
|
|
issue := ExtraLeadingSpace{BaseIssue: base}
|
|
issue.BaseIssue.replacement = removeWhitespace
|
|
issue.BaseIssue.replacement.Inline.NewString = " " // assume a single space was intended
|
|
issues = append(issues, issue)
|
|
}
|
|
}
|
|
|
|
fullMatches := fullDirectivePattern.FindStringSubmatch(comment.Text)
|
|
if len(fullMatches) == 0 {
|
|
issues = append(issues, ParseError{BaseIssue: base})
|
|
continue
|
|
}
|
|
|
|
lintersText, explanation := fullMatches[1], fullMatches[2]
|
|
var linters []string
|
|
if len(lintersText) > 0 {
|
|
lls := strings.Split(lintersText, ",")
|
|
linters = make([]string, 0, len(lls))
|
|
rangeStart := (pos.Column - 1) + len("//") + len(leadingSpace) + len("nolint:")
|
|
for i, ll := range lls {
|
|
rangeEnd := rangeStart + len(ll)
|
|
if i < len(lls)-1 {
|
|
rangeEnd++ // include trailing comma
|
|
}
|
|
trimmedLinterName := strings.TrimSpace(ll)
|
|
if trimmedLinterName != "" {
|
|
linters = append(linters, trimmedLinterName)
|
|
}
|
|
rangeStart = rangeEnd
|
|
}
|
|
}
|
|
|
|
if (l.needs & NeedsSpecific) != 0 {
|
|
if len(linters) == 0 {
|
|
issues = append(issues, NotSpecific{BaseIssue: base})
|
|
}
|
|
}
|
|
|
|
// when detecting unused directives, we send all the directives through and filter them out in the nolint processor
|
|
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 {
|
|
issue := UnusedCandidate{BaseIssue: base}
|
|
issue.replacement = removeNolintCompletely
|
|
issues = append(issues, issue)
|
|
} else {
|
|
for _, linter := range linters {
|
|
issue := UnusedCandidate{BaseIssue: base, ExpectedLinter: linter}
|
|
// only offer replacement if there is a single linter
|
|
// because of issues around commas and the possibility of all
|
|
// linters being removed
|
|
if len(linters) == 1 {
|
|
issue.replacement = removeNolintCompletely
|
|
}
|
|
issues = append(issues, issue)
|
|
}
|
|
}
|
|
}
|
|
|
|
if (l.needs&NeedsExplanation) != 0 && (explanation == "" || strings.TrimSpace(explanation) == "//") {
|
|
needsExplanation := len(linters) == 0 // if no linters are mentioned, we must have explanation
|
|
// otherwise, check if we are excluding all the mentioned linters
|
|
for _, ll := range linters {
|
|
if !l.excludeByLinter[ll] { // if a linter does require explanation
|
|
needsExplanation = true
|
|
break
|
|
}
|
|
}
|
|
|
|
if needsExplanation {
|
|
fullDirectiveWithoutExplanation := trailingBlankExplanation.ReplaceAllString(comment.Text, "")
|
|
issues = append(issues, NoExplanation{
|
|
BaseIssue: base,
|
|
fullDirectiveWithoutExplanation: fullDirectiveWithoutExplanation,
|
|
})
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return issues, nil
|
|
}
|