gopls/internal/golang/completion: honor std symbol versions (imported)

This change is the counterpart to CL 569435 for completions
in imported packages.

Updates golang/go#46136

Change-Id: I57011897c395d37a89a8e3a99e8c3511de017ad3
Reviewed-on: https://go-review.googlesource.com/c/tools/+/569796
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Robert Findley <rfindley@google.com>
diff --git a/gopls/internal/analysis/stdversion/stdversion.go b/gopls/internal/analysis/stdversion/stdversion.go
index 5166837..3e165ff 100644
--- a/gopls/internal/analysis/stdversion/stdversion.go
+++ b/gopls/internal/analysis/stdversion/stdversion.go
@@ -60,7 +60,7 @@
 		k := key{pkg, version}
 		disallowed, ok := memo[k]
 		if !ok {
-			disallowed = disallowedSymbols(pkg, version)
+			disallowed = DisallowedSymbols(pkg, version)
 			memo[k] = disallowed
 		}
 		return disallowed
@@ -107,10 +107,12 @@
 	return nil, nil
 }
 
-// disallowedSymbols computes the set of package-level symbols
-// exported by direct imports of pkg that are not available at the
-// specified version. The result maps each symbol to its minimum version.
-func disallowedSymbols(pkg *types.Package, version string) map[types.Object]string {
+// DisallowedSymbols computes the set of package-level symbols
+// exported by pkg that are not available at the specified version.
+// The result maps each symbol to its minimum version.
+//
+// (It is exported for use in gopls' completion.)
+func DisallowedSymbols(pkg *types.Package, version string) map[types.Object]string {
 	disallowed := make(map[types.Object]string)
 
 	// Pass 1: package-level symbols.
diff --git a/gopls/internal/golang/completion/completion.go b/gopls/internal/golang/completion/completion.go
index 6f3b7c1..fe66afa 100644
--- a/gopls/internal/golang/completion/completion.go
+++ b/gopls/internal/golang/completion/completion.go
@@ -10,6 +10,7 @@
 	"context"
 	"fmt"
 	"go/ast"
+	"go/build"
 	"go/constant"
 	"go/parser"
 	"go/printer"
@@ -28,6 +29,7 @@
 
 	"golang.org/x/sync/errgroup"
 	"golang.org/x/tools/go/ast/astutil"
+	"golang.org/x/tools/gopls/internal/analysis/stdversion"
 	"golang.org/x/tools/gopls/internal/cache"
 	"golang.org/x/tools/gopls/internal/cache/metadata"
 	"golang.org/x/tools/gopls/internal/file"
@@ -37,6 +39,7 @@
 	"golang.org/x/tools/gopls/internal/settings"
 	goplsastutil "golang.org/x/tools/gopls/internal/util/astutil"
 	"golang.org/x/tools/gopls/internal/util/safetoken"
+	"golang.org/x/tools/gopls/internal/util/slices"
 	"golang.org/x/tools/gopls/internal/util/typesutil"
 	"golang.org/x/tools/internal/aliases"
 	"golang.org/x/tools/internal/event"
@@ -194,6 +197,11 @@
 	// file is the AST of the file associated with this completion request.
 	file *ast.File
 
+	// goversion is the version of Go in force in the file, as
+	// defined by x/tools/internal/versions. Empty if unknown.
+	// TODO(adonovan): with go1.22+ it should always be known.
+	goversion string
+
 	// (tokFile, pos) is the position at which the request was triggered.
 	tokFile *token.File
 	pos     token.Pos
@@ -238,6 +246,13 @@
 	// for deep completions.
 	methodSetCache map[methodSetKey]*types.MethodSet
 
+	// tooNewSymbolsCache is a cache of
+	// [stdversion.DisallowedSymbols], recording for each std
+	// package which of its exported symbols are too new for
+	// the version of Go in force in the completion file.
+	// (The value is the minimum version in the form "go1.%d".)
+	tooNewSymbolsCache map[*types.Package]map[types.Object]string
+
 	// mapper converts the positions in the file from which the completion originated.
 	mapper *protocol.Mapper
 
@@ -257,6 +272,21 @@
 	scopes []*types.Scope
 }
 
+// tooNew reports whether obj is a standard library symbol that is too
+// new for the specified Go version.
+func (c *completer) tooNew(obj types.Object) bool {
+	pkg := obj.Pkg()
+	if pkg == nil {
+		return false // unsafe.Pointer or error.Error
+	}
+	disallowed, ok := c.tooNewSymbolsCache[pkg]
+	if !ok {
+		disallowed = stdversion.DisallowedSymbols(pkg, c.goversion)
+		c.tooNewSymbolsCache[pkg] = disallowed
+	}
+	return disallowed[obj] != ""
+}
+
 // funcInfo holds info about a function object.
 type funcInfo struct {
 	// sig is the function declaration enclosing the position.
@@ -530,6 +560,12 @@
 	scopes := golang.CollectScopes(pkg.GetTypesInfo(), path, pos)
 	scopes = append(scopes, pkg.GetTypes().Scope(), types.Universe)
 
+	var goversion string // "" => no version check
+	// Prior go1.22, the behavior of FileVersion is not useful to us.
+	if slices.Contains(build.Default.ReleaseTags, "go1.22") {
+		goversion = versions.FileVersion(pkg.GetTypesInfo(), pgf.File) // may be ""
+	}
+
 	opts := snapshot.Options()
 	c := &completer{
 		pkg:      pkg,
@@ -544,6 +580,7 @@
 		filename:                  fh.URI().Path(),
 		tokFile:                   pgf.Tok,
 		file:                      pgf.File,
+		goversion:                 goversion,
 		path:                      path,
 		pos:                       pos,
 		seen:                      make(map[types.Object]bool),
@@ -564,11 +601,12 @@
 			completeFunctionCalls: opts.CompleteFunctionCalls,
 		},
 		// default to a matcher that always matches
-		matcher:        prefixMatcher(""),
-		methodSetCache: make(map[methodSetKey]*types.MethodSet),
-		mapper:         pgf.Mapper,
-		startTime:      startTime,
-		scopes:         scopes,
+		matcher:            prefixMatcher(""),
+		methodSetCache:     make(map[methodSetKey]*types.MethodSet),
+		tooNewSymbolsCache: make(map[*types.Package]map[types.Object]string),
+		mapper:             pgf.Mapper,
+		startTime:          startTime,
+		scopes:             scopes,
 	}
 
 	ctx, cancel := context.WithCancel(ctx)
@@ -1469,6 +1507,9 @@
 	scope := pkg.Scope()
 	for _, name := range scope.Names() {
 		obj := scope.Lookup(name)
+		if c.tooNew(obj) {
+			continue // std symbol too new for file's Go version
+		}
 		cb(candidate{
 			obj:         obj,
 			score:       score,
@@ -1506,6 +1547,11 @@
 	}
 
 	for i := 0; i < mset.Len(); i++ {
+		obj := mset.At(i).Obj()
+		// to the other side of the cb() queue?
+		if c.tooNew(obj) {
+			continue // std method too new for file's Go version
+		}
 		cb(candidate{
 			obj:         mset.At(i).Obj(),
 			score:       stdScore,
@@ -1516,6 +1562,9 @@
 
 	// Add fields of T.
 	eachField(typ, func(v *types.Var) {
+		if c.tooNew(v) {
+			return // std field too new for file's Go version
+		}
 		cb(candidate{
 			obj:         v,
 			score:       stdScore - 0.01,
diff --git a/gopls/internal/test/marker/testdata/completion/imported-std.txt b/gopls/internal/test/marker/testdata/completion/imported-std.txt
new file mode 100644
index 0000000..436bbba
--- /dev/null
+++ b/gopls/internal/test/marker/testdata/completion/imported-std.txt
@@ -0,0 +1,65 @@
+Test of imported completions respecting the effective Go version of the file.
+
+(See "un-" prefixed file for same test of unimported completions.)
+
+These symbols below were introduced in go1.20:
+
+  types.Satisfied
+  ast.File.FileStart
+  (*token.FileSet).RemoveFile
+
+The underlying logic depends on versions.FileVersion, which only
+behaves correctly in go1.22. (When go1.22 is assured, we can remove
+the min_go flag but leave the test inputs unchanged.)
+
+-- flags --
+-ignore_extra_diags -min_go=go1.22
+
+-- go.mod --
+module example.com
+
+go 1.19
+
+-- a/a.go --
+package a
+
+import "go/ast"
+import "go/token"
+import "go/types"
+
+// package-level func
+var _ = types.Imple //@rankl("Imple", "Implements")
+var _ = types.Satis //@rankl("Satis", "!Satisfies")
+
+// (Apparently we don't even offer completions of methods
+// of types from unimported packages, so the fact that
+// we don't implement std version filtering isn't evident.)
+
+// field
+var _ = new(ast.File).Packa //@rankl("Packa", "Package")
+var _ = new(ast.File).FileS //@rankl("FileS", "!FileStart")
+
+// method
+var _ = new(token.FileSet).Add //@rankl("Add", "AddFile")
+var _ = new(token.FileSet).Remove //@rankl("Remove", "!RemoveFile")
+
+-- b/b.go --
+//go:build go1.20
+
+package a
+
+import "go/ast"
+import "go/token"
+import "go/types"
+
+// package-level func
+var _ = types.Imple //@rankl("Imple", "Implements")
+var _ = types.Satis //@rankl("Satis", "Satisfies")
+
+// field
+var _ = new(ast.File).Packa //@rankl("Packa", "Package")
+var _ = new(ast.File).FileS //@rankl("FileS", "FileStart")
+
+// method
+var _ = new(token.FileSet).Add //@rankl("Add", "AddFile")
+var _ = new(token.FileSet).Remove //@rankl("Remove", "RemoveFile")
diff --git a/gopls/internal/test/marker/testdata/completion/unimported-std.txt b/gopls/internal/test/marker/testdata/completion/unimported-std.txt
index b99e85a..ce07546 100644
--- a/gopls/internal/test/marker/testdata/completion/unimported-std.txt
+++ b/gopls/internal/test/marker/testdata/completion/unimported-std.txt
@@ -1,5 +1,7 @@
 Test of unimported completions respecting the effective Go version of the file.
 
+(See unprefixed file for same test of imported completions.)
+
 These symbols below were introduced in go1.20:
 
   types.Satisfied