forked from ebhomengo/niki
289 lines
4.3 KiB
Go
289 lines
4.3 KiB
Go
package depth
|
|
|
|
import (
|
|
"bytes"
|
|
"go/build"
|
|
"path"
|
|
"sort"
|
|
"strings"
|
|
)
|
|
|
|
// Pkg represents a Go source package, and its dependencies.
|
|
|
|
type Pkg struct {
|
|
Name string `json:"name"`
|
|
|
|
SrcDir string `json:"-"`
|
|
|
|
Internal bool `json:"internal"`
|
|
|
|
Resolved bool `json:"resolved"`
|
|
|
|
Test bool `json:"-"`
|
|
|
|
Tree *Tree `json:"-"`
|
|
|
|
Parent *Pkg `json:"-"`
|
|
|
|
Deps []Pkg `json:"deps"`
|
|
|
|
Raw *build.Package `json:"-"`
|
|
}
|
|
|
|
// Resolve recursively finds all dependencies for the Pkg and the packages it depends on.
|
|
|
|
func (p *Pkg) Resolve(i Importer) {
|
|
|
|
// Resolved is always true, regardless of if we skip the import,
|
|
|
|
// it is only false if there is an error while importing.
|
|
|
|
p.Resolved = true
|
|
|
|
name := p.cleanName()
|
|
|
|
if name == "" {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
// Stop resolving imports if we've reached max depth or found a duplicate.
|
|
|
|
var importMode build.ImportMode
|
|
|
|
if p.Tree.hasSeenImport(name) || p.Tree.isAtMaxDepth(p) {
|
|
|
|
importMode = build.FindOnly
|
|
|
|
}
|
|
|
|
pkg, err := i.Import(name, p.SrcDir, importMode)
|
|
|
|
if err != nil {
|
|
|
|
// TODO: Check the error type?
|
|
|
|
p.Resolved = false
|
|
|
|
return
|
|
|
|
}
|
|
|
|
p.Raw = pkg
|
|
|
|
// Update the name with the fully qualified import path.
|
|
|
|
p.Name = pkg.ImportPath
|
|
|
|
// If this is an internal dependency, we may need to skip it.
|
|
|
|
if pkg.Goroot {
|
|
|
|
p.Internal = true
|
|
|
|
if !p.Tree.shouldResolveInternal(p) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
//first we set the regular dependencies, then we add the test dependencies
|
|
|
|
//sharing the same set. This allows us to mark all test-only deps linearly
|
|
|
|
unique := make(map[string]struct{})
|
|
|
|
p.setDeps(i, pkg.Imports, pkg.Dir, unique, false)
|
|
|
|
if p.Tree.ResolveTest {
|
|
|
|
p.setDeps(i, append(pkg.TestImports, pkg.XTestImports...), pkg.Dir, unique, true)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// setDeps takes a slice of import paths and the source directory they are relative to,
|
|
|
|
// and creates the Deps of the Pkg. Each dependency is also further resolved prior to being added
|
|
|
|
// to the Pkg.
|
|
|
|
func (p *Pkg) setDeps(i Importer, imports []string, srcDir string, unique map[string]struct{}, isTest bool) {
|
|
|
|
for _, imp := range imports {
|
|
|
|
// Mostly for testing files where cyclic imports are allowed.
|
|
|
|
if imp == p.Name {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
// Skip duplicates.
|
|
|
|
if _, ok := unique[imp]; ok {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
unique[imp] = struct{}{}
|
|
|
|
p.addDep(i, imp, srcDir, isTest)
|
|
|
|
}
|
|
|
|
sort.Sort(byInternalAndName(p.Deps))
|
|
|
|
}
|
|
|
|
// addDep creates a Pkg and it's dependencies from an imported package name.
|
|
|
|
func (p *Pkg) addDep(i Importer, name string, srcDir string, isTest bool) {
|
|
|
|
dep := Pkg{
|
|
|
|
Name: name,
|
|
|
|
SrcDir: srcDir,
|
|
|
|
Tree: p.Tree,
|
|
|
|
Parent: p,
|
|
|
|
Test: isTest,
|
|
}
|
|
|
|
dep.Resolve(i)
|
|
|
|
p.Deps = append(p.Deps, dep)
|
|
|
|
}
|
|
|
|
// isParent goes recursively up the chain of Pkgs to determine if the name provided is ever a
|
|
|
|
// parent of the current Pkg.
|
|
|
|
func (p *Pkg) isParent(name string) bool {
|
|
|
|
if p.Parent == nil {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
if p.Parent.Name == name {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
return p.Parent.isParent(name)
|
|
|
|
}
|
|
|
|
// depth returns the depth of the Pkg within the Tree.
|
|
|
|
func (p *Pkg) depth() int {
|
|
|
|
if p.Parent == nil {
|
|
|
|
return 0
|
|
|
|
}
|
|
|
|
return p.Parent.depth() + 1
|
|
|
|
}
|
|
|
|
// cleanName returns a cleaned version of the Pkg name used for resolving dependencies.
|
|
|
|
//
|
|
|
|
// If an empty string is returned, dependencies should not be resolved.
|
|
|
|
func (p *Pkg) cleanName() string {
|
|
|
|
name := p.Name
|
|
|
|
// C 'package' cannot be resolved.
|
|
|
|
if name == "C" {
|
|
|
|
return ""
|
|
|
|
}
|
|
|
|
// Internal golang_org/* packages must be prefixed with vendor/
|
|
|
|
//
|
|
|
|
// Thanks to @davecheney for this:
|
|
|
|
// https://github.com/davecheney/graphpkg/blob/master/main.go#L46
|
|
|
|
if strings.HasPrefix(name, "golang_org") {
|
|
|
|
name = path.Join("vendor", name)
|
|
|
|
}
|
|
|
|
return name
|
|
|
|
}
|
|
|
|
// String returns a string representation of the Pkg containing the Pkg name and status.
|
|
|
|
func (p *Pkg) String() string {
|
|
|
|
b := bytes.NewBufferString(p.Name)
|
|
|
|
if !p.Resolved {
|
|
|
|
b.Write([]byte(" (unresolved)"))
|
|
|
|
}
|
|
|
|
return b.String()
|
|
|
|
}
|
|
|
|
// byInternalAndName ensures a slice of Pkgs are sorted such that the internal stdlib
|
|
|
|
// packages are always above external packages (ie. github.com/whatever).
|
|
|
|
type byInternalAndName []Pkg
|
|
|
|
func (b byInternalAndName) Len() int {
|
|
|
|
return len(b)
|
|
|
|
}
|
|
|
|
func (b byInternalAndName) Swap(i, j int) {
|
|
|
|
b[i], b[j] = b[j], b[i]
|
|
|
|
}
|
|
|
|
func (b byInternalAndName) Less(i, j int) bool {
|
|
|
|
if b[i].Internal && !b[j].Internal {
|
|
|
|
return true
|
|
|
|
} else if !b[i].Internal && b[j].Internal {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
return b[i].Name < b[j].Name
|
|
|
|
}
|