go/analysis: Add modules to Pass

Adds optional Module information to Pass. Module contains
the go module information for the package of the Pass.
It contains the module's path, the module version, and the
GoVersion of the module.

Updates packages.PrintErrors to additionally print module
errors.

Fixes golang/go#66315

Change-Id: I7005b8e2f6290f16416c2438af0e6de2940ba9fc
Reviewed-on: https://go-review.googlesource.com/c/tools/+/577996
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Alan Donovan <adonovan@google.com>
diff --git a/go/analysis/analysis.go b/go/analysis/analysis.go
index ad27c27..aa02eed 100644
--- a/go/analysis/analysis.go
+++ b/go/analysis/analysis.go
@@ -100,6 +100,8 @@
 	TypesSizes   types.Sizes    // function for computing sizes of types
 	TypeErrors   []types.Error  // type errors (only if Analyzer.RunDespiteErrors)
 
+	Module *Module // the package's enclosing module (possibly nil in some drivers)
+
 	// Report reports a Diagnostic, a finding about a specific location
 	// in the analyzed source code such as a potential mistake.
 	// It may be called by the Run function.
@@ -238,3 +240,10 @@
 type Fact interface {
 	AFact() // dummy method to avoid type errors
 }
+
+// A Module describes the module to which a package belongs.
+type Module struct {
+	Path      string // module path
+	Version   string // module version ("" if unknown, such as for workspace modules)
+	GoVersion string // go version used in module (e.g. "go1.22.0")
+}
diff --git a/go/analysis/analysistest/analysistest_test.go b/go/analysis/analysistest/analysistest_test.go
index b22e2a1..eedbb5c 100644
--- a/go/analysis/analysistest/analysistest_test.go
+++ b/go/analysis/analysistest/analysistest_test.go
@@ -17,6 +17,8 @@
 	"golang.org/x/tools/go/analysis/analysistest"
 	"golang.org/x/tools/go/analysis/passes/findcall"
 	"golang.org/x/tools/internal/testenv"
+	"golang.org/x/tools/internal/testfiles"
+	"golang.org/x/tools/txtar"
 )
 
 func init() {
@@ -202,6 +204,62 @@
 	analysistest.RunWithSuggestedFixes(t, dir, noend, "a")
 }
 
+func TestModule(t *testing.T) {
+	const content = `
+Test that analysis.pass.Module is populated.
+
+-- go.mod --
+module golang.org/fake/mod
+
+go 1.21
+
+require golang.org/xyz/fake v0.12.34
+
+-- mod.go --
+// We expect a module.Path and a module.GoVersion, but an empty module.Version.
+
+package mod // want "golang.org/fake/mod,,1.21"
+
+import "golang.org/xyz/fake/ver"
+
+var _ ver.T
+
+-- vendor/modules.txt --
+# golang.org/xyz/fake v0.12.34
+## explicit; go 1.18
+golang.org/xyz/fake/ver
+
+-- vendor/golang.org/xyz/fake/ver/ver.go --
+// This package is vendored so that we can populate a non-empty
+// Pass.Module.Version is in a test.
+
+package ver //want "golang.org/xyz/fake,v0.12.34,1.18"
+
+type T string
+`
+	fs, err := txtar.FS(txtar.Parse([]byte(content)))
+	if err != nil {
+		t.Fatal(err)
+	}
+	dir := testfiles.CopyToTmp(t, fs)
+
+	filever := &analysis.Analyzer{
+		Name: "mod",
+		Doc:  "reports module information",
+		Run: func(pass *analysis.Pass) (any, error) {
+			msg := "no module info"
+			if m := pass.Module; m != nil {
+				msg = fmt.Sprintf("%s,%s,%s", m.Path, m.Version, m.GoVersion)
+			}
+			for _, file := range pass.Files {
+				pass.Reportf(file.Package, "%s", msg)
+			}
+			return nil, nil
+		},
+	}
+	analysistest.Run(t, dir, filever, "golang.org/fake/mod", "golang.org/xyz/fake/ver")
+}
+
 type errorfunc func(string)
 
 func (f errorfunc) Errorf(format string, args ...interface{}) {
diff --git a/go/analysis/internal/checker/checker.go b/go/analysis/internal/checker/checker.go
index 5a71465..8a80283 100644
--- a/go/analysis/internal/checker/checker.go
+++ b/go/analysis/internal/checker/checker.go
@@ -138,7 +138,7 @@
 	}
 
 	pkgsExitCode := 0
