github.com/sdboyer/gps@v0.16.3/pkgtree/pkgtree.go (about)

     1  package pkgtree
     2  
     3  import (
     4  	"fmt"
     5  	"go/build"
     6  	"go/parser"
     7  	gscan "go/scanner"
     8  	"go/token"
     9  	"os"
    10  	"path/filepath"
    11  	"sort"
    12  	"strconv"
    13  	"strings"
    14  	"unicode"
    15  )
    16  
    17  // Package represents a Go package. It contains a subset of the information
    18  // go/build.Package does.
    19  type Package struct {
    20  	Name        string   // Package name, as declared in the package statement
    21  	ImportPath  string   // Full import path, including the prefix provided to ListPackages()
    22  	CommentPath string   // Import path given in the comment on the package statement
    23  	Imports     []string // Imports from all go and cgo files
    24  	TestImports []string // Imports from all go test files (in go/build parlance: both TestImports and XTestImports)
    25  }
    26  
    27  // ListPackages reports Go package information about all directories in the tree
    28  // at or below the provided fileRoot.
    29  //
    30  // The importRoot parameter is prepended to the relative path when determining
    31  // the import path for each package. The obvious case is for something typical,
    32  // like:
    33  //
    34  //  fileRoot = "/home/user/go/src/github.com/foo/bar"
    35  //  importRoot = "github.com/foo/bar"
    36  //
    37  // where the fileRoot and importRoot align. However, if you provide:
    38  //
    39  //  fileRoot = "/home/user/workspace/path/to/repo"
    40  //  importRoot = "github.com/foo/bar"
    41  //
    42  // then the root package at path/to/repo will be ascribed import path
    43  // "github.com/foo/bar", and the package at
    44  // "/home/user/workspace/path/to/repo/baz" will be "github.com/foo/bar/baz".
    45  //
    46  // A PackageTree is returned, which contains the ImportRoot and map of import path
    47  // to PackageOrErr - each path under the root that exists will have either a
    48  // Package, or an error describing why the directory is not a valid package.
    49  func ListPackages(fileRoot, importRoot string) (PackageTree, error) {
    50  	ptree := PackageTree{
    51  		ImportRoot: importRoot,
    52  		Packages:   make(map[string]PackageOrErr),
    53  	}
    54  
    55  	var err error
    56  	fileRoot, err = filepath.Abs(fileRoot)
    57  	if err != nil {
    58  		return PackageTree{}, err
    59  	}
    60  
    61  	err = filepath.Walk(fileRoot, func(wp string, fi os.FileInfo, err error) error {
    62  		if err != nil && err != filepath.SkipDir {
    63  			return err
    64  		}
    65  		if !fi.IsDir() {
    66  			return nil
    67  		}
    68  
    69  		// Skip dirs that are known to hold non-local/dependency code.
    70  		//
    71  		// We don't skip _*, or testdata dirs because, while it may be poor
    72  		// form, importing them is not a compilation error.
    73  		switch fi.Name() {
    74  		case "vendor", "Godeps":
    75  			return filepath.SkipDir
    76  		}
    77  		// We do skip dot-dirs, though, because it's such a ubiquitous standard
    78  		// that they not be visited by normal commands, and because things get
    79  		// really weird if we don't.
    80  		if strings.HasPrefix(fi.Name(), ".") {
    81  			return filepath.SkipDir
    82  		}
    83  
    84  		// The entry error is nil when visiting a directory that itself is
    85  		// untraversable, as it's still governed by the parent directory's
    86  		// perms. We have to check readability of the dir here, because
    87  		// otherwise we'll have an empty package entry when we fail to read any
    88  		// of the dir's contents.
    89  		//
    90  		// If we didn't check here, then the next time this closure is called it
    91  		// would have an err with the same path as is called this time, as only
    92  		// then will filepath.Walk have attempted to descend into the directory
    93  		// and encountered an error.
    94  		var f *os.File
    95  		f, err = os.Open(wp)
    96  		if err != nil {
    97  			if os.IsPermission(err) {
    98  				return filepath.SkipDir
    99  			}
   100  			return err
   101  		}
   102  		f.Close()
   103  
   104  		// Compute the import path. Run the result through ToSlash(), so that
   105  		// windows file paths are normalized to slashes, as is expected of
   106  		// import paths.
   107  		ip := filepath.ToSlash(filepath.Join(importRoot, strings.TrimPrefix(wp, fileRoot)))
   108  
   109  		// Find all the imports, across all os/arch combos
   110  		//p, err := fullPackageInDir(wp)
   111  		p := &build.Package{
   112  			Dir: wp,
   113  		}
   114  		err = fillPackage(p)
   115  
   116  		var pkg Package
   117  		if err == nil {
   118  			pkg = Package{
   119  				ImportPath:  ip,
   120  				CommentPath: p.ImportComment,
   121  				Name:        p.Name,
   122  				Imports:     p.Imports,
   123  				TestImports: dedupeStrings(p.TestImports, p.XTestImports),
   124  			}
   125  		} else {
   126  			switch err.(type) {
   127  			case gscan.ErrorList, *gscan.Error, *build.NoGoError:
   128  				// This happens if we encounter malformed or nonexistent Go
   129  				// source code
   130  				ptree.Packages[ip] = PackageOrErr{
   131  					Err: err,
   132  				}
   133  				return nil
   134  			default:
   135  				return err
   136  			}
   137  		}
   138  
   139  		// This area has some...fuzzy rules, but check all the imports for
   140  		// local/relative/dot-ness, and record an error for the package if we
   141  		// see any.
   142  		var lim []string
   143  		for _, imp := range append(pkg.Imports, pkg.TestImports...) {
   144  			switch {
   145  			// Do allow the single-dot, at least for now
   146  			case imp == "..":
   147  				lim = append(lim, imp)
   148  			case strings.HasPrefix(imp, "./"):
   149  				lim = append(lim, imp)
   150  			case strings.HasPrefix(imp, "../"):
   151  				lim = append(lim, imp)
   152  			}
   153  		}
   154  
   155  		if len(lim) > 0 {
   156  			ptree.Packages[ip] = PackageOrErr{
   157  				Err: &LocalImportsError{
   158  					Dir:          wp,
   159  					ImportPath:   ip,
   160  					LocalImports: lim,
   161  				},
   162  			}
   163  		} else {
   164  			ptree.Packages[ip] = PackageOrErr{
   165  				P: pkg,
   166  			}
   167  		}
   168  
   169  		return nil
   170  	})
   171  
   172  	if err != nil {
   173  		return PackageTree{}, err
   174  	}
   175  
   176  	return ptree, nil
   177  }
   178  
   179  // fillPackage full of info. Assumes p.Dir is set at a minimum
   180  func fillPackage(p *build.Package) error {
   181  	var buildPrefix = "// +build "
   182  	var buildFieldSplit = func(r rune) bool {
   183  		return unicode.IsSpace(r) || r == ','
   184  	}
   185  
   186  	gofiles, err := filepath.Glob(filepath.Join(p.Dir, "*.go"))
   187  	if err != nil {
   188  		return err
   189  	}
   190  
   191  	if len(gofiles) == 0 {
   192  		return &build.NoGoError{Dir: p.Dir}
   193  	}
   194  
   195  	var testImports []string
   196  	var imports []string
   197  	for _, file := range gofiles {
   198  		// Skip underscore-led files, in keeping with the rest of the toolchain.
   199  		if filepath.Base(file)[0] == '_' {
   200  			continue
   201  		}
   202  		pf, err := parser.ParseFile(token.NewFileSet(), file, nil, parser.ImportsOnly|parser.ParseComments)
   203  		if err != nil {
   204  			if os.IsPermission(err) {
   205  				continue
   206  			}
   207  			return err
   208  		}
   209  		testFile := strings.HasSuffix(file, "_test.go")
   210  		fname := filepath.Base(file)
   211  
   212  		var ignored bool
   213  		for _, c := range pf.Comments {
   214  			if c.Pos() > pf.Package { // +build comment must come before package
   215  				continue
   216  			}
   217  
   218  			var ct string
   219  			for _, cl := range c.List {
   220  				if strings.HasPrefix(cl.Text, buildPrefix) {
   221  					ct = cl.Text
   222  					break
   223  				}
   224  			}
   225  			if ct == "" {
   226  				continue
   227  			}
   228  
   229  			for _, t := range strings.FieldsFunc(ct[len(buildPrefix):], buildFieldSplit) {
   230  				// hardcoded (for now) handling for the "ignore" build tag
   231  				// We "soft" ignore the files tagged with ignore so that we pull in their imports.
   232  				if t == "ignore" {
   233  					ignored = true
   234  				}
   235  			}
   236  		}
   237  
   238  		if testFile {
   239  			p.TestGoFiles = append(p.TestGoFiles, fname)
   240  			if p.Name == "" && !ignored {
   241  				p.Name = strings.TrimSuffix(pf.Name.Name, "_test")
   242  			}
   243  		} else {
   244  			if p.Name == "" && !ignored {
   245  				p.Name = pf.Name.Name
   246  			}
   247  			p.GoFiles = append(p.GoFiles, fname)
   248  		}
   249  
   250  		for _, is := range pf.Imports {
   251  			name, err := strconv.Unquote(is.Path.Value)
   252  			if err != nil {
   253  				return err // can't happen?
   254  			}
   255  			if testFile {
   256  				testImports = append(testImports, name)
   257  			} else {
   258  				imports = append(imports, name)
   259  			}
   260  		}
   261  	}
   262  
   263  	imports = uniq(imports)
   264  	testImports = uniq(testImports)
   265  	p.Imports = imports
   266  	p.TestImports = testImports
   267  	return nil
   268  }
   269  
   270  // LocalImportsError indicates that a package contains at least one relative
   271  // import that will prevent it from compiling.
   272  //
   273  // TODO(sdboyer) add a Files property once we're doing our own per-file parsing
   274  type LocalImportsError struct {
   275  	ImportPath   string
   276  	Dir          string
   277  	LocalImports []string
   278  }
   279  
   280  func (e *LocalImportsError) Error() string {
   281  	switch len(e.LocalImports) {
   282  	case 0:
   283  		// shouldn't be possible, but just cover the case
   284  		return fmt.Sprintf("import path %s had bad local imports", e.ImportPath)
   285  	case 1:
   286  		return fmt.Sprintf("import path %s had a local import: %q", e.ImportPath, e.LocalImports[0])
   287  	default:
   288  		return fmt.Sprintf("import path %s had local imports: %q", e.ImportPath, strings.Join(e.LocalImports, "\", \""))
   289  	}
   290  }
   291  
   292  type wm struct {
   293  	err error
   294  	ex  map[string]bool
   295  	in  map[string]bool
   296  }
   297  
   298  // PackageOrErr stores the results of attempting to parse a single directory for
   299  // Go source code.
   300  type PackageOrErr struct {
   301  	P   Package
   302  	Err error
   303  }
   304  
   305  // ProblemImportError describes the reason that a particular import path is
   306  // not safely importable.
   307  type ProblemImportError struct {
   308  	// The import path of the package with some problem rendering it
   309  	// unimportable.
   310  	ImportPath string
   311  	// The path to the internal package the problem package imports that is the
   312  	// original cause of this issue. If empty, the package itself is the
   313  	// problem.
   314  	Cause []string
   315  	// The actual error from ListPackages that is undermining importability for
   316  	// this package.
   317  	Err error
   318  }
   319  
   320  // Error formats the ProblemImportError as a string, reflecting whether the
   321  // error represents a direct or transitive problem.
   322  func (e *ProblemImportError) Error() string {
   323  	switch len(e.Cause) {
   324  	case 0:
   325  		return fmt.Sprintf("%q contains malformed code: %s", e.ImportPath, e.Err.Error())
   326  	case 1:
   327  		return fmt.Sprintf("%q imports %q, which contains malformed code: %s", e.ImportPath, e.Cause[0], e.Err.Error())
   328  	default:
   329  		return fmt.Sprintf("%q transitively (through %v packages) imports %q, which contains malformed code: %s", e.ImportPath, len(e.Cause)-1, e.Cause[len(e.Cause)-1], e.Err.Error())
   330  	}
   331  }
   332  
   333  // Helper func to create an error when a package is missing.
   334  func missingPkgErr(pkg string) error {
   335  	return fmt.Errorf("no package exists at %q", pkg)
   336  }
   337  
   338  // A PackageTree represents the results of recursively parsing a tree of
   339  // packages, starting at the ImportRoot. The results of parsing the files in the
   340  // directory identified by each import path - a Package or an error - are stored
   341  // in the Packages map, keyed by that import path.
   342  type PackageTree struct {
   343  	ImportRoot string
   344  	Packages   map[string]PackageOrErr
   345  }
   346  
   347  // ToReachMap looks through a PackageTree and computes the list of external
   348  // import statements (that is, import statements pointing to packages that are
   349  // not logical children of PackageTree.ImportRoot) that are transitively
   350  // imported by the internal packages in the tree.
   351  //
   352  // main indicates whether (true) or not (false) to include main packages in the
   353  // analysis. When utilized by gps' solver, main packages are generally excluded
   354  // from analyzing anything other than the root project, as they necessarily can't
   355  // be imported.
   356  //
   357  // tests indicates whether (true) or not (false) to include imports from test
   358  // files in packages when computing the reach map.
   359  //
   360  // backprop indicates whether errors (an actual PackageOrErr.Err, or an import
   361  // to a nonexistent internal package) should be backpropagated, transitively
   362  // "poisoning" all corresponding importers to all importers.
   363  //
   364  // ignore is a map of import paths that, if encountered, should be excluded from
   365  // analysis. This exclusion applies to both internal and external packages. If
   366  // an external import path is ignored, it is simply omitted from the results.
   367  //
   368  // If an internal path is ignored, then it not only does not appear in the final
   369  // map, but it is also excluded from the transitive calculations of other
   370  // internal packages.  That is, if you ignore A/foo, then the external package
   371  // list for all internal packages that import A/foo will not include external
   372  // packages that are only reachable through A/foo.
   373  //
   374  // Visually, this means that, given a PackageTree with root A and packages at A,
   375  // A/foo, and A/bar, and the following import chain:
   376  //
   377  //  A -> A/foo -> A/bar -> B/baz
   378  //
   379  // In this configuration, all of A's packages transitively import B/baz, so the
   380  // returned map would be:
   381  //
   382  //  map[string][]string{
   383  // 	"A": []string{"B/baz"},
   384  // 	"A/foo": []string{"B/baz"}
   385  // 	"A/bar": []string{"B/baz"},
   386  //  }
   387  //
   388  // However, if you ignore A/foo, then A's path to B/baz is broken, and A/foo is
   389  // omitted entirely. Thus, the returned map would be:
   390  //
   391  //  map[string][]string{
   392  // 	"A": []string{},
   393  // 	"A/bar": []string{"B/baz"},
   394  //  }
   395  //
   396  // If there are no packages to ignore, it is safe to pass a nil map.
   397  //
   398  // Finally, if an internal PackageOrErr contains an error, it is always omitted
   399  // from the result set. If backprop is true, then the error from that internal
   400  // package will be transitively propagated back to any other internal
   401  // PackageOrErrs that import it, causing them to also be omitted. So, with the
   402  // same import chain:
   403  //
   404  //  A -> A/foo -> A/bar -> B/baz
   405  //
   406  // If A/foo has an error, then it would backpropagate to A, causing both to be
   407  // omitted, and the returned map to contain only A/bar:
   408  //
   409  //  map[string][]string{
   410  // 	"A/bar": []string{"B/baz"},
   411  //  }
   412  //
   413  // If backprop is false, then errors will not backpropagate to internal
   414  // importers. So, with an error in A/foo, this would be the result map:
   415  //
   416  //  map[string][]string{
   417  // 	"A": []string{},
   418  // 	"A/bar": []string{"B/baz"},
   419  //  }
   420  func (t PackageTree) ToReachMap(main, tests, backprop bool, ignore map[string]bool) (ReachMap, map[string]*ProblemImportError) {
   421  	if ignore == nil {
   422  		ignore = make(map[string]bool)
   423  	}
   424  
   425  	// world's simplest adjacency list
   426  	workmap := make(map[string]wm)
   427  
   428  	var imps []string
   429  	for ip, perr := range t.Packages {
   430  		if perr.Err != nil {
   431  			workmap[ip] = wm{
   432  				err: perr.Err,
   433  			}
   434  			continue
   435  		}
   436  		p := perr.P
   437  
   438  		// Skip main packages, unless param says otherwise
   439  		if p.Name == "main" && !main {
   440  			continue
   441  		}
   442  		// Skip ignored packages
   443  		if ignore[ip] {
   444  			continue
   445  		}
   446  
   447  		imps = imps[:0]
   448  		if tests {
   449  			imps = dedupeStrings(p.Imports, p.TestImports)
   450  		} else {
   451  			imps = p.Imports
   452  		}
   453  
   454  		w := wm{
   455  			ex: make(map[string]bool),
   456  			in: make(map[string]bool),
   457  		}
   458  
   459  		// For each import, decide whether it should be ignored, or if it
   460  		// belongs in the external or internal imports list.
   461  		for _, imp := range imps {
   462  			if ignore[imp] {
   463  				continue
   464  			}
   465  
   466  			if !eqOrSlashedPrefix(imp, t.ImportRoot) {
   467  				w.ex[imp] = true
   468  			} else {
   469  				w.in[imp] = true
   470  			}
   471  		}
   472  
   473  		workmap[ip] = w
   474  	}
   475  
   476  	return wmToReach(workmap, backprop)
   477  }
   478  
   479  // Copy copies the PackageTree.
   480  //
   481  // This is really only useful as a defensive measure to prevent external state
   482  // mutations.
   483  func (t PackageTree) Copy() PackageTree {
   484  	t2 := PackageTree{
   485  		ImportRoot: t.ImportRoot,
   486  		Packages:   map[string]PackageOrErr{},
   487  	}
   488  
   489  	for path, poe := range t.Packages {
   490  		poe2 := PackageOrErr{
   491  			Err: poe.Err,
   492  			P:   poe.P,
   493  		}
   494  		if len(poe.P.Imports) > 0 {
   495  			poe2.P.Imports = make([]string, len(poe.P.Imports))
   496  			copy(poe2.P.Imports, poe.P.Imports)
   497  		}
   498  		if len(poe.P.TestImports) > 0 {
   499  			poe2.P.TestImports = make([]string, len(poe.P.TestImports))
   500  			copy(poe2.P.TestImports, poe.P.TestImports)
   501  		}
   502  
   503  		t2.Packages[path] = poe2
   504  	}
   505  
   506  	return t2
   507  }
   508  
   509  // wmToReach takes an internal "workmap" constructed by
   510  // PackageTree.ExternalReach(), transitively walks (via depth-first traversal)
   511  // all internal imports until they reach an external path or terminate, then
   512  // translates the results into a slice of external imports for each internal
   513  // pkg.
   514  //
   515  // It drops any packages with errors, and - if backprop is true - backpropagates
   516  // those errors, causing internal packages that (transitively) import other
   517  // internal packages having errors to also be dropped.
   518  func wmToReach(workmap map[string]wm, backprop bool) (ReachMap, map[string]*ProblemImportError) {
   519  	// Uses depth-first exploration to compute reachability into external
   520  	// packages, dropping any internal packages on "poisoned paths" - a path
   521  	// containing a package with an error, or with a dep on an internal package
   522  	// that's missing.
   523  
   524  	const (
   525  		white uint8 = iota
   526  		grey
   527  		black
   528  	)
   529  
   530  	colors := make(map[string]uint8)
   531  	exrsets := make(map[string]map[string]struct{})
   532  	inrsets := make(map[string]map[string]struct{})
   533  	errmap := make(map[string]*ProblemImportError)
   534  
   535  	// poison is a helper func to eliminate specific reachsets from exrsets and
   536  	// inrsets, and populate error information along the way.
   537  	poison := func(path []string, err *ProblemImportError) {
   538  		for k, ppkg := range path {
   539  			delete(exrsets, ppkg)
   540  			delete(inrsets, ppkg)
   541  
   542  			// Duplicate the err for this package
   543  			kerr := &ProblemImportError{
   544  				ImportPath: ppkg,
   545  				Err:        err.Err,
   546  			}
   547  
   548  			// Shift the slice bounds on the incoming err.Cause.
   549  			//
   550  			// This check will only be false on the final path element when
   551  			// entering via poisonWhite, where the last pkg is the underlying
   552  			// cause of the problem, and is thus expected to have an empty Cause
   553  			// slice.
   554  			if k+1 < len(err.Cause) {
   555  				// reuse the slice
   556  				kerr.Cause = err.Cause[k+1:]
   557  			}
   558  
   559  			// Both black and white cases can have the final element be a
   560  			// package that doesn't exist. If that's the case, don't write it
   561  			// directly to the errmap, as presence in the errmap indicates the
   562  			// package was present in the input PackageTree.
   563  			if k == len(path)-1 {
   564  				if _, exists := workmap[path[len(path)-1]]; !exists {
   565  					continue
   566  				}
   567  			}
   568  
   569  			// Direct writing to the errmap means that if multiple errors affect
   570  			// a given package, only the last error visited will be reported.
   571  			// But that should be sufficient; presumably, the user can
   572  			// iteratively resolve the errors.
   573  			errmap[ppkg] = kerr
   574  		}
   575  	}
   576  
   577  	// poisonWhite wraps poison for error recording in the white-poisoning case,
   578  	// where we're constructing a new poison path.
   579  	poisonWhite := func(path []string) {
   580  		err := &ProblemImportError{
   581  			Cause: make([]string, len(path)),
   582  		}
   583  		copy(err.Cause, path)
   584  
   585  		// find the tail err
   586  		tail := path[len(path)-1]
   587  		if w, exists := workmap[tail]; exists {
   588  			// If we make it to here, the dfe guarantees that the workmap
   589  			// will contain an error for this pkg.
   590  			err.Err = w.err
   591  		} else {
   592  			err.Err = missingPkgErr(tail)
   593  		}
   594  
   595  		poison(path, err)
   596  	}
   597  	// poisonBlack wraps poison for error recording in the black-poisoning case,
   598  	// where we're connecting to an existing poison path.
   599  	poisonBlack := func(path []string, from string) {
   600  		// Because the outer dfe loop ensures we never directly re-visit a pkg
   601  		// that was already completed (black), we don't have to defend against
   602  		// an empty path here.
   603  
   604  		fromErr := errmap[from]
   605  		err := &ProblemImportError{
   606  			Err:   fromErr.Err,
   607  			Cause: make([]string, 0, len(path)+len(fromErr.Cause)+1),
   608  		}
   609  		err.Cause = append(err.Cause, path...)
   610  		err.Cause = append(err.Cause, from)
   611  		err.Cause = append(err.Cause, fromErr.Cause...)
   612  
   613  		poison(path, err)
   614  	}
   615  
   616  	var dfe func(string, []string) bool
   617  
   618  	// dfe is the depth-first-explorer that computes a safe, error-free external
   619  	// reach map.
   620  	//
   621  	// pkg is the import path of the pkg currently being visited; path is the
   622  	// stack of parent packages we've visited to get to pkg. The return value
   623  	// indicates whether the level completed successfully (true) or if it was
   624  	// poisoned (false).
   625  	dfe = func(pkg string, path []string) bool {
   626  		// white is the zero value of uint8, which is what we want if the pkg
   627  		// isn't in the colors map, so this works fine
   628  		switch colors[pkg] {
   629  		case white:
   630  			// first visit to this pkg; mark it as in-process (grey)
   631  			colors[pkg] = grey
   632  
   633  			// make sure it's present and w/out errs
   634  			w, exists := workmap[pkg]
   635  
   636  			// Push current visitee onto the path slice. Passing path through
   637  			// recursion levels as a value has the effect of auto-popping the
   638  			// slice, while also giving us safe memory reuse.
   639  			path = append(path, pkg)
   640  
   641  			if !exists || w.err != nil {
   642  				if backprop {
   643  					// Does not exist or has an err; poison self and all parents
   644  					poisonWhite(path)
   645  				} else if exists {
   646  					// Only record something in the errmap if there's actually a
   647  					// package there, per the semantics of the errmap
   648  					errmap[pkg] = &ProblemImportError{
   649  						ImportPath: pkg,
   650  						Err:        w.err,
   651  					}
   652  				}
   653  
   654  				// we know we're done here, so mark it black
   655  				colors[pkg] = black
   656  				return false
   657  			}
   658  			// pkg exists with no errs; start internal and external reachsets for it.
   659  			rs := make(map[string]struct{})
   660  			irs := make(map[string]struct{})
   661  
   662  			// Dump this package's external pkgs into its own reachset. Separate
   663  			// loop from the parent dump to avoid nested map loop lookups.
   664  			for ex := range w.ex {
   665  				rs[ex] = struct{}{}
   666  			}
   667  			exrsets[pkg] = rs
   668  			// Same deal for internal imports
   669  			for in := range w.in {
   670  				irs[in] = struct{}{}
   671  			}
   672  			inrsets[pkg] = irs
   673  
   674  			// Push this pkg's imports into all parent reachsets. Not all
   675  			// parents will necessarily have a reachset; none, some, or all
   676  			// could have been poisoned by a different path than what we're on
   677  			// right now.
   678  			for _, ppkg := range path {
   679  				if prs, exists := exrsets[ppkg]; exists {
   680  					for ex := range w.ex {
   681  						prs[ex] = struct{}{}
   682  					}
   683  				}
   684  
   685  				if prs, exists := inrsets[ppkg]; exists {
   686  					for in := range w.in {
   687  						prs[in] = struct{}{}
   688  					}
   689  				}
   690  			}
   691  
   692  			// Now, recurse until done, or a false bubbles up, indicating the
   693  			// path is poisoned.
   694  			for in := range w.in {
   695  				// It's possible, albeit weird, for a package to import itself.
   696  				// If we try to visit self, though, then it erroneously poisons
   697  				// the path, as it would be interpreted as grey. In practice,
   698  				// self-imports are a no-op, so we can just skip it.
   699  				if in == pkg {
   700  					continue
   701  				}
   702  
   703  				clean := dfe(in, path)
   704  				if !clean && backprop {
   705  					// Path is poisoned. If we're backpropagating errors, then
   706  					// the  reachmap for the visitee was already deleted by the
   707  					// path we're returning from; mark the visitee black, then
   708  					// return false to bubble up the poison. This is OK to do
   709  					// early, before exploring all internal imports, because the
   710  					// outer loop visits all internal packages anyway.
   711  					//
   712  					// In fact, stopping early is preferable - white subpackages
   713  					// won't have to iterate pointlessly through a parent path
   714  					// with no reachset.
   715  					colors[pkg] = black
   716  					return false
   717  				}
   718  			}
   719  
   720  			// Fully done with this pkg; no transitive problems.
   721  			colors[pkg] = black
   722  			return true
   723  
   724  		case grey:
   725  			// Import cycles can arise in healthy situations through xtests, so
   726  			// allow them for now.
   727  			//
   728  			// FIXME(sdboyer) we need an improved model that allows us to
   729  			// accurately reject real import cycles.
   730  			return true
   731  			// grey means an import cycle; guaranteed badness right here. You'd
   732  			// hope we never encounter it in a dependency (really? you published
   733  			// that code?), but we have to defend against it.
   734  			//colors[pkg] = black
   735  			//poison(append(path, pkg)) // poison self and parents
   736  
   737  		case black:
   738  			// black means we're revisiting a package that was already
   739  			// completely explored. If it has an entry in exrsets, it completed
   740  			// successfully. If not, it was poisoned, and we need to bubble the
   741  			// poison back up.
   742  			rs, exists := exrsets[pkg]
   743  			if !exists {
   744  				if backprop {
   745  					// just poison parents; self was necessarily already poisoned
   746  					poisonBlack(path, pkg)
   747  				}
   748  				return false
   749  			}
   750  			// If external reachset existed, internal must (even if empty)
   751  			irs := inrsets[pkg]
   752  
   753  			// It's good; pull over the imports from its reachset into all
   754  			// non-poisoned parent reachsets
   755  			for _, ppkg := range path {
   756  				if prs, exists := exrsets[ppkg]; exists {
   757  					for ex := range rs {
   758  						prs[ex] = struct{}{}
   759  					}
   760  				}
   761  
   762  				if prs, exists := inrsets[ppkg]; exists {
   763  					for in := range irs {
   764  						prs[in] = struct{}{}
   765  					}
   766  				}
   767  			}
   768  			return true
   769  
   770  		default:
   771  			panic(fmt.Sprintf("invalid color marker %v for %s", colors[pkg], pkg))
   772  		}
   773  	}
   774  
   775  	// Run the depth-first exploration.
   776  	//
   777  	// Don't bother computing graph sources, this straightforward loop works
   778  	// comparably well, and fits nicely with an escape hatch in the dfe.
   779  	var path []string
   780  	for pkg := range workmap {
   781  		// However, at least check that the package isn't already fully visited;
   782  		// this saves a bit of time and implementation complexity inside the
   783  		// closures.
   784  		if colors[pkg] != black {
   785  			dfe(pkg, path)
   786  		}
   787  	}
   788  
   789  	type ie struct {
   790  		Internal, External []string
   791  	}
   792  
   793  	// Flatten exrsets into reachmap
   794  	rm := make(ReachMap)
   795  	for pkg, rs := range exrsets {
   796  		rlen := len(rs)
   797  		if rlen == 0 {
   798  			rm[pkg] = ie{}
   799  			continue
   800  		}
   801  
   802  		edeps := make([]string, 0, rlen)
   803  		for opkg := range rs {
   804  			edeps = append(edeps, opkg)
   805  		}
   806  
   807  		sort.Strings(edeps)
   808  
   809  		sets := rm[pkg]
   810  		sets.External = edeps
   811  		rm[pkg] = sets
   812  	}
   813  
   814  	// Flatten inrsets into reachmap
   815  	for pkg, rs := range inrsets {
   816  		rlen := len(rs)
   817  		if rlen == 0 {
   818  			continue
   819  		}
   820  
   821  		ideps := make([]string, 0, rlen)
   822  		for opkg := range rs {
   823  			ideps = append(ideps, opkg)
   824  		}
   825  
   826  		sort.Strings(ideps)
   827  
   828  		sets := rm[pkg]
   829  		sets.Internal = ideps
   830  		rm[pkg] = sets
   831  	}
   832  
   833  	return rm, errmap
   834  }
   835  
   836  // eqOrSlashedPrefix checks to see if the prefix is either equal to the string,
   837  // or that it is a prefix and the next char in the string is "/".
   838  func eqOrSlashedPrefix(s, prefix string) bool {
   839  	if !strings.HasPrefix(s, prefix) {
   840  		return false
   841  	}
   842  
   843  	prflen, pathlen := len(prefix), len(s)
   844  	return prflen == pathlen || strings.Index(s[prflen:], "/") == 0
   845  }
   846  
   847  // helper func to merge, dedupe, and sort strings
   848  func dedupeStrings(s1, s2 []string) (r []string) {
   849  	dedupe := make(map[string]bool)
   850  
   851  	if len(s1) > 0 && len(s2) > 0 {
   852  		for _, i := range s1 {
   853  			dedupe[i] = true
   854  		}
   855  		for _, i := range s2 {
   856  			dedupe[i] = true
   857  		}
   858  
   859  		for i := range dedupe {
   860  			r = append(r, i)
   861  		}
   862  		// And then re-sort them
   863  		sort.Strings(r)
   864  	} else if len(s1) > 0 {
   865  		r = s1
   866  	} else if len(s2) > 0 {
   867  		r = s2
   868  	}
   869  
   870  	return
   871  }
   872  
   873  func uniq(a []string) []string {
   874  	if a == nil {
   875  		return make([]string, 0)
   876  	}
   877  	var s string
   878  	var i int
   879  	if !sort.StringsAreSorted(a) {
   880  		sort.Strings(a)
   881  	}
   882  	for _, t := range a {
   883  		if t != s {
   884  			a[i] = t
   885  			i++
   886  			s = t
   887  		}
   888  	}
   889  	return a[:i]
   890  }