github.com/tools/godep@v0.0.0-20180126220526-ce0bfadeb516/list.go (about)

     1  package main
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"go/build"
     7  	"go/parser"
     8  	"go/token"
     9  	"log"
    10  	"os"
    11  	"path/filepath"
    12  	"regexp"
    13  	"strconv"
    14  	"strings"
    15  	"unicode"
    16  
    17  	pathpkg "path"
    18  )
    19  
    20  var (
    21  	gorootSrc            = filepath.Join(build.Default.GOROOT, "src")
    22  	ignoreTags           = []string{"appengine", "ignore"} //TODO: appengine is a special case for now: https://github.com/tools/godep/issues/353
    23  	versionMatch         = regexp.MustCompile(`\Ago\d+\.\d+\z`)
    24  	versionNegativeMatch = regexp.MustCompile(`\A\!go\d+\.\d+\z`)
    25  )
    26  
    27  type errorMissingDep struct {
    28  	i, dir string // import, dir
    29  }
    30  
    31  func (e errorMissingDep) Error() string {
    32  	return "Unable to find dependent package " + e.i + " in context of " + e.dir
    33  }
    34  
    35  // packageContext is used to track an import and which package imported it.
    36  type packageContext struct {
    37  	pkg *build.Package // package that imports the import
    38  	imp string         // import
    39  }
    40  
    41  // depScanner tracks the processed and to be processed packageContexts
    42  type depScanner struct {
    43  	processed []packageContext
    44  	todo      []packageContext
    45  }
    46  
    47  // Next package and import to process
    48  func (ds *depScanner) Next() (*build.Package, string) {
    49  	c := ds.todo[0]
    50  	ds.processed = append(ds.processed, c)
    51  	ds.todo = ds.todo[1:]
    52  	return c.pkg, c.imp
    53  }
    54  
    55  // Continue looping?
    56  func (ds *depScanner) Continue() bool {
    57  	return len(ds.todo) > 0
    58  }
    59  
    60  // Add a package and imports to the depScanner. Skips already processed/pending package/import combos
    61  func (ds *depScanner) Add(pkg *build.Package, imports ...string) {
    62  NextImport:
    63  	for _, i := range imports {
    64  		if i == "C" {
    65  			i = "runtime/cgo"
    66  		}
    67  		for _, epc := range ds.processed {
    68  			if pkg.Dir == epc.pkg.Dir && i == epc.imp {
    69  				debugln("ctxts epc.pkg.Dir == pkg.Dir && i == epc.imp, skipping", epc.pkg.Dir, i)
    70  				continue NextImport
    71  			}
    72  		}
    73  		for _, epc := range ds.todo {
    74  			if pkg.Dir == epc.pkg.Dir && i == epc.imp {
    75  				debugln("ctxts epc.pkg.Dir == pkg.Dir && i == epc.imp, skipping", epc.pkg.Dir, i)
    76  				continue NextImport
    77  			}
    78  		}
    79  		pc := packageContext{pkg, i}
    80  		debugln("Adding pc:", pc.pkg.Dir, pc.imp)
    81  		ds.todo = append(ds.todo, pc)
    82  	}
    83  }
    84  
    85  var (
    86  	pkgCache = make(map[string]*build.Package) // dir => *build.Package
    87  )
    88  
    89  // returns the package in dir either from a cache or by importing it and then caching it
    90  func fullPackageInDir(dir string) (*build.Package, error) {
    91  	var err error
    92  	pkg, ok := pkgCache[dir]
    93  	if !ok {
    94  		pkg, _ = build.ImportDir(dir, build.FindOnly)
    95  		if pkg.Goroot {
    96  			pkg, err = build.ImportDir(pkg.Dir, 0)
    97  		} else {
    98  			err = fillPackage(pkg)
    99  		}
   100  		if err == nil {
   101  			pkgCache[dir] = pkg
   102  		}
   103  	}
   104  	return pkg, err
   105  }
   106  
   107  // listPackage specified by path
   108  func listPackage(path string) (*Package, error) {
   109  	debugln("listPackage", path)
   110  	var lp *build.Package
   111  	dir, err := findDirForPath(path, nil)
   112  	if err != nil {
   113  		return nil, err
   114  	}
   115  	lp, err = fullPackageInDir(dir)
   116  	p := &Package{
   117  		Dir:            lp.Dir,
   118  		Root:           lp.Root,
   119  		ImportPath:     lp.ImportPath,
   120  		XTestImports:   lp.XTestImports,
   121  		TestImports:    lp.TestImports,
   122  		GoFiles:        lp.GoFiles,
   123  		CgoFiles:       lp.CgoFiles,
   124  		TestGoFiles:    lp.TestGoFiles,
   125  		XTestGoFiles:   lp.XTestGoFiles,
   126  		IgnoredGoFiles: lp.IgnoredGoFiles,
   127  	}
   128  	p.Standard = lp.Goroot && lp.ImportPath != "" && !strings.Contains(lp.ImportPath, ".")
   129  	if err != nil || p.Standard {
   130  		return p, err
   131  	}
   132  	debugln("Looking For Package:", path, "in", dir)
   133  	ppln(lp)
   134  
   135  	ds := depScanner{}
   136  	ds.Add(lp, lp.Imports...)
   137  	for ds.Continue() {
   138  		ip, i := ds.Next()
   139  
   140  		debugf("Processing import %s for %s\n", i, ip.Dir)
   141  		pdir, err := findDirForPath(i, ip)
   142  		if err != nil {
   143  			return nil, err
   144  		}
   145  		dp, err := fullPackageInDir(pdir)
   146  		if err != nil { // This really should happen in this context though
   147  			ppln(err)
   148  			return nil, errorMissingDep{i: i, dir: ip.Dir}
   149  		}
   150  		ppln(dp)
   151  		if !dp.Goroot {
   152  			// Don't bother adding packages in GOROOT to the dependency scanner, they don't import things from outside of it.
   153  			ds.Add(dp, dp.Imports...)
   154  		}
   155  		debugln("lp:")
   156  		ppln(lp)
   157  		debugln("ip:")
   158  		ppln(ip)
   159  		if lp == ip {
   160  			debugln("lp == ip")
   161  			p.Imports = append(p.Imports, dp.ImportPath)
   162  		}
   163  		p.Deps = append(p.Deps, dp.ImportPath)
   164  		p.Dependencies = addDependency(p.Dependencies, dp)
   165  	}
   166  	p.Imports = uniq(p.Imports)
   167  	p.Deps = uniq(p.Deps)
   168  	debugln("Done Looking For Package:", path, "in", dir)
   169  	ppln(p)
   170  	return p, nil
   171  }
   172  
   173  func addDependency(deps []build.Package, d *build.Package) []build.Package {
   174  	for i := range deps {
   175  		if deps[i].Dir == d.Dir {
   176  			return deps
   177  		}
   178  	}
   179  	return append(deps, *d)
   180  }
   181  
   182  // finds the directory for the given import path in the context of the provided build.Package (if provided)
   183  func findDirForPath(path string, ip *build.Package) (string, error) {
   184  	debugln("findDirForPath", path, ip)
   185  	var search []string
   186  
   187  	if build.IsLocalImport(path) {
   188  		dir := path
   189  		if !filepath.IsAbs(dir) {
   190  			if abs, err := filepath.Abs(dir); err == nil {
   191  				// interpret relative to current directory
   192  				dir = abs
   193  			}
   194  		}
   195  		return dir, nil
   196  	}
   197  
   198  	// We need to check to see if the import exists in vendor/ folders up the hierarchy of the importing package
   199  	if VendorExperiment && ip != nil {
   200  		debugln("resolving vendor posibilities:", ip.Dir, ip.Root)
   201  		cr := cleanPath(ip.Root)
   202  
   203  		for base := cleanPath(ip.Dir); !pathEqual(base, cr); base = cleanPath(filepath.Dir(base)) {
   204  			s := filepath.Join(base, "vendor", path)
   205  			debugln("Adding search dir:", s)
   206  			search = append(search, s)
   207  		}
   208  	}
   209  
   210  	for _, base := range build.Default.SrcDirs() {
   211  		search = append(search, filepath.Join(base, path))
   212  	}
   213  
   214  	for _, dir := range search {
   215  		debugln("searching", dir)
   216  		fi, err := stat(dir)
   217  		if err == nil && fi.IsDir() {
   218  			return dir, nil
   219  		}
   220  	}
   221  
   222  	return "", errPackageNotFound{path}
   223  }
   224  
   225  type statEntry struct {
   226  	fi  os.FileInfo
   227  	err error
   228  }
   229  
   230  var (
   231  	statCache = make(map[string]statEntry)
   232  )
   233  
   234  func clearStatCache() {
   235  	statCache = make(map[string]statEntry)
   236  }
   237  
   238  func stat(p string) (os.FileInfo, error) {
   239  	if e, ok := statCache[p]; ok {
   240  		return e.fi, e.err
   241  	}
   242  	fi, err := os.Stat(p)
   243  	statCache[p] = statEntry{fi, err}
   244  	return fi, err
   245  }
   246  
   247  // fillPackage full of info. Assumes p.Dir is set at a minimum
   248  func fillPackage(p *build.Package) error {
   249  	if p.Goroot {
   250  		return nil
   251  	}
   252  
   253  	if p.SrcRoot == "" {
   254  		for _, base := range build.Default.SrcDirs() {
   255  			if strings.HasPrefix(p.Dir, base) {
   256  				p.SrcRoot = base
   257  			}
   258  		}
   259  	}
   260  
   261  	if p.SrcRoot == "" {
   262  		return errors.New("Unable to find SrcRoot for package " + p.ImportPath)
   263  	}
   264  
   265  	if p.Root == "" {
   266  		p.Root = filepath.Dir(p.SrcRoot)
   267  	}
   268  
   269  	var buildMatch = "+build "
   270  	var buildFieldSplit = func(r rune) bool {
   271  		return unicode.IsSpace(r) || r == ','
   272  	}
   273  
   274  	debugln("Filling package:", p.ImportPath, "from", p.Dir)
   275  	gofiles, err := filepath.Glob(filepath.Join(p.Dir, "*.go"))
   276  	if err != nil {
   277  		debugln("Error globbing", err)
   278  		return err
   279  	}
   280  
   281  	if len(gofiles) == 0 {
   282  		return &build.NoGoError{Dir: p.Dir}
   283  	}
   284  
   285  	var testImports []string
   286  	var imports []string
   287  NextFile:
   288  	for _, file := range gofiles {
   289  		debugln(file)
   290  		pf, err := parser.ParseFile(token.NewFileSet(), file, nil, parser.ImportsOnly|parser.ParseComments)
   291  		if err != nil {
   292  			return err
   293  		}
   294  		testFile := strings.HasSuffix(file, "_test.go")
   295  		fname := filepath.Base(file)
   296  		for _, c := range pf.Comments {
   297  			ct := c.Text()
   298  			if i := strings.Index(ct, buildMatch); i != -1 {
   299  				for _, t := range strings.FieldsFunc(ct[i+len(buildMatch):], buildFieldSplit) {
   300  					for _, tag := range ignoreTags {
   301  						if t == tag {
   302  							p.IgnoredGoFiles = append(p.IgnoredGoFiles, fname)
   303  							continue NextFile
   304  						}
   305  					}
   306  
   307  					if versionMatch.MatchString(t) && !isSameOrNewer(t, majorGoVersion) {
   308  						debugln("Adding", fname, "to ignored list because of version tag", t)
   309  						p.IgnoredGoFiles = append(p.IgnoredGoFiles, fname)
   310  						continue NextFile
   311  					}
   312  					if versionNegativeMatch.MatchString(t) && isSameOrNewer(t[1:], majorGoVersion) {
   313  						debugln("Adding", fname, "to ignored list because of version tag", t)
   314  						p.IgnoredGoFiles = append(p.IgnoredGoFiles, fname)
   315  						continue NextFile
   316  					}
   317  				}
   318  			}
   319  		}
   320  		if testFile {
   321  			p.TestGoFiles = append(p.TestGoFiles, fname)
   322  		} else {
   323  			p.GoFiles = append(p.GoFiles, fname)
   324  		}
   325  		for _, is := range pf.Imports {
   326  			name, err := strconv.Unquote(is.Path.Value)
   327  			if err != nil {
   328  				return err // can't happen?
   329  			}
   330  			if testFile {
   331  				testImports = append(testImports, name)
   332  			} else {
   333  				imports = append(imports, name)
   334  			}
   335  		}
   336  	}
   337  	imports = uniq(imports)
   338  	testImports = uniq(testImports)
   339  	p.Imports = imports
   340  	p.TestImports = testImports
   341  	return nil
   342  }
   343  
   344  // All of the following functions were vendored from go proper. Locations are noted in comments, but may change in future Go versions.
   345  
   346  // importPaths returns the import paths to use for the given command line.
   347  // $GOROOT/src/cmd/main.go:366
   348  func importPaths(args []string) []string {
   349  	debugf("importPathsNoDotExpansion(%q) == ", args)
   350  	args = importPathsNoDotExpansion(args)
   351  	debugf("%q\n", args)
   352  	var out []string
   353  	for _, a := range args {
   354  		if strings.Contains(a, "...") {
   355  			if build.IsLocalImport(a) {
   356  				debugf("build.IsLocalImport(%q) == true\n", a)
   357  				pkgs := allPackagesInFS(a)
   358  				debugf("allPackagesInFS(%q) == %q\n", a, pkgs)
   359  				out = append(out, pkgs...)
   360  			} else {
   361  				debugf("build.IsLocalImport(%q) == false\n", a)
   362  				pkgs := allPackages(a)
   363  				debugf("allPackages(%q) == %q\n", a, pkgs)
   364  				out = append(out, allPackages(a)...)
   365  			}
   366  			continue
   367  		}
   368  		out = append(out, a)
   369  	}
   370  	return out
   371  }
   372  
   373  // importPathsNoDotExpansion returns the import paths to use for the given
   374  // command line, but it does no ... expansion.
   375  // $GOROOT/src/cmd/main.go:332
   376  func importPathsNoDotExpansion(args []string) []string {
   377  	if len(args) == 0 {
   378  		return []string{"."}
   379  	}
   380  	var out []string
   381  	for _, a := range args {
   382  		// Arguments are supposed to be import paths, but
   383  		// as a courtesy to Windows developers, rewrite \ to /
   384  		// in command-line arguments.  Handles .\... and so on.
   385  		if filepath.Separator == '\\' {
   386  			a = strings.Replace(a, `\`, `/`, -1)
   387  		}
   388  
   389  		// Put argument in canonical form, but preserve leading ./.
   390  		if strings.HasPrefix(a, "./") {
   391  			a = "./" + pathpkg.Clean(a)
   392  			if a == "./." {
   393  				a = "."
   394  			}
   395  		} else {
   396  			a = pathpkg.Clean(a)
   397  		}
   398  		if a == "all" || a == "std" || a == "cmd" {
   399  			out = append(out, allPackages(a)...)
   400  			continue
   401  		}
   402  		out = append(out, a)
   403  	}
   404  	return out
   405  }
   406  
   407  // allPackagesInFS is like allPackages but is passed a pattern
   408  // beginning ./ or ../, meaning it should scan the tree rooted
   409  // at the given directory.  There are ... in the pattern too.
   410  // $GOROOT/src/cmd/main.go:620
   411  func allPackagesInFS(pattern string) []string {
   412  	pkgs := matchPackagesInFS(pattern)
   413  	if len(pkgs) == 0 {
   414  		fmt.Fprintf(os.Stderr, "warning: %q matched no packages\n", pattern)
   415  	}
   416  	return pkgs
   417  }
   418  
   419  // allPackages returns all the packages that can be found
   420  // under the $GOPATH directories and $GOROOT matching pattern.
   421  // The pattern is either "all" (all packages), "std" (standard packages),
   422  // "cmd" (standard commands), or a path including "...".
   423  // $GOROOT/src/cmd/main.go:542
   424  func allPackages(pattern string) []string {
   425  	pkgs := matchPackages(pattern)
   426  	if len(pkgs) == 0 {
   427  		fmt.Fprintf(os.Stderr, "warning: %q matched no packages\n", pattern)
   428  	}
   429  	return pkgs
   430  }
   431  
   432  // $GOROOT/src/cmd/main.go:554
   433  // This has been changed to not use build.ImportDir
   434  func matchPackages(pattern string) []string {
   435  	match := func(string) bool { return true }
   436  	treeCanMatch := func(string) bool { return true }
   437  	if pattern != "all" && pattern != "std" && pattern != "cmd" {
   438  		match = matchPattern(pattern)
   439  		treeCanMatch = treeCanMatchPattern(pattern)
   440  	}
   441  
   442  	have := map[string]bool{
   443  		"builtin": true, // ignore pseudo-package that exists only for documentation
   444  	}
   445  	if !build.Default.CgoEnabled {
   446  		have["runtime/cgo"] = true // ignore during walk
   447  	}
   448  	var pkgs []string
   449  
   450  	for _, src := range build.Default.SrcDirs() {
   451  		if (pattern == "std" || pattern == "cmd") && src != gorootSrc {
   452  			continue
   453  		}
   454  		src = filepath.Clean(src) + string(filepath.Separator)
   455  		root := src
   456  		if pattern == "cmd" {
   457  			root += "cmd" + string(filepath.Separator)
   458  		}
   459  		filepath.Walk(root, func(path string, fi os.FileInfo, err error) error {
   460  			if err != nil || !fi.IsDir() || path == src {
   461  				return nil
   462  			}
   463  
   464  			// Avoid .foo, _foo, and testdata directory trees.
   465  			_, elem := filepath.Split(path)
   466  			if strings.HasPrefix(elem, ".") || strings.HasPrefix(elem, "_") || elem == "testdata" {
   467  				return filepath.SkipDir
   468  			}
   469  
   470  			name := filepath.ToSlash(path[len(src):])
   471  			if pattern == "std" && (strings.Contains(name, ".") || name == "cmd") {
   472  				// The name "std" is only the standard library.
   473  				// If the name has a dot, assume it's a domain name for go get,
   474  				// and if the name is cmd, it's the root of the command tree.
   475  				return filepath.SkipDir
   476  			}
   477  			if !treeCanMatch(name) {
   478  				return filepath.SkipDir
   479  			}
   480  			if have[name] {
   481  				return nil
   482  			}
   483  			have[name] = true
   484  			if !match(name) {
   485  				return nil
   486  			}
   487  
   488  			ap, err := filepath.Abs(path)
   489  			if err != nil {
   490  				return nil
   491  			}
   492  			if _, err = fullPackageInDir(ap); err != nil {
   493  				debugf("matchPackage(%q) ap=%q Error: %q\n", ap, pattern, err)
   494  				if _, noGo := err.(*build.NoGoError); noGo {
   495  					return nil
   496  				}
   497  			}
   498  			pkgs = append(pkgs, name)
   499  			return nil
   500  		})
   501  	}
   502  	return pkgs
   503  }
   504  
   505  // treeCanMatchPattern(pattern)(name) reports whether
   506  // name or children of name can possibly match pattern.
   507  // Pattern is the same limited glob accepted by matchPattern.
   508  // $GOROOT/src/cmd/main.go:527
   509  func treeCanMatchPattern(pattern string) func(name string) bool {
   510  	wildCard := false
   511  	if i := strings.Index(pattern, "..."); i >= 0 {
   512  		wildCard = true
   513  		pattern = pattern[:i]
   514  	}
   515  	return func(name string) bool {
   516  		return len(name) <= len(pattern) && hasPathPrefix(pattern, name) ||
   517  			wildCard && strings.HasPrefix(name, pattern)
   518  	}
   519  }
   520  
   521  // hasPathPrefix reports whether the path s begins with the
   522  // elements in prefix.
   523  // $GOROOT/src/cmd/main.go:489
   524  func hasPathPrefix(s, prefix string) bool {
   525  	switch {
   526  	default:
   527  		return false
   528  	case len(s) == len(prefix):
   529  		return s == prefix
   530  	case len(s) > len(prefix):
   531  		if prefix != "" && prefix[len(prefix)-1] == '/' {
   532  			return strings.HasPrefix(s, prefix)
   533  		}
   534  		return s[len(prefix)] == '/' && s[:len(prefix)] == prefix
   535  	}
   536  }
   537  
   538  // $GOROOT/src/cmd/go/main.go:631
   539  // This has been changed to not use build.ImportDir
   540  func matchPackagesInFS(pattern string) []string {
   541  	// Find directory to begin the scan.
   542  	// Could be smarter but this one optimization
   543  	// is enough for now, since ... is usually at the
   544  	// end of a path.
   545  	i := strings.Index(pattern, "...")
   546  	dir, _ := pathpkg.Split(pattern[:i])
   547  
   548  	// pattern begins with ./ or ../.
   549  	// path.Clean will discard the ./ but not the ../.
   550  	// We need to preserve the ./ for pattern matching
   551  	// and in the returned import paths.
   552  	prefix := ""
   553  	if strings.HasPrefix(pattern, "./") {
   554  		prefix = "./"
   555  	}
   556  	match := matchPattern(pattern)
   557  
   558  	var pkgs []string
   559  	filepath.Walk(dir, func(path string, fi os.FileInfo, err error) error {
   560  		if err != nil || !fi.IsDir() {
   561  			return nil
   562  		}
   563  		if path == dir {
   564  			// filepath.Walk starts at dir and recurses. For the recursive case,
   565  			// the path is the result of filepath.Join, which calls filepath.Clean.
   566  			// The initial case is not Cleaned, though, so we do this explicitly.
   567  			//
   568  			// This converts a path like "./io/" to "io". Without this step, running
   569  			// "cd $GOROOT/src; go list ./io/..." would incorrectly skip the io
   570  			// package, because prepending the prefix "./" to the unclean path would
   571  			// result in "././io", and match("././io") returns false.
   572  			path = filepath.Clean(path)
   573  		}
   574  
   575  		// Avoid .foo, _foo, and testdata directory trees, but do not avoid "." or "..".
   576  		_, elem := filepath.Split(path)
   577  		dot := strings.HasPrefix(elem, ".") && elem != "." && elem != ".."
   578  		if dot || strings.HasPrefix(elem, "_") || elem == "testdata" {
   579  			return filepath.SkipDir
   580  		}
   581  
   582  		name := prefix + filepath.ToSlash(path)
   583  		if !match(name) {
   584  			return nil
   585  		}
   586  		ap, err := filepath.Abs(path)
   587  		if err != nil {
   588  			return nil
   589  		}
   590  		if _, err = fullPackageInDir(ap); err != nil {
   591  			debugf("matchPackageInFS(%q) ap=%q Error: %q\n", ap, pattern, err)
   592  			if _, noGo := err.(*build.NoGoError); !noGo {
   593  				log.Print(err)
   594  			}
   595  			return nil
   596  		}
   597  		pkgs = append(pkgs, name)
   598  		return nil
   599  	})
   600  	return pkgs
   601  }