github.com/v2fly/tools@v0.100.0/internal/gopathwalk/walk.go (about) 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 "fmt" 13 "io/ioutil" 14 "log" 15 "os" 16 "path/filepath" 17 "strings" 18 "time" 19 20 "github.com/v2fly/tools/internal/fastwalk" 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 ...interface{}) 27 // Search module caches. Also disables legacy goimports ignore rules. 28 ModulesEnabled bool 29 } 30 31 // RootType indicates the type of a Root. 32 type RootType int 33 34 const ( 35 RootUnknown RootType = iota 36 RootGOROOT 37 RootGOPATH 38 RootCurrentModule 39 RootModuleCache 40 RootOther 41 ) 42 43 // A Root is a starting point for a Walk. 44 type Root struct { 45 Path string 46 Type RootType 47 } 48 49 // Walk walks Go source directories ($GOROOT, $GOPATH, etc) to find packages. 50 // For each package found, add will be called (concurrently) with the absolute 51 // paths of the containing source directory and the package directory. 52 // add will be called concurrently. 53 func Walk(roots []Root, add func(root Root, dir string), opts Options) { 54 WalkSkip(roots, add, func(Root, string) bool { return false }, opts) 55 } 56 57 // WalkSkip walks Go source directories ($GOROOT, $GOPATH, etc) to find packages. 58 // For each package found, add will be called (concurrently) with the absolute 59 // paths of the containing source directory and the package directory. 60 // For each directory that will be scanned, skip will be called (concurrently) 61 // with the absolute paths of the containing source directory and the directory. 62 // If skip returns false on a directory it will be processed. 63 // add will be called concurrently. 64 // skip will be called concurrently. 65 func WalkSkip(roots []Root, add func(root Root, dir string), skip func(root Root, dir string) bool, opts Options) { 66 for _, root := range roots { 67 walkDir(root, add, skip, opts) 68 } 69 } 70 71 // walkDir creates a walker and starts fastwalk with this walker. 72 func walkDir(root Root, add func(Root, string), skip func(root Root, dir string) bool, opts Options) { 73 if _, err := os.Stat(root.Path); os.IsNotExist(err) { 74 if opts.Logf != nil { 75 opts.Logf("skipping nonexistent directory: %v", root.Path) 76 } 77 return 78 } 79 start := time.Now() 80 if opts.Logf != nil { 81 opts.Logf("gopathwalk: scanning %s", root.Path) 82 } 83 w := &walker{ 84 root: root, 85 add: add, 86 skip: skip, 87 opts: opts, 88 } 89 w.init() 90 if err := fastwalk.Walk(root.Path, w.walk); err != nil { 91 log.Printf("gopathwalk: scanning directory %v: %v", root.Path, err) 92 } 93 94 if opts.Logf != nil { 95 opts.Logf("gopathwalk: scanned %s in %v", root.Path, time.Since(start)) 96 } 97 } 98 99 // walker is the callback for fastwalk.Walk. 100 type walker struct { 101 root Root // The source directory to scan. 102 add func(Root, string) // The callback that will be invoked for every possible Go package dir. 103 skip func(Root, string) bool // The callback that will be invoked for every dir. dir is skipped if it returns true. 104 opts Options // Options passed to Walk by the user. 105 106 ignoredDirs []os.FileInfo // The ignored directories, loaded from .goimportsignore files. 107 } 108 109 // init initializes the walker based on its Options 110 func (w *walker) init() { 111 var ignoredPaths []string 112 if w.root.Type == RootModuleCache { 113 ignoredPaths = []string{"cache"} 114 } 115 if !w.opts.ModulesEnabled && w.root.Type == RootGOPATH { 116 ignoredPaths = w.getIgnoredDirs(w.root.Path) 117 ignoredPaths = append(ignoredPaths, "v", "mod") 118 } 119 120 for _, p := range ignoredPaths { 121 full := filepath.Join(w.root.Path, p) 122 if fi, err := os.Stat(full); err == nil { 123 w.ignoredDirs = append(w.ignoredDirs, fi) 124 if w.opts.Logf != nil { 125 w.opts.Logf("Directory added to ignore list: %s", full) 126 } 127 } else if w.opts.Logf != nil { 128 w.opts.Logf("Error statting ignored directory: %v", err) 129 } 130 } 131 } 132 133 // getIgnoredDirs reads an optional config file at <path>/.goimportsignore 134 // of relative directories to ignore when scanning for go files. 135 // The provided path is one of the $GOPATH entries with "src" appended. 136 func (w *walker) getIgnoredDirs(path string) []string { 137 file := filepath.Join(path, ".goimportsignore") 138 slurp, err := ioutil.ReadFile(file) 139 if w.opts.Logf != nil { 140 if err != nil { 141 w.opts.Logf("%v", err) 142 } else { 143 w.opts.Logf("Read %s", file) 144 } 145 } 146 if err != nil { 147 return nil 148 } 149 150 var ignoredDirs []string 151 bs := bufio.NewScanner(bytes.NewReader(slurp)) 152 for bs.Scan() { 153 line := strings.TrimSpace(bs.Text()) 154 if line == "" || strings.HasPrefix(line, "#") { 155 continue 156 } 157 ignoredDirs = append(ignoredDirs, line) 158 } 159 return ignoredDirs 160 } 161 162 // shouldSkipDir reports whether the file should be skipped or not. 163 func (w *walker) shouldSkipDir(fi os.FileInfo, dir string) bool { 164 for _, ignoredDir := range w.ignoredDirs { 165 if os.SameFile(fi, ignoredDir) { 166 return true 167 } 168 } 169 if w.skip != nil { 170 // Check with the user specified callback. 171 return w.skip(w.root, dir) 172 } 173 return false 174 } 175 176 // walk walks through the given path. 177 func (w *walker) walk(path string, typ os.FileMode) error { 178 dir := filepath.Dir(path) 179 if typ.IsRegular() { 180 if dir == w.root.Path && (w.root.Type == RootGOROOT || w.root.Type == RootGOPATH) { 181 // Doesn't make sense to have regular files 182 // directly in your $GOPATH/src or $GOROOT/src. 183 return fastwalk.ErrSkipFiles 184 } 185 if !strings.HasSuffix(path, ".go") { 186 return nil 187 } 188 189 w.add(w.root, dir) 190 return fastwalk.ErrSkipFiles 191 } 192 if typ == os.ModeDir { 193 base := filepath.Base(path) 194 if base == "" || base[0] == '.' || base[0] == '_' || 195 base == "testdata" || 196 (w.root.Type == RootGOROOT && w.opts.ModulesEnabled && base == "vendor") || 197 (!w.opts.ModulesEnabled && base == "node_modules") { 198 return filepath.SkipDir 199 } 200 fi, err := os.Lstat(path) 201 if err == nil && w.shouldSkipDir(fi, path) { 202 return filepath.SkipDir 203 } 204 return nil 205 } 206 if typ == os.ModeSymlink { 207 base := filepath.Base(path) 208 if strings.HasPrefix(base, ".#") { 209 // Emacs noise. 210 return nil 211 } 212 fi, err := os.Lstat(path) 213 if err != nil { 214 // Just ignore it. 215 return nil 216 } 217 if w.shouldTraverse(dir, fi) { 218 return fastwalk.ErrTraverseLink 219 } 220 } 221 return nil 222 } 223 224 // shouldTraverse reports whether the symlink fi, found in dir, 225 // should be followed. It makes sure symlinks were never visited 226 // before to avoid symlink loops. 227 func (w *walker) shouldTraverse(dir string, fi os.FileInfo) bool { 228 path := filepath.Join(dir, fi.Name()) 229 target, err := filepath.EvalSymlinks(path) 230 if err != nil { 231 return false 232 } 233 ts, err := os.Stat(target) 234 if err != nil { 235 fmt.Fprintln(os.Stderr, err) 236 return false 237 } 238 if !ts.IsDir() { 239 return false 240 } 241 if w.shouldSkipDir(ts, dir) { 242 return false 243 } 244 // Check for symlink loops by statting each directory component 245 // and seeing if any are the same file as ts. 246 for { 247 parent := filepath.Dir(path) 248 if parent == path { 249 // Made it to the root without seeing a cycle. 250 // Use this symlink. 251 return true 252 } 253 parentInfo, err := os.Stat(parent) 254 if err != nil { 255 return false 256 } 257 if os.SameFile(ts, parentInfo) { 258 // Cycle. Don't traverse. 259 return false 260 } 261 path = parent 262 } 263 264 }