gopls/internal/cache: improve missing import error message

Make the missing import error knowledgeable of the view type, so that it
can correctly reference modules, GOROOT, GOPATH, or go/packages driver
as applicable.

While at it, fix some duplicated and broken logic for determining if the
view is in go/packages driver mode, consolidate on representing the
driver accurately as GoEnv.EffectiveGOPACKAGESDRIVER.

Fixes golang/go#64980

Change-Id: I7961aade981173098ab02cbe1862ac2eca2c394b
Reviewed-on: https://go-review.googlesource.com/c/tools/+/589215
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Alan Donovan <adonovan@google.com>
Auto-Submit: Robert Findley <rfindley@google.com>
diff --git a/gopls/internal/cache/check.go b/gopls/internal/cache/check.go
index 11d30d4..3340306 100644
--- a/gopls/internal/cache/check.go
+++ b/gopls/internal/cache/check.go
@@ -1356,9 +1356,9 @@
 	// TODO(rfindley): consider storing less data in gobDiagnostics, and
 	// interpreting each diagnostic in the context of a fixed set of options.
 	// Then these fields need not be part of the type checking inputs.
-	relatedInformation bool
-	linkTarget         string
-	moduleMode         bool
+	supportsRelatedInformation bool
+	linkTarget                 string
+	viewType                   ViewType
 }
 
 func (s *Snapshot) typeCheckInputs(ctx context.Context, mp *metadata.Package) (typeCheckInputs, error) {
@@ -1396,9 +1396,9 @@
 		depsByImpPath:   mp.DepsByImpPath,
 		goVersion:       goVersion,
 
-		relatedInformation: s.Options().RelatedInformationSupported,
-		linkTarget:         s.Options().LinkTarget,
-		moduleMode:         s.view.moduleMode(),
+		supportsRelatedInformation: s.Options().RelatedInformationSupported,
+		linkTarget:                 s.Options().LinkTarget,
+		viewType:                   s.view.typ,
 	}, nil
 }
 
@@ -1455,9 +1455,9 @@
 	maxAlign := inputs.sizes.Alignof(types.NewPointer(types.Typ[types.Int64]))
 	fmt.Fprintf(hasher, "sizes: %d %d\n", wordSize, maxAlign)
 
-	fmt.Fprintf(hasher, "relatedInformation: %t\n", inputs.relatedInformation)
+	fmt.Fprintf(hasher, "relatedInformation: %t\n", inputs.supportsRelatedInformation)
 	fmt.Fprintf(hasher, "linkTarget: %s\n", inputs.linkTarget)
-	fmt.Fprintf(hasher, "moduleMode: %t\n", inputs.moduleMode)
+	fmt.Fprintf(hasher, "viewType: %d\n", inputs.viewType)
 
 	var hash [sha256.Size]byte
 	hasher.Sum(hash[:0])
@@ -1595,7 +1595,7 @@
 		}
 	}
 
