internal/lsp/cache: extract module load errors when go.work is used

When using go.work, the go command packs module-specific errors into
synthetic results corresponding to the module query ("modulepath/...").

Extract these for use in diagnostics, by packing them into a new
moduleErrorMap error type.

Fixes golang/go#50862

Change-Id: I68bf9cf4e9ebf4595acdc4da0347ed97171d637f
Reviewed-on: https://go-review.googlesource.com/c/tools/+/405259
Reviewed-by: Hyang-Ah Hana Kim <hyangah@gmail.com>
gopls-CI: kokoro <noreply+kokoro@google.com>
TryBot-Result: Gopher Robot <gobot@golang.org>
Run-TryBot: Robert Findley <rfindley@google.com>
This commit is contained in:
Robert Findley 2022-03-10 14:17:29 -05:00
parent 6bfd3a440e
commit a3d129cecf
5 changed files with 137 additions and 50 deletions

View File

@ -705,7 +705,7 @@ use (
WithOptions(
ProxyFiles(workspaceModuleProxy),
).Run(t, multiModule, func(t *testing.T, env *Env) {
// Initially, the gopls.mod should cause only the a.com module to be
// Initially, the go.work should cause only the a.com module to be
// loaded. Validate this by jumping to a definition in b.com and ensuring
// that we go to the module cache.
env.OpenFile("moda/a/a.go")
@ -726,7 +726,7 @@ use (
t.Fatal(err)
}
// Now, modify the gopls.mod file on disk to activate the b.com module in
// Now, modify the go.work file on disk to activate the b.com module in
// the workspace.
env.WriteWorkspaceFile("go.work", `
go 1.17
@ -742,26 +742,22 @@ use (
env.OpenFile("modb/go.mod")
env.Await(env.DoneWithOpen())
// TODO(golang/go#50862): the go command drops error messages when using
// go.work, so we need to build our go.mod diagnostics in a different way.
if testenv.Go1Point() < 18 {
var d protocol.PublishDiagnosticsParams
env.Await(
OnceMet(
env.DiagnosticAtRegexpWithMessage("modb/go.mod", `require example.com v1.2.3`, "has not been downloaded"),
ReadDiagnostics("modb/go.mod", &d),
),
)
env.ApplyQuickFixes("modb/go.mod", d.Diagnostics)
env.Await(env.DiagnosticAtRegexp("modb/b/b.go", "x"))
}
var d protocol.PublishDiagnosticsParams
env.Await(
OnceMet(
env.DiagnosticAtRegexpWithMessage("modb/go.mod", `require example.com v1.2.3`, "has not been downloaded"),
ReadDiagnostics("modb/go.mod", &d),
),
)
env.ApplyQuickFixes("modb/go.mod", d.Diagnostics)
env.Await(env.DiagnosticAtRegexp("modb/b/b.go", "x"))
// Jumping to definition should now go to b.com in the workspace.
if err := checkHelloLocation("modb/b/b.go"); err != nil {
t.Fatal(err)
}
// Now, let's modify the gopls.mod *overlay* (not on disk), and verify that
// Now, let's modify the go.work *overlay* (not on disk), and verify that
// this change is only picked up once it is saved.
env.OpenFile("go.work")
env.Await(env.DoneWithOpen())
@ -771,22 +767,26 @@ use (
./moda/a
)`)
// Editing the gopls.mod removes modb from the workspace modules, and so
// should clear outstanding diagnostics...
env.Await(OnceMet(
env.DoneWithChange(),
EmptyOrNoDiagnostics("modb/go.mod"),
))
// ...but does not yet cause a workspace reload, so we should still jump to modb.
// Simply modifying the go.work file does not cause a reload, so we should
// still jump within the workspace.
//
// TODO: should editing the go.work above cause modb diagnostics to be
// suppressed?
env.Await(env.DoneWithChange())
if err := checkHelloLocation("modb/b/b.go"); err != nil {
t.Fatal(err)
}
// Saving should reload the workspace.
env.SaveBufferWithoutActions("go.work")
if err := checkHelloLocation("b.com@v1.2.3/b/b.go"); err != nil {
t.Fatal(err)
}
// This fails if guarded with a OnceMet(DoneWithSave(), ...), because it is
// debounced (and therefore not synchronous with the change).
env.Await(EmptyOrNoDiagnostics("modb/go.mod"))
// Test Formatting.
env.SetBufferContent("go.work", `go 1.18
use (

View File

@ -5,6 +5,7 @@
package cache
import (
"bytes"
"context"
"crypto/sha256"
"errors"
@ -29,9 +30,16 @@ import (
// load calls packages.Load for the given scopes, updating package metadata,
// import graph, and mapped files with the result.
//
// The resulting error may wrap the moduleErrorMap error type, representing
// errors associated with specific modules.
func (s *snapshot) load(ctx context.Context, allowNetwork bool, scopes ...interface{}) (err error) {
var query []string
var containsDir bool // for logging
// Keep track of module query -> module path so that we can later correlate query
// errors with errors.
moduleQueries := make(map[string]string)
for _, scope := range scopes {
if !s.shouldLoad(scope) {
continue
@ -66,7 +74,9 @@ func (s *snapshot) load(ctx context.Context, allowNetwork bool, scopes ...interf
case "std", "cmd":
query = append(query, string(scope))
default:
query = append(query, fmt.Sprintf("%s/...", scope))
modQuery := fmt.Sprintf("%s/...", scope)
query = append(query, modQuery)
moduleQueries[modQuery] = string(scope)
}
case viewLoadScope:
// If we are outside of GOPATH, a module, or some other known
@ -137,7 +147,24 @@ func (s *snapshot) load(ctx context.Context, allowNetwork bool, scopes ...interf
}
return fmt.Errorf("%v: %w", err, source.PackagesLoadError)
}
moduleErrs := make(map[string][]packages.Error) // module path -> errors
for _, pkg := range pkgs {
// The Go command returns synthetic list results for module queries that
// encountered module errors.
//
// For example, given a module path a.mod, we'll query for "a.mod/..." and
// the go command will return a package named "a.mod/..." holding this
// error. Save it for later interpretation.
//
// See golang/go#50862 for more details.
if mod := moduleQueries[pkg.PkgPath]; mod != "" { // a synthetic result for the unloadable module
if len(pkg.Errors) > 0 {
moduleErrs[mod] = pkg.Errors
}
continue
}
if !containsDir || s.view.Options().VerboseOutput {
event.Log(ctx, "go/packages.Load",
tag.Snapshot.Of(s.ID()),
@ -180,9 +207,35 @@ func (s *snapshot) load(ctx context.Context, allowNetwork bool, scopes ...interf
// Rebuild the import graph when the metadata is updated.
s.clearAndRebuildImportGraph()
if len(moduleErrs) > 0 {
return &moduleErrorMap{moduleErrs}
}
return nil
}
type moduleErrorMap struct {
errs map[string][]packages.Error // module path -> errors
}
func (m *moduleErrorMap) Error() string {
var paths []string // sort for stability
for path, errs := range m.errs {
if len(errs) > 0 { // should always be true, but be cautious
paths = append(paths, path)
}
}
sort.Strings(paths)
var buf bytes.Buffer
fmt.Fprintf(&buf, "%d modules have errors:\n", len(paths))
for _, path := range paths {
fmt.Fprintf(&buf, "\t%s", m.errs[path][0].Msg)
}
return buf.String()
}
// workspaceLayoutErrors returns a diagnostic for every open file, as well as
// an error message if there are no open files.
func (s *snapshot) workspaceLayoutError(ctx context.Context) *source.CriticalError {

View File

@ -6,6 +6,7 @@ package cache
import (
"context"
"errors"
"fmt"
"path/filepath"
"regexp"
@ -297,36 +298,68 @@ func (s *snapshot) ModWhy(ctx context.Context, fh source.FileHandle) (map[string
// extractGoCommandError tries to parse errors that come from the go command
// and shape them into go.mod diagnostics.
func (s *snapshot) extractGoCommandErrors(ctx context.Context, goCmdError string) ([]*source.Diagnostic, error) {
diagLocations := map[*source.ParsedModule]span.Span{}
backupDiagLocations := map[*source.ParsedModule]span.Span{}
// The go command emits parse errors for completely invalid go.mod files.
// Those are reported by our own diagnostics and can be ignored here.
// As of writing, we are not aware of any other errors that include
// file/position information, so don't even try to find it.
if strings.Contains(goCmdError, "errors parsing go.mod") {
return nil, nil
// TODO: rename this to 'load errors'
func (s *snapshot) extractGoCommandErrors(ctx context.Context, goCmdError error) []*source.Diagnostic {
if goCmdError == nil {
return nil
}
type locatedErr struct {
spn span.Span
msg string
}
diagLocations := map[*source.ParsedModule]locatedErr{}
backupDiagLocations := map[*source.ParsedModule]locatedErr{}
// If moduleErrs is non-nil, go command errors are scoped to specific
// modules.
var moduleErrs *moduleErrorMap
_ = errors.As(goCmdError, &moduleErrs)
// Match the error against all the mod files in the workspace.
for _, uri := range s.ModFiles() {
fh, err := s.GetFile(ctx, uri)
if err != nil {
return nil, err
event.Error(ctx, "getting modfile for Go command error", err)
continue
}
pm, err := s.ParseMod(ctx, fh)
if err != nil {
return nil, err
// Parsing errors are reported elsewhere
return nil
}
spn, found, err := s.matchErrorToModule(ctx, pm, goCmdError)
if err != nil {
return nil, err
}
if found {
diagLocations[pm] = spn
var msgs []string // error messages to consider
if moduleErrs != nil {
if pm.File.Module != nil {
for _, mes := range moduleErrs.errs[pm.File.Module.Mod.Path] {
msgs = append(msgs, mes.Error())
}
}
} else {
backupDiagLocations[pm] = spn
msgs = append(msgs, goCmdError.Error())
}
for _, msg := range msgs {
if strings.Contains(goCmdError.Error(), "errors parsing go.mod") {
// The go command emits parse errors for completely invalid go.mod files.
// Those are reported by our own diagnostics and can be ignored here.
// As of writing, we are not aware of any other errors that include
// file/position information, so don't even try to find it.
continue
}
spn, found, err := s.matchErrorToModule(ctx, pm, msg)
if err != nil {
event.Error(ctx, "matching error to module", err)
continue
}
le := locatedErr{
spn: spn,
msg: msg,
}
if found {
diagLocations[pm] = le
} else {
backupDiagLocations[pm] = le
}
}
}
@ -336,14 +369,15 @@ func (s *snapshot) extractGoCommandErrors(ctx context.Context, goCmdError string
}
var srcErrs []*source.Diagnostic
for pm, spn := range diagLocations {
diag, err := s.goCommandDiagnostic(pm, spn, goCmdError)
for pm, le := range diagLocations {
diag, err := s.goCommandDiagnostic(pm, le.spn, le.msg)
if err != nil {
return nil, err
event.Error(ctx, "building go command diagnostic", err)
continue
}
srcErrs = append(srcErrs, diag)
}
return srcErrs, nil
return srcErrs
}
var moduleVersionInErrorRe = regexp.MustCompile(`[:\s]([+-._~0-9A-Za-z]+)@([+-._~0-9A-Za-z]+)[:\s]`)

View File

@ -1485,14 +1485,14 @@ func (s *snapshot) awaitLoadedAllErrors(ctx context.Context) *source.CriticalErr
}
if err := s.reloadWorkspace(ctx); err != nil {
diags, _ := s.extractGoCommandErrors(ctx, err.Error())
diags := s.extractGoCommandErrors(ctx, err)
return &source.CriticalError{
MainError: err,
DiagList: diags,
}
}
if err := s.reloadOrphanedFiles(ctx); err != nil {
diags, _ := s.extractGoCommandErrors(ctx, err.Error())
diags := s.extractGoCommandErrors(ctx, err)
return &source.CriticalError{
MainError: err,
DiagList: diags,

View File

@ -671,7 +671,7 @@ func (s *snapshot) loadWorkspace(ctx context.Context, firstAttempt bool) {
}
case err != nil:
event.Error(ctx, "initial workspace load failed", err)
extractedDiags, _ := s.extractGoCommandErrors(ctx, err.Error())
extractedDiags := s.extractGoCommandErrors(ctx, err)
criticalErr = &source.CriticalError{
MainError: err,
DiagList: append(modDiagnostics, extractedDiags...),