internal/imports: provide export completions for unimported packages

Add a function that returns all the exported identifiers associated with
a package name that doesn't have an import yet. This will allow
completions like rand<> to return rand.Seed (from math/rand) and
rand.Prime (from crypto/rand).

Updates golang/go#31906

Change-Id: Iee290c786de263d42acbfabd76bf0edbf303afc9
Reviewed-on: https://go-review.googlesource.com/c/tools/+/204204
Run-TryBot: Heschi Kreinick <heschi@google.com>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Rebecca Stambler <rstambler@golang.org>
This commit is contained in:
Heschi Kreinick 2019-10-30 19:30:14 -04:00
parent 979d74e0bb
commit 8266eea4ea
3 changed files with 142 additions and 31 deletions

View File

@ -585,49 +585,39 @@ func getFixes(fset *token.FileSet, f *ast.File, filename string, env *ProcessEnv
return fixes, nil return fixes, nil
} }
// getAllCandidates gets all of the candidates to be imported, regardless of if they are needed. // getCandidatePkgs returns the list of pkgs that are accessible from filename,
func getAllCandidates(filename string, env *ProcessEnv) ([]ImportFix, error) { // optionall filtered to only packages named pkgName.
func getCandidatePkgs(pkgName, filename string, env *ProcessEnv) ([]*pkg, error) {
// TODO(heschi): filter out current package. (Don't forget x_test can import x.) // TODO(heschi): filter out current package. (Don't forget x_test can import x.)
var result []*pkg
// Start off with the standard library. // Start off with the standard library.
var imports []ImportFix
for importPath := range stdlib { for importPath := range stdlib {
imports = append(imports, ImportFix{ if pkgName != "" && path.Base(importPath) != pkgName {
StmtInfo: ImportInfo{ continue
ImportPath: importPath, }
}, result = append(result, &pkg{
IdentName: path.Base(importPath), dir: filepath.Join(env.GOROOT, "src", importPath),
FixType: AddImport, importPathShort: importPath,
packageName: path.Base(importPath),
relevance: 0,
}) })
} }
// Sort the stdlib bits solely by name.
sort.Slice(imports, func(i int, j int) bool {
return imports[i].StmtInfo.ImportPath < imports[j].StmtInfo.ImportPath
})
// Exclude goroot results -- getting them is relatively expensive, not cached, // Exclude goroot results -- getting them is relatively expensive, not cached,
// and generally redundant with the in-memory version. // and generally redundant with the in-memory version.
exclude := []gopathwalk.RootType{gopathwalk.RootGOROOT} exclude := []gopathwalk.RootType{gopathwalk.RootGOROOT}
// Only the go/packages resolver uses the first argument, and nobody uses that resolver. // Only the go/packages resolver uses the first argument, and nobody uses that resolver.
pkgs, err := env.GetResolver().scan(nil, true, exclude) scannedPkgs, err := env.GetResolver().scan(nil, true, exclude)
if err != nil { if err != nil {
return nil, err return nil, err
} }
// Sort first by relevance, then by name, so that when we add them they're
// still in order.
sort.Slice(pkgs, func(i, j int) bool {
pi, pj := pkgs[i], pkgs[j]
if pi.relevance < pj.relevance {
return true
}
if pi.relevance > pj.relevance {
return false
}
return pi.packageName < pj.packageName
})
dupCheck := map[string]struct{}{} dupCheck := map[string]struct{}{}
for _, pkg := range pkgs { for _, pkg := range scannedPkgs {
if pkgName != "" && pkg.packageName != pkgName {
continue
}
if !canUse(filename, pkg.dir) { if !canUse(filename, pkg.dir) {
continue continue
} }
@ -635,7 +625,33 @@ func getAllCandidates(filename string, env *ProcessEnv) ([]ImportFix, error) {
continue continue
} }
dupCheck[pkg.importPathShort] = struct{}{} dupCheck[pkg.importPathShort] = struct{}{}
imports = append(imports, ImportFix{ result = append(result, pkg)
}
// Sort first by relevance, then by package name, with import path as a tiebreaker.
sort.Slice(result, func(i, j int) bool {
pi, pj := result[i], result[j]
if pi.relevance != pj.relevance {
return pi.relevance < pj.relevance
}
if pi.packageName != pj.packageName {
return pi.packageName < pj.packageName
}
return pi.importPathShort < pj.importPathShort
})
return result, nil
}
// getAllCandidates gets all of the candidates to be imported, regardless of if they are needed.
func getAllCandidates(filename string, env *ProcessEnv) ([]ImportFix, error) {
pkgs, err := getCandidatePkgs("", filename, env)
if err != nil {
return nil, err
}
result := make([]ImportFix, 0, len(pkgs))
for _, pkg := range pkgs {
result = append(result, ImportFix{
StmtInfo: ImportInfo{ StmtInfo: ImportInfo{
ImportPath: pkg.importPathShort, ImportPath: pkg.importPathShort,
}, },
@ -643,7 +659,54 @@ func getAllCandidates(filename string, env *ProcessEnv) ([]ImportFix, error) {
FixType: AddImport, FixType: AddImport,
}) })
} }
return imports, nil return result, nil
}
// A PackageExport is a package and its exports.
type PackageExport struct {
Fix *ImportFix
Exports []string
}
func getPackageExports(completePackage, filename string, env *ProcessEnv) ([]PackageExport, error) {
pkgs, err := getCandidatePkgs(completePackage, filename, env)
if err != nil {
return nil, err
}
results := make([]PackageExport, 0, len(pkgs))
for _, pkg := range pkgs {
fix := &ImportFix{
StmtInfo: ImportInfo{
ImportPath: pkg.importPathShort,
},
IdentName: pkg.packageName,
FixType: AddImport,
}
var exportsMap map[string]bool
if e, ok := stdlib[pkg.importPathShort]; ok {
exportsMap = e
} else {
exportsMap, err = env.GetResolver().loadExports(context.TODO(), completePackage, pkg)
if err != nil {
if env.Debug {
env.Logf("while completing %q, error loading exports from %q: %v", completePackage, pkg.importPathShort, err)
}
continue
}
}
var exports []string
for export := range exportsMap {
exports = append(exports, export)
}
sort.Strings(exports)
results = append(results, PackageExport{
Fix: fix,
Exports: exports,
})
}
return results, nil
} }
// ProcessEnv contains environment variables and settings that affect the use of // ProcessEnv contains environment variables and settings that affect the use of

