internal/versions: updates the meaning of FileVersions.

For Go >=1.22, FileVersions returns an unknown [invalid] Future version
when the file versions would be invalid. This better matches
go/types.

For Go <=1.21, FileVersions returns either the runtime.Version()
or the Future version.

Adds AtLeast and Before for doing comparisons with Future.

Updates golang/go#65612
Fixes golang/go#66007

Change-Id: I93ff1681b0f9117765614a20d82642749597307c
Reviewed-on: https://go-review.googlesource.com/c/tools/+/567635
Reviewed-by: Robert Findley <rfindley@google.com>
TryBot-Result: Gopher Robot <gobot@golang.org>
Run-TryBot: Tim King <taking@google.com>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
diff --git a/go/analysis/passes/loopclosure/loopclosure.go b/go/analysis/passes/loopclosure/loopclosure.go
index 7b78939..fe05eda 100644
--- a/go/analysis/passes/loopclosure/loopclosure.go
+++ b/go/analysis/passes/loopclosure/loopclosure.go
@@ -55,9 +55,8 @@
 		switch n := n.(type) {
 		case *ast.File:
 			// Only traverse the file if its goversion is strictly before go1.22.
-			goversion := versions.Lang(versions.FileVersions(pass.TypesInfo, n))
-			// goversion is empty for older go versions (or the version is invalid).
-			return goversion == "" || versions.Compare(goversion, "go1.22") < 0
+			goversion := versions.FileVersion(pass.TypesInfo, n)
+			return versions.Before(goversion, versions.Go1_22)
 		case *ast.RangeStmt:
 			body = n.Body
 			addVar(n.Key)
diff --git a/go/analysis/passes/loopclosure/loopclosure_test.go b/go/analysis/passes/loopclosure/loopclosure_test.go
index 386f532..c8aab48 100644
--- a/go/analysis/passes/loopclosure/loopclosure_test.go
+++ b/go/analysis/passes/loopclosure/loopclosure_test.go
@@ -17,6 +17,9 @@
 )
 
 func Test(t *testing.T) {
+	// legacy loopclosure test expectations are incorrect > 1.21.
+	testenv.SkipAfterGo1Point(t, 21)
+
 	testdata := analysistest.TestData()
 	analysistest.Run(t, testdata, loopclosure.Analyzer,
 		"a", "golang.org/...", "subtests", "typeparams")
diff --git a/go/analysis/passes/loopclosure/testdata/src/a/a.go b/go/analysis/passes/loopclosure/testdata/src/a/a.go
index 7a7f05f..eb4d2a6 100644
--- a/go/analysis/passes/loopclosure/testdata/src/a/a.go
+++ b/go/analysis/passes/loopclosure/testdata/src/a/a.go
@@ -2,7 +2,8 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
-// This file contains tests for the loopclosure checker.
+// This file contains legacy tests for the loopclosure checker.
+// Legacy expectations are incorrect after go1.22.
 
 package testdata
 
diff --git a/go/analysis/passes/loopclosure/testdata/src/subtests/subtest.go b/go/analysis/passes/loopclosure/testdata/src/subtests/subtest.go
index 50283ec..faf9838 100644
--- a/go/analysis/passes/loopclosure/testdata/src/subtests/subtest.go
+++ b/go/analysis/passes/loopclosure/testdata/src/subtests/subtest.go
@@ -2,8 +2,9 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
-// This file contains tests that the loopclosure analyzer detects leaked
+// This file contains legacy tests that the loopclosure analyzer detects leaked
 // references via parallel subtests.
+// Legacy expectations are incorrect after go1.22.
 
 package subtests
 
diff --git a/go/analysis/passes/loopclosure/testdata/src/typeparams/typeparams.go b/go/analysis/passes/loopclosure/testdata/src/typeparams/typeparams.go
index 55e129c..ef5b143 100644
--- a/go/analysis/passes/loopclosure/testdata/src/typeparams/typeparams.go
+++ b/go/analysis/passes/loopclosure/testdata/src/typeparams/typeparams.go
@@ -2,7 +2,8 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
-// This file contains tests for the loopclosure checker.
+// This file contains legacy tests for the loopclosure checker for GoVersion <go1.22.
+// Expectations are incorrect after go1.22.
 
 //go:build go1.18
 
@@ -45,16 +46,16 @@
 	a P
 }
 
