blob: 67c3718d97d61fd62f7efe1f4090c2ce0ab83eec [file] [log] [blame]
package imports
/*
Utilities for finding import relationships between Go packages.
*/
import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"strings"
"go.skia.org/infra/go/exec"
"go.skia.org/infra/go/sklog"
"go.skia.org/infra/go/util"
)
var (
allPkgData map[string]*Package
cachedPkgData = map[string]*Package{}
)
// Package contains information about a Go package.
// Ideally we would just reuse cmd/go/internal/load.Package, but we're not
// allowed to import internal packages. So we've copy/pasted the relevant
// parts of that struct below.
type Package struct {
Dir string `json:",omitempty"` // directory containing package sources
ImportPath string `json:",omitempty"` // import path of package in dir
ImportComment string `json:",omitempty"` // path in import comment on package statement
Name string `json:",omitempty"` // package name
Doc string `json:",omitempty"` // package documentation string
Target string `json:",omitempty"` // installed target for this package (may be executable)
ForTest string `json:",omitempty"` // package is only for use in named test
Export string `json:",omitempty"` // file containing export data (set by go list -export)
Match []string `json:",omitempty"` // command-line patterns matching this package
Standard bool `json:",omitempty"` // is this package part of the standard Go library?
// Dependency information
Imports []string `json:",omitempty"` // import paths used by this package
Deps []string `json:",omitempty"` // all (recursively) imported dependencies
}
// pkgDataWriter is an io.Writer which reads package data from "go list".
type pkgDataWriter struct {
buf []byte
idx int
cb func(*Package)
total int
}
// See documentation for io.Writer.
func (w *pkgDataWriter) Write(b []byte) (int, error) {
// Unfortunately, when listing multiple packages, "go list" doesn't
// return valid JSON. Instead, it returns one JSON dict for each
// package, separated by newline. We have to look for "\n}" to mark
// the end of each package.
marker := "\n}"
newBuf := append(w.buf, b...) // Combine new data with existing data.
newStartIdx := 0 // Start index of the next package in the buffer.
// Loop through the new bytes looking for the marker string. When we
// find it, we can slice the buffer from newStartIdx to idx to find the
// complete data for the current package, parse it as JSON, and run the
// callback function.
for idx := len(w.buf); idx < len(newBuf); idx++ {
if idx >= len(marker) && string(newBuf[idx-len(marker):idx]) == marker {
// We've found a complete package description. Parse
// it as JSON and run the callback function.
var pkg Package
slice := newBuf[newStartIdx:idx]
if err := json.Unmarshal(slice, &pkg); err != nil {
sklog.Errorf("Error parsing JSON from output: %s", err)
// Return the number of bytes we read which did
// not contain an error, ie. everything up to
// newStartIdx.
read := 0
if newStartIdx > len(w.buf) {
read = newStartIdx - len(w.buf)
}
return read, err
}
w.cb(&pkg)
// Bump the newStartIdx to the current idx to mark the
// start of the next package.
newStartIdx = idx
}
}
w.buf = newBuf[newStartIdx:]
w.total += len(b)
return len(b), nil
}
func newPkgDataWriter(cb func(*Package)) io.Writer {
return &pkgDataWriter{
buf: []byte{},
cb: cb,
}
}
// getPackageData is a helper function which returns data for the given
// package(s). Returns a map to facilitate searching for multiple packages in
// the same call to "go list", eg. "go.skia.org/...", which is much faster.
func getPackageData(ctx context.Context, name string) (map[string]*Package, error) {
// Return the cached data, if it exists.
if pkg, ok := cachedPkgData[name]; ok {
return map[string]*Package{
name: pkg,
}, nil
}
// Run "go list" to obtain information about the given package(s).
pkgs := map[string]*Package{}
cmd := &exec.Command{
Name: "go",
Args: []string{"list", "--json", name},
Stdout: newPkgDataWriter(func(pkg *Package) {
pkgs[pkg.ImportPath] = pkg
}),
}
if err := exec.Run(ctx, cmd); err != nil {
return nil, err
}
// Cache the returned data.
for k, v := range pkgs {
cachedPkgData[k] = v
}
return pkgs, nil
}
// GetPackageData returns information about the given package.
func GetPackageData(ctx context.Context, name string) (*Package, error) {
pkgs, err := getPackageData(ctx, name)
if err != nil {
return nil, err
}
if len(pkgs) != 1 {
return nil, fmt.Errorf("Found multiple entries for %s: %v", name, pkgs)
}
for _, pkg := range pkgs {
return pkg, nil
}
return nil, errors.New("Shouldn't hit this case.")
}
// LoadAllPackageData obtains information about all packages under
// go.skia.org/infra/... and caches it, returning it for convenience.
func LoadAllPackageData(ctx context.Context) (map[string]*Package, error) {
// In addition to maintaining the cache for individual packages, we
// cache the result of this function for convenience.
if allPkgData != nil {
return allPkgData, nil
}
allPkgs, err := getPackageData(ctx, "go.skia.org/infra/...")
if err != nil {
return nil, err
}
allPkgData = allPkgs
return allPkgs, nil
}
// IsBuiltIn returns true if the given package name looks like a built-in
// package.
func IsBuiltIn(pkgName string) bool {
// This is kind of a hack, but it works in practice: builtin packages
// do not have a dot in their first path component, while others do.
// TODO(borenet): We could probably use Package.Standard instead, but
// that would require running "go list" to obtain data for the standard
// packages as well.
return !strings.Contains(strings.SplitN(pkgName, "/", 1)[0], ".")
}
// FindImportPaths returns a slice of slices indicating the import paths from
// one package to another.
func FindImportPaths(ctx context.Context, startPkg, findPkg string) ([][]string, error) {
// Cache values for each package to prevent repeating the same work.
cache := map[string][][]string{}
var helper func(string) ([][]string, error)
helper = func(currentPkg string) ([][]string, error) {
// Returned the cached value, if any.
if rv, ok := cache[currentPkg]; ok {
return rv, nil
}
// Find the imports for startPkg.
data, err := GetPackageData(ctx, currentPkg)
if err != nil {
return nil, err
}
foundPaths := [][]string{}
for _, imp := range data.Imports {
if imp == findPkg {
foundPaths = append(foundPaths, []string{findPkg})
} else if !IsBuiltIn(imp) {
// Recursively search non-built-in packages.
recFoundPaths, err := helper(imp)
if err != nil {
return nil, err
}
foundPaths = append(foundPaths, recFoundPaths...)
}
}
for idx := range foundPaths {
foundPaths[idx] = append(foundPaths[idx], currentPkg)
}
// Cache the return value.
cache[currentPkg] = foundPaths
return foundPaths, nil
}
return helper(startPkg)
}
// FindImporters returns a slice of package names indicating which packages
// under go.skia.org/infra/... directly import the given package.
func FindImporters(ctx context.Context, findPkg string) ([]string, error) {
allPkgs, err := LoadAllPackageData(ctx)
if err != nil {
return nil, err
}
rv := []string{}
for name, pkg := range allPkgs {
if util.In(findPkg, pkg.Imports) {
rv = append(rv, name)
}
}
return rv, nil
}