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>
diff --git a/internal/imports/fix.go b/internal/imports/fix.go
index 72b43bd..fb70790 100644
--- a/internal/imports/fix.go
+++ b/internal/imports/fix.go
@@ -585,49 +585,39 @@
 	return fixes, nil
 }
 
-// getAllCandidates gets all of the candidates to be imported, regardless of if they are needed.
-func getAllCandidates(filename string, env *ProcessEnv) ([]ImportFix, error) {
+// getCandidatePkgs returns the list of pkgs that are accessible from filename,
+// 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.)
 
+	var result []*pkg
 	// Start off with the standard library.
-	var imports []ImportFix
 	for importPath := range stdlib {
-		imports = append(imports, ImportFix{
-			StmtInfo: ImportInfo{
-				ImportPath: importPath,
-			},
-			IdentName: path.Base(importPath),
-			FixType:   AddImport,
+		if pkgName != "" && path.Base(importPath) != pkgName {
+			continue
+		}
+		result = append(result, &pkg{
+			dir:             filepath.Join(env.GOROOT, "src", importPath),
+			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,
 	// and generally redundant with the in-memory version.
 	exclude := []gopathwalk.RootType{gopathwalk.RootGOROOT}
 	// 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 {
 		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{}{}
-	for _, pkg := range pkgs {
+	for _, pkg := range scannedPkgs {
+		if pkgName != "" && pkg.packageName != pkgName {
+			continue
+		}
 		if !canUse(filename, pkg.dir) {
 			continue
 		}
@@ -635,7 +625,33 @@
 			continue
 		}
 		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{
 				ImportPath: pkg.importPathShort,
 			},
@@ -643,7 +659,54 @@
 			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
diff --git a/internal/imports/fix_test.go b/internal/imports/fix_test.go
index a29fc6e..f58cc3a 100644
--- a/internal/imports/fix_test.go
+++ b/internal/imports/fix_test.go
@@ -2522,9 +2522,9 @@
 	}
 	want := []res{
 		{"bytes", "bytes"},
+		{"http", "net/http"},
 		{"rand", "crypto/rand"},
 		{"rand", "math/rand"},
-		{"http", "net/http"},
 		{"bar", "bar.com/bar"},
 		{"foo", "foo.com/foo"},
 	}
@@ -2560,6 +2560,45 @@
 	})
 }
 
+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.
 func TestConcurrentProcess(t *testing.T) {
 	testConfig{
diff --git a/internal/imports/imports.go b/internal/imports/imports.go
index 2c074cb..ed3867b 100644
--- a/internal/imports/imports.go
+++ b/internal/imports/imports.go
@@ -105,13 +105,22 @@
 // GetAllCandidates gets all of the standard library candidate packages to import in
 // sorted order on import path.
 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 {
 		return nil, err
 	}
 	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.
 // If they are provided, they are not changed. Otherwise opt is set to the
 // default values and src is read from the file system.