github.com/elek/golangci-lint@v1.42.2-0.20211208090441-c05b7fcb3a9a/pkg/golinters/nolintlint/nolintlint.go (about)

     1  // Package nolintlint provides a linter to ensure that all //nolint directives are followed by explanations
     2  package nolintlint
     3  
     4  import (
     5  	"fmt"
     6  	"go/ast"
     7  	"go/token"
     8  	"regexp"
     9  	"strings"
    10  	"unicode"
    11  
    12  	"github.com/elek/golangci-lint/pkg/result"
    13  )
    14  
    15  type BaseIssue struct {
    16  	fullDirective                     string
    17  	directiveWithOptionalLeadingSpace string
    18  	position                          token.Position
    19  	replacement                       *result.Replacement
    20  }
    21  
    22  //nolint:gocritic // TODO must be change in the future.
    23  func (b BaseIssue) Position() token.Position {
    24  	return b.position
    25  }
    26  
    27  //nolint:gocritic // TODO must be change in the future.
    28  func (b BaseIssue) Replacement() *result.Replacement {
    29  	return b.replacement
    30  }
    31  
    32  type ExtraLeadingSpace struct {
    33  	BaseIssue
    34  }
    35  
    36  //nolint:gocritic // TODO must be change in the future.
    37  func (i ExtraLeadingSpace) Details() string {
    38  	return fmt.Sprintf("directive `%s` should not have more than one leading space", i.fullDirective)
    39  }
    40  
    41  //nolint:gocritic // TODO must be change in the future.
    42  func (i ExtraLeadingSpace) String() string { return toString(i) }
    43  
    44  type NotMachine struct {
    45  	BaseIssue
    46  }
    47  
    48  //nolint:gocritic // TODO must be change in the future.
    49  func (i NotMachine) Details() string {
    50  	expected := i.fullDirective[:2] + strings.TrimLeftFunc(i.fullDirective[2:], unicode.IsSpace)
    51  	return fmt.Sprintf("directive `%s` should be written without leading space as `%s`",
    52  		i.fullDirective, expected)
    53  }
    54  
    55  //nolint:gocritic // TODO must be change in the future.
    56  func (i NotMachine) String() string { return toString(i) }
    57  
    58  type NotSpecific struct {
    59  	BaseIssue
    60  }
    61  
    62  //nolint:gocritic // TODO must be change in the future.
    63  func (i NotSpecific) Details() string {
    64  	return fmt.Sprintf("directive `%s` should mention specific linter such as `%s:my-linter`",
    65  		i.fullDirective, i.directiveWithOptionalLeadingSpace)
    66  }
    67  
    68  //nolint:gocritic // TODO must be change in the future.
    69  func (i NotSpecific) String() string { return toString(i) }
    70  
    71  type ParseError struct {
    72  	BaseIssue
    73  }
    74  
    75  //nolint:gocritic // TODO must be change in the future.
    76  func (i ParseError) Details() string {
    77  	return fmt.Sprintf("directive `%s` should match `%s[:<comma-separated-linters>] [// <explanation>]`",
    78  		i.fullDirective,
    79  		i.directiveWithOptionalLeadingSpace)
    80  }
    81  
    82  //nolint:gocritic // TODO must be change in the future.
    83  func (i ParseError) String() string { return toString(i) }
    84  
    85  type NoExplanation struct {
    86  	BaseIssue
    87  	fullDirectiveWithoutExplanation string
    88  }
    89  
    90  //nolint:gocritic // TODO must be change in the future.
    91  func (i NoExplanation) Details() string {
    92  	return fmt.Sprintf("directive `%s` should provide explanation such as `%s // this is why`",
    93  		i.fullDirective, i.fullDirectiveWithoutExplanation)
    94  }
    95  
    96  //nolint:gocritic // TODO must be change in the future.
    97  func (i NoExplanation) String() string { return toString(i) }
    98  
    99  type UnusedCandidate struct {
   100  	BaseIssue
   101  	ExpectedLinter string
   102  }
   103  
   104  //nolint:gocritic // TODO must be change in the future.
   105  func (i UnusedCandidate) Details() string {
   106  	details := fmt.Sprintf("directive `%s` is unused", i.fullDirective)
   107  	if i.ExpectedLinter != "" {
   108  		details += fmt.Sprintf(" for linter %q", i.ExpectedLinter)
   109  	}
   110  	return details
   111  }
   112  
   113  //nolint:gocritic // TODO must be change in the future.
   114  func (i UnusedCandidate) String() string { return toString(i) }
   115  
   116  func toString(i Issue) string {
   117  	return fmt.Sprintf("%s at %s", i.Details(), i.Position())
   118  }
   119  
   120  type Issue interface {
   121  	Details() string
   122  	Position() token.Position
   123  	String() string
   124  	Replacement() *result.Replacement
   125  }
   126  
   127  type Needs uint
   128  
   129  const (
   130  	NeedsMachineOnly Needs = 1 << iota
   131  	NeedsSpecific
   132  	NeedsExplanation
   133  	NeedsUnused
   134  	NeedsAll = NeedsMachineOnly | NeedsSpecific | NeedsExplanation
   135  )
   136  
   137  var commentPattern = regexp.MustCompile(`^//\s*(nolint)(:\s*[\w-]+\s*(?:,\s*[\w-]+\s*)*)?\b`)
   138  
   139  // matches a complete nolint directive
   140  var fullDirectivePattern = regexp.MustCompile(`^//\s*nolint(?::(\s*[\w-]+\s*(?:,\s*[\w-]+\s*)*))?\s*(//.*)?\s*\n?$`)
   141  
   142  type Linter struct {
   143  	needs           Needs // indicates which linter checks to perform
   144  	excludeByLinter map[string]bool
   145  }
   146  
   147  // NewLinter creates a linter that enforces that the provided directives fulfill the provided requirements
   148  func NewLinter(needs Needs, excludes []string) (*Linter, error) {
   149  	excludeByName := make(map[string]bool)
   150  	for _, e := range excludes {
   151  		excludeByName[e] = true
   152  	}
   153  
   154  	return &Linter{
   155  		needs:           needs,
   156  		excludeByLinter: excludeByName,
   157  	}, nil
   158  }
   159  
   160  var leadingSpacePattern = regexp.MustCompile(`^//(\s*)`)
   161  var trailingBlankExplanation = regexp.MustCompile(`\s*(//\s*)?$`)
   162  
   163  //nolint:funlen,gocyclo
   164  func (l Linter) Run(fset *token.FileSet, nodes ...ast.Node) ([]Issue, error) {
   165  	var issues []Issue
   166  
   167  	for _, node := range nodes {
   168  		file, ok := node.(*ast.File)
   169  		if !ok {
   170  			continue
   171  		}
   172  
   173  		for _, c := range file.Comments {
   174  			for _, comment := range c.List {
   175  				if !commentPattern.MatchString(comment.Text) {
   176  					continue
   177  				}
   178  
   179  				// check for a space between the "//" and the directive
   180  				leadingSpaceMatches := leadingSpacePattern.FindStringSubmatch(comment.Text)
   181  
   182  				var leadingSpace string
   183  				if len(leadingSpaceMatches) > 0 {
   184  					leadingSpace = leadingSpaceMatches[1]
   185  				}
   186  
   187  				directiveWithOptionalLeadingSpace := comment.Text
   188  				if len(leadingSpace) > 0 {
   189  					split := strings.Split(strings.SplitN(comment.Text, ":", 2)[0], "//")
   190  					directiveWithOptionalLeadingSpace = "// " + strings.TrimSpace(split[1])
   191  				}
   192  
   193  				pos := fset.Position(comment.Pos())
   194  				end := fset.Position(comment.End())
   195  
   196  				base := BaseIssue{
   197  					fullDirective:                     comment.Text,
   198  					directiveWithOptionalLeadingSpace: directiveWithOptionalLeadingSpace,
   199  					position:                          pos,
   200  				}
   201  
   202  				// check for, report and eliminate leading spaces so we can check for other issues
   203  				if len(leadingSpace) > 0 {
   204  					removeWhitespace := &result.Replacement{
   205  						Inline: &result.InlineFix{
   206  							StartCol:  pos.Column + 1,
   207  							Length:    len(leadingSpace),
   208  							NewString: "",
   209  						},
   210  					}
   211  					if (l.needs & NeedsMachineOnly) != 0 {
   212  						issue := NotMachine{BaseIssue: base}
   213  						issue.BaseIssue.replacement = removeWhitespace
   214  						issues = append(issues, issue)
   215  					} else if len(leadingSpace) > 1 {
   216  						issue := ExtraLeadingSpace{BaseIssue: base}
   217  						issue.BaseIssue.replacement = removeWhitespace
   218  						issue.BaseIssue.replacement.Inline.NewString = " " // assume a single space was intended
   219  						issues = append(issues, issue)
   220  					}
   221  				}
   222  
   223  				fullMatches := fullDirectivePattern.FindStringSubmatch(comment.Text)
   224  				if len(fullMatches) == 0 {
   225  					issues = append(issues, ParseError{BaseIssue: base})
   226  					continue
   227  				}
   228  
   229  				lintersText, explanation := fullMatches[1], fullMatches[2]
   230  				var linters []string
   231  				if len(lintersText) > 0 {
   232  					lls := strings.Split(lintersText, ",")
   233  					linters = make([]string, 0, len(lls))
   234  					rangeStart := (pos.Column - 1) + len("//") + len(leadingSpace) + len("nolint:")
   235  					for i, ll := range lls {
   236  						rangeEnd := rangeStart + len(ll)
   237  						if i < len(lls)-1 {
   238  							rangeEnd++ // include trailing comma
   239  						}
   240  						trimmedLinterName := strings.TrimSpace(ll)
   241  						if trimmedLinterName != "" {
   242  							linters = append(linters, trimmedLinterName)
   243  						}
   244  						rangeStart = rangeEnd
   245  					}
   246  				}
   247  
   248  				if (l.needs & NeedsSpecific) != 0 {
   249  					if len(linters) == 0 {
   250  						issues = append(issues, NotSpecific{BaseIssue: base})
   251  					}
   252  				}
   253  
   254  				// when detecting unused directives, we send all the directives through and filter them out in the nolint processor
   255  				if (l.needs & NeedsUnused) != 0 {
   256  					removeNolintCompletely := &result.Replacement{
   257  						Inline: &result.InlineFix{
   258  							StartCol:  pos.Column - 1,
   259  							Length:    end.Column - pos.Column,
   260  							NewString: "",
   261  						},
   262  					}
   263  
   264  					if len(linters) == 0 {
   265  						issue := UnusedCandidate{BaseIssue: base}
   266  						issue.replacement = removeNolintCompletely
   267  						issues = append(issues, issue)
   268  					} else {
   269  						for _, linter := range linters {
   270  							issue := UnusedCandidate{BaseIssue: base, ExpectedLinter: linter}
   271  							// only offer replacement if there is a single linter
   272  							// because of issues around commas and the possibility of all
   273  							// linters being removed
   274  							if len(linters) == 1 {
   275  								issue.replacement = removeNolintCompletely
   276  							}
   277  							issues = append(issues, issue)
   278  						}
   279  					}
   280  				}
   281  
   282  				if (l.needs&NeedsExplanation) != 0 && (explanation == "" || strings.TrimSpace(explanation) == "//") {
   283  					needsExplanation := len(linters) == 0 // if no linters are mentioned, we must have explanation
   284  					// otherwise, check if we are excluding all of the mentioned linters
   285  					for _, ll := range linters {
   286  						if !l.excludeByLinter[ll] { // if a linter does require explanation
   287  							needsExplanation = true
   288  							break
   289  						}
   290  					}
   291  
   292  					if needsExplanation {
   293  						fullDirectiveWithoutExplanation := trailingBlankExplanation.ReplaceAllString(comment.Text, "")
   294  						issues = append(issues, NoExplanation{
   295  							BaseIssue:                       base,
   296  							fullDirectiveWithoutExplanation: fullDirectiveWithoutExplanation,
   297  						})
   298  					}
   299  				}
   300  			}
   301  		}
   302  	}
   303  
   304  	return issues, nil
   305  }