1package processors
2
3import (
4	"fmt"
5	"go/parser"
6	"go/token"
7	"path/filepath"
8	"strings"
9
10	"github.com/pkg/errors"
11
12	"github.com/golangci/golangci-lint/pkg/logutils"
13	"github.com/golangci/golangci-lint/pkg/result"
14)
15
16var autogenDebugf = logutils.Debug("autogen_exclude")
17
18type ageFileSummary struct {
19	isGenerated bool
20}
21
22type ageFileSummaryCache map[string]*ageFileSummary
23
24type AutogeneratedExclude struct {
25	fileSummaryCache ageFileSummaryCache
26}
27
28func NewAutogeneratedExclude() *AutogeneratedExclude {
29	return &AutogeneratedExclude{
30		fileSummaryCache: ageFileSummaryCache{},
31	}
32}
33
34var _ Processor = &AutogeneratedExclude{}
35
36func (p AutogeneratedExclude) Name() string {
37	return "autogenerated_exclude"
38}
39
40func (p *AutogeneratedExclude) Process(issues []result.Issue) ([]result.Issue, error) {
41	return filterIssuesErr(issues, p.shouldPassIssue)
42}
43
44func isSpecialAutogeneratedFile(filePath string) bool {
45	fileName := filepath.Base(filePath)
46	// fake files or generation definitions to which //line points to for generated files
47	return filepath.Ext(fileName) != ".go"
48}
49
50func (p *AutogeneratedExclude) shouldPassIssue(i *result.Issue) (bool, error) {
51	if i.FromLinter == "typecheck" {
52		// don't hide typechecking errors in generated files: users expect to see why the project isn't compiling
53		return true, nil
54	}
55
56	if filepath.Base(i.FilePath()) == "go.mod" {
57		return true, nil
58	}
59
60	if isSpecialAutogeneratedFile(i.FilePath()) {
61		return false, nil
62	}
63
64	fs, err := p.getOrCreateFileSummary(i)
65	if err != nil {
66		return false, err
67	}
68
69	// don't report issues for autogenerated files
70	return !fs.isGenerated, nil
71}
72
73// isGenerated reports whether the source file is generated code.
74// Using a bit laxer rules than https://golang.org/s/generatedcode to
75// match more generated code. See #48 and #72.
76func isGeneratedFileByComment(doc string) bool {
77	const (
78		genCodeGenerated = "code generated"
79		genDoNotEdit     = "do not edit"
80		genAutoFile      = "autogenerated file" // easyjson
81	)
82
83	markers := []string{genCodeGenerated, genDoNotEdit, genAutoFile}
84	doc = strings.ToLower(doc)
85	for _, marker := range markers {
86		if strings.Contains(doc, marker) {
87			autogenDebugf("doc contains marker %q: file is generated", marker)
88			return true
89		}
90	}
91
92	autogenDebugf("doc of len %d doesn't contain any of markers: %s", len(doc), markers)
93	return false
94}
95
96func (p *AutogeneratedExclude) getOrCreateFileSummary(i *result.Issue) (*ageFileSummary, error) {
97	fs := p.fileSummaryCache[i.FilePath()]
98	if fs != nil {
99		return fs, nil
100	}
101
102	fs = &ageFileSummary{}
103	p.fileSummaryCache[i.FilePath()] = fs
104
105	if i.FilePath() == "" {
106		return nil, fmt.Errorf("no file path for issue")
107	}
108
109	doc, err := getDoc(i.FilePath())
110	if err != nil {
111		return nil, errors.Wrapf(err, "failed to get doc of file %s", i.FilePath())
112	}
113
114	fs.isGenerated = isGeneratedFileByComment(doc)
115	autogenDebugf("file %q is generated: %t", i.FilePath(), fs.isGenerated)
116	return fs, nil
117}
118
119func getDoc(filePath string) (string, error) {
120	fset := token.NewFileSet()
121	syntax, err := parser.ParseFile(fset, filePath, nil, parser.PackageClauseOnly|parser.ParseComments)
122	if err != nil {
123		return "", errors.Wrap(err, "failed to parse file")
124	}
125
126	var docLines []string
127	for _, c := range syntax.Comments {
128		docLines = append(docLines, strings.TrimSpace(c.Text()))
129	}
130
131	return strings.Join(docLines, "\n"), nil
132}
133
134func (p AutogeneratedExclude) Finish() {}
135