github.com/afking/bazel-gazelle@v0.0.0-20180301150245-c02bc0f529e8/internal/packages/walk.go (about) 1 /* Copyright 2016 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 packages 17 18 import ( 19 "go/build" 20 "io/ioutil" 21 "log" 22 "os" 23 "path" 24 "path/filepath" 25 "strings" 26 27 "github.com/bazelbuild/bazel-gazelle/internal/config" 28 "github.com/bazelbuild/bazel-gazelle/internal/pathtools" 29 bf "github.com/bazelbuild/buildtools/build" 30 ) 31 32 // A WalkFunc is a callback called by Walk in each visited directory. 33 // 34 // dir is the absolute file system path to the directory being visited. 35 // 36 // rel is the relative slash-separated path to the directory from the 37 // repository root. Will be "" for the repository root directory itself. 38 // 39 // c is the configuration for the current directory. This may have been 40 // modified by directives in the directory's build file. 41 // 42 // pkg contains information about how to build source code in the directory. 43 // Will be nil for directories that don't contain buildable code, directories 44 // that Gazelle was not asked update, and directories where Walk 45 // encountered errors. 46 // 47 // oldFile is the existing build file in the directory. Will be nil if there 48 // was no file. 49 // 50 // isUpdateDir is true for directories that Gazelle was asked to update. 51 type WalkFunc func(dir, rel string, c *config.Config, pkg *Package, oldFile *bf.File, isUpdateDir bool) 52 53 // Walk traverses a directory tree. In each directory, Walk parses existing 54 // build files. In directories that Gazelle was asked to update (c.Dirs), Walk 55 // also parses source files and infers build information. 56 // 57 // c is the base configuration for the repository. c may be copied and modified 58 // by directives found in build files. 59 // 60 // root is an absolute file path to the directory to traverse. 61 // 62 // f is a function that will be called for each visited directory. 63 func Walk(c *config.Config, root string, f WalkFunc) { 64 // Determine relative paths for the directories to be updated. 65 var updateRels []string 66 for _, dir := range c.Dirs { 67 rel, err := filepath.Rel(c.RepoRoot, dir) 68 if err != nil { 69 // This should have been verified when c was built. 70 log.Panicf("%s: not a subdirectory of repository root %q", dir, c.RepoRoot) 71 } 72 rel = filepath.ToSlash(rel) 73 if rel == "." || rel == "/" { 74 rel = "" 75 } 76 updateRels = append(updateRels, rel) 77 } 78 rootRel, err := filepath.Rel(c.RepoRoot, root) 79 if err != nil { 80 log.Panicf("%s: not a subdirectory of repository root %q", root, c.RepoRoot) 81 } 82 if rootRel == "." || rootRel == "/" { 83 rootRel = "" 84 } 85 86 symlinks := symlinkResolver{root: root, visited: []string{root}} 87 88 // visit walks the directory tree in post-order. It returns whether the 89 // given directory or any subdirectory contained a build file or buildable 90 // source code. This affects whether "testdata" directories are considered 91 // data dependencies. 92 var visit func(*config.Config, string, string, bool, []string) bool 93 visit = func(c *config.Config, dir, rel string, isUpdateDir bool, excluded []string) bool { 94 // Check if this directory should be updated. 95 if !isUpdateDir { 96 for _, updateRel := range updateRels { 97 if pathtools.HasPrefix(rel, updateRel) { 98 isUpdateDir = true 99 } 100 } 101 } 102 103 // Look for an existing BUILD file. 104 var oldFile *bf.File 105 haveError := false 106 for _, base := range c.ValidBuildFileNames { 107 oldPath := filepath.Join(dir, base) 108 st, err := os.Stat(oldPath) 109 if os.IsNotExist(err) || err == nil && st.IsDir() { 110 continue 111 } 112 oldData, err := ioutil.ReadFile(oldPath) 113 if err != nil { 114 log.Print(err) 115 haveError = true 116 continue 117 } 118 if oldFile != nil { 119 log.Printf("in directory %s, multiple Bazel files are present: %s, %s", 120 dir, filepath.Base(oldFile.Path), base) 121 haveError = true 122 continue 123 } 124 oldFile, err = bf.Parse(oldPath, oldData) 125 if err != nil { 126 log.Print(err) 127 haveError = true 128 continue 129 } 130 } 131 132 // Process directives in the build file. If this is a vendor directory, 133 // set an empty prefix. 134 if path.Base(rel) == "vendor" { 135 cCopy := *c 136 cCopy.GoPrefix = "" 137 cCopy.GoPrefixRel = rel 138 c = &cCopy 139 } 140 var directives []config.Directive 141 if oldFile != nil { 142 directives = config.ParseDirectives(oldFile) 143 c = config.ApplyDirectives(c, directives, rel) 144 } 145 c = config.InferProtoMode(c, rel, oldFile, directives) 146 147 var ignore bool 148 for _, d := range directives { 149 switch d.Key { 150 case "exclude": 151 excluded = append(excluded, d.Value) 152 case "ignore": 153 ignore = true 154 } 155 } 156 157 // List files and subdirectories. 158 files, err := ioutil.ReadDir(dir) 159 if err != nil { 160 log.Print(err) 161 return false 162 } 163 if c.ProtoMode == config.DefaultProtoMode { 164 excluded = append(excluded, findPbGoFiles(files, excluded)...) 165 } 166 167 var pkgFiles, otherFiles, subdirs []string 168 for _, f := range files { 169 base := f.Name() 170 switch { 171 case base == "" || base[0] == '.' || base[0] == '_' || isExcluded(excluded, base): 172 continue 173 174 case f.IsDir(): 175 subdirs = append(subdirs, base) 176 177 case strings.HasSuffix(base, ".go") || 178 (c.ProtoMode != config.DisableProtoMode && strings.HasSuffix(base, ".proto")): 179 pkgFiles = append(pkgFiles, base) 180 181 case f.Mode()&os.ModeSymlink != 0 && symlinks.follow(dir, base): 182 subdirs = append(subdirs, base) 183 184 default: 185 otherFiles = append(otherFiles, base) 186 } 187 } 188 // Recurse into subdirectories. 189 hasTestdata := false 190 subdirHasPackage := false 191 for _, sub := range subdirs { 192 subdirExcluded := excludedForSubdir(excluded, sub) 193 hasPackage := visit(c, filepath.Join(dir, sub), path.Join(rel, sub), isUpdateDir, subdirExcluded) 194 if sub == "testdata" && !hasPackage { 195 hasTestdata = true 196 } 197 subdirHasPackage = subdirHasPackage || hasPackage 198 } 199 200 hasPackage := subdirHasPackage || oldFile != nil 201 if haveError || !isUpdateDir || ignore { 202 f(dir, rel, c, nil, oldFile, false) 203 return hasPackage 204 } 205 206 // Build a package from files in this directory. 207 var genFiles []string 208 if oldFile != nil { 209 genFiles = findGenFiles(oldFile, excluded) 210 } 211 pkg := buildPackage(c, dir, rel, pkgFiles, otherFiles, genFiles, hasTestdata) 212 f(dir, rel, c, pkg, oldFile, true) 213 return hasPackage || pkg != nil 214 } 215 216 visit(c, root, rootRel, false, nil) 217 } 218 219 // buildPackage reads source files in a given directory and returns a Package 220 // containing information about those files and how to build them. 221 // 222 // If no buildable .go files are found in the directory, nil will be returned. 223 // If the directory contains multiple buildable packages, the package whose 224 // name matches the directory base name will be returned. If there is no such 225 // package or if an error occurs, an error will be logged, and nil will be 226 // returned. 227 func buildPackage(c *config.Config, dir, rel string, pkgFiles, otherFiles, genFiles []string, hasTestdata bool) *Package { 228 // Process .go and .proto files first, since these determine the package name. 229 packageMap := make(map[string]*packageBuilder) 230 cgo := false 231 var pkgFilesWithUnknownPackage []fileInfo 232 for _, f := range pkgFiles { 233 var info fileInfo 234 switch path.Ext(f) { 235 case ".go": 236 info = goFileInfo(c, dir, rel, f) 237 case ".proto": 238 info = protoFileInfo(c, dir, rel, f) 239 default: 240 log.Panicf("file cannot determine package name: %s", f) 241 } 242 if info.packageName == "" { 243 pkgFilesWithUnknownPackage = append(pkgFilesWithUnknownPackage, info) 244 continue 245 } 246 if info.packageName == "documentation" { 247 // go/build ignores this package 248 continue 249 } 250 251 cgo = cgo || info.isCgo 252 253 if _, ok := packageMap[info.packageName]; !ok { 254 packageMap[info.packageName] = &packageBuilder{ 255 name: info.packageName, 256 dir: dir, 257 rel: rel, 258 hasTestdata: hasTestdata, 259 } 260 } 261 if err := packageMap[info.packageName].addFile(c, info, false); err != nil { 262 log.Print(err) 263 } 264 } 265 266 // Select a package to generate rules for. 267 pkg, err := selectPackage(c, dir, packageMap) 268 if err != nil { 269 if _, ok := err.(*build.NoGoError); !ok { 270 log.Print(err) 271 } 272 return nil 273 } 274 275 // Add files with unknown packages. This happens when there are parse 276 // or I/O errors. We should keep the file in the srcs list and let the 277 // compiler deal with the error. 278 for _, info := range pkgFilesWithUnknownPackage { 279 if err := pkg.addFile(c, info, cgo); err != nil { 280 log.Print(err) 281 } 282 } 283 284 // Process the other static files. 285 for _, file := range otherFiles { 286 info := otherFileInfo(dir, rel, file) 287 if err := pkg.addFile(c, info, cgo); err != nil { 288 log.Print(err) 289 } 290 } 291 292 // Process generated files. Note that generated files may have the same names 293 // as static files. Bazel will use the generated files, but we will look at 294 // the content of static files, assuming they will be the same. 295 staticFiles := make(map[string]bool) 296 for _, f := range pkgFiles { 297 staticFiles[f] = true 298 } 299 for _, f := range otherFiles { 300 staticFiles[f] = true 301 } 302 for _, f := range genFiles { 303 if staticFiles[f] { 304 continue 305 } 306 info := fileNameInfo(dir, rel, f) 307 if err := pkg.addFile(c, info, cgo); err != nil { 308 log.Print(err) 309 } 310 } 311 312 if pkg.importPath == "" { 313 if err := pkg.inferImportPath(c); err != nil { 314 log.Print(err) 315 return nil 316 } 317 } 318 return pkg.build() 319 } 320 321 func selectPackage(c *config.Config, dir string, packageMap map[string]*packageBuilder) (*packageBuilder, error) { 322 buildablePackages := make(map[string]*packageBuilder) 323 for name, pkg := range packageMap { 324 if pkg.isBuildable(c) { 325 buildablePackages[name] = pkg 326 } 327 } 328 329 if len(buildablePackages) == 0 { 330 return nil, &build.NoGoError{Dir: dir} 331 } 332 333 if len(buildablePackages) == 1 { 334 for _, pkg := range buildablePackages { 335 return pkg, nil 336 } 337 } 338 339 if pkg, ok := buildablePackages[defaultPackageName(c, dir)]; ok { 340 return pkg, nil 341 } 342 343 err := &build.MultiplePackageError{Dir: dir} 344 for name, pkg := range buildablePackages { 345 // Add the first file for each package for the error message. 346 // Error() method expects these lists to be the same length. File 347 // lists must be non-empty. These lists are only created by 348 // buildPackage for packages with .go files present. 349 err.Packages = append(err.Packages, name) 350 err.Files = append(err.Files, pkg.firstGoFile()) 351 } 352 return nil, err 353 } 354 355 func defaultPackageName(c *config.Config, dir string) string { 356 if dir != c.RepoRoot { 357 return filepath.Base(dir) 358 } 359 name := path.Base(c.GoPrefix) 360 if name == "." || name == "/" { 361 // This can happen if go_prefix is empty or is all slashes. 362 return "unnamed" 363 } 364 return name 365 } 366 367 func findGenFiles(f *bf.File, excluded []string) []string { 368 var strs []string 369 for _, r := range f.Rules("") { 370 for _, key := range []string{"out", "outs"} { 371 switch e := r.Attr(key).(type) { 372 case *bf.StringExpr: 373 strs = append(strs, e.Value) 374 case *bf.ListExpr: 375 for _, elem := range e.List { 376 if s, ok := elem.(*bf.StringExpr); ok { 377 strs = append(strs, s.Value) 378 } 379 } 380 } 381 } 382 } 383 384 var genFiles []string 385 for _, s := range strs { 386 if !isExcluded(excluded, s) { 387 genFiles = append(genFiles, s) 388 } 389 } 390 return genFiles 391 } 392 393 func findPbGoFiles(files []os.FileInfo, excluded []string) []string { 394 var pbGoFiles []string 395 for _, f := range files { 396 name := f.Name() 397 if strings.HasSuffix(name, ".proto") && !isExcluded(excluded, name) { 398 pbGoFiles = append(pbGoFiles, name[:len(name)-len(".proto")]+".pb.go") 399 } 400 } 401 return pbGoFiles 402 } 403 404 func isExcluded(excluded []string, base string) bool { 405 for _, e := range excluded { 406 if base == e { 407 return true 408 } 409 } 410 return false 411 } 412 413 func excludedForSubdir(excluded []string, subdir string) []string { 414 var filtered []string 415 for _, e := range excluded { 416 i := strings.IndexByte(e, '/') 417 if i < 0 || i == len(e)-1 || e[:i] != subdir { 418 continue 419 } 420 filtered = append(filtered, e[i+1:]) 421 } 422 return filtered 423 } 424 425 type symlinkResolver struct { 426 root string 427 visited []string 428 } 429 430 // Decide if symlink dir/base should be followed. 431 func (r *symlinkResolver) follow(dir, base string) bool { 432 if dir == r.root && strings.HasPrefix(base, "bazel-") { 433 // Links such as bazel-<workspace>, bazel-out, bazel-genfiles are created by 434 // Bazel to point to internal build directories. 435 return false 436 } 437 // See if the symlink points to a tree that has been already visited. 438 fullpath := filepath.Join(dir, base) 439 dest, err := filepath.EvalSymlinks(fullpath) 440 if err != nil { 441 return false 442 } 443 if !filepath.IsAbs(dest) { 444 dest, err = filepath.Abs(filepath.Join(dir, dest)) 445 if err != nil { 446 return false 447 } 448 } 449 for _, p := range r.visited { 450 if pathtools.HasPrefix(dest, p) || pathtools.HasPrefix(p, dest) { 451 return false 452 } 453 } 454 r.visited = append(r.visited, dest) 455 stat, err := os.Stat(fullpath) 456 if err != nil { 457 return false 458 } 459 return stat.IsDir() 460 }