go/analysis: pass package's Go version to type checker

Type checking of a package depends on the Go language version in
effect for that package. We have been not setting it and assuming
"latest" is good enough, but that is likely to become untrue in the
future, and it violates Go 1.21's emphasis on forward compatibility,
namely tools recognizing when they shouldn't be processing newer code.

Pass the Go version along from go/analysis to go/types, to allow
go/types to apply the version when type-checking.

In the one analysis pass that does its own type-checking (cgocall),
pass the Go version along explicitly there too.

Fixes golang/go#61174.
Fixes golang/go#61176.

Change-Id: I5a357f7c357c41b77d0eb83c905490cc1f866956
Reviewed-on: https://go-review.googlesource.com/c/tools/+/507880
gopls-CI: kokoro <noreply+kokoro@google.com>
Reviewed-by: Alan Donovan <adonovan@google.com>
Reviewed-by: Robert Findley <rfindley@google.com>
Run-TryBot: Russ Cox <rsc@golang.org>
TryBot-Result: Gopher Robot <gobot@golang.org>
diff --git a/go/analysis/analysistest/analysistest.go b/go/analysis/analysistest/analysistest.go
index 5d9505b..6a27edb 100644
--- a/go/analysis/analysistest/analysistest.go
+++ b/go/analysis/analysistest/analysistest.go
@@ -242,12 +242,16 @@
 
 // Run applies an analysis to the packages denoted by the "go list" patterns.
 //
-// It loads the packages from the specified GOPATH-style project
+// It loads the packages from the specified
 // directory using golang.org/x/tools/go/packages, runs the analysis on
 // them, and checks that each analysis emits the expected diagnostics
 // and facts specified by the contents of '// want ...' comments in the
 // package's source files. It treats a comment of the form
-// "//...// want..." or "/*...// want... */" as if it starts at 'want'
+// "//...// want..." or "/*...// want... */" as if it starts at 'want'.
+//
+// If the directory contains a go.mod file, Run treats it as the root of the
+// Go module in which to work. Otherwise, Run treats it as the root of a
+// GOPATH-style tree, with package contained in the src subdirectory.
 //
 // An expectation of a Diagnostic is specified by a string literal
 // containing a regular expression that must match the diagnostic
@@ -309,10 +313,17 @@
 type Result = checker.TestAnalyzerResult
 
 // loadPackages uses go/packages to load a specified packages (from source, with
