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  }