1
2
3
4
5
6
7 package gopathwalk
8
9 import (
10 "bufio"
11 "bytes"
12 "io"
13 "io/fs"
14 "os"
15 "path/filepath"
16 "runtime"
17 "strings"
18 "sync"
19 "time"
20 )
21
22
23 type Options struct {
24
25 Logf func(format string, args ...interface{})
26
27
28 ModulesEnabled bool
29
30
31
32 Concurrency int
33 }
34
35
36 type RootType int
37
38 const (
39 RootUnknown RootType = iota
40 RootGOROOT
41 RootGOPATH
42 RootCurrentModule
43 RootModuleCache
44 RootOther
45 )
46
47
48 type Root struct {
49 Path string
50 Type RootType
51 }
52
53
54
55
56
57
58
59
60 func Walk(roots []Root, add func(root Root, dir string), opts Options) {
61 WalkSkip(roots, add, func(Root, string) bool { return false }, opts)
62 }
63
64
65
66
67
68
69
70
71
72
73
74
75 func WalkSkip(roots []Root, add func(root Root, dir string), skip func(root Root, dir string) bool, opts Options) {
76 for _, root := range roots {
77 walkDir(root, add, skip, opts)
78 }
79 }
80
81
82 func walkDir(root Root, add func(Root, string), skip func(root Root, dir string) bool, opts Options) {
83 if opts.Logf == nil {
84 opts.Logf = func(format string, args ...interface{}) {}
85 }
86 if _, err := os.Stat(root.Path); os.IsNotExist(err) {
87 opts.Logf("skipping nonexistent directory: %v", root.Path)
88 return
89 }
90 start := time.Now()
91 opts.Logf("scanning %s", root.Path)
92
93 concurrency := opts.Concurrency
94 if concurrency == 0 {
95
96
97
98
99
100
101
102
103 concurrency = runtime.GOMAXPROCS(0)
104 }
105 w := &walker{
106 root: root,
107 add: add,
108 skip: skip,
109 opts: opts,
110 sem: make(chan struct{}, concurrency),
111 }
112 w.init()
113
114 w.sem <- struct{}{}
115 path := root.Path
116 if path == "" {
117 path = "."
118 }
119 if fi, err := os.Lstat(path); err == nil {
120 w.walk(path, nil, fs.FileInfoToDirEntry(fi))
121 } else {
122 w.opts.Logf("scanning directory %v: %v", root.Path, err)
123 }
124 <-w.sem
125 w.walking.Wait()
126
127 opts.Logf("scanned %s in %v", root.Path, time.Since(start))
128 }
129
130
131 type walker struct {
132 root Root
133 add func(Root, string)
134 skip func(Root, string) bool
135 opts Options
136
137 walking sync.WaitGroup
138 sem chan struct{}
139 ignoredDirs []string
140
141 added sync.Map
142 }
143
144
145
146 type symlinkList struct {
147 info os.FileInfo
148 prev *symlinkList
149 }
150
151
152 func (w *walker) init() {
153 var ignoredPaths []string
154 if w.root.Type == RootModuleCache {
155 ignoredPaths = []string{"cache"}
156 }
157 if !w.opts.ModulesEnabled && w.root.Type == RootGOPATH {
158 ignoredPaths = w.getIgnoredDirs(w.root.Path)
159 ignoredPaths = append(ignoredPaths, "v", "mod")
160 }
161
162 for _, p := range ignoredPaths {
163 full := filepath.Join(w.root.Path, p)
164 w.ignoredDirs = append(w.ignoredDirs, full)
165 w.opts.Logf("Directory added to ignore list: %s", full)
166 }
167 }
168
169
170
171
172 func (w *walker) getIgnoredDirs(path string) []string {
173 file := filepath.Join(path, ".goimportsignore")
174 slurp, err := os.ReadFile(file)
175 if err != nil {
176 w.opts.Logf("%v", err)
177 } else {
178 w.opts.Logf("Read %s", file)
179 }
180 if err != nil {
181 return nil
182 }
183
184 var ignoredDirs []string
185 bs := bufio.NewScanner(bytes.NewReader(slurp))
186 for bs.Scan() {
187 line := strings.TrimSpace(bs.Text())
188 if line == "" || strings.HasPrefix(line, "#") {
189 continue
190 }
191 ignoredDirs = append(ignoredDirs, line)
192 }
193 return ignoredDirs
194 }
195
196
197 func (w *walker) shouldSkipDir(dir string) bool {
198 for _, ignoredDir := range w.ignoredDirs {
199 if dir == ignoredDir {
200 return true
201 }
202 }
203 if w.skip != nil {
204
205 return w.skip(w.root, dir)
206 }
207 return false
208 }
209
210
211
212
213 func (w *walker) walk(path string, pathSymlinks *symlinkList, d fs.DirEntry) {
214 if d.Type()&os.ModeSymlink != 0 {
215
216
217
218
219
220
221
222
223
224 fi, err := os.Stat(path)
225 if err != nil {
226 w.opts.Logf("%v", err)
227 return
228 }
229
230
231
232
233
234
235
236
237 for parent := pathSymlinks; parent != nil; parent = parent.prev {
238 if os.SameFile(fi, parent.info) {
239 return
240 }
241 }
242
243 pathSymlinks = &symlinkList{
244 info: fi,
245 prev: pathSymlinks,
246 }
247 d = fs.FileInfoToDirEntry(fi)
248 }
249
250 if d.Type().IsRegular() {
251 if !strings.HasSuffix(path, ".go") {
252 return
253 }
254
255 dir := filepath.Dir(path)
256 if dir == w.root.Path && (w.root.Type == RootGOROOT || w.root.Type == RootGOPATH) {
257
258
259
260
261
262
263
264 return
265 }
266
267 if _, dup := w.added.LoadOrStore(dir, true); !dup {
268 w.add(w.root, dir)
269 }
270 }
271
272 if !d.IsDir() {
273 return
274 }
275
276 base := filepath.Base(path)
277 if base == "" || base[0] == '.' || base[0] == '_' ||
278 base == "testdata" ||
279 (w.root.Type == RootGOROOT && w.opts.ModulesEnabled && base == "vendor") ||
280 (!w.opts.ModulesEnabled && base == "node_modules") ||
281 w.shouldSkipDir(path) {
282 return
283 }
284
285
286
287 f, err := os.Open(path)
288 if err != nil {
289 w.opts.Logf("%v", err)
290 return
291 }
292 defer f.Close()
293
294 for {
295
296
297
298
299
300
301
302
303
304 ents, err := f.ReadDir(1024)
305 if err != nil {
306 if err != io.EOF {
307 w.opts.Logf("%v", err)
308 }
309 break
310 }
311
312 for _, d := range ents {
313 nextPath := filepath.Join(path, d.Name())
314 if d.IsDir() {
315 select {
316 case w.sem <- struct{}{}:
317
318 d := d
319 w.walking.Add(1)
320 go func() {
321 defer func() {
322 <-w.sem
323 w.walking.Done()
324 }()
325 w.walk(nextPath, pathSymlinks, d)
326 }()
327 continue
328
329 default:
330
331 }
332 }
333
334 w.walk(nextPath, pathSymlinks, d)
335 }
336 }
337 }
338
View as plain text