-// dependencies) from dir, which is the root of a GOPATH-style project
-// tree. It returns an error if any package had an error, or the pattern
+// dependencies) from dir, which is the root of a GOPATH-style project tree.
+// loadPackages returns an error if any package had an error, or the pattern
 // matched no packages.
 func loadPackages(a *analysis.Analyzer, dir string, patterns ...string) ([]*packages.Package, error) {
+	env := []string{"GOPATH=" + dir, "GO111MODULE=off"} // GOPATH mode
+
+	// Undocumented module mode. Will be replaced by something better.
+	if _, err := os.Stat(filepath.Join(dir, "go.mod")); err == nil {
+		env = []string{"GO111MODULE=on", "GOPROXY=off"} // module mode
+	}
+
 	// packages.Load loads the real standard library, not a minimal
 	// fake version, which would be more efficient, especially if we
 	// have many small tests that import, say, net/http.
@@ -322,12 +333,12 @@
 
 	mode := packages.NeedName | packages.NeedFiles | packages.NeedCompiledGoFiles | packages.NeedImports |
 		packages.NeedTypes | packages.NeedTypesSizes | packages.NeedSyntax | packages.NeedTypesInfo |
-		packages.NeedDeps
+		packages.NeedDeps | packages.NeedModule
 	cfg := &packages.Config{
 		Mode:  mode,
 		Dir:   dir,
 		Tests: true,
-		Env:   append(os.Environ(), "GOPATH="+dir, "GO111MODULE=off", "GOPROXY=off"),
+		Env:   append(os.Environ(), env...),
 	}
 	pkgs, err := packages.Load(cfg, patterns...)
 	if err != nil {
diff --git a/go/analysis/internal/checker/checker.go b/go/analysis/internal/checker/checker.go
index 35e3c5d..2da4692 100644
--- a/go/analysis/internal/checker/checker.go
+++ b/go/analysis/internal/checker/checker.go
@@ -172,6 +172,7 @@
 	if allSyntax {
 		mode = packages.LoadAllSyntax
 	}
+	mode |= packages.NeedModule
 	conf := packages.Config{
 		Mode:  mode,
 		Tests: IncludeTests,
diff --git a/go/analysis/internal/versiontest/version_test.go b/go/analysis/internal/versiontest/version_test.go
new file mode 100644
index 0000000..45eef8b
--- /dev/null
+++ b/go/analysis/internal/versiontest/version_test.go
@@ -0,0 +1,102 @@
+// 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.
+
+//go:build go1.21
+
+// Check that GoVersion propagates through to checkers.
+// Depends on Go 1.21 go/types.
+
+package versiontest
+
+import (
+	"os"
+	"os/exec"
+	"path/filepath"
+	"strings"
+	"testing"
+
+	"golang.org/x/tools/go/analysis"
+	"golang.org/x/tools/go/analysis/analysistest"
+	"golang.org/x/tools/go/analysis/multichecker"
+	"golang.org/x/tools/go/analysis/singlechecker"
+)
+
+var analyzer = &analysis.Analyzer{
+	Name: "versiontest",
+	Doc:  "off",
+	Run: func(pass *analysis.Pass) (interface{}, error) {
+		pass.Reportf(pass.Files[0].Package, "goversion=%s", pass.Pkg.GoVersion())
+		return nil, nil
+	},
+}
+
+func init() {
+	if os.Getenv("VERSIONTEST_MULTICHECKER") == "1" {
+		multichecker.Main(analyzer)
+		os.Exit(0)
+	}
+	if os.Getenv("VERSIONTEST_SINGLECHECKER") == "1" {
+		singlechecker.Main(analyzer)
+		os.Exit(0)
+	}
+}
+
+func testDir(t *testing.T) (dir string) {
+	dir = t.TempDir()
+	if err := os.WriteFile(filepath.Join(dir, "go.mod"), []byte("go 1.20\nmodule m\n"), 0666); err != nil {
+		t.Fatal(err)
+	}
+	if err := os.WriteFile(filepath.Join(dir, "x.go"), []byte("package main // want \"goversion=go1.20\"\n"), 0666); err != nil {
+		t.Fatal(err)
+	}
+	return dir
+}
+
+// There are many ways to run analyzers. Test all the ones here in x/tools.
+
+func TestAnalysistest(t *testing.T) {
+	analysistest.Run(t, testDir(t), analyzer)
+}
+
+func TestMultichecker(t *testing.T) {
+	exe, err := os.Executable()
+	if err != nil {
+		t.Fatal(err)
+	}
+	cmd := exec.Command(exe, ".")
+	cmd.Dir = testDir(t)
+	cmd.Env = append(os.Environ(), "VERSIONTEST_MULTICHECKER=1")
+	out, err := cmd.CombinedOutput()
+	if err == nil || !strings.Contains(string(out), "x.go:1:1: goversion=go1.20\n") {
+		t.Fatalf("multichecker: %v\n%s", err, out)
+	}
+}
+
+func TestSinglechecker(t *testing.T) {
+	exe, err := os.Executable()
+	if err != nil {
+		t.Fatal(err)
+	}
+	cmd := exec.Command(exe, ".")
+	cmd.Dir = testDir(t)
+	cmd.Env = append(os.Environ(), "VERSIONTEST_SINGLECHECKER=1")
+	out, err := cmd.CombinedOutput()
+	if err == nil || !strings.Contains(string(out), "x.go:1:1: goversion=go1.20\n") {
+		t.Fatalf("multichecker: %v\n%s", err, out)
+	}
+}
+
+func TestVettool(t *testing.T) {
+	exe, err := os.Executable()
+	if err != nil {
+		t.Fatal(err)
+	}
+	cmd := exec.Command("go", "vet", "-vettool="+exe, ".")
+	cmd.Dir = testDir(t)
+	cmd.Env = append(os.Environ(), "VERSIONTEST_MULTICHECKER=1")
+	out, err := cmd.CombinedOutput()
+	if err == nil || !strings.Contains(string(out), "x.go:1:1: goversion=go1.20\n") {
+		t.Fatalf("vettool: %v\n%s", err, out)
+	}
+}
diff --git a/go/analysis/passes/cgocall/cgocall.go b/go/analysis/passes/cgocall/cgocall.go
index afff0d8..c18b84b 100644
--- a/go/analysis/passes/cgocall/cgocall.go
+++ b/go/analysis/passes/cgocall/cgocall.go
@@ -271,6 +271,7 @@
 		Sizes: sizes,
 		Error: func(error) {}, // ignore errors (e.g. unused import)
 	}
+	setGoVersion(tc, pkg)
 
 	// It's tempting to record the new types in the
 	// existing pass.TypesInfo, but we don't own it.
diff --git a/go/analysis/passes/cgocall/cgocall_go120.go b/go/analysis/passes/cgocall/cgocall_go120.go
new file mode 100644
index 0000000..06b5494
--- /dev/null
+++ b/go/analysis/passes/cgocall/cgocall_go120.go
@@ -0,0 +1,13 @@
+// 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.
+
+//go:build !go1.21
+
+package cgocall
+
+import "go/types"
+
+func setGoVersion(tc *types.Config, pkg *types.Package) {
+	// no types.Package.GoVersion until Go 1.21
+}
diff --git a/go/analysis/passes/cgocall/cgocall_go121.go b/go/analysis/passes/cgocall/cgocall_go121.go
new file mode 100644
index 0000000..2a3e1fa
--- /dev/null
+++ b/go/analysis/passes/cgocall/cgocall_go121.go
@@ -0,0 +1,13 @@
+// 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.
+
+//go:build go1.21
+
+package cgocall
+
+import "go/types"
+
+func setGoVersion(tc *types.Config, pkg *types.Package) {
+	tc.GoVersion = pkg.GoVersion()
+}
diff --git a/go/analysis/unitchecker/unitchecker.go b/go/analysis/unitchecker/unitchecker.go
index ff22d23..10c76bc 100644
--- a/go/analysis/unitchecker/unitchecker.go
+++ b/go/analysis/unitchecker/unitchecker.go
@@ -62,6 +62,7 @@
 	Compiler                  string
 	Dir                       string
 	ImportPath                string
+	GoVersion                 string // minimum required Go version, such as "go1.21.0"
 	GoFiles                   []string
 	NonGoFiles                []string
 	IgnoredFiles              []string
@@ -217,8 +218,9 @@
 		return compilerImporter.Import(path)
 	})
 	tc := &types.Config{
-		Importer: importer,
-		Sizes:    types.SizesFor("gc", build.Default.GOARCH), // assume gccgo ≡ gc?
+		Importer:  importer,
+		Sizes:     types.SizesFor("gc", build.Default.GOARCH), // assume gccgo ≡ gc?
+		GoVersion: cfg.GoVersion,
 	}
 	info := &types.Info{
 		Types:      make(map[ast.Expr]types.TypeAndValue),