-	diags := typeErrorsToDiagnostics(pkg, pkg.typeErrors, inputs.linkTarget, inputs.moduleMode, inputs.relatedInformation)
+	diags := typeErrorsToDiagnostics(pkg, inputs, pkg.typeErrors)
 	for _, diag := range diags {
 		// If the file didn't parse cleanly, it is highly likely that type
 		// checking errors will be confusing or redundant. But otherwise, type
@@ -1630,7 +1630,7 @@
 			depPH := b.handles[id]
 			if depPH == nil {
 				// e.g. missing metadata for dependencies in buildPackageHandle
-				return nil, missingPkgError(inputs.id, path, inputs.moduleMode)
+				return nil, missingPkgError(inputs.id, path, inputs.viewType)
 			}
 			if !metadata.IsValidImport(inputs.pkgPath, depPH.mp.PkgPath) {
 				return nil, fmt.Errorf("invalid use of internal package %q", path)
@@ -1825,20 +1825,23 @@
 
 // missingPkgError returns an error message for a missing package that varies
 // based on the user's workspace mode.
-func missingPkgError(from PackageID, pkgPath string, moduleMode bool) error {
-	// TODO(rfindley): improve this error. Previous versions of this error had
-	// access to the full snapshot, and could provide more information (such as
-	// the initialization error).
-	if moduleMode {
+func missingPkgError(from PackageID, pkgPath string, viewType ViewType) error {
+	switch viewType {
+	case GoModView, GoWorkView:
 		if metadata.IsCommandLineArguments(from) {
 			return fmt.Errorf("current file is not included in a workspace module")
 		} else {
 			// Previously, we would present the initialization error here.
 			return fmt.Errorf("no required module provides package %q", pkgPath)
 		}
-	} else {
-		// Previously, we would list the directories in GOROOT and GOPATH here.
+	case AdHocView:
+		return fmt.Errorf("cannot find package %q in GOROOT", pkgPath)
+	case GoPackagesDriverView:
+		return fmt.Errorf("go/packages driver could not load %q", pkgPath)
+	case GOPATHView:
 		return fmt.Errorf("cannot find package %q in GOROOT or GOPATH", pkgPath)
+	default:
+		return fmt.Errorf("unable to load package")
 	}
 }
 
@@ -1852,9 +1855,8 @@
 // to the previous error in the errs slice (such as if they were printed in
 // sequence to a terminal).
 //
-// The linkTarget, moduleMode, and supportsRelatedInformation parameters affect
-// the construction of protocol objects (see the code for details).
-func typeErrorsToDiagnostics(pkg *syntaxPackage, errs []types.Error, linkTarget string, moduleMode, supportsRelatedInformation bool) []*Diagnostic {
+// Fields in typeCheckInputs may affect the resulting diagnostics.
+func typeErrorsToDiagnostics(pkg *syntaxPackage, inputs typeCheckInputs, errs []types.Error) []*Diagnostic {
 	var result []*Diagnostic
 
 	// batch records diagnostics for a set of related types.Errors.
@@ -1944,7 +1946,7 @@
 			}
 			msg := related[0].Msg // primary
 			if i > 0 {
-				if supportsRelatedInformation {
+				if inputs.supportsRelatedInformation {
 					msg += " (see details)"
 				} else {
 					msg += fmt.Sprintf(" (this error: %v)", e.Msg)
@@ -1959,16 +1961,16 @@
 			}
 			if code != 0 {
 				diag.Code = code.String()
-				diag.CodeHref = typesCodeHref(linkTarget, code)
+				diag.CodeHref = typesCodeHref(inputs.linkTarget, code)
 			}
 			if code == typesinternal.UnusedVar || code == typesinternal.UnusedImport {
 				diag.Tags = append(diag.Tags, protocol.Unnecessary)
 			}
 			if match := importErrorRe.FindStringSubmatch(e.Msg); match != nil {
-				diag.SuggestedFixes = append(diag.SuggestedFixes, goGetQuickFixes(moduleMode, pgf.URI, match[1])...)
+				diag.SuggestedFixes = append(diag.SuggestedFixes, goGetQuickFixes(inputs.viewType.usesModules(), pgf.URI, match[1])...)
 			}
 			if match := unsupportedFeatureRe.FindStringSubmatch(e.Msg); match != nil {
-				diag.SuggestedFixes = append(diag.SuggestedFixes, editGoDirectiveQuickFix(moduleMode, pgf.URI, match[1])...)
+				diag.SuggestedFixes = append(diag.SuggestedFixes, editGoDirectiveQuickFix(inputs.viewType.usesModules(), pgf.URI, match[1])...)
 			}
 
 			// Link up related information. For the primary error, all related errors
diff --git a/gopls/internal/cache/errors.go b/gopls/internal/cache/errors.go
index acb5385..7aa1e2c 100644
--- a/gopls/internal/cache/errors.go
+++ b/gopls/internal/cache/errors.go
@@ -129,9 +129,9 @@
 var importErrorRe = regexp.MustCompile(`could not import ([^\s]+)`)
 var unsupportedFeatureRe = regexp.MustCompile(`.*require.* go(\d+\.\d+) or later`)
 
-func goGetQuickFixes(moduleMode bool, uri protocol.DocumentURI, pkg string) []SuggestedFix {
+func goGetQuickFixes(haveModule bool, uri protocol.DocumentURI, pkg string) []SuggestedFix {
 	// Go get only supports module mode for now.
-	if !moduleMode {
+	if !haveModule {
 		return nil
 	}
 	title := fmt.Sprintf("go get package %v", pkg)
@@ -147,9 +147,9 @@
 	return []SuggestedFix{SuggestedFixFromCommand(cmd, protocol.QuickFix)}
 }
 
-func editGoDirectiveQuickFix(moduleMode bool, uri protocol.DocumentURI, version string) []SuggestedFix {
+func editGoDirectiveQuickFix(haveModule bool, uri protocol.DocumentURI, version string) []SuggestedFix {
 	// Go mod edit only supports module mode.
-	if !moduleMode {
+	if !haveModule {
 		return nil
 	}
 	title := fmt.Sprintf("go mod edit -go=%s", version)
diff --git a/gopls/internal/cache/load.go b/gopls/internal/cache/load.go
index 5995cce..e42290d 100644
--- a/gopls/internal/cache/load.go
+++ b/gopls/internal/cache/load.go
@@ -676,7 +676,7 @@
 	//
 	// For module views (of type GoMod or GoWork), packages must in any case be
 	// in a workspace module (enforced below).
-	if !s.view.moduleMode() || !s.Options().ExpandWorkspaceToModule {
+	if !s.view.typ.usesModules() || !s.Options().ExpandWorkspaceToModule {
 		folder := s.view.folder.Dir.Path()
 		inFolder := false
 		for uri := range uris {
@@ -692,7 +692,7 @@
 
 	// In module mode, a workspace package must be contained in a workspace
 	// module.
-	if s.view.moduleMode() {
+	if s.view.typ.usesModules() {
 		var modURI protocol.DocumentURI
 		if pkg.Module != nil {
 			modURI = protocol.URIFromPath(pkg.Module.GoMod)
diff --git a/gopls/internal/cache/snapshot.go b/gopls/internal/cache/snapshot.go
index b300827..9f05cbb 100644
--- a/gopls/internal/cache/snapshot.go
+++ b/gopls/internal/cache/snapshot.go
@@ -878,7 +878,7 @@
 	watchGoFiles := fmt.Sprintf("**/*.{%s}", extensions)
 
 	var dirs []string
-	if s.view.moduleMode() {
+	if s.view.typ.usesModules() {
 		if s.view.typ == GoWorkView {
 			workVendorDir := filepath.Join(s.view.gowork.Dir().Path(), "vendor")
 			workVendorURI := protocol.URIFromPath(workVendorDir)
diff --git a/gopls/internal/cache/view.go b/gopls/internal/cache/view.go
index 56e26bc..6f76c55 100644
--- a/gopls/internal/cache/view.go
+++ b/gopls/internal/cache/view.go
@@ -72,8 +72,19 @@
 	GoVersionOutput string // complete go version output
 
 	// OS environment variables (notably not go env).
-	GOWORK           string
-	GOPACKAGESDRIVER string
+
+	// ExplicitGOWORK is the GOWORK value set explicitly in the environment. This
+	// may differ from `go env GOWORK` when the GOWORK value is implicit from the
+	// working directory.
+	ExplicitGOWORK string
+
+	// EffectiveGOPACKAGESDRIVER is the effective go/packages driver binary that
+	// will be used. This may be set via GOPACKAGESDRIVER, or may be discovered
+	// via os.LookPath("gopackagesdriver"). The latter functionality is
+	// undocumented and may be removed in the future.
+	//
+	// If GOPACKAGESDRIVER is set to "off", EffectiveGOPACKAGESDRIVER is "".
+	EffectiveGOPACKAGESDRIVER string
 }
 
 // View represents a single build for a workspace.
@@ -315,9 +326,9 @@
 	}
 }
 
-// moduleMode reports whether the view uses Go modules.
-func (w viewDefinition) moduleMode() bool {
-	switch w.typ {
+// usesModules reports whether the view uses Go modules.
+func (typ ViewType) usesModules() bool {
+	switch typ {
 	case GoModView, GoWorkView:
 		return true
 	default:
@@ -829,9 +840,9 @@
 	var err error
 	dirURI := protocol.URIFromPath(dir)
 	goworkFromEnv := false
-	if folder.Env.GOWORK != "off" && folder.Env.GOWORK != "" {
+	if folder.Env.ExplicitGOWORK != "off" && folder.Env.ExplicitGOWORK != "" {
 		goworkFromEnv = true
-		def.gowork = protocol.URIFromPath(folder.Env.GOWORK)
+		def.gowork = protocol.URIFromPath(folder.Env.ExplicitGOWORK)
 	} else {
 		def.gowork, err = findRootPattern(ctx, dirURI, "go.work", fs)
 		if err != nil {
@@ -855,20 +866,10 @@
 	//  - def.envOverlay.
 
 	// If GOPACKAGESDRIVER is set it takes precedence.
-	{
-		// The value of GOPACKAGESDRIVER is not returned through the go command.
-		gopackagesdriver := os.Getenv("GOPACKAGESDRIVER")
-		// A user may also have a gopackagesdriver binary on their machine, which
-		// works the same way as setting GOPACKAGESDRIVER.
-		//
-		// TODO(rfindley): remove this call to LookPath. We should not support this
-		// undocumented method of setting GOPACKAGESDRIVER.
-		tool, err := exec.LookPath("gopackagesdriver")
-		if gopackagesdriver != "off" && (gopackagesdriver != "" || (err == nil && tool != "")) {
-			def.typ = GoPackagesDriverView
-			def.root = dirURI
-			return def, nil
-		}
+	if def.folder.Env.EffectiveGOPACKAGESDRIVER != "" {
+		def.typ = GoPackagesDriverView
+		def.root = dirURI
+		return def, nil
 	}
 
 	// From go.dev/ref/mod, module mode is active if GO111MODULE=on, or
@@ -894,7 +895,7 @@
 
 	// Prefer a go.work file if it is available and contains the module relevant
 	// to forURI.
-	if def.adjustedGO111MODULE() != "off" && folder.Env.GOWORK != "off" && def.gowork != "" {
+	if def.adjustedGO111MODULE() != "off" && folder.Env.ExplicitGOWORK != "off" && def.gowork != "" {
 		def.typ = GoWorkView
 		if goworkFromEnv {
 			// The go.work file could be anywhere, which can lead to confusing error
@@ -997,18 +998,20 @@
 
 	// The value of GOPACKAGESDRIVER is not returned through the go command.
 	if driver, ok := opts.Env["GOPACKAGESDRIVER"]; ok {
-		env.GOPACKAGESDRIVER = driver
-	} else {
-		env.GOPACKAGESDRIVER = os.Getenv("GOPACKAGESDRIVER")
+		if driver != "off" {
+			env.EffectiveGOPACKAGESDRIVER = driver
+		}
+	} else if driver := os.Getenv("GOPACKAGESDRIVER"); driver != "off" {
+		env.EffectiveGOPACKAGESDRIVER = driver
 		// A user may also have a gopackagesdriver binary on their machine, which
 		// works the same way as setting GOPACKAGESDRIVER.
 		//
 		// TODO(rfindley): remove this call to LookPath. We should not support this
 		// undocumented method of setting GOPACKAGESDRIVER.
-		if env.GOPACKAGESDRIVER == "" {
+		if env.EffectiveGOPACKAGESDRIVER == "" {
 			tool, err := exec.LookPath("gopackagesdriver")
 			if err == nil && tool != "" {
-				env.GOPACKAGESDRIVER = tool
+				env.EffectiveGOPACKAGESDRIVER = tool
 			}
 		}
 	}
@@ -1017,9 +1020,9 @@
 	// between an explicit GOWORK value and one which is implicit from the file
 	// system. The former doesn't change unless the environment changes.
 	if gowork, ok := opts.Env["GOWORK"]; ok {
-		env.GOWORK = gowork
+		env.ExplicitGOWORK = gowork
 	} else {
-		env.GOWORK = os.Getenv("GOWORK")
+		env.ExplicitGOWORK = os.Getenv("GOWORK")
 	}
 	return env, nil
 }
diff --git a/gopls/internal/test/integration/diagnostics/diagnostics_test.go b/gopls/internal/test/integration/diagnostics/diagnostics_test.go
index 2862a86..195089f 100644
--- a/gopls/internal/test/integration/diagnostics/diagnostics_test.go
+++ b/gopls/internal/test/integration/diagnostics/diagnostics_test.go
@@ -43,7 +43,13 @@
 	// This test is very basic: start with a clean Go program, make an error, and
 	// get a diagnostic for that error. However, it also demonstrates how to
 	// combine Expectations to await more complex state in the editor.
-	Run(t, exampleProgram, func(t *testing.T, env *Env) {
+	RunMultiple{
+		{"golist", WithOptions(Modes(Default))},
+		{"gopackages", WithOptions(
+			Modes(Default),
+			FakeGoPackagesDriver(t),
+		)},
+	}.Run(t, exampleProgram, func(t *testing.T, env *Env) {
 		// Deleting the 'n' at the end of Println should generate a single error
 		// diagnostic.
 		env.OpenFile("main.go")
@@ -84,7 +90,15 @@
 
 const Foo = "abc
 `
-	Run(t, brokenFile, func(t *testing.T, env *Env) {
+	RunMultiple{
+		{"golist", WithOptions(Modes(Default))},
+		// Since this test requires loading an overlay,
+		// it verifies that the fake go/packages driver honors overlays.
+		{"gopackages", WithOptions(
+			Modes(Default),
+			FakeGoPackagesDriver(t),
+		)},
+	}.Run(t, brokenFile, func(t *testing.T, env *Env) {
 		env.CreateBuffer("broken.go", brokenFile)
 		env.AfterChange(Diagnostics(env.AtRegexp("broken.go", "\"abc")))
 	})
@@ -559,7 +573,7 @@
 			NoOutstandingWork(IgnoreTelemetryPromptWork),
 			Diagnostics(
 				env.AtRegexp("a.go", `"mod.com`),
-				WithMessage("GOROOT or GOPATH"),
+				WithMessage("in GOROOT"),
 			),
 		)
 		// Deleting the import dismisses the warning.
diff --git a/gopls/internal/test/integration/diagnostics/gopackagesdriver_test.go b/gopls/internal/test/integration/diagnostics/gopackagesdriver_test.go
new file mode 100644
index 0000000..7ed6d2a
--- /dev/null
+++ b/gopls/internal/test/integration/diagnostics/gopackagesdriver_test.go
@@ -0,0 +1,48 @@
+// 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 diagnostics
+
+import (
+	"testing"
+
+	. "golang.org/x/tools/gopls/internal/test/integration"
+)
+
+// Test that the import error does not mention GOPATH when building with
+// go/packages driver.
+func TestBrokenWorkspace_GOPACKAGESDRIVER(t *testing.T) {
+	// A go.mod file is actually needed here, because the fake go/packages driver
+	// uses go list behind the scenes, and we load go/packages driver workspaces
+	// with ./...
+	const files = `
+-- go.mod --
+module m
+go 1.12
+
+-- a.go --
+package foo
+
+import "mod.com/hello"
+
+func f() {
+}
+`
+	WithOptions(
+		FakeGoPackagesDriver(t),
+	).Run(t, files, func(t *testing.T, env *Env) {
+		env.OpenFile("a.go")
+		env.AfterChange(
+			Diagnostics(
+				env.AtRegexp("a.go", `"mod.com`),
+				WithMessage("go/packages driver"),
+			),
+		)
+		// Deleting the import removes the error.
+		env.RegexpReplace("a.go", `import "mod.com/hello"`, "")
+		env.AfterChange(
+			NoDiagnostics(ForFile("a.go")),
+		)
+	})
+}
diff --git a/gopls/internal/test/integration/options.go b/gopls/internal/test/integration/options.go
index d6c21e6..87be211 100644
--- a/gopls/internal/test/integration/options.go
+++ b/gopls/internal/test/integration/options.go
@@ -5,8 +5,12 @@
 package integration
 
 import (
+	"strings"
+	"testing"
+
 	"golang.org/x/tools/gopls/internal/protocol"
 	"golang.org/x/tools/gopls/internal/test/integration/fake"
+	"golang.org/x/tools/internal/drivertest"
 )
 
 type runConfig struct {
@@ -161,6 +165,18 @@
 	}
 }
 
+// FakeGoPackagesDriver configures gopls to run with a fake GOPACKAGESDRIVER
+// environment variable.
+func FakeGoPackagesDriver(t *testing.T) RunOption {
+	env := drivertest.Env(t)
+	vars := make(EnvVars)
+	for _, e := range env {
+		kv := strings.SplitN(e, "=", 2)
+		vars[kv[0]] = kv[1]
+	}
+	return vars
+}
+
 // InGOPATH configures the workspace working directory to be GOPATH, rather
 // than a separate working directory for use with modules.
 func InGOPATH() RunOption {
diff --git a/gopls/internal/test/integration/regtest.go b/gopls/internal/test/integration/regtest.go
index 96c1044..b676fd4 100644
--- a/gopls/internal/test/integration/regtest.go
+++ b/gopls/internal/test/integration/regtest.go
@@ -15,6 +15,7 @@
 
 	"golang.org/x/tools/gopls/internal/cache"
 	"golang.org/x/tools/gopls/internal/cmd"
+	"golang.org/x/tools/internal/drivertest"
 	"golang.org/x/tools/internal/gocommand"
 	"golang.org/x/tools/internal/memoize"
 	"golang.org/x/tools/internal/testenv"
@@ -45,12 +46,6 @@
 
 var runner *Runner
 
-// The integrationTestRunner interface abstracts the Run operation,
-// enables decorators for various optional features.
-type integrationTestRunner interface {
-	Run(t *testing.T, files string, f TestFunc)
-}
-
 func Run(t *testing.T, files string, f TestFunc) {
 	runner.Run(t, files, f)
 }
@@ -79,9 +74,15 @@
 	runner.Run(t, files, f, r.opts...)
 }
 
+// RunMultiple runs a test multiple times, with different options.
+// The runner should be constructed with [WithOptions].
+//
+// TODO(rfindley): replace Modes with selective use of RunMultiple.
 type RunMultiple []struct {
 	Name   string
-	Runner integrationTestRunner
+	Runner interface {
+		Run(t *testing.T, files string, f TestFunc)
+	}
 }
 
 func (r RunMultiple) Run(t *testing.T, files string, f TestFunc) {
@@ -112,6 +113,9 @@
 
 // Main sets up and tears down the shared integration test state.
 func Main(m *testing.M) (code int) {
+	// Provide an entrypoint for tests that use a fake go/packages driver.
+	drivertest.RunIfChild()
+
 	defer func() {
 		if runner != nil {
 			if err := runner.Close(); err != nil {