Skip to content

Commit b05e397

Browse files
authored
feat: option to strictly follow Go autogenerated file convention (#4507)
1 parent d736d09 commit b05e397

9 files changed

+204
-107
lines changed

.golangci.next.reference.yml

+6
Original file line numberDiff line numberDiff line change
@@ -2814,6 +2814,12 @@ issues:
28142814
# Default: false
28152815
exclude-case-sensitive: false
28162816

2817+
# To follow strict Go autogenerated file convention.
2818+
# https://go.dev/s/generatedcode
2819+
# By default a lax pattern is applied.
2820+
# Default: false
2821+
exclude-autogenerated-strict: true
2822+
28172823
# The list of ids of default excludes to include or disable.
28182824
# https://golangci-lint.run/usage/false-positives/#default-exclusions
28192825
# Default: []

jsonschema/golangci.next.jsonschema.json

+5
Original file line numberDiff line numberDiff line change
@@ -3342,6 +3342,11 @@
33423342
"type": "boolean",
33433343
"default": false
33443344
},
3345+
"exclude-autogenerated-strict": {
3346+
"description": "To follow strict Go autogenerated file convention",
3347+
"type": "boolean",
3348+
"default": false
3349+
},
33453350
"include": {
33463351
"description": "The list of ids of default excludes to include or disable.",
33473352
"type": "array",

pkg/config/issues.go

+6-5
Original file line numberDiff line numberDiff line change
@@ -105,11 +105,12 @@ var DefaultExcludePatterns = []ExcludePattern{
105105
}
106106

107107
type Issues struct {
108-
IncludeDefaultExcludes []string `mapstructure:"include"`
109-
ExcludeCaseSensitive bool `mapstructure:"exclude-case-sensitive"`
110-
ExcludePatterns []string `mapstructure:"exclude"`
111-
ExcludeRules []ExcludeRule `mapstructure:"exclude-rules"`
112-
UseDefaultExcludes bool `mapstructure:"exclude-use-default"`
108+
IncludeDefaultExcludes []string `mapstructure:"include"`
109+
ExcludeCaseSensitive bool `mapstructure:"exclude-case-sensitive"`
110+
ExcludePatterns []string `mapstructure:"exclude"`
111+
ExcludeRules []ExcludeRule `mapstructure:"exclude-rules"`
112+
ExcludeAutogeneratedStrict bool `mapstructure:"exclude-autogenerated-strict"`
113+
UseDefaultExcludes bool `mapstructure:"exclude-use-default"`
113114

114115
MaxIssuesPerLinter int `mapstructure:"max-issues-per-linter"`
115116
MaxSameIssues int `mapstructure:"max-same-issues"`

pkg/lint/runner.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ func NewRunner(log logutils.Log, cfg *config.Config, goenv *goutil.Env,
7272
skipFilesProcessor,
7373
skipDirsProcessor, // must be after path prettifier
7474

75-
processors.NewAutogeneratedExclude(),
75+
processors.NewAutogeneratedExclude(cfg.Issues.ExcludeAutogeneratedStrict),
7676

7777
// Must be before exclude because users see already marked output and configure excluding by it.
7878
processors.NewIdentifierMarker(),

pkg/result/processors/autogenerated_exclude.go

+88-49
Original file line numberDiff line numberDiff line change
@@ -6,32 +6,43 @@ import (
66
"go/parser"
77
"go/token"
88
"path/filepath"
9+
"regexp"
910
"strings"
1011

1112
"github.com/golangci/golangci-lint/pkg/logutils"
1213
"github.com/golangci/golangci-lint/pkg/result"
1314
)
1415

15-
var autogenDebugf = logutils.Debug(logutils.DebugKeyAutogenExclude)
16+
const (
17+
genCodeGenerated = "code generated"
18+
genDoNotEdit = "do not edit"
19+
genAutoFile = "autogenerated file" // easyjson
20+
)
1621

17-
type ageFileSummary struct {
18-
isGenerated bool
19-
}
22+
var _ Processor = &AutogeneratedExclude{}
2023

21-
type ageFileSummaryCache map[string]*ageFileSummary
24+
type fileSummary struct {
25+
generated bool
26+
}
2227

2328
type AutogeneratedExclude struct {
24-
fileSummaryCache ageFileSummaryCache
29+
debugf logutils.DebugFunc
30+
31+
strict bool
32+
strictPattern *regexp.Regexp
33+
34+
fileSummaryCache map[string]*fileSummary
2535
}
2636

27-
func NewAutogeneratedExclude() *AutogeneratedExclude {
37+
func NewAutogeneratedExclude(strict bool) *AutogeneratedExclude {
2838
return &AutogeneratedExclude{
29-
fileSummaryCache: ageFileSummaryCache{},
39+
debugf: logutils.Debug(logutils.DebugKeyAutogenExclude),
40+
strict: strict,
41+
strictPattern: regexp.MustCompile(`^// Code generated .* DO NOT EDIT\.$`),
42+
fileSummaryCache: map[string]*fileSummary{},
3043
}
3144
}
3245

33-
var _ Processor = &AutogeneratedExclude{}
34-
3546
func (p *AutogeneratedExclude) Name() string {
3647
return "autogenerated_exclude"
3748
}
@@ -40,11 +51,7 @@ func (p *AutogeneratedExclude) Process(issues []result.Issue) ([]result.Issue, e
4051
return filterIssuesErr(issues, p.shouldPassIssue)
4152
}
4253

43-
func isSpecialAutogeneratedFile(filePath string) bool {
44-
fileName := filepath.Base(filePath)
45-
// fake files or generation definitions to which //line points to for generated files
46-
return filepath.Ext(fileName) != ".go"
47-
}
54+
func (p *AutogeneratedExclude) Finish() {}
4855

4956
func (p *AutogeneratedExclude) shouldPassIssue(issue *result.Issue) (bool, error) {
5057
if issue.FromLinter == "typecheck" {
@@ -56,66 +63,96 @@ func (p *AutogeneratedExclude) shouldPassIssue(issue *result.Issue) (bool, error
5663
return true, nil
5764
}
5865

59-
if isSpecialAutogeneratedFile(issue.FilePath()) {
66+
if !isGoFile(issue.FilePath()) {
6067
return false, nil
6168
}
6269

63-
fs, err := p.getOrCreateFileSummary(issue)
64-
if err != nil {
65-
return false, err
70+
// The file is already known.
71+
fs := p.fileSummaryCache[issue.FilePath()]
72+
if fs != nil {
73+
return !fs.generated, nil
6674
}
6775

76+
fs = &fileSummary{}
77+
p.fileSummaryCache[issue.FilePath()] = fs
78+
79+
if issue.FilePath() == "" {
80+
return false, errors.New("no file path for issue")
81+
}
82+
83+
if p.strict {
84+
var err error
85+
fs.generated, err = p.isGeneratedFileStrict(issue.FilePath())
86+
if err != nil {
87+
return false, fmt.Errorf("failed to get doc of file %s: %w", issue.FilePath(), err)
88+
}
89+
} else {
90+
doc, err := getComments(issue.FilePath())
91+
if err != nil {
92+
return false, fmt.Errorf("failed to get doc of file %s: %w", issue.FilePath(), err)
93+
}
94+
95+
fs.generated = p.isGeneratedFileLax(doc)
96+
}
97+
98+
p.debugf("file %q is generated: %t", issue.FilePath(), fs.generated)
99+
68100
// don't report issues for autogenerated files
69-
return !fs.isGenerated, nil
101+
return !fs.generated, nil
70102
}
71103

72-
// isGenerated reports whether the source file is generated code.
73-
// Using a bit laxer rules than https://go.dev/s/generatedcode to
74-
// match more generated code. See #48 and #72.
75-
func isGeneratedFileByComment(doc string) bool {
76-
const (
77-
genCodeGenerated = "code generated"
78-
genDoNotEdit = "do not edit"
79-
genAutoFile = "autogenerated file" // easyjson
80-
)
81-
104+
// isGeneratedFileLax reports whether the source file is generated code.
105+
// Using a bit laxer rules than https://go.dev/s/generatedcode to match more generated code.
106+
// See https://github.com/golangci/golangci-lint/issues/48 and https://github.com/golangci/golangci-lint/issues/72.
107+
func (p *AutogeneratedExclude) isGeneratedFileLax(doc string) bool {
82108
markers := []string{genCodeGenerated, genDoNotEdit, genAutoFile}
109+
83110
doc = strings.ToLower(doc)
111+
84112
for _, marker := range markers {
85113
if strings.Contains(doc, marker) {
86-
autogenDebugf("doc contains marker %q: file is generated", marker)
114+
p.debugf("doc contains marker %q: file is generated", marker)
115+
87116
return true
88117
}
89118
}
90119

91-
autogenDebugf("doc of len %d doesn't contain any of markers: %s", len(doc), markers)
120+
p.debugf("doc of len %d doesn't contain any of markers: %s", len(doc), markers)
121+
92122
return false
93123
}
94124

95-
func (p *AutogeneratedExclude) getOrCreateFileSummary(issue *result.Issue) (*ageFileSummary, error) {
96-
fs := p.fileSummaryCache[issue.FilePath()]
97-
if fs != nil {
98-
return fs, nil
125+
// Based on https://go.dev/s/generatedcode
126+
// > This line must appear before the first non-comment, non-blank text in the file.
127+
func (p *AutogeneratedExclude) isGeneratedFileStrict(filePath string) (bool, error) {
128+
file, err := parser.ParseFile(token.NewFileSet(), filePath, nil, parser.PackageClauseOnly|parser.ParseComments)
129+
if err != nil {
130+
return false, fmt.Errorf("failed to parse file: %w", err)
99131
}
100132

101-
fs = &ageFileSummary{}
102-
p.fileSummaryCache[issue.FilePath()] = fs
103-
104-
if issue.FilePath() == "" {
105-
return nil, errors.New("no file path for issue")
133+
if file == nil || len(file.Comments) == 0 {
134+
return false, nil
106135
}
107136

108-
doc, err := getDoc(issue.FilePath())
109-
if err != nil {
110-
return nil, fmt.Errorf("failed to get doc of file %s: %w", issue.FilePath(), err)
137+
for _, comment := range file.Comments {
138+
if comment.Pos() > file.Package {
139+
return false, nil
140+
}
141+
142+
for _, line := range comment.List {
143+
generated := p.strictPattern.MatchString(line.Text)
144+
if generated {
145+
p.debugf("doc contains ignore expression: file is generated")
146+
147+
return true, nil
148+
}
149+
}
111150
}
112151

113-
fs.isGenerated = isGeneratedFileByComment(doc)
114-
autogenDebugf("file %q is generated: %t", issue.FilePath(), fs.isGenerated)
115-
return fs, nil
152+
return false, nil
116153
}
117154

118-
func getDoc(filePath string) (string, error) {
155+
func getComments(filePath string) (string, error) {
119156
fset := token.NewFileSet()
120157
syntax, err := parser.ParseFile(fset, filePath, nil, parser.PackageClauseOnly|parser.ParseComments)
121158
if err != nil {
@@ -130,4 +167,6 @@ func getDoc(filePath string) (string, error) {
130167
return strings.Join(docLines, "\n"), nil
131168
}
132169

133-
func (p *AutogeneratedExclude) Finish() {}
170+
func isGoFile(name string) bool {
171+
return filepath.Ext(name) == ".go"
172+
}

0 commit comments

Comments
 (0)