mirror of
https://github.com/golang/go
synced 2024-11-18 20:14:43 -07:00
9c309ee22f
go/packages needs to call `go list` multiple times, which causes redundant work and slows down goimports. If we reimplement `go list` in memory, we can reuse state, saving time. `go list` also does work we don't really need, like adding stuff to go.mod, and skipping that saves more time. We start with `go list -m`, which does MVS and such. The remaining work is mostly mapping import paths and directories through the in-scope modules to make sure we're giving the right answers. Unfortunately this is quite subtle, and I don't know where all the traps are. I did my best. cmd/go already has tests for `go list`, of course, and packagestest is not well suited to tests of this complexity. So I ripped off the script tests in cmd/go that seemed relevant and made sure that our logic returns the right stuff in each case. I'm sure that there are more cases to cover, but this hit all the stuff I knew about and quite a bit I didn't. Since we may want to use the go/packages code path in the future, e.g. for Bazel, I left that in place. It won't be used unless the magic env var is set. Files in internal and imports/testdata/mod were copied verbatim from cmd/go. Change-Id: I1248d99c400c1a0c7ef180d4460b9b8a3db0246b Reviewed-on: https://go-review.googlesource.com/c/158097 Run-TryBot: Heschi Kreinick <heschi@google.com> TryBot-Result: Gobot Gobot <gobot@golang.org> Reviewed-by: Ian Cottrell <iancottrell@google.com>
1240 lines
33 KiB
Go
1240 lines
33 KiB
Go
// Copyright 2013 The Go Authors. All rights reserved.
|
|
// Use of this source code is governed by a BSD-style
|
|
// license that can be found in the LICENSE file.
|
|
|
|
package imports
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"fmt"
|
|
"go/ast"
|
|
"go/build"
|
|
"go/parser"
|
|
"go/token"
|
|
"io/ioutil"
|
|
"log"
|
|
"os"
|
|
"os/exec"
|
|
"path"
|
|
"path/filepath"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"golang.org/x/tools/go/ast/astutil"
|
|
"golang.org/x/tools/go/packages"
|
|
"golang.org/x/tools/internal/gopathwalk"
|
|
)
|
|
|
|
// Debug controls verbose logging.
|
|
var Debug = false
|
|
|
|
// LocalPrefix is a comma-separated string of import path prefixes, which, if
|
|
// set, instructs Process to sort the import paths with the given prefixes
|
|
// into another group after 3rd-party packages.
|
|
var LocalPrefix string
|
|
|
|
func localPrefixes() []string {
|
|
if LocalPrefix != "" {
|
|
return strings.Split(LocalPrefix, ",")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// importToGroup is a list of functions which map from an import path to
|
|
// a group number.
|
|
var importToGroup = []func(importPath string) (num int, ok bool){
|
|
func(importPath string) (num int, ok bool) {
|
|
for _, p := range localPrefixes() {
|
|
if strings.HasPrefix(importPath, p) || strings.TrimSuffix(p, "/") == importPath {
|
|
return 3, true
|
|
}
|
|
}
|
|
return
|
|
},
|
|
func(importPath string) (num int, ok bool) {
|
|
if strings.HasPrefix(importPath, "appengine") {
|
|
return 2, true
|
|
}
|
|
return
|
|
},
|
|
func(importPath string) (num int, ok bool) {
|
|
if strings.Contains(importPath, ".") {
|
|
return 1, true
|
|
}
|
|
return
|
|
},
|
|
}
|
|
|
|
func importGroup(importPath string) int {
|
|
for _, fn := range importToGroup {
|
|
if n, ok := fn(importPath); ok {
|
|
return n
|
|
}
|
|
}
|
|
return 0
|
|
}
|
|
|
|
// An importInfo represents a single import statement.
|
|
type importInfo struct {
|
|
importPath string // import path, e.g. "crypto/rand".
|
|
name string // import name, e.g. "crand", or "" if none.
|
|
}
|
|
|
|
// A packageInfo represents what's known about a package.
|
|
type packageInfo struct {
|
|
name string // real package name, if known.
|
|
exports map[string]bool // known exports.
|
|
}
|
|
|
|
// parseOtherFiles parses all the Go files in srcDir except filename, including
|
|
// test files if filename looks like a test.
|
|
func parseOtherFiles(fset *token.FileSet, srcDir, filename string) []*ast.File {
|
|
// This could use go/packages but it doesn't buy much, and it fails
|
|
// with https://golang.org/issue/26296 in LoadFiles mode in some cases.
|
|
considerTests := strings.HasSuffix(filename, "_test.go")
|
|
|
|
fileBase := filepath.Base(filename)
|
|
packageFileInfos, err := ioutil.ReadDir(srcDir)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
|
|
var files []*ast.File
|
|
for _, fi := range packageFileInfos {
|
|
if fi.Name() == fileBase || !strings.HasSuffix(fi.Name(), ".go") {
|
|
continue
|
|
}
|
|
if !considerTests && strings.HasSuffix(fi.Name(), "_test.go") {
|
|
continue
|
|
}
|
|
|
|
f, err := parser.ParseFile(fset, filepath.Join(srcDir, fi.Name()), nil, 0)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
|
|
files = append(files, f)
|
|
}
|
|
|
|
return files
|
|
}
|
|
|
|
// addGlobals puts the names of package vars into the provided map.
|
|
func addGlobals(f *ast.File, globals map[string]bool) {
|
|
for _, decl := range f.Decls {
|
|
genDecl, ok := decl.(*ast.GenDecl)
|
|
if !ok {
|
|
continue
|
|
}
|
|
|
|
for _, spec := range genDecl.Specs {
|
|
valueSpec, ok := spec.(*ast.ValueSpec)
|
|
if !ok {
|
|
continue
|
|
}
|
|
globals[valueSpec.Names[0].Name] = true
|
|
}
|
|
}
|
|
}
|
|
|
|
// collectReferences builds a map of selector expressions, from
|
|
// left hand side (X) to a set of right hand sides (Sel).
|
|
func collectReferences(f *ast.File) references {
|
|
refs := references{}
|
|
|
|
var visitor visitFn
|
|
visitor = func(node ast.Node) ast.Visitor {
|
|
if node == nil {
|
|
return visitor
|
|
}
|
|
switch v := node.(type) {
|
|
case *ast.SelectorExpr:
|
|
xident, ok := v.X.(*ast.Ident)
|
|
if !ok {
|
|
break
|
|
}
|
|
if xident.Obj != nil {
|
|
// If the parser can resolve it, it's not a package ref.
|
|
break
|
|
}
|
|
if !ast.IsExported(v.Sel.Name) {
|
|
// Whatever this is, it's not exported from a package.
|
|
break
|
|
}
|
|
pkgName := xident.Name
|
|
r := refs[pkgName]
|
|
if r == nil {
|
|
r = make(map[string]bool)
|
|
refs[pkgName] = r
|
|
}
|
|
r[v.Sel.Name] = true
|
|
}
|
|
return visitor
|
|
}
|
|
ast.Walk(visitor, f)
|
|
return refs
|
|
}
|
|
|
|
// collectImports returns all the imports in f, keyed by their package name as
|
|
// determined by pathToName. Unnamed imports (., _) and "C" are ignored.
|
|
func collectImports(f *ast.File) []*importInfo {
|
|
var imports []*importInfo
|
|
for _, imp := range f.Imports {
|
|
var name string
|
|
if imp.Name != nil {
|
|
name = imp.Name.Name
|
|
}
|
|
if imp.Path.Value == `"C"` || name == "_" || name == "." {
|
|
continue
|
|
}
|
|
path := strings.Trim(imp.Path.Value, `"`)
|
|
imports = append(imports, &importInfo{
|
|
name: name,
|
|
importPath: path,
|
|
})
|
|
}
|
|
return imports
|
|
}
|
|
|
|
// findMissingImport searches pass's candidates for an import that provides
|
|
// pkg, containing all of syms.
|
|
func (p *pass) findMissingImport(pkg string, syms map[string]bool) *importInfo {
|
|
for _, candidate := range p.candidates {
|
|
pkgInfo, ok := p.knownPackages[candidate.importPath]
|
|
if !ok {
|
|
continue
|
|
}
|
|
if p.importIdentifier(candidate) != pkg {
|
|
continue
|
|
}
|
|
|
|
allFound := true
|
|
for right := range syms {
|
|
if !pkgInfo.exports[right] {
|
|
allFound = false
|
|
break
|
|
}
|
|
}
|
|
|
|
if allFound {
|
|
return candidate
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// references is set of references found in a Go file. The first map key is the
|
|
// left hand side of a selector expression, the second key is the right hand
|
|
// side, and the value should always be true.
|
|
type references map[string]map[string]bool
|
|
|
|
// A pass contains all the inputs and state necessary to fix a file's imports.
|
|
// It can be modified in some ways during use; see comments below.
|
|
type pass struct {
|
|
// Inputs. These must be set before a call to load, and not modified after.
|
|
fset *token.FileSet // fset used to parse f and its siblings.
|
|
f *ast.File // the file being fixed.
|
|
srcDir string // the directory containing f.
|
|
fixEnv *fixEnv // the environment to use for go commands, etc.
|
|
loadRealPackageNames bool // if true, load package names from disk rather than guessing them.
|
|
otherFiles []*ast.File // sibling files.
|
|
|
|
// Intermediate state, generated by load.
|
|
existingImports map[string]*importInfo
|
|
allRefs references
|
|
missingRefs references
|
|
|
|
// Inputs to fix. These can be augmented between successive fix calls.
|
|
lastTry bool // indicates that this is the last call and fix should clean up as best it can.
|
|
candidates []*importInfo // candidate imports in priority order.
|
|
knownPackages map[string]*packageInfo // information about all known packages.
|
|
}
|
|
|
|
// loadPackageNames saves the package names for everything referenced by imports.
|
|
func (p *pass) loadPackageNames(imports []*importInfo) error {
|
|
var unknown []string
|
|
for _, imp := range imports {
|
|
if _, ok := p.knownPackages[imp.importPath]; ok {
|
|
continue
|
|
}
|
|
unknown = append(unknown, imp.importPath)
|
|
}
|
|
|
|
names, err := p.fixEnv.getResolver().loadPackageNames(unknown, p.srcDir)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for path, name := range names {
|
|
p.knownPackages[path] = &packageInfo{
|
|
name: name,
|
|
exports: map[string]bool{},
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// importIdentifier returns the identifier that imp will introduce. It will
|
|
// guess if the package name has not been loaded, e.g. because the source
|
|
// is not available.
|
|
func (p *pass) importIdentifier(imp *importInfo) string {
|
|
if imp.name != "" {
|
|
return imp.name
|
|
}
|
|
known := p.knownPackages[imp.importPath]
|
|
if known != nil && known.name != "" {
|
|
return known.name
|
|
}
|
|
return importPathToNameBasic(imp.importPath, p.srcDir)
|
|
}
|
|
|
|
// load reads in everything necessary to run a pass, and reports whether the
|
|
// file already has all the imports it needs. It fills in p.missingRefs with the
|
|
// file's missing symbols, if any, or removes unused imports if not.
|
|
func (p *pass) load() bool {
|
|
p.knownPackages = map[string]*packageInfo{}
|
|
p.missingRefs = references{}
|
|
p.existingImports = map[string]*importInfo{}
|
|
|
|
// Load basic information about the file in question.
|
|
p.allRefs = collectReferences(p.f)
|
|
|
|
// Load stuff from other files in the same package:
|
|
// global variables so we know they don't need resolving, and imports
|
|
// that we might want to mimic.
|
|
globals := map[string]bool{}
|
|
for _, otherFile := range p.otherFiles {
|
|
// Don't load globals from files that are in the same directory
|
|
// but a different package. Using them to suggest imports is OK.
|
|
if p.f.Name.Name == otherFile.Name.Name {
|
|
addGlobals(otherFile, globals)
|
|
}
|
|
p.candidates = append(p.candidates, collectImports(otherFile)...)
|
|
}
|
|
|
|
// Resolve all the import paths we've seen to package names, and store
|
|
// f's imports by the identifier they introduce.
|
|
imports := collectImports(p.f)
|
|
if p.loadRealPackageNames {
|
|
err := p.loadPackageNames(append(imports, p.candidates...))
|
|
if err != nil {
|
|
if Debug {
|
|
log.Printf("loading package names: %v", err)
|
|
}
|
|
return false
|
|
}
|
|
}
|
|
for _, imp := range imports {
|
|
p.existingImports[p.importIdentifier(imp)] = imp
|
|
}
|
|
|
|
// Find missing references.
|
|
for left, rights := range p.allRefs {
|
|
if globals[left] {
|
|
continue
|
|
}
|
|
_, ok := p.existingImports[left]
|
|
if !ok {
|
|
p.missingRefs[left] = rights
|
|
continue
|
|
}
|
|
}
|
|
if len(p.missingRefs) != 0 {
|
|
return false
|
|
}
|
|
|
|
return p.fix()
|
|
}
|
|
|
|
// fix attempts to satisfy missing imports using p.candidates. If it finds
|
|
// everything, or if p.lastTry is true, it adds the imports it found,
|
|
// removes anything unused, and returns true.
|
|
func (p *pass) fix() bool {
|
|
// Find missing imports.
|
|
var selected []*importInfo
|
|
for left, rights := range p.missingRefs {
|
|
if imp := p.findMissingImport(left, rights); imp != nil {
|
|
selected = append(selected, imp)
|
|
}
|
|
}
|
|
|
|
if !p.lastTry && len(selected) != len(p.missingRefs) {
|
|
return false
|
|
}
|
|
|
|
// Found everything, or giving up. Add the new imports and remove any unused.
|
|
for _, imp := range p.existingImports {
|
|
// We deliberately ignore globals here, because we can't be sure
|
|
// they're in the same package. People do things like put multiple
|
|
// main packages in the same directory, and we don't want to
|
|
// remove imports if they happen to have the same name as a var in
|
|
// a different package.
|
|
if _, ok := p.allRefs[p.importIdentifier(imp)]; !ok {
|
|
astutil.DeleteNamedImport(p.fset, p.f, imp.name, imp.importPath)
|
|
}
|
|
}
|
|
|
|
for _, imp := range selected {
|
|
astutil.AddNamedImport(p.fset, p.f, imp.name, imp.importPath)
|
|
}
|
|
|
|
if p.loadRealPackageNames {
|
|
for _, imp := range p.f.Imports {
|
|
if imp.Name != nil {
|
|
continue
|
|
}
|
|
path := strings.Trim(imp.Path.Value, `""`)
|
|
ident := p.importIdentifier(&importInfo{importPath: path})
|
|
if ident != importPathToNameBasic(path, p.srcDir) {
|
|
imp.Name = &ast.Ident{Name: ident, NamePos: imp.Pos()}
|
|
}
|
|
}
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
// assumeSiblingImportsValid assumes that siblings' use of packages is valid,
|
|
// adding the exports they use.
|
|
func (p *pass) assumeSiblingImportsValid() {
|
|
for _, f := range p.otherFiles {
|
|
refs := collectReferences(f)
|
|
imports := collectImports(f)
|
|
importsByName := map[string]*importInfo{}
|
|
for _, imp := range imports {
|
|
importsByName[p.importIdentifier(imp)] = imp
|
|
}
|
|
for left, rights := range refs {
|
|
if imp, ok := importsByName[left]; ok {
|
|
if _, ok := stdlib[imp.importPath]; ok {
|
|
// We have the stdlib in memory; no need to guess.
|
|
rights = stdlib[imp.importPath]
|
|
}
|
|
p.addCandidate(imp, &packageInfo{
|
|
// no name; we already know it.
|
|
exports: rights,
|
|
})
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// addCandidate adds a candidate import to p, and merges in the information
|
|
// in pkg.
|
|
func (p *pass) addCandidate(imp *importInfo, pkg *packageInfo) {
|
|
p.candidates = append(p.candidates, imp)
|
|
if existing, ok := p.knownPackages[imp.importPath]; ok {
|
|
if existing.name == "" {
|
|
existing.name = pkg.name
|
|
}
|
|
for export := range pkg.exports {
|
|
existing.exports[export] = true
|
|
}
|
|
} else {
|
|
p.knownPackages[imp.importPath] = pkg
|
|
}
|
|
}
|
|
|
|
// fixImports adds and removes imports from f so that all its references are
|
|
// satisfied and there are no unused imports.
|
|
//
|
|
// This is declared as a variable rather than a function so goimports can
|
|
// easily be extended by adding a file with an init function.
|
|
var fixImports = fixImportsDefault
|
|
|
|
func fixImportsDefault(fset *token.FileSet, f *ast.File, filename string, env *fixEnv) error {
|
|
abs, err := filepath.Abs(filename)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
srcDir := filepath.Dir(abs)
|
|
if Debug {
|
|
log.Printf("fixImports(filename=%q), abs=%q, srcDir=%q ...", filename, abs, srcDir)
|
|
}
|
|
|
|
// First pass: looking only at f, and using the naive algorithm to
|
|
// derive package names from import paths, see if the file is already
|
|
// complete. We can't add any imports yet, because we don't know
|
|
// if missing references are actually package vars.
|
|
p := &pass{fset: fset, f: f, srcDir: srcDir}
|
|
if p.load() {
|
|
return nil
|
|
}
|
|
|
|
otherFiles := parseOtherFiles(fset, srcDir, filename)
|
|
|
|
// Second pass: add information from other files in the same package,
|
|
// like their package vars and imports.
|
|
p.otherFiles = otherFiles
|
|
if p.load() {
|
|
return nil
|
|
}
|
|
|
|
// Now we can try adding imports from the stdlib.
|
|
p.assumeSiblingImportsValid()
|
|
addStdlibCandidates(p, p.missingRefs)
|
|
if p.fix() {
|
|
return nil
|
|
}
|
|
|
|
// Third pass: get real package names where we had previously used
|
|
// the naive algorithm. This is the first step that will use the
|
|
// environment, so we provide it here for the first time.
|
|
p = &pass{fset: fset, f: f, srcDir: srcDir, fixEnv: env}
|
|
p.loadRealPackageNames = true
|
|
p.otherFiles = otherFiles
|
|
if p.load() {
|
|
return nil
|
|
}
|
|
|
|
addStdlibCandidates(p, p.missingRefs)
|
|
p.assumeSiblingImportsValid()
|
|
if p.fix() {
|
|
return nil
|
|
}
|
|
|
|
// Go look for candidates in $GOPATH, etc. We don't necessarily load
|
|
// the real exports of sibling imports, so keep assuming their contents.
|
|
if err := addExternalCandidates(p, p.missingRefs, filename); err != nil {
|
|
return err
|
|
}
|
|
|
|
p.lastTry = true
|
|
p.fix()
|
|
return nil
|
|
}
|
|
|
|
// fixEnv contains environment variables and settings that affect the use of
|
|
// the go command, the go/build package, etc.
|
|
type fixEnv struct {
|
|
// If non-empty, these will be used instead of the
|
|
// process-wide values.
|
|
GOPATH, GOROOT, GO111MODULE, GOPROXY, GOFLAGS string
|
|
WorkingDir string
|
|
|
|
// If true, use go/packages regardless of the environment.
|
|
ForceGoPackages bool
|
|
|
|
resolver resolver
|
|
}
|
|
|
|
func (e *fixEnv) env() []string {
|
|
env := os.Environ()
|
|
add := func(k, v string) {
|
|
if v != "" {
|
|
env = append(env, k+"="+v)
|
|
}
|
|
}
|
|
add("GOPATH", e.GOPATH)
|
|
add("GOROOT", e.GOROOT)
|
|
add("GO111MODULE", e.GO111MODULE)
|
|
add("GOPROXY", e.GOPROXY)
|
|
add("GOFLAGS", e.GOFLAGS)
|
|
if e.WorkingDir != "" {
|
|
add("PWD", e.WorkingDir)
|
|
}
|
|
return env
|
|
}
|
|
|
|
func (e *fixEnv) getResolver() resolver {
|
|
if e.resolver != nil {
|
|
return e.resolver
|
|
}
|
|
if e.ForceGoPackages {
|
|
return &goPackagesResolver{env: e}
|
|
}
|
|
|
|
out, err := e.invokeGo("env", "GOMOD")
|
|
if err != nil || len(bytes.TrimSpace(out.Bytes())) == 0 {
|
|
return &gopathResolver{env: e}
|
|
}
|
|
return &moduleResolver{env: e}
|
|
}
|
|
|
|
func (e *fixEnv) newPackagesConfig(mode packages.LoadMode) *packages.Config {
|
|
return &packages.Config{
|
|
Mode: mode,
|
|
Dir: e.WorkingDir,
|
|
Env: e.env(),
|
|
}
|
|
}
|
|
|
|
func (e *fixEnv) buildContext() *build.Context {
|
|
ctx := build.Default
|
|
ctx.GOROOT = e.GOROOT
|
|
ctx.GOPATH = e.GOPATH
|
|
return &ctx
|
|
}
|
|
|
|
func (e *fixEnv) invokeGo(args ...string) (*bytes.Buffer, error) {
|
|
cmd := exec.Command("go", args...)
|
|
stdout := &bytes.Buffer{}
|
|
stderr := &bytes.Buffer{}
|
|
cmd.Stdout = stdout
|
|
cmd.Stderr = stderr
|
|
cmd.Env = e.env()
|
|
cmd.Dir = e.WorkingDir
|
|
|
|
if Debug {
|
|
defer func(start time.Time) { log.Printf("%s for %v", time.Since(start), cmdDebugStr(cmd)) }(time.Now())
|
|
}
|
|
if err := cmd.Run(); err != nil {
|
|
return nil, fmt.Errorf("running go: %v (stderr:\n%s)", err, stderr)
|
|
}
|
|
return stdout, nil
|
|
}
|
|
|
|
func cmdDebugStr(cmd *exec.Cmd) string {
|
|
env := make(map[string]string)
|
|
for _, kv := range cmd.Env {
|
|
split := strings.Split(kv, "=")
|
|
k, v := split[0], split[1]
|
|
env[k] = v
|
|
}
|
|
|
|
return fmt.Sprintf("GOROOT=%v GOPATH=%v GO111MODULE=%v GOPROXY=%v PWD=%v go %v", env["GOROOT"], env["GOPATH"], env["GO111MODULE"], env["GOPROXY"], env["PWD"], cmd.Args)
|
|
}
|
|
|
|
func addStdlibCandidates(pass *pass, refs references) {
|
|
add := func(pkg string) {
|
|
pass.addCandidate(
|
|
&importInfo{importPath: pkg},
|
|
&packageInfo{name: path.Base(pkg), exports: stdlib[pkg]})
|
|
}
|
|
for left := range refs {
|
|
if left == "rand" {
|
|
// Make sure we try crypto/rand before math/rand.
|
|
add("crypto/rand")
|
|
add("math/rand")
|
|
continue
|
|
}
|
|
for importPath := range stdlib {
|
|
if path.Base(importPath) == left {
|
|
add(importPath)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// A resolver does the build-system-specific parts of goimports.
|
|
type resolver interface {
|
|
// loadPackageNames loads the package names in importPaths.
|
|
loadPackageNames(importPaths []string, srcDir string) (map[string]string, error)
|
|
// scan finds (at least) the packages satisfying refs. The returned slice is unordered.
|
|
scan(refs references) ([]*pkg, error)
|
|
}
|
|
|
|
// gopathResolver implements resolver for GOPATH and module workspaces using go/packages.
|
|
type goPackagesResolver struct {
|
|
env *fixEnv
|
|
}
|
|
|
|
func (r *goPackagesResolver) loadPackageNames(importPaths []string, srcDir string) (map[string]string, error) {
|
|
cfg := r.env.newPackagesConfig(packages.LoadFiles)
|
|
pkgs, err := packages.Load(cfg, importPaths...)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
names := map[string]string{}
|
|
for _, pkg := range pkgs {
|
|
names[VendorlessPath(pkg.PkgPath)] = pkg.Name
|
|
}
|
|
// We may not have found all the packages. Guess the rest.
|
|
for _, path := range importPaths {
|
|
if _, ok := names[path]; ok {
|
|
continue
|
|
}
|
|
names[path] = importPathToNameBasic(path, srcDir)
|
|
}
|
|
return names, nil
|
|
|
|
}
|
|
|
|
func (r *goPackagesResolver) scan(refs references) ([]*pkg, error) {
|
|
var loadQueries []string
|
|
for pkgName := range refs {
|
|
loadQueries = append(loadQueries, "name="+pkgName)
|
|
}
|
|
sort.Strings(loadQueries)
|
|
cfg := r.env.newPackagesConfig(packages.LoadFiles)
|
|
goPackages, err := packages.Load(cfg, loadQueries...)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var scan []*pkg
|
|
for _, goPackage := range goPackages {
|
|
scan = append(scan, &pkg{
|
|
dir: filepath.Dir(goPackage.CompiledGoFiles[0]),
|
|
importPathShort: VendorlessPath(goPackage.PkgPath),
|
|
goPackage: goPackage,
|
|
})
|
|
}
|
|
return scan, nil
|
|
}
|
|
|
|
func addExternalCandidates(pass *pass, refs references, filename string) error {
|
|
dirScan, err := pass.fixEnv.getResolver().scan(refs)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Search for imports matching potential package references.
|
|
type result struct {
|
|
imp *importInfo
|
|
pkg *packageInfo
|
|
}
|
|
results := make(chan result, len(refs))
|
|
|
|
ctx, cancel := context.WithCancel(context.TODO())
|
|
var wg sync.WaitGroup
|
|
defer func() {
|
|
cancel()
|
|
wg.Wait()
|
|
}()
|
|
var (
|
|
firstErr error
|
|
firstErrOnce sync.Once
|
|
)
|
|
for pkgName, symbols := range refs {
|
|
wg.Add(1)
|
|
go func(pkgName string, symbols map[string]bool) {
|
|
defer wg.Done()
|
|
|
|
found, err := findImport(ctx, pass.fixEnv, dirScan, pkgName, symbols, filename)
|
|
|
|
if err != nil {
|
|
firstErrOnce.Do(func() {
|
|
firstErr = err
|
|
cancel()
|
|
})
|
|
return
|
|
}
|
|
|
|
if found == nil {
|
|
return // No matching package.
|
|
}
|
|
|
|
imp := &importInfo{
|
|
importPath: found.importPathShort,
|
|
}
|
|
|
|
pkg := &packageInfo{
|
|
name: pkgName,
|
|
exports: symbols,
|
|
}
|
|
results <- result{imp, pkg}
|
|
}(pkgName, symbols)
|
|
}
|
|
go func() {
|
|
wg.Wait()
|
|
close(results)
|
|
}()
|
|
|
|
for result := range results {
|
|
pass.addCandidate(result.imp, result.pkg)
|
|
}
|
|
return firstErr
|
|
}
|
|
|
|
// importPathToNameBasic assumes the package name is the base of import path,
|
|
// except that if the path ends in foo/vN, it assumes the package name is foo.
|
|
func importPathToNameBasic(importPath, srcDir string) (packageName string) {
|
|
base := path.Base(importPath)
|
|
if strings.HasPrefix(base, "v") {
|
|
if _, err := strconv.Atoi(base[1:]); err == nil {
|
|
dir := path.Dir(importPath)
|
|
if dir != "." {
|
|
return path.Base(dir)
|
|
}
|
|
}
|
|
}
|
|
return base
|
|
}
|
|
|
|
// gopathResolver implements resolver for GOPATH workspaces.
|
|
type gopathResolver struct {
|
|
env *fixEnv
|
|
}
|
|
|
|
func (r *gopathResolver) loadPackageNames(importPaths []string, srcDir string) (map[string]string, error) {
|
|
names := map[string]string{}
|
|
for _, path := range importPaths {
|
|
names[path] = importPathToName(r.env, path, srcDir)
|
|
}
|
|
return names, nil
|
|
}
|
|
|
|
// importPathToNameGoPath finds out the actual package name, as declared in its .go files.
|
|
// If there's a problem, it returns "".
|
|
func importPathToName(env *fixEnv, importPath, srcDir string) (packageName string) {
|
|
// Fast path for standard library without going to disk.
|
|
if _, ok := stdlib[importPath]; ok {
|
|
return path.Base(importPath) // stdlib packages always match their paths.
|
|
}
|
|
|
|
buildPkg, err := env.buildContext().Import(importPath, srcDir, build.FindOnly)
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
pkgName, err := packageDirToName(buildPkg.Dir)
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
return pkgName
|
|
}
|
|
|
|
// packageDirToName is a faster version of build.Import if
|
|
// the only thing desired is the package name. It uses build.FindOnly
|
|
// to find the directory and then only parses one file in the package,
|
|
// trusting that the files in the directory are consistent.
|
|
func packageDirToName(dir string) (packageName string, err error) {
|
|
d, err := os.Open(dir)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
names, err := d.Readdirnames(-1)
|
|
d.Close()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
sort.Strings(names) // to have predictable behavior
|
|
var lastErr error
|
|
var nfile int
|
|
for _, name := range names {
|
|
if !strings.HasSuffix(name, ".go") {
|
|
continue
|
|
}
|
|
if strings.HasSuffix(name, "_test.go") {
|
|
continue
|
|
}
|
|
nfile++
|
|
fullFile := filepath.Join(dir, name)
|
|
|
|
fset := token.NewFileSet()
|
|
f, err := parser.ParseFile(fset, fullFile, nil, parser.PackageClauseOnly)
|
|
if err != nil {
|
|
lastErr = err
|
|
continue
|
|
}
|
|
pkgName := f.Name.Name
|
|
if pkgName == "documentation" {
|
|
// Special case from go/build.ImportDir, not
|
|
// handled by ctx.MatchFile.
|
|
continue
|
|
}
|
|
if pkgName == "main" {
|
|
// Also skip package main, assuming it's a +build ignore generator or example.
|
|
// Since you can't import a package main anyway, there's no harm here.
|
|
continue
|
|
}
|
|
return pkgName, nil
|
|
}
|
|
if lastErr != nil {
|
|
return "", lastErr
|
|
}
|
|
return "", fmt.Errorf("no importable package found in %d Go files", nfile)
|
|
}
|
|
|
|
type pkg struct {
|
|
goPackage *packages.Package
|
|
dir string // absolute file path to pkg directory ("/usr/lib/go/src/net/http")
|
|
importPathShort string // vendorless import path ("net/http", "a/b")
|
|
}
|
|
|
|
type pkgDistance struct {
|
|
pkg *pkg
|
|
distance int // relative distance to target
|
|
}
|
|
|
|
// byDistanceOrImportPathShortLength sorts by relative distance breaking ties
|
|
// on the short import path length and then the import string itself.
|
|
type byDistanceOrImportPathShortLength []pkgDistance
|
|
|
|
func (s byDistanceOrImportPathShortLength) Len() int { return len(s) }
|
|
func (s byDistanceOrImportPathShortLength) Less(i, j int) bool {
|
|
di, dj := s[i].distance, s[j].distance
|
|
if di == -1 {
|
|
return false
|
|
}
|
|
if dj == -1 {
|
|
return true
|
|
}
|
|
if di != dj {
|
|
return di < dj
|
|
}
|
|
|
|
vi, vj := s[i].pkg.importPathShort, s[j].pkg.importPathShort
|
|
if len(vi) != len(vj) {
|
|
return len(vi) < len(vj)
|
|
}
|
|
return vi < vj
|
|
}
|
|
func (s byDistanceOrImportPathShortLength) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
|
|
|
|
func distance(basepath, targetpath string) int {
|
|
p, err := filepath.Rel(basepath, targetpath)
|
|
if err != nil {
|
|
return -1
|
|
}
|
|
if p == "." {
|
|
return 0
|
|
}
|
|
return strings.Count(p, string(filepath.Separator)) + 1
|
|
}
|
|
|
|
func (r *gopathResolver) scan(_ references) ([]*pkg, error) {
|
|
dupCheck := make(map[string]bool)
|
|
var result []*pkg
|
|
|
|
var mu sync.Mutex
|
|
|
|
add := func(root gopathwalk.Root, dir string) {
|
|
mu.Lock()
|
|
defer mu.Unlock()
|
|
|
|
if _, dup := dupCheck[dir]; dup {
|
|
return
|
|
}
|
|
dupCheck[dir] = true
|
|
importpath := filepath.ToSlash(dir[len(root.Path)+len("/"):])
|
|
result = append(result, &pkg{
|
|
importPathShort: VendorlessPath(importpath),
|
|
dir: dir,
|
|
})
|
|
}
|
|
gopathwalk.Walk(gopathwalk.SrcDirsRoots(r.env.buildContext()), add, gopathwalk.Options{Debug: Debug, ModulesEnabled: false})
|
|
return result, nil
|
|
}
|
|
|
|
// VendorlessPath returns the devendorized version of the import path ipath.
|
|
// For example, VendorlessPath("foo/bar/vendor/a/b") returns "a/b".
|
|
func VendorlessPath(ipath string) string {
|
|
// Devendorize for use in import statement.
|
|
if i := strings.LastIndex(ipath, "/vendor/"); i >= 0 {
|
|
return ipath[i+len("/vendor/"):]
|
|
}
|
|
if strings.HasPrefix(ipath, "vendor/") {
|
|
return ipath[len("vendor/"):]
|
|
}
|
|
return ipath
|
|
}
|
|
|
|
// loadExports returns the set of exported symbols in the package at dir.
|
|
// It returns nil on error or if the package name in dir does not match expectPackage.
|
|
func loadExports(ctx context.Context, env *fixEnv, expectPackage string, pkg *pkg) (map[string]bool, error) {
|
|
if Debug {
|
|
log.Printf("loading exports in dir %s (seeking package %s)", pkg.dir, expectPackage)
|
|
}
|
|
if pkg.goPackage != nil {
|
|
exports := map[string]bool{}
|
|
fset := token.NewFileSet()
|
|
for _, fname := range pkg.goPackage.CompiledGoFiles {
|
|
f, err := parser.ParseFile(fset, fname, nil, 0)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("parsing %s: %v", fname, err)
|
|
}
|
|
for name := range f.Scope.Objects {
|
|
if ast.IsExported(name) {
|
|
exports[name] = true
|
|
}
|
|
}
|
|
}
|
|
return exports, nil
|
|
}
|
|
|
|
exports := make(map[string]bool)
|
|
|
|
// Look for non-test, buildable .go files which could provide exports.
|
|
all, err := ioutil.ReadDir(pkg.dir)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var files []os.FileInfo
|
|
for _, fi := range all {
|
|
name := fi.Name()
|
|
if !strings.HasSuffix(name, ".go") || strings.HasSuffix(name, "_test.go") {
|
|
continue
|
|
}
|
|
match, err := env.buildContext().MatchFile(pkg.dir, fi.Name())
|
|
if err != nil || !match {
|
|
continue
|
|
}
|
|
files = append(files, fi)
|
|
}
|
|
|
|
if len(files) == 0 {
|
|
return nil, fmt.Errorf("dir %v contains no buildable, non-test .go files", pkg.dir)
|
|
}
|
|
|
|
fset := token.NewFileSet()
|
|
for _, fi := range files {
|
|
select {
|
|
case <-ctx.Done():
|
|
return nil, ctx.Err()
|
|
default:
|
|
}
|
|
|
|
fullFile := filepath.Join(pkg.dir, fi.Name())
|
|
f, err := parser.ParseFile(fset, fullFile, nil, 0)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("parsing %s: %v", fullFile, err)
|
|
}
|
|
pkgName := f.Name.Name
|
|
if pkgName == "documentation" {
|
|
// Special case from go/build.ImportDir, not
|
|
// handled by MatchFile above.
|
|
continue
|
|
}
|
|
if pkgName != expectPackage {
|
|
return nil, fmt.Errorf("scan of dir %v is not expected package %v (actually %v)", pkg.dir, expectPackage, pkgName)
|
|
}
|
|
for name := range f.Scope.Objects {
|
|
if ast.IsExported(name) {
|
|
exports[name] = true
|
|
}
|
|
}
|
|
}
|
|
|
|
if Debug {
|
|
exportList := make([]string, 0, len(exports))
|
|
for k := range exports {
|
|
exportList = append(exportList, k)
|
|
}
|
|
sort.Strings(exportList)
|
|
log.Printf("loaded exports in dir %v (package %v): %v", pkg.dir, expectPackage, strings.Join(exportList, ", "))
|
|
}
|
|
return exports, nil
|
|
}
|
|
|
|
// findImport searches for a package with the given symbols.
|
|
// If no package is found, findImport returns ("", false, nil)
|
|
func findImport(ctx context.Context, env *fixEnv, dirScan []*pkg, pkgName string, symbols map[string]bool, filename string) (*pkg, error) {
|
|
pkgDir, err := filepath.Abs(filename)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
pkgDir = filepath.Dir(pkgDir)
|
|
|
|
// Find candidate packages, looking only at their directory names first.
|
|
var candidates []pkgDistance
|
|
for _, pkg := range dirScan {
|
|
if pkgIsCandidate(filename, pkgName, pkg) {
|
|
candidates = append(candidates, pkgDistance{
|
|
pkg: pkg,
|
|
distance: distance(pkgDir, pkg.dir),
|
|
})
|
|
}
|
|
}
|
|
|
|
// Sort the candidates by their import package length,
|
|
// assuming that shorter package names are better than long
|
|
// ones. Note that this sorts by the de-vendored name, so
|
|
// there's no "penalty" for vendoring.
|
|
sort.Sort(byDistanceOrImportPathShortLength(candidates))
|
|
if Debug {
|
|
for i, c := range candidates {
|
|
log.Printf("%s candidate %d/%d: %v in %v", pkgName, i+1, len(candidates), c.pkg.importPathShort, c.pkg.dir)
|
|
}
|
|
}
|
|
|
|
// Collect exports for packages with matching names.
|
|
|
|
rescv := make([]chan *pkg, len(candidates))
|
|
for i := range candidates {
|
|
rescv[i] = make(chan *pkg, 1)
|
|
}
|
|
const maxConcurrentPackageImport = 4
|
|
loadExportsSem := make(chan struct{}, maxConcurrentPackageImport)
|
|
|
|
ctx, cancel := context.WithCancel(ctx)
|
|
var wg sync.WaitGroup
|
|
defer func() {
|
|
cancel()
|
|
wg.Wait()
|
|
}()
|
|
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
for i, c := range candidates {
|
|
select {
|
|
case loadExportsSem <- struct{}{}:
|
|
case <-ctx.Done():
|
|
return
|
|
}
|
|
|
|
wg.Add(1)
|
|
go func(c pkgDistance, resc chan<- *pkg) {
|
|
defer func() {
|
|
<-loadExportsSem
|
|
wg.Done()
|
|
}()
|
|
|
|
exports, err := loadExports(ctx, env, pkgName, c.pkg)
|
|
if err != nil {
|
|
if Debug {
|
|
log.Printf("loading exports in dir %s (seeking package %s): %v", c.pkg.dir, pkgName, err)
|
|
}
|
|
resc <- nil
|
|
return
|
|
}
|
|
|
|
// If it doesn't have the right
|
|
// symbols, send nil to mean no match.
|
|
for symbol := range symbols {
|
|
if !exports[symbol] {
|
|
resc <- nil
|
|
return
|
|
}
|
|
}
|
|
resc <- c.pkg
|
|
}(c, rescv[i])
|
|
}
|
|
}()
|
|
|
|
for _, resc := range rescv {
|
|
pkg := <-resc
|
|
if pkg == nil {
|
|
continue
|
|
}
|
|
return pkg, nil
|
|
}
|
|
return nil, nil
|
|
}
|
|
|
|
// pkgIsCandidate reports whether pkg is a candidate for satisfying the
|
|
// finding which package pkgIdent in the file named by filename is trying
|
|
// to refer to.
|
|
//
|
|
// This check is purely lexical and is meant to be as fast as possible
|
|
// because it's run over all $GOPATH directories to filter out poor
|
|
// candidates in order to limit the CPU and I/O later parsing the
|
|
// exports in candidate packages.
|
|
//
|
|
// filename is the file being formatted.
|
|
// pkgIdent is the package being searched for, like "client" (if
|
|
// searching for "client.New")
|
|
func pkgIsCandidate(filename, pkgIdent string, pkg *pkg) bool {
|
|
// Check "internal" and "vendor" visibility:
|
|
if !canUse(filename, pkg.dir) {
|
|
return false
|
|
}
|
|
|
|
// Speed optimization to minimize disk I/O:
|
|
// the last two components on disk must contain the
|
|
// package name somewhere.
|
|
//
|
|
// This permits mismatch naming like directory
|
|
// "go-foo" being package "foo", or "pkg.v3" being "pkg",
|
|
// or directory "google.golang.org/api/cloudbilling/v1"
|
|
// being package "cloudbilling", but doesn't
|
|
// permit a directory "foo" to be package
|
|
// "bar", which is strongly discouraged
|
|
// anyway. There's no reason goimports needs
|
|
// to be slow just to accommodate that.
|
|
lastTwo := lastTwoComponents(pkg.importPathShort)
|
|
if strings.Contains(lastTwo, pkgIdent) {
|
|
return true
|
|
}
|
|
if hasHyphenOrUpperASCII(lastTwo) && !hasHyphenOrUpperASCII(pkgIdent) {
|
|
lastTwo = lowerASCIIAndRemoveHyphen(lastTwo)
|
|
if strings.Contains(lastTwo, pkgIdent) {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
func hasHyphenOrUpperASCII(s string) bool {
|
|
for i := 0; i < len(s); i++ {
|
|
b := s[i]
|
|
if b == '-' || ('A' <= b && b <= 'Z') {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func lowerASCIIAndRemoveHyphen(s string) (ret string) {
|
|
buf := make([]byte, 0, len(s))
|
|
for i := 0; i < len(s); i++ {
|
|
b := s[i]
|
|
switch {
|
|
case b == '-':
|
|
continue
|
|
case 'A' <= b && b <= 'Z':
|
|
buf = append(buf, b+('a'-'A'))
|
|
default:
|
|
buf = append(buf, b)
|
|
}
|
|
}
|
|
return string(buf)
|
|
}
|
|
|
|
// canUse reports whether the package in dir is usable from filename,
|
|
// respecting the Go "internal" and "vendor" visibility rules.
|
|
func canUse(filename, dir string) bool {
|
|
// Fast path check, before any allocations. If it doesn't contain vendor
|
|
// or internal, it's not tricky:
|
|
// Note that this can false-negative on directories like "notinternal",
|
|
// but we check it correctly below. This is just a fast path.
|
|
if !strings.Contains(dir, "vendor") && !strings.Contains(dir, "internal") {
|
|
return true
|
|
}
|
|
|
|
dirSlash := filepath.ToSlash(dir)
|
|
if !strings.Contains(dirSlash, "/vendor/") && !strings.Contains(dirSlash, "/internal/") && !strings.HasSuffix(dirSlash, "/internal") {
|
|
return true
|
|
}
|
|
// Vendor or internal directory only visible from children of parent.
|
|
// That means the path from the current directory to the target directory
|
|
// can contain ../vendor or ../internal but not ../foo/vendor or ../foo/internal
|
|
// or bar/vendor or bar/internal.
|
|
// After stripping all the leading ../, the only okay place to see vendor or internal
|
|
// is at the very beginning of the path.
|
|
absfile, err := filepath.Abs(filename)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
absdir, err := filepath.Abs(dir)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
rel, err := filepath.Rel(absfile, absdir)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
relSlash := filepath.ToSlash(rel)
|
|
if i := strings.LastIndex(relSlash, "../"); i >= 0 {
|
|
relSlash = relSlash[i+len("../"):]
|
|
}
|
|
return !strings.Contains(relSlash, "/vendor/") && !strings.Contains(relSlash, "/internal/") && !strings.HasSuffix(relSlash, "/internal")
|
|
}
|
|
|
|
// lastTwoComponents returns at most the last two path components
|
|
// of v, using either / or \ as the path separator.
|
|
func lastTwoComponents(v string) string {
|
|
nslash := 0
|
|
for i := len(v) - 1; i >= 0; i-- {
|
|
if v[i] == '/' || v[i] == '\\' {
|
|
nslash++
|
|
if nslash == 2 {
|
|
return v[i:]
|
|
}
|
|
}
|
|
}
|
|
return v
|
|
}
|
|
|
|
type visitFn func(node ast.Node) ast.Visitor
|
|
|
|
func (fn visitFn) Visit(node ast.Node) ast.Visitor {
|
|
return fn(node)
|
|
}
|