gopkg.in/alecthomas/gometalinter.v3@v3.0.0/_linters/src/github.com/kisielk/gotool/internal/load/search.go (about)

     1  // Copyright 2017 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  // +build go1.9
     6  
     7  package load
     8  
     9  import (
    10  	"fmt"
    11  	"go/build"
    12  	"log"
    13  	"os"
    14  	"path"
    15  	"path/filepath"
    16  	"regexp"
    17  	"strings"
    18  )
    19  
    20  // Context specifies values for operation of ImportPaths that would
    21  // otherwise come from cmd/go/internal/cfg package.
    22  //
    23  // This is a construct added for gotool purposes and doesn't have
    24  // an equivalent upstream in cmd/go.
    25  type Context struct {
    26  	// BuildContext is the build context to use.
    27  	BuildContext build.Context
    28  
    29  	// GOROOTsrc is the location of the src directory in GOROOT.
    30  	// At this time, it's used only in MatchPackages to skip
    31  	// GOOROOT/src entry from BuildContext.SrcDirs output.
    32  	GOROOTsrc string
    33  }
    34  
    35  // allPackages returns all the packages that can be found
    36  // under the $GOPATH directories and $GOROOT matching pattern.
    37  // The pattern is either "all" (all packages), "std" (standard packages),
    38  // "cmd" (standard commands), or a path including "...".
    39  func (c *Context) allPackages(pattern string) []string {
    40  	pkgs := c.MatchPackages(pattern)
    41  	if len(pkgs) == 0 {
    42  		fmt.Fprintf(os.Stderr, "warning: %q matched no packages\n", pattern)
    43  	}
    44  	return pkgs
    45  }
    46  
    47  // allPackagesInFS is like allPackages but is passed a pattern
    48  // beginning ./ or ../, meaning it should scan the tree rooted
    49  // at the given directory. There are ... in the pattern too.
    50  func (c *Context) allPackagesInFS(pattern string) []string {
    51  	pkgs := c.MatchPackagesInFS(pattern)
    52  	if len(pkgs) == 0 {
    53  		fmt.Fprintf(os.Stderr, "warning: %q matched no packages\n", pattern)
    54  	}
    55  	return pkgs
    56  }
    57  
    58  // MatchPackages returns a list of package paths matching pattern
    59  // (see go help packages for pattern syntax).
    60  func (c *Context) MatchPackages(pattern string) []string {
    61  	match := func(string) bool { return true }
    62  	treeCanMatch := func(string) bool { return true }
    63  	if !IsMetaPackage(pattern) {
    64  		match = matchPattern(pattern)
    65  		treeCanMatch = treeCanMatchPattern(pattern)
    66  	}
    67  
    68  	have := map[string]bool{
    69  		"builtin": true, // ignore pseudo-package that exists only for documentation
    70  	}
    71  	if !c.BuildContext.CgoEnabled {
    72  		have["runtime/cgo"] = true // ignore during walk
    73  	}
    74  	var pkgs []string
    75  
    76  	for _, src := range c.BuildContext.SrcDirs() {
    77  		if (pattern == "std" || pattern == "cmd") && src != c.GOROOTsrc {
    78  			continue
    79  		}
    80  		src = filepath.Clean(src) + string(filepath.Separator)
    81  		root := src
    82  		if pattern == "cmd" {
    83  			root += "cmd" + string(filepath.Separator)
    84  		}
    85  		filepath.Walk(root, func(path string, fi os.FileInfo, err error) error {
    86  			if err != nil || path == src {
    87  				return nil
    88  			}
    89  
    90  			want := true
    91  			// Avoid .foo, _foo, and testdata directory trees.
    92  			_, elem := filepath.Split(path)
    93  			if strings.HasPrefix(elem, ".") || strings.HasPrefix(elem, "_") || elem == "testdata" {
    94  				want = false
    95  			}
    96  
    97  			name := filepath.ToSlash(path[len(src):])
    98  			if pattern == "std" && (!isStandardImportPath(name) || name == "cmd") {
    99  				// The name "std" is only the standard library.
   100  				// If the name is cmd, it's the root of the command tree.
   101  				want = false
   102  			}
   103  			if !treeCanMatch(name) {
   104  				want = false
   105  			}
   106  
   107  			if !fi.IsDir() {
   108  				if fi.Mode()&os.ModeSymlink != 0 && want {
   109  					if target, err := os.Stat(path); err == nil && target.IsDir() {
   110  						fmt.Fprintf(os.Stderr, "warning: ignoring symlink %s\n", path)
   111  					}
   112  				}
   113  				return nil
   114  			}
   115  			if !want {
   116  				return filepath.SkipDir
   117  			}
   118  
   119  			if have[name] {
   120  				return nil
   121  			}
   122  			have[name] = true
   123  			if !match(name) {
   124  				return nil
   125  			}
   126  			pkg, err := c.BuildContext.ImportDir(path, 0)
   127  			if err != nil {
   128  				if _, noGo := err.(*build.NoGoError); noGo {
   129  					return nil
   130  				}
   131  			}
   132  
   133  			// If we are expanding "cmd", skip main
   134  			// packages under cmd/vendor. At least as of
   135  			// March, 2017, there is one there for the
   136  			// vendored pprof tool.
   137  			if pattern == "cmd" && strings.HasPrefix(pkg.ImportPath, "cmd/vendor") && pkg.Name == "main" {
   138  				return nil
   139  			}
   140  
   141  			pkgs = append(pkgs, name)
   142  			return nil
   143  		})
   144  	}
   145  	return pkgs
   146  }
   147  
   148  // MatchPackagesInFS returns a list of package paths matching pattern,
   149  // which must begin with ./ or ../
   150  // (see go help packages for pattern syntax).
   151  func (c *Context) MatchPackagesInFS(pattern string) []string {
   152  	// Find directory to begin the scan.
   153  	// Could be smarter but this one optimization
   154  	// is enough for now, since ... is usually at the
   155  	// end of a path.
   156  	i := strings.Index(pattern, "...")
   157  	dir, _ := path.Split(pattern[:i])
   158  
   159  	// pattern begins with ./ or ../.
   160  	// path.Clean will discard the ./ but not the ../.
   161  	// We need to preserve the ./ for pattern matching
   162  	// and in the returned import paths.
   163  	prefix := ""
   164  	if strings.HasPrefix(pattern, "./") {
   165  		prefix = "./"
   166  	}
   167  	match := matchPattern(pattern)
   168  
   169  	var pkgs []string
   170  	filepath.Walk(dir, func(path string, fi os.FileInfo, err error) error {
   171  		if err != nil || !fi.IsDir() {
   172  			return nil
   173  		}
   174  		if path == dir {
   175  			// filepath.Walk starts at dir and recurses. For the recursive case,
   176  			// the path is the result of filepath.Join, which calls filepath.Clean.
   177  			// The initial case is not Cleaned, though, so we do this explicitly.
   178  			//
   179  			// This converts a path like "./io/" to "io". Without this step, running
   180  			// "cd $GOROOT/src; go list ./io/..." would incorrectly skip the io
   181  			// package, because prepending the prefix "./" to the unclean path would
   182  			// result in "././io", and match("././io") returns false.
   183  			path = filepath.Clean(path)
   184  		}
   185  
   186  		// Avoid .foo, _foo, and testdata directory trees, but do not avoid "." or "..".
   187  		_, elem := filepath.Split(path)
   188  		dot := strings.HasPrefix(elem, ".") && elem != "." && elem != ".."
   189  		if dot || strings.HasPrefix(elem, "_") || elem == "testdata" {
   190  			return filepath.SkipDir
   191  		}
   192  
   193  		name := prefix + filepath.ToSlash(path)
   194  		if !match(name) {
   195  			return nil
   196  		}
   197  
   198  		// We keep the directory if we can import it, or if we can't import it
   199  		// due to invalid Go source files. This means that directories containing
   200  		// parse errors will be built (and fail) instead of being silently skipped
   201  		// as not matching the pattern. Go 1.5 and earlier skipped, but that
   202  		// behavior means people miss serious mistakes.
   203  		// See golang.org/issue/11407.
   204  		if p, err := c.BuildContext.ImportDir(path, 0); err != nil && (p == nil || len(p.InvalidGoFiles) == 0) {
   205  			if _, noGo := err.(*build.NoGoError); !noGo {
   206  				log.Print(err)
   207  			}
   208  			return nil
   209  		}
   210  		pkgs = append(pkgs, name)
   211  		return nil
   212  	})
   213  	return pkgs
   214  }
   215  
   216  // treeCanMatchPattern(pattern)(name) reports whether
   217  // name or children of name can possibly match pattern.
   218  // Pattern is the same limited glob accepted by matchPattern.
   219  func treeCanMatchPattern(pattern string) func(name string) bool {
   220  	wildCard := false
   221  	if i := strings.Index(pattern, "..."); i >= 0 {
   222  		wildCard = true
   223  		pattern = pattern[:i]
   224  	}
   225  	return func(name string) bool {
   226  		return len(name) <= len(pattern) && hasPathPrefix(pattern, name) ||
   227  			wildCard && strings.HasPrefix(name, pattern)
   228  	}
   229  }
   230  
   231  // matchPattern(pattern)(name) reports whether
   232  // name matches pattern. Pattern is a limited glob
   233  // pattern in which '...' means 'any string' and there
   234  // is no other special syntax.
   235  // Unfortunately, there are two special cases. Quoting "go help packages":
   236  //
   237  // First, /... at the end of the pattern can match an empty string,
   238  // so that net/... matches both net and packages in its subdirectories, like net/http.
   239  // Second, any slash-separted pattern element containing a wildcard never
   240  // participates in a match of the "vendor" element in the path of a vendored
   241  // package, so that ./... does not match packages in subdirectories of
   242  // ./vendor or ./mycode/vendor, but ./vendor/... and ./mycode/vendor/... do.
   243  // Note, however, that a directory named vendor that itself contains code
   244  // is not a vendored package: cmd/vendor would be a command named vendor,
   245  // and the pattern cmd/... matches it.
   246  func matchPattern(pattern string) func(name string) bool {
   247  	// Convert pattern to regular expression.
   248  	// The strategy for the trailing /... is to nest it in an explicit ? expression.
   249  	// The strategy for the vendor exclusion is to change the unmatchable
   250  	// vendor strings to a disallowed code point (vendorChar) and to use
   251  	// "(anything but that codepoint)*" as the implementation of the ... wildcard.
   252  	// This is a bit complicated but the obvious alternative,
   253  	// namely a hand-written search like in most shell glob matchers,
   254  	// is too easy to make accidentally exponential.
   255  	// Using package regexp guarantees linear-time matching.
   256  
   257  	const vendorChar = "\x00"
   258  
   259  	if strings.Contains(pattern, vendorChar) {
   260  		return func(name string) bool { return false }
   261  	}
   262  
   263  	re := regexp.QuoteMeta(pattern)
   264  	re = replaceVendor(re, vendorChar)
   265  	switch {
   266  	case strings.HasSuffix(re, `/`+vendorChar+`/\.\.\.`):
   267  		re = strings.TrimSuffix(re, `/`+vendorChar+`/\.\.\.`) + `(/vendor|/` + vendorChar + `/\.\.\.)`
   268  	case re == vendorChar+`/\.\.\.`:
   269  		re = `(/vendor|/` + vendorChar + `/\.\.\.)`
   270  	case strings.HasSuffix(re, `/\.\.\.`):
   271  		re = strings.TrimSuffix(re, `/\.\.\.`) + `(/\.\.\.)?`
   272  	}
   273  	re = strings.Replace(re, `\.\.\.`, `[^`+vendorChar+`]*`, -1)
   274  
   275  	reg := regexp.MustCompile(`^` + re + `$`)
   276  
   277  	return func(name string) bool {
   278  		if strings.Contains(name, vendorChar) {
   279  			return false
   280  		}
   281  		return reg.MatchString(replaceVendor(name, vendorChar))
   282  	}
   283  }
   284  
   285  // replaceVendor returns the result of replacing
   286  // non-trailing vendor path elements in x with repl.
   287  func replaceVendor(x, repl string) string {
   288  	if !strings.Contains(x, "vendor") {
   289  		return x
   290  	}
   291  	elem := strings.Split(x, "/")
   292  	for i := 0; i < len(elem)-1; i++ {
   293  		if elem[i] == "vendor" {
   294  			elem[i] = repl
   295  		}
   296  	}
   297  	return strings.Join(elem, "/")
   298  }
   299  
   300  // ImportPaths returns the import paths to use for the given command line.
   301  func (c *Context) ImportPaths(args []string) []string {
   302  	args = c.ImportPathsNoDotExpansion(args)
   303  	var out []string
   304  	for _, a := range args {
   305  		if strings.Contains(a, "...") {
   306  			if build.IsLocalImport(a) {
   307  				out = append(out, c.allPackagesInFS(a)...)
   308  			} else {
   309  				out = append(out, c.allPackages(a)...)
   310  			}
   311  			continue
   312  		}
   313  		out = append(out, a)
   314  	}
   315  	return out
   316  }
   317  
   318  // ImportPathsNoDotExpansion returns the import paths to use for the given
   319  // command line, but it does no ... expansion.
   320  func (c *Context) ImportPathsNoDotExpansion(args []string) []string {
   321  	if len(args) == 0 {
   322  		return []string{"."}
   323  	}
   324  	var out []string
   325  	for _, a := range args {
   326  		// Arguments are supposed to be import paths, but
   327  		// as a courtesy to Windows developers, rewrite \ to /
   328  		// in command-line arguments. Handles .\... and so on.
   329  		if filepath.Separator == '\\' {
   330  			a = strings.Replace(a, `\`, `/`, -1)
   331  		}
   332  
   333  		// Put argument in canonical form, but preserve leading ./.
   334  		if strings.HasPrefix(a, "./") {
   335  			a = "./" + path.Clean(a)
   336  			if a == "./." {
   337  				a = "."
   338  			}
   339  		} else {
   340  			a = path.Clean(a)
   341  		}
   342  		if IsMetaPackage(a) {
   343  			out = append(out, c.allPackages(a)...)
   344  			continue
   345  		}
   346  		out = append(out, a)
   347  	}
   348  	return out
   349  }
   350  
   351  // IsMetaPackage checks if name is a reserved package name that expands to multiple packages.
   352  func IsMetaPackage(name string) bool {
   353  	return name == "std" || name == "cmd" || name == "all"
   354  }