github.com/chenfeining/golangci-lint@v1.0.2-0.20230730162517-14c6c67868df/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/chenfeining/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 | NeedsMachineOnly,
   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 := "//"
   188  				if len(leadingSpace) > 0 {
   189  					directiveWithOptionalLeadingSpace += " "
   190  				}
   191  
   192  				split := strings.Split(strings.SplitN(comment.Text, ":", 2)[0], "//")
   193  				directiveWithOptionalLeadingSpace += strings.TrimSpace(split[1])
   194  
   195  				pos := fset.Position(comment.Pos())
   196  				end := fset.Position(comment.End())
   197  
   198  				base := BaseIssue{
   199  					fullDirective:                     comment.Text,
   200  					directiveWithOptionalLeadingSpace: directiveWithOptionalLeadingSpace,
   201  					position:                          pos,
   202  				}
   203  
   204  				// check for, report and eliminate leading spaces, so we can check for other issues
   205  				if len(leadingSpace) > 0 {
   206  					removeWhitespace := &result.Replacement{
   207  						Inline: &result.InlineFix{
   208  							StartCol:  pos.Column + 1,
   209  							Length:    len(leadingSpace),
   210  							NewString: "",
   211  						},
   212  					}
   213  					if (l.needs & NeedsMachineOnly) != 0 {
   214  						issue := NotMachine{BaseIssue: base}
   215  						issue.BaseIssue.replacement = removeWhitespace
   216  						issues = append(issues, issue)
   217  					} else if len(leadingSpace) > 1 {
   218  						issue := ExtraLeadingSpace{BaseIssue: base}
   219  						issue.BaseIssue.replacement = removeWhitespace
   220  						issue.BaseIssue.replacement.Inline.NewString = " " // assume a single space was intended
   221  						issues = append(issues, issue)
   222  					}
   223  				}
   224  
   225  				fullMatches := fullDirectivePattern.FindStringSubmatch(comment.Text)
   226  				if len(fullMatches) == 0 {
   227  					issues = append(issues, ParseError{BaseIssue: base})
   228  					continue
   229  				}
   230  
   231  				lintersText, explanation := fullMatches[1], fullMatches[2]
   232  
   233  				var linters []string
   234  				if len(lintersText) > 0 && !strings.HasPrefix(lintersText, "all") {
   235  					lls := strings.Split(lintersText, ",")
   236  					linters = make([]string, 0, len(lls))
   237  					rangeStart := (pos.Column - 1) + len("//") + len(leadingSpace) + len("nolint:")
   238  					for i, ll := range lls {
   239  						rangeEnd := rangeStart + len(ll)
   240  						if i < len(lls)-1 {
   241  							rangeEnd++ // include trailing comma
   242  						}
   243  						trimmedLinterName := strings.TrimSpace(ll)
   244  						if trimmedLinterName != "" {
   245  							linters = append(linters, trimmedLinterName)
   246  						}
   247  						rangeStart = rangeEnd
   248  					}
   249  				}
   250  
   251  				if (l.needs & NeedsSpecific) != 0 {
   252  					if len(linters) == 0 {
   253  						issues = append(issues, NotSpecific{BaseIssue: base})
   254  					}
   255  				}
   256  
   257  				// when detecting unused directives, we send all the directives through and filter them out in the nolint processor
   258  				if (l.needs & NeedsUnused) != 0 {
   259  					removeNolintCompletely := &result.Replacement{
   260  						Inline: &result.InlineFix{
   261  							StartCol:  pos.Column - 1,
   262  							Length:    end.Column - pos.Column,
   263  							NewString: "",
   264  						},
   265  					}
   266  
   267  					if len(linters) == 0 {
   268  						issue := UnusedCandidate{BaseIssue: base}
   269  						issue.replacement = removeNolintCompletely
   270  						issues = append(issues, issue)
   271  					} else {
   272  						for _, linter := range linters {
   273  							issue := UnusedCandidate{BaseIssue: base, ExpectedLinter: linter}
   274  							// only offer replacement if there is a single linter
   275  							// because of issues around commas and the possibility of all
   276  							// linters being removed
   277  							if len(linters) == 1 {
   278  								issue.replacement = removeNolintCompletely
   279  							}
   280  							issues = append(issues, issue)
   281  						}
   282  					}
   283  				}
   284  
   285  				if (l.needs&NeedsExplanation) != 0 && (explanation == "" || strings.TrimSpace(explanation) == "//") {
   286  					needsExplanation := len(linters) == 0 // if no linters are mentioned, we must have explanation
   287  					// otherwise, check if we are excluding all the mentioned linters
   288  					for _, ll := range linters {
   289  						if !l.excludeByLinter[ll] { // if a linter does require explanation
   290  							needsExplanation = true
   291  							break
   292  						}
   293  					}
   294  
   295  					if needsExplanation {
   296  						fullDirectiveWithoutExplanation := trailingBlankExplanation.ReplaceAllString(comment.Text, "")
   297  						issues = append(issues, NoExplanation{
   298  							BaseIssue:                       base,
   299  							fullDirectiveWithoutExplanation: fullDirectiveWithoutExplanation,
   300  						})
   301  					}
   302  				}
   303  			}
   304  		}
   305  	}
   306  
   307  	return issues, nil
   308  }