-	// Print package errors regardless of RunDespiteErrors.
+	// Print package and module errors regardless of RunDespiteErrors.
 	// Do not exit if there are errors, yet.
 	if n := packages.PrintErrors(initial); n > 0 {
 		pkgsExitCode = 1
@@ -720,6 +720,13 @@
 		}
 	}
 
+	module := &analysis.Module{} // possibly empty (non nil) in go/analysis drivers.
+	if mod := act.pkg.Module; mod != nil {
+		module.Path = mod.Path
+		module.Version = mod.Version
+		module.GoVersion = mod.GoVersion
+	}
+
 	// Run the analysis.
 	pass := &analysis.Pass{
 		Analyzer:     act.a,
@@ -731,6 +738,7 @@
 		TypesInfo:    act.pkg.TypesInfo,
 		TypesSizes:   act.pkg.TypesSizes,
 		TypeErrors:   act.pkg.TypeErrors,
+		Module:       module,
 
 		ResultOf:          inputs,
 		Report:            func(d analysis.Diagnostic) { act.diagnostics = append(act.diagnostics, d) },
diff --git a/go/analysis/unitchecker/separate_test.go b/go/analysis/unitchecker/separate_test.go
index 827ca4c..8f4a919 100644
--- a/go/analysis/unitchecker/separate_test.go
+++ b/go/analysis/unitchecker/separate_test.go
@@ -15,6 +15,7 @@
 	"io"
 	"os"
 	"path/filepath"
+	"sort"
 	"strings"
 	"sync/atomic"
 	"testing"
@@ -167,6 +168,8 @@
 			if v := pkg.Module.GoVersion; v != "" {
 				cfg.GoVersion = "go" + v
 			}
+			cfg.ModulePath = pkg.Module.Path
+			cfg.ModuleVersion = pkg.Module.Version
 		}
 
 		// Write the JSON configuration message to a file.
@@ -220,6 +223,7 @@
 	// from separate analysis of "main", "lib", and "fmt":
 
 	const want = `/main/main.go:6:2: [printf] separate/lib.MyPrintf format %s has arg 123 of wrong type int`
+	sort.Strings(allDiagnostics)
 	if got := strings.Join(allDiagnostics, "\n"); got != want {
 		t.Errorf("Got: %s\nWant: %s", got, want)
 	}
diff --git a/go/analysis/unitchecker/unitchecker.go b/go/analysis/unitchecker/unitchecker.go
index d77fb20..71ebbfa 100644
--- a/go/analysis/unitchecker/unitchecker.go
+++ b/go/analysis/unitchecker/unitchecker.go
@@ -66,6 +66,8 @@
 	GoFiles                   []string
 	NonGoFiles                []string
 	IgnoredFiles              []string
+	ModulePath                string            // module path
+	ModuleVersion             string            // module version
 	ImportMap                 map[string]string // maps import path to package path
 	PackageFile               map[string]string // maps package path to file of type information
 	Standard                  map[string]bool   // package belongs to standard library
@@ -359,6 +361,12 @@
 				factFilter[reflect.TypeOf(f)] = true
 			}
 
+			module := &analysis.Module{
+				Path:      cfg.ModulePath,
+				Version:   cfg.ModuleVersion,
+				GoVersion: cfg.GoVersion,
+			}
+
 			pass := &analysis.Pass{
 				Analyzer:          a,
 				Fset:              fset,
@@ -377,6 +385,7 @@
 				ImportPackageFact: facts.ImportPackageFact,
 				ExportPackageFact: facts.ExportPackageFact,
 				AllPackageFacts:   func() []analysis.PackageFact { return facts.AllPackageFacts(factFilter) },
+				Module:            module,
 			}
 			pass.ReadFile = analysisinternal.MakeReadFile(pass)
 
diff --git a/go/packages/visit.go b/go/packages/visit.go
index a1dcc40..df14ffd 100644
--- a/go/packages/visit.go
+++ b/go/packages/visit.go
@@ -49,11 +49,20 @@
 // PrintErrors returns the number of errors printed.
 func PrintErrors(pkgs []*Package) int {
 	var n int
+	errModules := make(map[*Module]bool)
 	Visit(pkgs, nil, func(pkg *Package) {
 		for _, err := range pkg.Errors {
 			fmt.Fprintln(os.Stderr, err)
 			n++
 		}
+
+		// Print pkg.Module.Error once if present.
+		mod := pkg.Module
+		if mod != nil && mod.Error != nil && !errModules[mod] {
+			errModules[mod] = true
+			fmt.Fprintln(os.Stderr, mod.Error.Err)
+			n++
+		}
 	})
 	return n
 }