golangci-lint/pkg/result/processors/autogenerated_exclude.go

177 lines
4.5 KiB
Go

package processors
import (
"errors"
"fmt"
"go/parser"
"go/token"
"path/filepath"
"regexp"
"strings"
"github.com/golangci/golangci-lint/pkg/logutils"
"github.com/golangci/golangci-lint/pkg/result"
)
const (
genCodeGenerated = "code generated"
genDoNotEdit = "do not edit"
genAutoFile = "autogenerated file" // easyjson
)
var _ Processor = &AutogeneratedExclude{}
type fileSummary struct {
generated bool
}
type AutogeneratedExclude struct {
debugf logutils.DebugFunc
strict bool
strictPattern *regexp.Regexp
fileSummaryCache map[string]*fileSummary
}
func NewAutogeneratedExclude(strict bool) *AutogeneratedExclude {
return &AutogeneratedExclude{
debugf: logutils.Debug(logutils.DebugKeyAutogenExclude),
strict: strict,
strictPattern: regexp.MustCompile(`^// Code generated .* DO NOT EDIT\.$`),
fileSummaryCache: map[string]*fileSummary{},
}
}
func (p *AutogeneratedExclude) Name() string {
return "autogenerated_exclude"
}
func (p *AutogeneratedExclude) Process(issues []result.Issue) ([]result.Issue, error) {
return filterIssuesErr(issues, p.shouldPassIssue)
}
func (p *AutogeneratedExclude) Finish() {}
func (p *AutogeneratedExclude) shouldPassIssue(issue *result.Issue) (bool, error) {
if issue.FromLinter == "typecheck" {
// don't hide typechecking errors in generated files: users expect to see why the project isn't compiling
return true, nil
}
if filepath.Base(issue.FilePath()) == "go.mod" {
return true, nil
}
if issue.FilePath() == "" {
return false, errors.New("no file path for issue")
}
if !isGoFile(issue.FilePath()) {
return false, nil
}
// The file is already known.
fs := p.fileSummaryCache[issue.FilePath()]
if fs != nil {
return !fs.generated, nil
}
fs = &fileSummary{}
p.fileSummaryCache[issue.FilePath()] = fs
if p.strict {
var err error
fs.generated, err = p.isGeneratedFileStrict(issue.FilePath())
if err != nil {
return false, fmt.Errorf("failed to get doc (strict) of file %s: %w", issue.FilePath(), err)
}
} else {
doc, err := getComments(issue.FilePath())
if err != nil {
return false, fmt.Errorf("failed to get doc (lax) of file %s: %w", issue.FilePath(), err)
}
fs.generated = p.isGeneratedFileLax(doc)
}
p.debugf("file %q is generated: %t", issue.FilePath(), fs.generated)
// don't report issues for autogenerated files
return !fs.generated, nil
}
// isGeneratedFileLax reports whether the source file is generated code.
// The function uses a bit laxer rules than isGeneratedFileStrict to match more generated code.
// See https://github.com/golangci/golangci-lint/issues/48 and https://github.com/golangci/golangci-lint/issues/72.
func (p *AutogeneratedExclude) isGeneratedFileLax(doc string) bool {
markers := []string{genCodeGenerated, genDoNotEdit, genAutoFile}
doc = strings.ToLower(doc)
for _, marker := range markers {
if strings.Contains(doc, marker) {
p.debugf("doc contains marker %q: file is generated", marker)
return true
}
}
p.debugf("doc of len %d doesn't contain any of markers: %s", len(doc), markers)
return false
}
// isGeneratedFileStrict returns true if the source file has a line that matches the regular expression:
//
// ^// Code generated .* DO NOT EDIT\.$
//
// This line must appear before the first non-comment, non-blank text in the file.
// Based on https://go.dev/s/generatedcode.
func (p *AutogeneratedExclude) isGeneratedFileStrict(filePath string) (bool, error) {
file, err := parser.ParseFile(token.NewFileSet(), filePath, nil, parser.PackageClauseOnly|parser.ParseComments)
if err != nil {
return false, fmt.Errorf("failed to parse file: %w", err)
}
if file == nil || len(file.Comments) == 0 {
return false, nil
}
for _, comment := range file.Comments {
if comment.Pos() > file.Package {
return false, nil
}
for _, line := range comment.List {
generated := p.strictPattern.MatchString(line.Text)
if generated {
p.debugf("doc contains ignore expression: file is generated")
return true, nil
}
}
}
return false, nil
}
func getComments(filePath string) (string, error) {
fset := token.NewFileSet()
syntax, err := parser.ParseFile(fset, filePath, nil, parser.PackageClauseOnly|parser.ParseComments)
if err != nil {
return "", fmt.Errorf("failed to parse file: %w", err)
}
var docLines []string
for _, c := range syntax.Comments {
docLines = append(docLines, strings.TrimSpace(c.Text()))
}
return strings.Join(docLines, "\n"), nil
}
func isGoFile(name string) bool {
return filepath.Ext(name) == ".go"
}