-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathscan.go
More file actions
318 lines (263 loc) · 9.68 KB
/
Copy pathscan.go
File metadata and controls
318 lines (263 loc) · 9.68 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
package gogenfilter
import (
"fmt"
"io/fs"
"path/filepath"
"regexp"
"sort"
"strings"
)
// GeneratedFile holds information about a single detected generated file.
type GeneratedFile struct {
Path string // relative path within the scanned directory
Reason FilterReason // which generator was detected
}
// String returns a human-readable representation of the generated file.
func (f GeneratedFile) String() string {
return f.Path + " (" + string(f.Reason) + ")"
}
// ScanResult holds the results of scanning a project for generated files.
type ScanResult struct {
Files []GeneratedFile // all detected generated files
ByGenerator map[string][]string // generator name → list of file paths
ScannedFiles int // total .go files scanned
Generators []string // unique generator names, sorted
Exclusions []Exclusion // derived exclusion patterns
}
// String returns a human-readable summary of the scan result.
func (r *ScanResult) String() string {
return fmt.Sprintf("ScanResult(scanned=%d, generated=%d, generators=%v)",
r.ScannedFiles, len(r.Files), r.Generators)
}
// Exclusion represents a path-based exclusion pattern for auto-generated code.
type Exclusion struct {
Pattern string // regex pattern suitable for golangci-lint exclusions.paths
Reason string // human-readable reason for the exclusion
}
// String returns a human-readable representation of the exclusion.
func (e Exclusion) String() string {
return e.Pattern + " # " + e.Reason
}
// Scan phase identifiers used in ScanError.Phase for diagnostic reporting.
const (
scanPhaseConfigure = "configure"
scanPhaseCreateFilter = "create_filter"
scanPhaseCollect = "collect"
scanPhaseWalk = "walk"
)
// Generator exclusion pattern literals. Extracted to constants so the same
// pattern is reused for the lookup table and the reason string.
const (
templExclusionPattern = `_templ\.go$`
templExclusionReason = "templ generated HTML components"
)
// exclusionPatterns maps FilterReason values to fixed regex patterns for generators
// with consistent filename conventions. Used by ExclusionPattern() and deriveExclusions().
// Generators not in this map need directory-based derivation (oapi-codegen, generic, etc.).
//
//nolint:gochecknoglobals // immutable lookup table, never mutated
var exclusionPatterns = map[FilterReason]string{
ReasonSQLC: `\.sql\.go$`,
ReasonTempl: templExclusionPattern,
ReasonProtobuf: `\.pb\.go$`,
ReasonGoEnum: `_enum\.go$`,
ReasonDeepcopy: `zz_generated\..*\.go$`,
ReasonWire: `wire_gen\.go$`,
ReasonMoq: `_moq\.go$`,
ReasonMockgen: `_mock\.go$`,
ReasonStringer: `_string\.go$`,
ReasonMockery: `mock_.*\.go$`,
ReasonEasyjson: `_easyjson\.go$`,
ReasonCounterfeiter: `fake_.*\.go$`,
}
// ExclusionPattern returns a fixed regex pattern for generators that have
// consistent filename conventions. Returns ("", false) for generators
// that need directory-based derivation (oapi-codegen, ent, gqlgen, generic).
func (r FilterReason) ExclusionPattern() (string, bool) {
p, ok := exclusionPatterns[r]
return p, ok
}
// ScanProject scans a project directory for auto-generated Go files.
// It walks the filesystem, detects generated files using the provided options,
// and returns a ScanResult with per-generator file lists and derived exclusion patterns.
//
// Default options (FilterAll) are used when no configs are provided.
// Directories named vendor, node_modules, and hidden directories (starting with .)
// are skipped during scanning.
//
// The fsys parameter provides the filesystem to scan. Use os.DirFS(projectDir)
// for the real filesystem.
func ScanProject(fsys fs.FS, configs ...FilterConfig) (*ScanResult, error) {
if len(configs) == 0 {
opts, err := WithFilterOptions(FilterAll)
if err != nil {
return nil, &ScanError{Code: CodeScanConfig, Phase: scanPhaseConfigure, Err: err}
}
configs = []FilterConfig{opts}
}
configs = append(configs, WithFS(fsys))
filter, err := NewFilter(configs...)
if err != nil {
return nil, &ScanError{Code: CodeScanConfig, Phase: scanPhaseCreateFilter, Err: err}
}
goFiles, err := collectGoFiles(fsys)
if err != nil {
return nil, &ScanError{Code: CodeScanWalk, Phase: scanPhaseCollect, Err: err}
}
byGenerator := make(map[string][]string)
var files []GeneratedFile
for _, file := range goFiles {
result, filterErr := filter.FilterDetailed(file)
if filterErr != nil {
continue
}
if !result.Filtered || result.Reason == ReasonNotFiltered {
continue
}
generator := string(result.Reason)
byGenerator[generator] = append(byGenerator[generator], file)
files = append(files, GeneratedFile{
Path: file,
Reason: result.Reason,
})
}
generators := make([]string, 0, len(byGenerator))
for gen := range byGenerator {
generators = append(generators, gen)
}
sort.Strings(generators)
exclusions := deriveExclusions(byGenerator)
return &ScanResult{
Files: files,
ByGenerator: byGenerator,
ScannedFiles: len(goFiles),
Generators: generators,
Exclusions: exclusions,
}, nil
}
// collectGoFiles walks the filesystem and collects all .go file paths
// relative to the root, excluding vendor, node_modules, and hidden directories.
func collectGoFiles(fsys fs.FS) ([]string, error) {
var goFiles []string
err := fs.WalkDir(fsys, ".", func(path string, dirEntry fs.DirEntry, err error) error {
if err != nil {
return fmt.Errorf("walking %s: %w", path, err)
}
if !dirEntry.IsDir() {
if strings.HasSuffix(dirEntry.Name(), ".go") {
goFiles = append(goFiles, path)
}
return nil
}
if path != "." && shouldSkipScanDir(dirEntry.Name()) {
return fs.SkipDir
}
return nil
})
if err != nil {
return nil, &ScanError{Code: CodeScanWalk, Phase: scanPhaseWalk, Err: err}
}
return goFiles, nil
}
func shouldSkipScanDir(name string) bool {
if strings.HasPrefix(name, ".") {
return true
}
return name == vendorDir || name == nodeModulesDir
}
// deriveExclusions converts detected files into exclusion patterns.
// Generators with fixed filename patterns use ExclusionPattern().
// Others (oapi-codegen, ent, gqlgen, generic) use directory-based patterns.
func deriveExclusions(byGenerator map[string][]string) []Exclusion {
// Count total possible entries for preallocation.
maxExclusions := 0
for _, files := range byGenerator {
maxExclusions += len(files)
}
exclusions := make([]Exclusion, 0, maxExclusions)
for generator, files := range byGenerator {
exclusions = append(exclusions, exclusionsForGenerator(generator, files)...)
}
sort.Slice(exclusions, func(i, j int) bool {
return exclusions[i].Pattern < exclusions[j].Pattern
})
return exclusions
}
// generatorExclusionReasons returns a map of generator names to human-readable reasons.
// Returned as a function-local value so the lookup table does not need to be a package-level
// mutable global.
func generatorExclusionReasons() map[string]string {
return map[string]string{
string(FilterTempl): templExclusionReason,
string(FilterProtobuf): "protobuf generated code",
string(FilterGoEnum): "go-enum generated enumerations",
string(deepcopyGeneratorName): "deepcopy-gen generated code",
string(FilterWire): "wire generated dependency injection",
string(moqGeneratorName): "moq generated mocks",
string(FilterMockgen): "mockgen generated mocks",
string(FilterStringer): "stringer generated string methods",
string(FilterMockery): "mockery generated mocks",
string(FilterEasyjson): "easyjson generated marshalers",
string(FilterMsgp): "msgp generated serialization",
string(FilterCounterfeiter): "counterfeiter generated fakes",
string(FilterSQLC): "sqlc generated database code",
string(FilterOapi): "oapi-codegen generated API code",
}
}
func exclusionsForGenerator(generator string, files []string) []Exclusion {
if pattern, ok := FilterReason(generator).ExclusionPattern(); ok {
reason := generatorExclusionReasons()[generator]
return []Exclusion{{Pattern: pattern, Reason: reason}}
}
switch generator {
case string(FilterOapi):
return oapiExclusions(files)
case string(FilterEnt), string(FilterGqlgen), string(FilterGoSwagger), string(FilterGeneric):
return dirBasedExclusions(files, generatorExclusionReasons()[generator])
default:
reason, ok := generatorExclusionReasons()[generator]
if !ok {
reason = "auto-generated code"
}
return dirBasedExclusions(files, reason)
}
}
// oapiExclusions generates exclusion patterns for oapi-codegen files.
func oapiExclusions(files []string) []Exclusion {
for _, f := range files {
if strings.HasSuffix(f, ".gen.go") {
return []Exclusion{
{Pattern: `.gen.go$`, Reason: generatorExclusionReasons()[string(FilterOapi)]},
}
}
}
return dirBasedExclusions(files, generatorExclusionReasons()["oapi-codegen"])
}
// dirBasedExclusions generates one exclusion per unique parent directory.
func dirBasedExclusions(files []string, reason string) []Exclusion {
dirSet := make(map[string]struct{})
for _, f := range files {
dirSet[filepath.Dir(f)] = struct{}{}
}
dirs := make([]string, 0, len(dirSet))
for dir := range dirSet {
dirs = append(dirs, dir)
}
sort.Strings(dirs)
exclusions := make([]Exclusion, 0, len(dirs))
for _, dir := range dirs {
exclusions = append(exclusions, Exclusion{
Pattern: regexp.QuoteMeta(dir) + "/",
Reason: reason,
})
}
return exclusions
}
// ExclusionPaths extracts just the pattern strings from a slice of Exclusions.
func ExclusionPaths(exclusions []Exclusion) []string {
paths := make([]string, 0, len(exclusions))
for _, e := range exclusions {
paths = append(paths, e.Pattern)
}
return paths
}