blob: b91d2a5edb1b912845fceb8eea7f4ce10e9c2900 [file] [log] [blame]
package vsolver
import (
"bytes"
"fmt"
"go/build"
"io"
"io/ioutil"
"os"
"path/filepath"
"sort"
"strings"
"text/scanner"
)
var osList []string
var archList []string
var stdlib = make(map[string]struct{})
const stdlibPkgs string = "archive archive/tar archive/zip bufio builtin bytes compress compress/bzip2 compress/flate compress/gzip compress/lzw compress/zlib container container/heap container/list container/ring context crypto crypto/aes crypto/cipher crypto/des crypto/dsa crypto/ecdsa crypto/elliptic crypto/hmac crypto/md5 crypto/rand crypto/rc4 crypto/rsa crypto/sha1 crypto/sha256 crypto/sha512 crypto/subtle crypto/tls crypto/x509 crypto/x509/pkix database database/sql database/sql/driver debug debug/dwarf debug/elf debug/gosym debug/macho debug/pe debug/plan9obj encoding encoding/ascii85 encoding/asn1 encoding/base32 encoding/base64 encoding/binary encoding/csv encoding/gob encoding/hex encoding/json encoding/pem encoding/xml errors expvar flag fmt go go/ast go/build go/constant go/doc go/format go/importer go/parser go/printer go/scanner go/token go/types hash hash/adler32 hash/crc32 hash/crc64 hash/fnv html html/template image image/color image/color/palette image/draw image/gif image/jpeg image/png index index/suffixarray io io/ioutil log log/syslog math math/big math/cmplx math/rand mime mime/multipart mime/quotedprintable net net/http net/http/cgi net/http/cookiejar net/http/fcgi net/http/httptest net/http/httputil net/http/pprof net/mail net/rpc net/rpc/jsonrpc net/smtp net/textproto net/url os os/exec os/signal os/user path path/filepath reflect regexp regexp/syntax runtime runtime/cgo runtime/debug runtime/msan runtime/pprof runtime/race runtime/trace sort strconv strings sync sync/atomic syscall testing testing/iotest testing/quick text text/scanner text/tabwriter text/template text/template/parse time unicode unicode/utf16 unicode/utf8 unsafe"
func init() {
// The supported systems are listed in
// https://github.com/golang/go/blob/master/src/go/build/syslist.go
// The lists are not exported so we need to duplicate them here.
osListString := "android darwin dragonfly freebsd linux nacl netbsd openbsd plan9 solaris windows"
osList = strings.Split(osListString, " ")
archListString := "386 amd64 amd64p32 arm armbe arm64 arm64be ppc64 ppc64le mips mipsle mips64 mips64le mips64p32 mips64p32le ppc s390 s390x sparc sparc64"
archList = strings.Split(archListString, " ")
for _, pkg := range strings.Split(stdlibPkgs, " ") {
stdlib[pkg] = struct{}{}
}
}
// listPackages lists info for all packages at or below the provided fileRoot.
//
// Directories without any valid Go files are excluded. Directories with
// multiple packages are excluded.
//
// The importRoot parameter is prepended to the relative path when determining
// the import path for each package. The obvious case is for something typical,
// like:
//
// fileRoot = "/home/user/go/src/github.com/foo/bar"
// importRoot = "github.com/foo/bar"
//
// where the fileRoot and importRoot align. However, if you provide:
//
// fileRoot = "/home/user/workspace/path/to/repo"
// importRoot = "github.com/foo/bar"
//
// then the root package at path/to/repo will be ascribed import path
// "github.com/foo/bar", and its subpackage "baz" will be
// "github.com/foo/bar/baz".
//
// A PackageTree is returned, which contains the ImportRoot and map of import path
// to PackageOrErr - each path under the root that exists will have either a
// Package, or an error describing why the directory is not a valid package.
func listPackages(fileRoot, importRoot string) (PackageTree, error) {
// Set up a build.ctx for parsing
ctx := build.Default
ctx.GOROOT = ""
ctx.GOPATH = ""
ctx.UseAllFiles = true
ptree := PackageTree{
ImportRoot: importRoot,
Packages: make(map[string]PackageOrErr),
}
// mkfilter returns two funcs that can be injected into a
// build.Context, letting us filter the results into an "in" and "out" set.
mkfilter := func(files map[string]struct{}) (in, out func(dir string) (fi []os.FileInfo, err error)) {
in = func(dir string) (fi []os.FileInfo, err error) {
all, err := ioutil.ReadDir(dir)
if err != nil {
return nil, err
}
for _, f := range all {
if _, exists := files[f.Name()]; exists {
fi = append(fi, f)
}
}
return fi, nil
}
out = func(dir string) (fi []os.FileInfo, err error) {
all, err := ioutil.ReadDir(dir)
if err != nil {
return nil, err
}
for _, f := range all {
if _, exists := files[f.Name()]; !exists {
fi = append(fi, f)
}
}
return fi, nil
}
return
}
// helper func to create a Package from a *build.Package
happy := func(importPath string, p *build.Package) Package {
// Happy path - simple parsing worked
pkg := Package{
ImportPath: importPath,
CommentPath: p.ImportComment,
Name: p.Name,
Imports: p.Imports,
TestImports: dedupeStrings(p.TestImports, p.XTestImports),
}
return pkg
}
err := filepath.Walk(fileRoot, func(path string, fi os.FileInfo, err error) error {
if err != nil && err != filepath.SkipDir {
return err
}
if !fi.IsDir() {
return nil
}
// Skip a few types of dirs
if !localSrcDir(fi) {
return filepath.SkipDir
}
// Compute the import path. Run the result through ToSlash(), so that windows
// paths are normalized to Unix separators, as import paths are expected
// to be.
ip := filepath.ToSlash(filepath.Join(importRoot, strings.TrimPrefix(path, fileRoot)))
// Find all the imports, across all os/arch combos
p, err := ctx.ImportDir(path, analysisImportMode())
var pkg Package
if err == nil {
pkg = happy(ip, p)
} else {
switch terr := err.(type) {
case *build.NoGoError:
ptree.Packages[ip] = PackageOrErr{
Err: err,
}
return nil
case *build.MultiplePackageError:
// Set this up preemptively, so we can easily just return out if
// something goes wrong. Otherwise, it'll get transparently
// overwritten later.
ptree.Packages[ip] = PackageOrErr{
Err: err,
}
// For now, we're punting entirely on dealing with os/arch
// combinations. That will be a more significant refactor.
//
// However, there is one case we want to allow here - a single
// file, with "+build ignore", that's a main package. (Ignore is
// just a convention, but for now it's good enough to just check
// that.) This is a fairly common way to make a more
// sophisticated build system than a Makefile allows, so we want
// to support that case. So, transparently lump the deps
// together.
mains := make(map[string]struct{})
for k, pkgname := range terr.Packages {
if pkgname == "main" {
tags, err2 := readFileBuildTags(filepath.Join(path, terr.Files[k]))
if err2 != nil {
return nil
}
var hasignore bool
for _, t := range tags {
if t == "ignore" {
hasignore = true
break
}
}
if !hasignore {
// No ignore tag found - bail out
return nil
}
mains[terr.Files[k]] = struct{}{}
}
}
// Make filtering funcs that will let us look only at the main
// files, and exclude the main files; inf and outf, respectively
inf, outf := mkfilter(mains)
// outf first; if there's another err there, we bail out with a
// return
ctx.ReadDir = outf
po, err2 := ctx.ImportDir(path, analysisImportMode())
if err2 != nil {
return nil
}
ctx.ReadDir = inf
pi, err2 := ctx.ImportDir(path, analysisImportMode())
if err2 != nil {
return nil
}
ctx.ReadDir = nil
// Use the other files as baseline, they're the main stuff
pkg = happy(ip, po)
mpkg := happy(ip, pi)
pkg.Imports = dedupeStrings(pkg.Imports, mpkg.Imports)
pkg.TestImports = dedupeStrings(pkg.TestImports, mpkg.TestImports)
default:
return err
}
}
ptree.Packages[ip] = PackageOrErr{
P: pkg,
}
return nil
})
if err != nil {
return PackageTree{}, err
}
return ptree, nil
}
type wm struct {
ex map[string]struct{}
in map[string]struct{}
}
// wmToReach takes an externalReach()-style workmap and transitively walks all
// internal imports until they reach an external path or terminate, then
// translates the results into a slice of external imports for each internal
// pkg.
//
// The basedir string, with a trailing slash ensured, will be stripped from the
// keys of the returned map.
func wmToReach(workmap map[string]wm, basedir string) (rm map[string][]string, err error) {
// Just brute-force through the workmap, repeating until we make no
// progress, either because no packages have any unresolved internal
// packages left (in which case we're done), or because some packages can't
// find something in the 'in' list (which shouldn't be possible)
//
// This implementation is hilariously inefficient in pure computational
// complexity terms - worst case is some flavor of polynomial, versus O(n)
// for the filesystem scan done in externalReach(). However, the coefficient
// for filesystem access is so much larger than for memory twiddling that it
// would probably take an absurdly large and snaky project to ever have that
// worst-case polynomial growth supercede (or even become comparable to) the
// linear side.
//
// But, if that day comes, we can improve this algorithm.
rm = make(map[string][]string)
var complete bool
for !complete {
var progress bool
complete = true
for pkg, w := range workmap {
if len(w.in) == 0 {
continue
}
complete = false
// Each pass should always empty the original in list, but there
// could be more in lists inherited from the other package
// (transitive internal deps)
for in := range w.in {
if w2, exists := workmap[in]; !exists {
return nil, fmt.Errorf("Should be impossible: %s depends on %s, but %s not in workmap", pkg, in, in)
} else {
progress = true
delete(w.in, in)
for i := range w2.ex {
w.ex[i] = struct{}{}
}
for i := range w2.in {
w.in[i] = struct{}{}
}
}
}
}
if !complete && !progress {
// Can't conceive of a way that we'd hit this, but this guards
// against infinite loop
panic("unreachable")
}
}
// finally, transform to slice for return
rm = make(map[string][]string)
// ensure we have a version of the basedir w/trailing slash, for stripping
rt := strings.TrimSuffix(basedir, string(os.PathSeparator)) + string(os.PathSeparator)
for pkg, w := range workmap {
if len(w.ex) == 0 {
rm[strings.TrimPrefix(pkg, rt)] = nil
continue
}
edeps := make([]string, len(w.ex))
k := 0
for opkg := range w.ex {
edeps[k] = opkg
k++
}
sort.Strings(edeps)
rm[strings.TrimPrefix(pkg, rt)] = edeps
}
return rm, nil
}
func localSrcDir(fi os.FileInfo) bool {
// Ignore _foo and .foo, and testdata
name := fi.Name()
if strings.HasPrefix(name, ".") || strings.HasPrefix(name, "_") || name == "testdata" {
return false
}
// Ignore dirs that are expressly intended for non-project source
switch name {
case "vendor", "Godeps":
return false
default:
return true
}
}
func readBuildTags(p string) ([]string, error) {
_, err := os.Stat(p)
if err != nil {
return []string{}, err
}
d, err := os.Open(p)
if err != nil {
return []string{}, err
}
objects, err := d.Readdir(-1)
if err != nil {
return []string{}, err
}
var tags []string
for _, obj := range objects {
// only process Go files
if strings.HasSuffix(obj.Name(), ".go") {
fp := filepath.Join(p, obj.Name())
co, err := readGoContents(fp)
if err != nil {
return []string{}, err
}
// Only look at places where we had a code comment.
if len(co) > 0 {
t := findTags(co)
for _, tg := range t {
found := false
for _, tt := range tags {
if tt == tg {
found = true
}
}
if !found {
tags = append(tags, tg)
}
}
}
}
}
return tags, nil
}
func readFileBuildTags(fp string) ([]string, error) {
co, err := readGoContents(fp)
if err != nil {
return []string{}, err
}
var tags []string
// Only look at places where we had a code comment.
if len(co) > 0 {
t := findTags(co)
for _, tg := range t {
found := false
for _, tt := range tags {
if tt == tg {
found = true
}
}
if !found {
tags = append(tags, tg)
}
}
}
return tags, nil
}
// Read contents of a Go file up to the package declaration. This can be used
// to find the the build tags.
func readGoContents(fp string) ([]byte, error) {
f, err := os.Open(fp)
defer f.Close()
if err != nil {
return []byte{}, err
}
var s scanner.Scanner
s.Init(f)
var tok rune
var pos scanner.Position
for tok != scanner.EOF {
tok = s.Scan()
// Getting the token text will skip comments by default.
tt := s.TokenText()
// build tags will not be after the package declaration.
if tt == "package" {
pos = s.Position
break
}
}
var buf bytes.Buffer
f.Seek(0, 0)
_, err = io.CopyN(&buf, f, int64(pos.Offset))
if err != nil {
return []byte{}, err
}
return buf.Bytes(), nil
}
// From a byte slice of a Go file find the tags.
func findTags(co []byte) []string {
p := co
var tgs []string
for len(p) > 0 {
line := p
if i := bytes.IndexByte(line, '\n'); i >= 0 {
line, p = line[:i], p[i+1:]
} else {
p = p[len(p):]
}
line = bytes.TrimSpace(line)
// Only look at comment lines that are well formed in the Go style
if bytes.HasPrefix(line, []byte("//")) {
line = bytes.TrimSpace(line[len([]byte("//")):])
if len(line) > 0 && line[0] == '+' {
f := strings.Fields(string(line))
// We've found a +build tag line.
if f[0] == "+build" {
for _, tg := range f[1:] {
tgs = append(tgs, tg)
}
}
}
}
}
return tgs
}
// Get an OS value that's not the one passed in.
func getOsValue(n string) string {
for _, o := range osList {
if o != n {
return o
}
}
return n
}
func isSupportedOs(n string) bool {
for _, o := range osList {
if o == n {
return true
}
}
return false
}
// Get an Arch value that's not the one passed in.
func getArchValue(n string) string {
for _, o := range archList {
if o != n {
return o
}
}
return n
}
func isSupportedArch(n string) bool {
for _, o := range archList {
if o == n {
return true
}
}
return false
}
func ensureTrailingSlash(s string) string {
return strings.TrimSuffix(s, string(os.PathSeparator)) + string(os.PathSeparator)
}
// helper func to merge, dedupe, and sort strings
func dedupeStrings(s1, s2 []string) (r []string) {
dedupe := make(map[string]bool)
if len(s1) > 0 && len(s2) > 0 {
for _, i := range s1 {
dedupe[i] = true
}
for _, i := range s2 {
dedupe[i] = true
}
for i := range dedupe {
r = append(r, i)
}
// And then re-sort them
sort.Strings(r)
} else if len(s1) > 0 {
r = s1
} else if len(s2) > 0 {
r = s2
}
return
}
// A PackageTree represents the results of recursively parsing a tree of
// packages, starting at the ImportRoot. The results of parsing the files in the
// directory identified by each import path - a Package or an error - are stored
// in the Packages map, keyed by that import path.
type PackageTree struct {
ImportRoot string
Packages map[string]PackageOrErr
}
// PackageOrErr stores the results of attempting to parse a single directory for
// Go source code.
type PackageOrErr struct {
P Package
Err error
}
// ExternalReach looks through a PackageTree and computes the list of external
// packages (not logical children of PackageTree.ImportRoot) that are
// transitively imported by the internal packages in the tree.
//
// main indicates whether (true) or not (false) to include main packages in the
// analysis. main packages should generally be excluded when analyzing the
// non-root dependency, as they inherently can't be imported.
//
// tests indicates whether (true) or not (false) to include imports from test
// files in packages when computing the reach map.
//
// ignore is a map of import paths that, if encountered, should be excluded from
// analysis. This exclusion applies to both internal and external packages. If
// an external import path is ignored, it is simply omitted from the results.
//
// If an internal path is ignored, then it is excluded from all transitive
// dependency chains and does not appear as a key in the final map. That is, if
// you ignore A/foo, then the external package list for all internal packages
// that import A/foo will not include external packages were only reachable
// through A/foo.
//
// Visually, this means that, given a PackageTree with root A and packages at A,
// A/foo, and A/bar, and the following import chain:
//
// A -> A/foo -> A/bar -> B/baz
//
// If you ignore A/foo, then the returned map would be:
//
// map[string][]string{
// "A": []string{},
// "A/bar": []string{"B/baz"},
// }
//
// It is safe to pass a nil map if there are no packages to ignore.
func (t PackageTree) ExternalReach(main, tests bool, ignore map[string]bool) (map[string][]string, error) {
var someerrs bool
if ignore == nil {
ignore = make(map[string]bool)
}
// world's simplest adjacency list
workmap := make(map[string]wm)
var imps []string
for ip, perr := range t.Packages {
if perr.Err != nil {
someerrs = true
continue
}
p := perr.P
// Skip main packages, unless param says otherwise
if p.Name == "main" && !main {
continue
}
// Skip ignored packages
if ignore[ip] {
continue
}
imps = imps[:0]
imps = p.Imports
if tests {
imps = dedupeStrings(imps, p.TestImports)
}
w := wm{
ex: make(map[string]struct{}),
in: make(map[string]struct{}),
}
for _, imp := range imps {
if ignore[imp] {
continue
}
if !checkPrefixSlash(filepath.Clean(imp), t.ImportRoot) {
w.ex[imp] = struct{}{}
} else {
if w2, seen := workmap[imp]; seen {
for i := range w2.ex {
w.ex[i] = struct{}{}
}
for i := range w2.in {
w.in[i] = struct{}{}
}
} else {
w.in[imp] = struct{}{}
}
}
}
workmap[ip] = w
}
if len(workmap) == 0 {
if someerrs {
// TODO proper errs
return nil, fmt.Errorf("no packages without errors in %s", t.ImportRoot)
}
return nil, nil
}
//return wmToReach(workmap, t.ImportRoot)
return wmToReach(workmap, "") // TODO this passes tests, but doesn't seem right
}
// ListExternalImports computes a sorted, deduplicated list of all the external
// packages that are imported by all packages in the PackageTree.
//
// "External" is defined as anything not prefixed, after path cleaning, by the
// PackageTree.ImportRoot. This includes stdlib.
//
// If an internal path is ignored, all of the external packages that it uniquely
// imports are omitted. Note, however, that no internal transitivity checks are
// made here - every non-ignored package in the tree is considered
// independently. That means, given a PackageTree with root A and packages at A,
// A/foo, and A/bar, and the following import chain:
//
// A -> A/foo -> A/bar -> B/baz
//
// If you ignore A or A/foo, A/bar will still be visited, and B/baz will be
// returned, because this method visits ALL packages in the tree, not only those reachable
// from the root (or any other) packages. If your use case requires interrogating
// external imports with respect to only specific package entry points, you need
// ExternalReach() instead.
//
// It is safe to pass a nil map if there are no packages to ignore.
func (t PackageTree) ListExternalImports(main, tests bool, ignore map[string]bool) ([]string, error) {
var someerrs bool
exm := make(map[string]struct{})
if ignore == nil {
ignore = make(map[string]bool)
}
var imps []string
for ip, perr := range t.Packages {
if perr.Err != nil {
someerrs = true
continue
}
p := perr.P
// Skip main packages, unless param says otherwise
if p.Name == "main" && !main {
continue
}
// Skip ignored packages
if ignore[ip] {
continue
}
imps = imps[:0]
imps = p.Imports
if tests {
imps = dedupeStrings(imps, p.TestImports)
}
for _, imp := range imps {
if !checkPrefixSlash(filepath.Clean(imp), t.ImportRoot) && !ignore[imp] {
exm[imp] = struct{}{}
}
}
}
if len(exm) == 0 {
if someerrs {
// TODO proper errs
return nil, fmt.Errorf("No packages without errors in %s", t.ImportRoot)
}
return nil, nil
}
ex := make([]string, len(exm))
k := 0
for p := range exm {
ex[k] = p
k++
}
sort.Strings(ex)
return ex, nil
}
// checkPrefixSlash checks to see if the prefix is a prefix of the string as-is,
// and that it is either equal OR the prefix + / is still a prefix.
func checkPrefixSlash(s, prefix string) bool {
if !strings.HasPrefix(s, prefix) {
return false
}
return s == prefix || strings.HasPrefix(s, ensureTrailingSlash(prefix))
}