github.com/wolfd/bazel-gazelle@v0.14.0/internal/walk/walk.go (about) 1 /* Copyright 2018 The Bazel Authors. All rights reserved. 2 3 Licensed under the Apache License, Version 2.0 (the "License"); 4 you may not use this file except in compliance with the License. 5 You may obtain a copy of the License at 6 7 http://www.apache.org/licenses/LICENSE-2.0 8 9 Unless required by applicable law or agreed to in writing, software 10 distributed under the License is distributed on an "AS IS" BASIS, 11 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 See the License for the specific language governing permissions and 13 limitations under the License. 14 */ 15 16 package walk 17 18 import ( 19 "io/ioutil" 20 "log" 21 "os" 22 "path" 23 "path/filepath" 24 "strings" 25 26 "github.com/bazelbuild/bazel-gazelle/internal/config" 27 "github.com/bazelbuild/bazel-gazelle/internal/pathtools" 28 "github.com/bazelbuild/bazel-gazelle/internal/rule" 29 ) 30 31 // WalkFunc is a callback called by Walk in each visited directory. 32 // 33 // dir is the absolute file system path to the directory being visited. 34 // 35 // rel is the relative slash-separated path to the directory from the 36 // repository root. Will be "" for the repository root directory itself. 37 // 38 // c is the configuration for the current directory. This may have been 39 // modified by directives in the directory's build file. 40 // 41 // update is true when the build file may be updated. 42 // 43 // f is the existing build file in the directory. Will be nil if there 44 // was no file. 45 // 46 // subdirs is a list of base names of subdirectories within dir, not 47 // including excluded files. 48 // 49 // regularFiles is a list of base names of regular files within dir, not 50 // including excluded files. 51 // 52 // genFiles is a list of names of generated files, found by reading 53 // "out" and "outs" attributes of rules in f. 54 type WalkFunc func(dir, rel string, c *config.Config, update bool, f *rule.File, subdirs, regularFiles, genFiles []string) 55 56 // Walk traverses the directory tree rooted at c.RepoRoot in depth-first order. 57 // 58 // Walk calls the Configure method on each configuration extension in cexts 59 // in each directory in pre-order, whether a build file is present in the 60 // directory or not. 61 // 62 // Walk calls the callback wf in post-order. 63 func Walk(c *config.Config, cexts []config.Configurer, wf WalkFunc) { 64 cexts = append(cexts, &walkConfigurer{}) 65 knownDirectives := make(map[string]bool) 66 for _, cext := range cexts { 67 for _, d := range cext.KnownDirectives() { 68 knownDirectives[d] = true 69 } 70 } 71 72 updateRels := buildUpdateRels(c.RepoRoot, c.Dirs) 73 symlinks := symlinkResolver{root: c.RepoRoot, visited: []string{c.RepoRoot}} 74 75 var visit func(*config.Config, string, string, bool) 76 visit = func(c *config.Config, dir, rel string, isUpdateDir bool) { 77 haveError := false 78 79 if !isUpdateDir { 80 isUpdateDir = shouldUpdateDir(rel, updateRels) 81 } 82 83 // TODO: OPT: ReadDir stats all the files, which is slow. We just care about 84 // names and modes, so we should use something like 85 // golang.org/x/tools/internal/fastwalk to speed this up. 86 files, err := ioutil.ReadDir(dir) 87 if err != nil { 88 log.Print(err) 89 return 90 } 91 92 f, err := loadBuildFile(c, rel, dir, files) 93 if err != nil { 94 log.Print(err) 95 haveError = true 96 } 97 98 c = configure(cexts, knownDirectives, c, rel, f) 99 wc := getWalkConfig(c) 100 101 var subdirs, regularFiles []string 102 for _, fi := range files { 103 base := fi.Name() 104 switch { 105 case base == "" || base[0] == '.' || base[0] == '_' || wc.isExcluded(base): 106 continue 107 108 case fi.IsDir() || fi.Mode()&os.ModeSymlink != 0 && symlinks.follow(dir, base): 109 subdirs = append(subdirs, base) 110 111 default: 112 regularFiles = append(regularFiles, base) 113 } 114 } 115 116 for _, sub := range subdirs { 117 visit(c, filepath.Join(dir, sub), path.Join(rel, sub), isUpdateDir) 118 } 119 120 genFiles := findGenFiles(wc, f) 121 update := !haveError && isUpdateDir && !wc.ignore 122 wf(dir, rel, c, update, f, subdirs, regularFiles, genFiles) 123 } 124 visit(c, c.RepoRoot, "", false) 125 } 126 127 // buildUpdateRels builds a list of relative paths from the repository root 128 // directory (passed as an absolute file path) to directories that Gazelle 129 // may update. The relative paths are slash-separated. "" represents the 130 // root directory itself. 131 func buildUpdateRels(root string, dirs []string) []string { 132 var updateRels []string 133 for _, dir := range dirs { 134 rel, err := filepath.Rel(root, dir) 135 if err != nil { 136 // This should have been verified when c was built. 137 log.Panicf("%s: not a subdirectory of repository root %q", dir, root) 138 } 139 rel = filepath.ToSlash(rel) 140 if rel == "." || rel == "/" { 141 rel = "" 142 } 143 updateRels = append(updateRels, rel) 144 } 145 return updateRels 146 } 147 148 func shouldUpdateDir(rel string, updateRels []string) bool { 149 for _, r := range updateRels { 150 if pathtools.HasPrefix(rel, r) { 151 return true 152 } 153 } 154 return false 155 } 156 157 func loadBuildFile(c *config.Config, pkg, dir string, files []os.FileInfo) (*rule.File, error) { 158 var err error 159 readDir := dir 160 readFiles := files 161 if c.ReadBuildFilesDir != "" { 162 readDir = filepath.Join(c.ReadBuildFilesDir, filepath.FromSlash(pkg)) 163 readFiles, err = ioutil.ReadDir(readDir) 164 if err != nil { 165 return nil, err 166 } 167 } 168 path := rule.MatchBuildFileName(readDir, c.ValidBuildFileNames, readFiles) 169 if path == "" { 170 return nil, nil 171 } 172 return rule.LoadFile(path, pkg) 173 } 174 175 func configure(cexts []config.Configurer, knownDirectives map[string]bool, c *config.Config, rel string, f *rule.File) *config.Config { 176 if rel != "" { 177 c = c.Clone() 178 } 179 if f != nil { 180 for _, d := range f.Directives { 181 if !knownDirectives[d.Key] { 182 log.Printf("%s: unknown directive: gazelle:%s", f.Path, d.Key) 183 } 184 } 185 } 186 for _, cext := range cexts { 187 cext.Configure(c, rel, f) 188 } 189 return c 190 } 191 192 func findGenFiles(wc walkConfig, f *rule.File) []string { 193 if f == nil { 194 return nil 195 } 196 var strs []string 197 for _, r := range f.Rules { 198 for _, key := range []string{"out", "outs"} { 199 if s := r.AttrString(key); s != "" { 200 strs = append(strs, s) 201 } else if ss := r.AttrStrings(key); len(ss) > 0 { 202 strs = append(strs, ss...) 203 } 204 } 205 } 206 207 var genFiles []string 208 for _, s := range strs { 209 if !wc.isExcluded(s) { 210 genFiles = append(genFiles, s) 211 } 212 } 213 return genFiles 214 } 215 216 type symlinkResolver struct { 217 root string 218 visited []string 219 } 220 221 // Decide if symlink dir/base should be followed. 222 func (r *symlinkResolver) follow(dir, base string) bool { 223 if dir == r.root && strings.HasPrefix(base, "bazel-") { 224 // Links such as bazel-<workspace>, bazel-out, bazel-genfiles are created by 225 // Bazel to point to internal build directories. 226 return false 227 } 228 // See if the symlink points to a tree that has been already visited. 229 fullpath := filepath.Join(dir, base) 230 dest, err := filepath.EvalSymlinks(fullpath) 231 if err != nil { 232 return false 233 } 234 if !filepath.IsAbs(dest) { 235 dest, err = filepath.Abs(filepath.Join(dir, dest)) 236 if err != nil { 237 return false 238 } 239 } 240 for _, p := range r.visited { 241 if pathtools.HasPrefix(dest, p) || pathtools.HasPrefix(p, dest) { 242 return false 243 } 244 } 245 r.visited = append(r.visited, dest) 246 stat, err := os.Stat(fullpath) 247 if err != nil { 248 return false 249 } 250 return stat.IsDir() 251 }