walk.go (9759B)
1 // Copyright 2018 The Go Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 // Package gopathwalk is like filepath.Walk but specialized for finding Go 6 // packages, particularly in $GOPATH and $GOROOT. 7 package gopathwalk 8 9 import ( 10 "bufio" 11 "bytes" 12 "io" 13 "io/fs" 14 "os" 15 "path/filepath" 16 "runtime" 17 "slices" 18 "strings" 19 "sync" 20 "time" 21 ) 22 23 // Options controls the behavior of a Walk call. 24 type Options struct { 25 // If Logf is non-nil, debug logging is enabled through this function. 26 Logf func(format string, args ...any) 27 28 // Search module caches. Also disables legacy goimports ignore rules. 29 ModulesEnabled bool 30 31 // Maximum number of concurrent calls to user-provided callbacks, 32 // or 0 for GOMAXPROCS. 33 Concurrency int 34 } 35 36 // RootType indicates the type of a Root. 37 type RootType int 38 39 const ( 40 RootUnknown RootType = iota 41 RootGOROOT 42 RootGOPATH 43 RootCurrentModule 44 RootModuleCache 45 RootOther 46 ) 47 48 // A Root is a starting point for a Walk. 49 type Root struct { 50 Path string 51 Type RootType 52 } 53 54 // Walk concurrently walks Go source directories ($GOROOT, $GOPATH, etc) to find packages. 55 // 56 // For each package found, add will be called with the absolute 57 // paths of the containing source directory and the package directory. 58 // 59 // Unlike filepath.WalkDir, Walk follows symbolic links 60 // (while guarding against cycles). 61 func Walk(roots []Root, add func(root Root, dir string), opts Options) { 62 WalkSkip(roots, add, func(Root, string) bool { return false }, opts) 63 } 64 65 // WalkSkip concurrently walks Go source directories ($GOROOT, $GOPATH, etc) to 66 // find packages. 67 // 68 // For each package found, add will be called with the absolute 69 // paths of the containing source directory and the package directory. 70 // For each directory that will be scanned, skip will be called 71 // with the absolute paths of the containing source directory and the directory. 72 // If skip returns false on a directory it will be processed. 73 // 74 // Unlike filepath.WalkDir, WalkSkip follows symbolic links 75 // (while guarding against cycles). 76 func WalkSkip(roots []Root, add func(root Root, dir string), skip func(root Root, dir string) bool, opts Options) { 77 for _, root := range roots { 78 walkDir(root, add, skip, opts) 79 } 80 } 81 82 // walkDir creates a walker and starts fastwalk with this walker. 83 func walkDir(root Root, add func(Root, string), skip func(root Root, dir string) bool, opts Options) { 84 if opts.Logf == nil { 85 opts.Logf = func(format string, args ...any) {} 86 } 87 if _, err := os.Stat(root.Path); os.IsNotExist(err) { 88 opts.Logf("skipping nonexistent directory: %v", root.Path) 89 return 90 } 91 start := time.Now() 92 opts.Logf("scanning %s", root.Path) 93 94 concurrency := opts.Concurrency 95 if concurrency == 0 { 96 // The walk be either CPU-bound or I/O-bound, depending on what the 97 // caller-supplied add function does and the details of the user's platform 98 // and machine. Rather than trying to fine-tune the concurrency level for a 99 // specific environment, we default to GOMAXPROCS: it is likely to be a good 100 // choice for a CPU-bound add function, and if it is instead I/O-bound, then 101 // dealing with I/O saturation is arguably the job of the kernel and/or 102 // runtime. (Oversaturating I/O seems unlikely to harm performance as badly 103 // as failing to saturate would.) 104 concurrency = runtime.GOMAXPROCS(0) 105 } 106 w := &walker{ 107 root: root, 108 add: add, 109 skip: skip, 110 opts: opts, 111 sem: make(chan struct{}, concurrency), 112 } 113 w.init() 114 115 w.sem <- struct{}{} 116 path := root.Path 117 if path == "" { 118 path = "." 119 } 120 if fi, err := os.Lstat(path); err == nil { 121 w.walk(path, nil, fs.FileInfoToDirEntry(fi)) 122 } else { 123 w.opts.Logf("scanning directory %v: %v", root.Path, err) 124 } 125 <-w.sem 126 w.walking.Wait() 127 128 opts.Logf("scanned %s in %v", root.Path, time.Since(start)) 129 } 130 131 // walker is the callback for fastwalk.Walk. 132 type walker struct { 133 root Root // The source directory to scan. 134 add func(Root, string) // The callback that will be invoked for every possible Go package dir. 135 skip func(Root, string) bool // The callback that will be invoked for every dir. dir is skipped if it returns true. 136 opts Options // Options passed to Walk by the user. 137 138 walking sync.WaitGroup 139 sem chan struct{} // Channel of semaphore tokens; send to acquire, receive to release. 140 ignoredDirs []string 141 142 added sync.Map // map[string]bool 143 } 144 145 // A symlinkList is a linked list of os.FileInfos for parent directories 146 // reached via symlinks. 147 type symlinkList struct { 148 info os.FileInfo 149 prev *symlinkList 150 } 151 152 // init initializes the walker based on its Options 153 func (w *walker) init() { 154 var ignoredPaths []string 155 if w.root.Type == RootModuleCache { 156 ignoredPaths = []string{"cache"} 157 } 158 if !w.opts.ModulesEnabled && w.root.Type == RootGOPATH { 159 ignoredPaths = w.getIgnoredDirs(w.root.Path) 160 ignoredPaths = append(ignoredPaths, "v", "mod") 161 } 162 163 for _, p := range ignoredPaths { 164 full := filepath.Join(w.root.Path, p) 165 w.ignoredDirs = append(w.ignoredDirs, full) 166 w.opts.Logf("Directory added to ignore list: %s", full) 167 } 168 } 169 170 // getIgnoredDirs reads an optional config file at <path>/.goimportsignore 171 // of relative directories to ignore when scanning for go files. 172 // The provided path is one of the $GOPATH entries with "src" appended. 173 func (w *walker) getIgnoredDirs(path string) []string { 174 file := filepath.Join(path, ".goimportsignore") 175 slurp, err := os.ReadFile(file) 176 if err != nil { 177 w.opts.Logf("%v", err) 178 } else { 179 w.opts.Logf("Read %s", file) 180 } 181 if err != nil { 182 return nil 183 } 184 185 var ignoredDirs []string 186 bs := bufio.NewScanner(bytes.NewReader(slurp)) 187 for bs.Scan() { 188 line := strings.TrimSpace(bs.Text()) 189 if line == "" || strings.HasPrefix(line, "#") { 190 continue 191 } 192 ignoredDirs = append(ignoredDirs, line) 193 } 194 return ignoredDirs 195 } 196 197 // shouldSkipDir reports whether the file should be skipped or not. 198 func (w *walker) shouldSkipDir(dir string) bool { 199 if slices.Contains(w.ignoredDirs, dir) { 200 return true 201 } 202 if w.skip != nil { 203 // Check with the user specified callback. 204 return w.skip(w.root, dir) 205 } 206 return false 207 } 208 209 // walk walks through the given path. 210 // 211 // Errors are logged if w.opts.Logf is non-nil, but otherwise ignored. 212 func (w *walker) walk(path string, pathSymlinks *symlinkList, d fs.DirEntry) { 213 if d.Type()&os.ModeSymlink != 0 { 214 // Walk the symlink's target rather than the symlink itself. 215 // 216 // (Note that os.Stat, unlike the lower-lever os.Readlink, 217 // follows arbitrarily many layers of symlinks, so it will eventually 218 // reach either a non-symlink or a nonexistent target.) 219 // 220 // TODO(bcmills): 'go list all' itself ignores symlinks within GOROOT/src 221 // and GOPATH/src. Do we really need to traverse them here? If so, why? 222 223 fi, err := os.Stat(path) 224 if err != nil { 225 w.opts.Logf("%v", err) 226 return 227 } 228 229 // Avoid walking symlink cycles: if we have already followed a symlink to 230 // this directory as a parent of itself, don't follow it again. 231 // 232 // This doesn't catch the first time through a cycle, but it also minimizes 233 // the number of extra stat calls we make if we *don't* encounter a cycle. 234 // Since we don't actually expect to encounter symlink cycles in practice, 235 // this seems like the right tradeoff. 236 for parent := pathSymlinks; parent != nil; parent = parent.prev { 237 if os.SameFile(fi, parent.info) { 238 return 239 } 240 } 241 242 pathSymlinks = &symlinkList{ 243 info: fi, 244 prev: pathSymlinks, 245 } 246 d = fs.FileInfoToDirEntry(fi) 247 } 248 249 if d.Type().IsRegular() { 250 if !strings.HasSuffix(path, ".go") { 251 return 252 } 253 254 dir := filepath.Dir(path) 255 if dir == w.root.Path && (w.root.Type == RootGOROOT || w.root.Type == RootGOPATH) { 256 // Doesn't make sense to have regular files 257 // directly in your $GOPATH/src or $GOROOT/src. 258 // 259 // TODO(bcmills): there are many levels of directory within 260 // RootModuleCache where this also wouldn't make sense, 261 // Can we generalize this to any directory without a corresponding 262 // import path? 263 return 264 } 265 266 if _, dup := w.added.LoadOrStore(dir, true); !dup { 267 w.add(w.root, dir) 268 } 269 } 270 271 if !d.IsDir() { 272 return 273 } 274 275 base := filepath.Base(path) 276 if base == "" || base[0] == '.' || base[0] == '_' || 277 base == "testdata" || 278 (w.root.Type == RootGOROOT && w.opts.ModulesEnabled && base == "vendor") || 279 (!w.opts.ModulesEnabled && base == "node_modules") || 280 w.shouldSkipDir(path) { 281 return 282 } 283 284 // Read the directory and walk its entries. 285 286 f, err := os.Open(path) 287 if err != nil { 288 w.opts.Logf("%v", err) 289 return 290 } 291 defer f.Close() 292 293 for { 294 // We impose an arbitrary limit on the number of ReadDir results per 295 // directory to limit the amount of memory consumed for stale or upcoming 296 // directory entries. The limit trades off CPU (number of syscalls to read 297 // the whole directory) against RAM (reachable directory entries other than 298 // the one currently being processed). 299 // 300 // Since we process the directories recursively, we will end up maintaining 301 // a slice of entries for each level of the directory tree. 302 // (Compare https://go.dev/issue/36197.) 303 ents, err := f.ReadDir(1024) 304 if err != nil { 305 if err != io.EOF { 306 w.opts.Logf("%v", err) 307 } 308 break 309 } 310 311 for _, d := range ents { 312 nextPath := filepath.Join(path, d.Name()) 313 if d.IsDir() { 314 select { 315 case w.sem <- struct{}{}: 316 // Got a new semaphore token, so we can traverse the directory concurrently. 317 d := d 318 w.walking.Add(1) 319 go func() { 320 defer func() { 321 <-w.sem 322 w.walking.Done() 323 }() 324 w.walk(nextPath, pathSymlinks, d) 325 }() 326 continue 327 328 default: 329 // No tokens available, so traverse serially. 330 } 331 } 332 333 w.walk(nextPath, pathSymlinks, d) 334 } 335 } 336 }