github.com/bazelbuild/bazel-gazelle@v0.36.1-0.20240520142334-61b277ba6fed/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 provides customizable functionality for visiting each 17 // subdirectory in a directory tree. 18 package walk 19 20 import ( 21 "io/fs" 22 "log" 23 "os" 24 "path" 25 "path/filepath" 26 "strings" 27 28 "github.com/bazelbuild/bazel-gazelle/config" 29 "github.com/bazelbuild/bazel-gazelle/rule" 30 ) 31 32 // Mode determines which directories Walk visits and which directories 33 // should be updated. 34 type Mode int 35 36 const ( 37 // In VisitAllUpdateSubdirsMode, Walk visits every directory in the 38 // repository. The directories given to Walk and their subdirectories are 39 // updated. 40 VisitAllUpdateSubdirsMode Mode = iota 41 42 // In VisitAllUpdateDirsMode, Walk visits every directory in the repository. 43 // Only the directories given to Walk are updated (not their subdirectories). 44 VisitAllUpdateDirsMode 45 46 // In UpdateDirsMode, Walk only visits and updates directories given to Walk. 47 // Build files in parent directories are read in order to produce a complete 48 // configuration, but the callback is not called for parent directories. 49 UpdateDirsMode 50 51 // In UpdateSubdirsMode, Walk visits and updates the directories given to Walk 52 // and their subdirectories. Build files in parent directories are read in 53 // order to produce a complete configuration, but the callback is not called 54 // for parent directories. 55 UpdateSubdirsMode 56 ) 57 58 // WalkFunc is a callback called by Walk in each visited directory. 59 // 60 // dir is the absolute file system path to the directory being visited. 61 // 62 // rel is the relative slash-separated path to the directory from the 63 // repository root. Will be "" for the repository root directory itself. 64 // 65 // c is the configuration for the current directory. This may have been 66 // modified by directives in the directory's build file. 67 // 68 // update is true when the build file may be updated. 69 // 70 // f is the existing build file in the directory. Will be nil if there 71 // was no file. 72 // 73 // subdirs is a list of base names of subdirectories within dir, not 74 // including excluded files. 75 // 76 // regularFiles is a list of base names of regular files within dir, not 77 // including excluded files or symlinks. 78 // 79 // genFiles is a list of names of generated files, found by reading 80 // "out" and "outs" attributes of rules in f. 81 type WalkFunc func(dir, rel string, c *config.Config, update bool, f *rule.File, subdirs, regularFiles, genFiles []string) 82 83 // Walk traverses the directory tree rooted at c.RepoRoot. Walk visits 84 // subdirectories in depth-first post-order. 85 // 86 // When Walk visits a directory, it lists the files and subdirectories within 87 // that directory. If a build file is present, Walk reads the build file and 88 // applies any directives to the configuration (a copy of the parent directory's 89 // configuration is made, and the copy is modified). After visiting 90 // subdirectories, the callback wf may be called, depending on the mode. 91 // 92 // c is the root configuration to start with. This includes changes made by 93 // command line flags, but not by the root build file. This configuration 94 // should not be modified. 95 // 96 // cexts is a list of configuration extensions. When visiting a directory, 97 // before visiting subdirectories, Walk makes a copy of the parent configuration 98 // and Configure for each extension on the copy. If Walk sees a directive 99 // that is not listed in KnownDirectives of any extension, an error will 100 // be logged. 101 // 102 // dirs is a list of absolute, canonical file system paths of directories 103 // to visit. 104 // 105 // mode determines whether subdirectories of dirs should be visited recursively, 106 // when the wf callback should be called, and when the "update" argument 107 // to the wf callback should be set. 108 // 109 // wf is a function that may be called in each directory. 110 func Walk(c *config.Config, cexts []config.Configurer, dirs []string, mode Mode, wf WalkFunc) { 111 knownDirectives := make(map[string]bool) 112 for _, cext := range cexts { 113 for _, d := range cext.KnownDirectives() { 114 knownDirectives[d] = true 115 } 116 } 117 118 updateRels := buildUpdateRelMap(c.RepoRoot, dirs) 119 120 var visit func(*config.Config, string, string, bool) 121 visit = func(c *config.Config, dir, rel string, updateParent bool) { 122 haveError := false 123 124 // TODO: OPT: ReadDir stats all the files, which is slow. We just care about 125 // names and modes, so we should use something like 126 // golang.org/x/tools/internal/fastwalk to speed this up. 127 ents, err := os.ReadDir(dir) 128 if err != nil { 129 log.Print(err) 130 return 131 } 132 133 f, err := loadBuildFile(c, rel, dir, ents) 134 if err != nil { 135 log.Print(err) 136 if c.Strict { 137 // TODO(https://github.com/bazelbuild/bazel-gazelle/issues/1029): 138 // Refactor to accumulate and propagate errors to main. 139 log.Fatal("Exit as strict mode is on") 140 } 141 haveError = true 142 } 143 144 c = configure(cexts, knownDirectives, c, rel, f) 145 wc := getWalkConfig(c) 146 147 if wc.isExcluded(rel, ".") { 148 return 149 } 150 151 var subdirs, regularFiles []string 152 for _, ent := range ents { 153 base := ent.Name() 154 ent := resolveFileInfo(wc, dir, rel, ent) 155 switch { 156 case ent == nil: 157 continue 158 case ent.IsDir(): 159 subdirs = append(subdirs, base) 160 default: 161 regularFiles = append(regularFiles, base) 162 } 163 } 164 165 shouldUpdate := shouldUpdate(rel, mode, updateParent, updateRels) 166 for _, sub := range subdirs { 167 if subRel := path.Join(rel, sub); shouldVisit(subRel, mode, shouldUpdate, updateRels) { 168 visit(c, filepath.Join(dir, sub), subRel, shouldUpdate) 169 } 170 } 171 172 update := !haveError && !wc.ignore && shouldUpdate 173 if shouldCall(rel, mode, updateParent, updateRels) { 174 genFiles := findGenFiles(wc, f) 175 wf(dir, rel, c, update, f, subdirs, regularFiles, genFiles) 176 } 177 } 178 visit(c, c.RepoRoot, "", false) 179 } 180 181 // buildUpdateRelMap builds a table of prefixes, used to determine which 182 // directories to update and visit. 183 // 184 // root and dirs must be absolute, canonical file paths. Each entry in dirs 185 // must be a subdirectory of root. The caller is responsible for checking this. 186 // 187 // buildUpdateRelMap returns a map from slash-separated paths relative to the 188 // root directory ("" for the root itself) to a boolean indicating whether 189 // the directory should be updated. 190 func buildUpdateRelMap(root string, dirs []string) map[string]bool { 191 relMap := make(map[string]bool) 192 for _, dir := range dirs { 193 rel, _ := filepath.Rel(root, dir) 194 rel = filepath.ToSlash(rel) 195 if rel == "." { 196 rel = "" 197 } 198 199 i := 0 200 for { 201 next := strings.IndexByte(rel[i:], '/') + i 202 if next-i < 0 { 203 relMap[rel] = true 204 break 205 } 206 prefix := rel[:next] 207 if _, ok := relMap[prefix]; !ok { 208 relMap[prefix] = false 209 } 210 i = next + 1 211 } 212 } 213 return relMap 214 } 215 216 // shouldCall returns true if Walk should call the callback in the 217 // directory rel. 218 func shouldCall(rel string, mode Mode, updateParent bool, updateRels map[string]bool) bool { 219 switch mode { 220 case VisitAllUpdateSubdirsMode, VisitAllUpdateDirsMode: 221 return true 222 case UpdateSubdirsMode: 223 return updateParent || updateRels[rel] 224 default: // UpdateDirsMode 225 return updateRels[rel] 226 } 227 } 228 229 // shouldUpdate returns true if Walk should pass true to the callback's update 230 // parameter in the directory rel. This indicates the build file should be 231 // updated. 232 func shouldUpdate(rel string, mode Mode, updateParent bool, updateRels map[string]bool) bool { 233 if (mode == VisitAllUpdateSubdirsMode || mode == UpdateSubdirsMode) && updateParent { 234 return true 235 } 236 return updateRels[rel] 237 } 238 239 // shouldVisit returns true if Walk should visit the subdirectory rel. 240 func shouldVisit(rel string, mode Mode, updateParent bool, updateRels map[string]bool) bool { 241 switch mode { 242 case VisitAllUpdateSubdirsMode, VisitAllUpdateDirsMode: 243 return true 244 case UpdateSubdirsMode: 245 _, ok := updateRels[rel] 246 return ok || updateParent 247 default: // UpdateDirsMode 248 _, ok := updateRels[rel] 249 return ok 250 } 251 } 252 253 func loadBuildFile(c *config.Config, pkg, dir string, ents []fs.DirEntry) (*rule.File, error) { 254 var err error 255 readDir := dir 256 readEnts := ents 257 if c.ReadBuildFilesDir != "" { 258 readDir = filepath.Join(c.ReadBuildFilesDir, filepath.FromSlash(pkg)) 259 readEnts, err = os.ReadDir(readDir) 260 if err != nil { 261 return nil, err 262 } 263 } 264 path := rule.MatchBuildFile(readDir, c.ValidBuildFileNames, readEnts) 265 if path == "" { 266 return nil, nil 267 } 268 return rule.LoadFile(path, pkg) 269 } 270 271 func configure(cexts []config.Configurer, knownDirectives map[string]bool, c *config.Config, rel string, f *rule.File) *config.Config { 272 if rel != "" { 273 c = c.Clone() 274 } 275 if f != nil { 276 for _, d := range f.Directives { 277 if !knownDirectives[d.Key] { 278 log.Printf("%s: unknown directive: gazelle:%s", f.Path, d.Key) 279 if c.Strict { 280 // TODO(https://github.com/bazelbuild/bazel-gazelle/issues/1029): 281 // Refactor to accumulate and propagate errors to main. 282 log.Fatal("Exit as strict mode is on") 283 } 284 } 285 } 286 } 287 for _, cext := range cexts { 288 cext.Configure(c, rel, f) 289 } 290 return c 291 } 292 293 func findGenFiles(wc *walkConfig, f *rule.File) []string { 294 if f == nil { 295 return nil 296 } 297 var strs []string 298 for _, r := range f.Rules { 299 for _, key := range []string{"out", "outs"} { 300 if s := r.AttrString(key); s != "" { 301 strs = append(strs, s) 302 } else if ss := r.AttrStrings(key); len(ss) > 0 { 303 strs = append(strs, ss...) 304 } 305 } 306 } 307 308 var genFiles []string 309 for _, s := range strs { 310 if !wc.isExcluded(f.Pkg, s) { 311 genFiles = append(genFiles, s) 312 } 313 } 314 return genFiles 315 } 316 317 func resolveFileInfo(wc *walkConfig, dir, rel string, ent fs.DirEntry) fs.DirEntry { 318 base := ent.Name() 319 if base == "" || wc.isExcluded(rel, base) { 320 return nil 321 } 322 if ent.Type()&os.ModeSymlink == 0 { 323 // Not a symlink, use the original FileInfo. 324 return ent 325 } 326 if !wc.shouldFollow(rel, ent.Name()) { 327 // A symlink, but not one we should follow. 328 return nil 329 } 330 fi, err := os.Stat(path.Join(dir, base)) 331 if err != nil { 332 // A symlink, but not one we could resolve. 333 return nil 334 } 335 return fs.FileInfoToDirEntry(fi) 336 }