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  }