View File

@ -2522,9 +2522,9 @@ func TestGetCandidates(t *testing.T) {
} }
want := []res{ want := []res{
{"bytes", "bytes"}, {"bytes", "bytes"},
{"http", "net/http"},
{"rand", "crypto/rand"}, {"rand", "crypto/rand"},
{"rand", "math/rand"}, {"rand", "math/rand"},
{"http", "net/http"},
{"bar", "bar.com/bar"}, {"bar", "bar.com/bar"},
{"foo", "foo.com/foo"}, {"foo", "foo.com/foo"},
} }
@ -2560,6 +2560,45 @@ func TestGetCandidates(t *testing.T) {
}) })
} }
func TestGetPackageCompletions(t *testing.T) {
type res struct {
name, path, symbol string
}
want := []res{
{"rand", "crypto/rand", "Prime"},
{"rand", "math/rand", "Seed"},
{"rand", "bar.com/rand", "Bar"},
}
testConfig{
modules: []packagestest.Module{
{
Name: "bar.com",
Files: fm{"rand/bar.go": "package rand\nvar Bar int\n"},
},
},
goPackagesIncompatible: true, // getPackageCompletions doesn't support the go/packages resolver.
}.test(t, func(t *goimportTest) {
candidates, err := getPackageExports("rand", "x.go", t.env)
if err != nil {
t.Fatalf("getPackageCompletions() = %v", err)
}
var got []res
for _, c := range candidates {
for _, csym := range c.Exports {
for _, w := range want {
if c.Fix.StmtInfo.ImportPath == w.path && csym == w.symbol {
got = append(got, res{c.Fix.IdentName, c.Fix.StmtInfo.ImportPath, csym})
}
}
}
}
if !reflect.DeepEqual(want, got) {
t.Errorf("wanted stdlib results in order %v, got %v", want, got)
}
})
}
// Tests #34895: process should not panic on concurrent calls. // Tests #34895: process should not panic on concurrent calls.
func TestConcurrentProcess(t *testing.T) { func TestConcurrentProcess(t *testing.T) {
testConfig{ testConfig{

View File

@ -105,13 +105,22 @@ func ApplyFixes(fixes []*ImportFix, filename string, src []byte, opt *Options) (
// GetAllCandidates gets all of the standard library candidate packages to import in // GetAllCandidates gets all of the standard library candidate packages to import in
// sorted order on import path. // sorted order on import path.
func GetAllCandidates(filename string, opt *Options) (pkgs []ImportFix, err error) { func GetAllCandidates(filename string, opt *Options) (pkgs []ImportFix, err error) {
_, opt, err = initialize(filename, []byte{}, opt) _, opt, err = initialize(filename, nil, opt)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return getAllCandidates(filename, opt.Env) return getAllCandidates(filename, opt.Env)
} }
// GetPackageExports returns all known packages with name pkg and their exports.
func GetPackageExports(pkg, filename string, opt *Options) (exports []PackageExport, err error) {
_, opt, err = initialize(filename, nil, opt)
if err != nil {
return nil, err
}
return getPackageExports(pkg, filename, opt.Env)
}
// initialize sets the values for opt and src. // initialize sets the values for opt and src.
// If they are provided, they are not changed. Otherwise opt is set to the // If they are provided, they are not changed. Otherwise opt is set to the
// default values and src is read from the file system. // default values and src is read from the file system.