-func (t T[P]) Go(func() error) { }
+func (t T[P]) Go(func() error) {}
 
 func _(g T[errgroup.Group]) {
 	var s []int
 	for i, v := range s {
 		// "T.a" is method "(*...errgroup.Group).Go".
 		g.a.Go(func() error {
-			print(i)  // want "loop variable i captured by func literal"
-			print(v)  // want "loop variable v captured by func literal"
+			print(i) // want "loop variable i captured by func literal"
+			print(v) // want "loop variable v captured by func literal"
 			return nil
 		})
 	}
-}
\ No newline at end of file
+}
diff --git a/go/ssa/builder.go b/go/ssa/builder.go
index 9588406..72e906c 100644
--- a/go/ssa/builder.go
+++ b/go/ssa/builder.go
@@ -1747,8 +1747,7 @@
 	// Use forStmtGo122 instead if it applies.
 	if s.Init != nil {
 		if assign, ok := s.Init.(*ast.AssignStmt); ok && assign.Tok == token.DEFINE {
-			afterGo122 := versions.Compare(fn.goversion, "go1.21") > 0
-			if afterGo122 {
+			if versions.AtLeast(fn.goversion, versions.Go1_22) {
 				b.forStmtGo122(fn, s, label)
 				return
 			}
@@ -2243,7 +2242,7 @@
 		}
 	}
 
-	afterGo122 := versions.Compare(fn.goversion, "go1.21") > 0
+	afterGo122 := versions.AtLeast(fn.goversion, versions.Go1_22)
 	if s.Tok == token.DEFINE && !afterGo122 {
 		// pre-go1.22: If iteration variables are defined (:=), this
 		// occurs once outside the loop.
diff --git a/go/ssa/create.go b/go/ssa/create.go
index 4545c17..f8f584a 100644
--- a/go/ssa/create.go
+++ b/go/ssa/create.go
@@ -245,7 +245,7 @@
 	if len(files) > 0 {
 		// Go source package.
 		for _, file := range files {
-			goversion := versions.Lang(versions.FileVersions(p.info, file))
+			goversion := versions.Lang(versions.FileVersion(p.info, file))
 			for _, decl := range file.Decls {
 				membersFromDecl(p, decl, goversion)
 			}
diff --git a/internal/versions/features.go b/internal/versions/features.go
new file mode 100644
index 0000000..b53f178
--- /dev/null
+++ b/internal/versions/features.go
@@ -0,0 +1,43 @@
+// Copyright 2023 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 versions
+
+// This file contains predicates for working with file versions to
+// decide when a tool should consider a language feature enabled.
+
+// GoVersions that features in x/tools can be gated to.
+const (
+	Go1_18 = "go1.18"
+	Go1_19 = "go1.19"
+	Go1_20 = "go1.20"
+	Go1_21 = "go1.21"
+	Go1_22 = "go1.22"
+)
+
+// Future is an invalid unknown Go version sometime in the future.
+// Do not use directly with Compare.
+const Future = ""
+
+// AtLeast reports whether the file version v comes after a Go release.
+//
+// Use this predicate to enable a behavior once a certain Go release
+// has happened (and stays enabled in the future).
+func AtLeast(v, release string) bool {
+	if v == Future {
+		return true // an unknown future version is always after y.
+	}
+	return Compare(Lang(v), Lang(release)) >= 0
+}
+
+// Before reports whether the file version v is strictly before a Go release.
+//
+// Use this predicate to disable a behavior once a certain Go release
+// has happened (and stays enabled in the future).
+func Before(v, release string) bool {
+	if v == Future {
+		return false // an unknown future version happens after y.
+	}
+	return Compare(Lang(v), Lang(release)) < 0
+}
diff --git a/internal/versions/toolchain.go b/internal/versions/toolchain.go
new file mode 100644
index 0000000..377bf7a
--- /dev/null
+++ b/internal/versions/toolchain.go
@@ -0,0 +1,14 @@
+// Copyright 2024 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 versions
+
+// toolchain is maximum version (<1.22) that the go toolchain used
+// to build the current tool is known to support.
+//
+// When a tool is built with >=1.22, the value of toolchain is unused.
+//
+// x/tools does not support building with go <1.18. So we take this
+// as the minimum possible maximum.
+var toolchain string = Go1_18
diff --git a/internal/versions/toolchain_go119.go b/internal/versions/toolchain_go119.go
new file mode 100644
index 0000000..f65beed
--- /dev/null
+++ b/internal/versions/toolchain_go119.go
@@ -0,0 +1,14 @@
+// Copyright 2024 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.
+
+//go:build go1.19
+// +build go1.19
+
+package versions
+
+func init() {
+	if Compare(toolchain, Go1_19) < 0 {
+		toolchain = Go1_19
+	}
+}
diff --git a/internal/versions/toolchain_go120.go b/internal/versions/toolchain_go120.go
new file mode 100644
index 0000000..1a9efa1
--- /dev/null
+++ b/internal/versions/toolchain_go120.go
@@ -0,0 +1,14 @@
+// Copyright 2024 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.
+
+//go:build go1.20
+// +build go1.20
+
+package versions
+
+func init() {
+	if Compare(toolchain, Go1_20) < 0 {
+		toolchain = Go1_20
+	}
+}
diff --git a/internal/versions/toolchain_go121.go b/internal/versions/toolchain_go121.go
new file mode 100644
index 0000000..b7ef216
--- /dev/null
+++ b/internal/versions/toolchain_go121.go
@@ -0,0 +1,14 @@
+// Copyright 2024 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.
+
+//go:build go1.21
+// +build go1.21
+
+package versions
+
+func init() {
+	if Compare(toolchain, Go1_21) < 0 {
+		toolchain = Go1_21
+	}
+}
diff --git a/internal/versions/types_go121.go b/internal/versions/types_go121.go
index a7b7920..b4345d3 100644
--- a/internal/versions/types_go121.go
+++ b/internal/versions/types_go121.go
@@ -12,9 +12,19 @@
 	"go/types"
 )
 
-// FileVersions always reports the a file's Go version as the
-// zero version at this Go version.
-func FileVersions(info *types.Info, file *ast.File) string { return "" }
+// FileVersion returns a language version (<=1.21) derived from runtime.Version()
+// or an unknown future version.
+func FileVersion(info *types.Info, file *ast.File) string {
+	// In x/tools built with Go <= 1.21, we do not have Info.FileVersions
+	// available. We use a go version derived from the toolchain used to
+	// compile the tool by default.
+	// This will be <= go1.21. We take this as the maximum version that
+	// this tool can support.
+	//
+	// There are no features currently in x/tools that need to tell fine grained
+	// differences for versions <1.22.
+	return toolchain
+}
 
-// InitFileVersions is a noop at this Go version.
+// InitFileVersions is a noop when compiled with this Go version.
 func InitFileVersions(*types.Info) {}
diff --git a/internal/versions/types_go122.go b/internal/versions/types_go122.go
index 7b9ba89..e818063 100644
--- a/internal/versions/types_go122.go
+++ b/internal/versions/types_go122.go
@@ -12,10 +12,27 @@
 	"go/types"
 )
 
-// FileVersions maps a file to the file's semantic Go version.
-// The reported version is the zero version if a version cannot be determined.
-func FileVersions(info *types.Info, file *ast.File) string {
-	return info.FileVersions[file]
+// FileVersions returns a file's Go version.
+// The reported version is an unknown Future version if a
+// version cannot be determined.
+func FileVersion(info *types.Info, file *ast.File) string {
+	// In tools built with Go >= 1.22, the Go version of a file
+	// follow a cascades of sources:
+	// 1) types.Info.FileVersion, which follows the cascade:
+	//   1.a) file version (ast.File.GoVersion),
+	//   1.b) the package version (types.Config.GoVersion), or
+	// 2) is some unknown Future version.
+	//
+	// File versions require a valid package version to be provided to types
+	// in Config.GoVersion. Config.GoVersion is either from the package's module
+	// or the toolchain (go run). This value should be provided by go/packages
+	// or unitchecker.Config.GoVersion.
+	if v := info.FileVersions[file]; IsValid(v) {
+		return v
+	}
+	// Note: we could instead return runtime.Version() [if valid].
+	// This would act as a max version on what a tool can support.
+	return Future
 }
 
 // InitFileVersions initializes info to record Go versions for Go files.
diff --git a/internal/versions/types_test.go b/internal/versions/types_test.go
index 0ffdd46..59f6d18 100644
--- a/internal/versions/types_test.go
+++ b/internal/versions/types_test.go
@@ -52,11 +52,11 @@
 			if got, want := versions.GoVersion(pkg), item.pversion; versions.Compare(got, want) != 0 {
 				t.Errorf("GoVersion()=%q. expected %q", got, want)
 			}
-			if got := versions.FileVersions(info, nil); got != "" {
+			if got := versions.FileVersion(info, nil); got != "" {
 				t.Errorf(`FileVersions(nil)=%q. expected ""`, got)
 			}
 			for i, test := range item.tests {
-				if got, want := versions.FileVersions(info, files[i]), test.want; got != want {
+				if got, want := versions.FileVersion(info, files[i]), test.want; got != want {
 					t.Errorf("FileVersions(%s)=%q. expected %q", test.fname, got, want)
 				}
 			}
diff --git a/internal/versions/versions.go b/internal/versions/versions.go
index f898284..8d1f745 100644
--- a/internal/versions/versions.go
+++ b/internal/versions/versions.go
@@ -4,7 +4,9 @@
 
 package versions
 
-import "strings"
+import (
+	"strings"
+)
 
 // Note: If we use build tags to use go/versions when go >=1.22,
 // we run into go.dev/issue/53737. Under some operations users would see an
diff --git a/internal/versions/versions_test.go b/internal/versions/versions_test.go
index 997de2a..dbc1c55 100644
--- a/internal/versions/versions_test.go
+++ b/internal/versions/versions_test.go
@@ -5,8 +5,13 @@
 package versions_test
 
 import (
+	"go/ast"
+	"go/parser"
+	"go/token"
+	"go/types"
 	"testing"
 
+	"golang.org/x/tools/internal/testenv"
 	"golang.org/x/tools/internal/versions"
 )
 
@@ -137,3 +142,115 @@
 	}
 
 }
+
+func TestKnown(t *testing.T) {
+	for _, v := range [...]string{
+		versions.Go1_18,
+		versions.Go1_19,
+		versions.Go1_20,
+		versions.Go1_21,
+		versions.Go1_22,
+	} {
+		if !versions.IsValid(v) {
+			t.Errorf("Expected known version %q to be valid.", v)
+		}
+		if v != versions.Lang(v) {
+			t.Errorf("Expected known version %q == Lang(%q).", v, versions.Lang(v))
+		}
+	}
+}
+
+func TestAtLeast(t *testing.T) {
+	for _, item := range [...]struct {
+		v, release string
+		want       bool
+	}{
+		{versions.Future, versions.Go1_22, true},
+		{versions.Go1_22, versions.Go1_22, true},
+		{"go1.21", versions.Go1_22, false},
+		{"invalid", versions.Go1_22, false},
+	} {
+		if got := versions.AtLeast(item.v, item.release); got != item.want {
+			t.Errorf("AtLeast(%q, %q)=%v. wanted %v", item.v, item.release, got, item.want)
+		}
+	}
+}
+
+func TestBefore(t *testing.T) {
+	for _, item := range [...]struct {
+		v, release string
+		want       bool
+	}{
+		{versions.Future, versions.Go1_22, false},
+		{versions.Go1_22, versions.Go1_22, false},
+		{"go1.21", versions.Go1_22, true},
+		{"invalid", versions.Go1_22, true}, // invalid < Go1_22
+	} {
+		if got := versions.Before(item.v, item.release); got != item.want {
+			t.Errorf("Before(%q, %q)=%v. wanted %v", item.v, item.release, got, item.want)
+		}
+	}
+}
+
+func TestFileVersions122(t *testing.T) {
+	testenv.NeedsGo1Point(t, 22)
+
+	const source = `
+	package P
+	`
+	fset := token.NewFileSet()
+	f, err := parser.ParseFile(fset, "hello.go", source, 0)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	for _, conf := range []types.Config{
+		{GoVersion: versions.Go1_22},
+		{}, // GoVersion is unset.
+	} {
+		info := &types.Info{}
+		versions.InitFileVersions(info)
+
+		_, err = conf.Check("P", fset, []*ast.File{f}, info)
+		if err != nil {
+			t.Fatal(err)
+		}
+
+		v := versions.FileVersion(info, f)
+		if !versions.AtLeast(v, versions.Go1_22) {
+			t.Errorf("versions.AtLeast(%q, %q) expected to hold", v, versions.Go1_22)
+		}
+
+		if versions.Before(v, versions.Go1_22) {
+			t.Errorf("versions.AtLeast(%q, %q) expected to be false", v, versions.Go1_22)
+		}
+
+		if conf.GoVersion == "" && v != versions.Future {
+			t.Error("Expected the FileVersion to be the Future when conf.GoVersion is unset")
+		}
+	}
+}
+
+func TestFileVersions121(t *testing.T) {
+	testenv.SkipAfterGo1Point(t, 21)
+
+	// If <1.22, info and file are ignored.
+	v := versions.FileVersion(nil, nil)
+	oneof := map[string]bool{
+		versions.Go1_18: true,
+		versions.Go1_19: true,
+		versions.Go1_20: true,
+		versions.Go1_21: true,
+	}
+	if !oneof[v] {
+		t.Errorf("FileVersion(...)=%q expected to be a known go version <1.22", v)
+	}
+
+	if versions.AtLeast(v, versions.Go1_22) {
+		t.Errorf("versions.AtLeast(%q, %q) expected to be false", v, versions.Go1_22)
+	}
+
+	if !versions.Before(v, versions.Go1_22) {
+		t.Errorf("versions.Before(%q, %q) expected to hold", v, versions.Go1_22)
+	}
+}