internal/lsp: try to parse diagnostics out of `go list` errors
This change attempts to parse diagnostics out of `go list` error
messages so that we can present them in a better way to the user. This
approach is definitely tailored to the unknown revision error described
in golang/go#38232, but we can modify it to handle other cases as well.
Fixes golang/go#38232
Change-Id: I0b0a8c39a189a127dc36894a25614535c804a3f0
Reviewed-on: https://go-review.googlesource.com/c/tools/+/242477
Run-TryBot: Rebecca Stambler <rstambler@golang.org>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Heschi Kreinick <heschi@google.com>
diff --git a/internal/lsp/cache/load.go b/internal/lsp/cache/load.go
index 1ce17fc..09b8f0c 100644
--- a/internal/lsp/cache/load.go
+++ b/internal/lsp/cache/load.go
@@ -124,7 +124,7 @@
event.Log(ctx, "go/packages.Load", tag.Snapshot.Of(s.ID()), tag.Directory.Of(cfg.Dir), tag.Query.Of(query), tag.PackageCount.Of(len(pkgs)))
}
if len(pkgs) == 0 {
- return err
+ return errors.Errorf("%v: %w", err, source.PackagesLoadError)
}
for _, pkg := range pkgs {
diff --git a/internal/lsp/diagnostics.go b/internal/lsp/diagnostics.go
index e5a83ad..fa62dae 100644
--- a/internal/lsp/diagnostics.go
+++ b/internal/lsp/diagnostics.go
@@ -16,6 +16,7 @@
"golang.org/x/tools/internal/lsp/protocol"
"golang.org/x/tools/internal/lsp/source"
"golang.org/x/tools/internal/xcontext"
+ "golang.org/x/xerrors"
)
type diagnosticKey struct {
@@ -61,13 +62,13 @@
var wg sync.WaitGroup
// Diagnose the go.mod file.
- reports, err := mod.Diagnostics(ctx, snapshot)
- if err != nil {
- event.Error(ctx, "warning: diagnose go.mod", err, tag.Directory.Of(snapshot.View().Folder().Filename()))
- }
+ reports, modErr := mod.Diagnostics(ctx, snapshot)
if ctx.Err() != nil {
return nil, nil
}
+ if modErr != nil {
+ event.Error(ctx, "warning: diagnose go.mod", modErr, tag.Directory.Of(snapshot.View().Folder().Filename()))
+ }
for id, diags := range reports {
if id.URI == "" {
event.Error(ctx, "missing URI for module diagnostics", fmt.Errorf("empty URI"), tag.Directory.Of(snapshot.View().Folder().Filename()))
@@ -83,7 +84,19 @@
// Diagnose all of the packages in the workspace.
wsPackages, err := snapshot.WorkspacePackages(ctx)
if err != nil {
- s.handleFatalErrors(ctx, snapshot, err)
+ // Try constructing a more helpful error message out of this error.
+ if s.handleFatalErrors(ctx, snapshot, modErr, err) {
+ return nil, nil
+ }
+ msg := `The code in the workspace failed to compile (see the error message below).
+If you believe this is a mistake, please file an issue: https://github.com/golang/go/issues/new.`
+ event.Error(ctx, msg, err, tag.Snapshot.Of(snapshot.ID()), tag.Directory.Of(snapshot.View().Folder()))
+ if err := s.client.ShowMessage(ctx, &protocol.ShowMessageParams{
+ Type: protocol.Error,
+ Message: fmt.Sprintf("%s\n%v", msg, err),
+ }); err != nil {
+ event.Error(ctx, "ShowMessage failed", err, tag.Directory.Of(snapshot.View().Folder()))
+ }
return nil, nil
}
var shows *protocol.ShowMessageParams
@@ -229,8 +242,15 @@
return reports
}
-func (s *Server) handleFatalErrors(ctx context.Context, snapshot source.Snapshot, err error) {
- switch err {
+func (s *Server) handleFatalErrors(ctx context.Context, snapshot source.Snapshot, modErr, loadErr error) bool {
+ modURI := snapshot.View().ModFile()
+
+ // We currently only have workarounds for errors associated with modules.
+ if modURI == "" {
+ return false
+ }
+
+ switch loadErr {
case source.InconsistentVendoring:
item, err := s.client.ShowMessageRequest(ctx, &protocol.ShowMessageRequestParams{
Type: protocol.Error,
@@ -240,27 +260,45 @@
{Title: "go mod vendor"},
},
})
- if item == nil || err != nil {
- event.Error(ctx, "go mod vendor ShowMessageRequest failed", err, tag.Directory.Of(snapshot.View().Folder()))
- return
+ // If the user closes the pop-up, don't show them further errors.
+ if item == nil {
+ return true
}
- modURI := snapshot.View().ModFile()
+ if err != nil {
+ event.Error(ctx, "go mod vendor ShowMessageRequest failed", err, tag.Directory.Of(snapshot.View().Folder()))
+ return true
+ }
if err := s.directGoModCommand(ctx, protocol.URIFromSpanURI(modURI), "mod", []string{"vendor"}...); err != nil {
if err := s.client.ShowMessage(ctx, &protocol.ShowMessageParams{
Type: protocol.Error,
Message: fmt.Sprintf(`"go mod vendor" failed with %v`, err),
}); err != nil {
- event.Error(ctx, "ShowMessage failed", err)
+ if err != nil {
+ event.Error(ctx, "go mod vendor ShowMessage failed", err, tag.Directory.Of(snapshot.View().Folder()))
+ }
}
}
- default:
- msg := "failed to load workspace packages, skipping diagnostics"
- event.Error(ctx, msg, err, tag.Snapshot.Of(snapshot.ID()), tag.Directory.Of(snapshot.View().Folder()))
- if err := s.client.ShowMessage(ctx, &protocol.ShowMessageParams{
- Type: protocol.Error,
- Message: fmt.Sprintf("%s: %v", msg, err),
- }); err != nil {
- event.Error(ctx, "ShowMessage failed", err, tag.Directory.Of(snapshot.View().Folder()))
- }
+ return true
}
+ // If there is a go.mod-related error, as well as a workspace load error,
+ // there is likely an issue with the go.mod file. Try to parse the error
+ // message and create a diagnostic.
+ if modErr == nil {
+ return false
+ }
+ if xerrors.Is(loadErr, source.PackagesLoadError) {
+ fh, err := snapshot.GetFile(ctx, modURI)
+ if err != nil {
+ return false
+ }
+ diag, err := mod.ExtractGoCommandError(ctx, snapshot, fh, loadErr)
+ if err != nil {
+ return false
+ }
+ s.publishReports(ctx, snapshot, map[diagnosticKey][]*source.Diagnostic{
+ {id: fh.Identity()}: {diag},
+ })
+ return true
+ }
+ return false
}
diff --git a/internal/lsp/mod/diagnostics.go b/internal/lsp/mod/diagnostics.go
index cd36883..e545992 100644
--- a/internal/lsp/mod/diagnostics.go
+++ b/internal/lsp/mod/diagnostics.go
@@ -8,11 +8,17 @@
import (
"context"
+ "fmt"
+ "regexp"
+ "strings"
+ "golang.org/x/mod/modfile"
+ "golang.org/x/mod/module"
"golang.org/x/tools/internal/event"
"golang.org/x/tools/internal/lsp/debug/tag"
"golang.org/x/tools/internal/lsp/protocol"
"golang.org/x/tools/internal/lsp/source"
+ "golang.org/x/tools/internal/span"
)
func Diagnostics(ctx context.Context, snapshot source.Snapshot) (map[source.FileIdentity][]*source.Diagnostic, error) {
@@ -32,6 +38,9 @@
if err == source.ErrTmpModfileUnsupported {
return nil, nil
}
+ reports := map[source.FileIdentity][]*source.Diagnostic{
+ fh.Identity(): {},
+ }
if err != nil {
return nil, err
}
@@ -39,9 +48,6 @@
if err != nil {
return nil, err
}
- reports := map[source.FileIdentity][]*source.Diagnostic{
- fh.Identity(): {},
- }
for _, e := range diagnostics {
diag := &source.Diagnostic{
Message: e.Message,
@@ -119,3 +125,90 @@
func sameDiagnostic(d protocol.Diagnostic, e source.Error) bool {
return d.Message == e.Message && protocol.CompareRange(d.Range, e.Range) == 0 && d.Source == e.Category
}
+
+var moduleAtVersionRe = regexp.MustCompile(`(?P<module>.*)@(?P<version>.*)`)
+
+// ExtractGoCommandError tries to parse errors that come from the go command
+// and shape them into go.mod diagnostics.
+func ExtractGoCommandError(ctx context.Context, snapshot source.Snapshot, fh source.FileHandle, loadErr error) (*source.Diagnostic, error) {
+ // We try to match module versions in error messages. Some examples:
+ //
+ // err: exit status 1: stderr: go: example.com@v1.2.2: reading example.com/@v/v1.2.2.mod: no such file or directory
+ // exit status 1: go: github.com/cockroachdb/apd/v2@v2.0.72: reading github.com/cockroachdb/apd/go.mod at revision v2.0.72: unknown revision v2.0.72
+ //
+ // We split on colons and attempt to match on something that matches
+ // module@version. If we're able to find a match, we try to find anything
+ // that matches it in the go.mod file.
+ var v module.Version
+ for _, s := range strings.Split(loadErr.Error(), ":") {
+ s = strings.TrimSpace(s)
+ match := moduleAtVersionRe.FindStringSubmatch(s)
+ if match == nil || len(match) < 3 {
+ continue
+ }
+ v.Path = match[1]
+ v.Version = match[2]
+ if err := module.Check(v.Path, v.Version); err == nil {
+ break
+ }
+ }
+ pmh, err := snapshot.ParseModHandle(ctx, fh)
+ if err != nil {
+ return nil, err
+ }
+ parsed, m, _, err := pmh.Parse(ctx)
+ if err != nil {
+ return nil, err
+ }
+ toDiagnostic := func(line *modfile.Line) (*source.Diagnostic, error) {
+ rng, err := rangeFromPositions(fh.URI(), m, line.Start, line.End)
+ if err != nil {
+ return nil, err
+ }
+ return &source.Diagnostic{
+ Message: loadErr.Error(),
+ Range: rng,
+ Severity: protocol.SeverityError,
+ }, nil
+ }
+ // Check if there are any require, exclude, or replace statements that
+ // match this module version.
+ for _, req := range parsed.Require {
+ if req.Mod != v {
+ continue
+ }
+ return toDiagnostic(req.Syntax)
+ }
+ for _, ex := range parsed.Exclude {
+ if ex.Mod != v {
+ continue
+ }
+ return toDiagnostic(ex.Syntax)
+ }
+ for _, rep := range parsed.Replace {
+ if rep.New != v && rep.Old != v {
+ continue
+ }
+ return toDiagnostic(rep.Syntax)
+ }
+ return nil, fmt.Errorf("no diagnostics for %v", loadErr)
+}
+
+func rangeFromPositions(uri span.URI, m *protocol.ColumnMapper, s, e modfile.Position) (protocol.Range, error) {
+ toPoint := func(offset int) (span.Point, error) {
+ l, c, err := m.Converter.ToPosition(offset)
+ if err != nil {
+ return span.Point{}, err
+ }
+ return span.NewPoint(l, c, offset), nil
+ }
+ start, err := toPoint(s.Byte)
+ if err != nil {
+ return protocol.Range{}, err
+ }
+ end, err := toPoint(e.Byte)
+ if err != nil {
+ return protocol.Range{}, err
+ }
+ return m.Range(span.New(uri, start, end))
+}
diff --git a/internal/lsp/regtest/modfile_test.go b/internal/lsp/regtest/modfile_test.go
index b71fdd3..2a27fe8 100644
--- a/internal/lsp/regtest/modfile_test.go
+++ b/internal/lsp/regtest/modfile_test.go
@@ -325,6 +325,8 @@
// Reproduces golang/go#38232.
func TestUnknownRevision(t *testing.T) {
+ testenv.NeedsGo1Point(t, 14)
+
const unknown = `
-- go.mod --
module mod.com
@@ -350,7 +352,7 @@
)
env.OpenFile("go.mod")
env.Await(
- SomeShowMessage("failed to load workspace packages, skipping diagnostics"),
+ env.DiagnosticAtRegexp("go.mod", "example.com v1.2.2"),
)
env.RegexpReplace("go.mod", "v1.2.2", "v1.2.3")
env.Editor.SaveBufferWithoutActions(env.Ctx, "go.mod") // go.mod changes must be on disk
@@ -387,7 +389,7 @@
env.RegexpReplace("go.mod", "v1.2.3", "v1.2.2")
env.Editor.SaveBufferWithoutActions(env.Ctx, "go.mod") // go.mod changes must be on disk
env.Await(
- SomeShowMessage("failed to load workspace packages, skipping diagnostics"),
+ env.DiagnosticAtRegexp("go.mod", "example.com v1.2.2"),
)
env.RegexpReplace("go.mod", "v1.2.2", "v1.2.3")
env.Editor.SaveBufferWithoutActions(env.Ctx, "go.mod") // go.mod changes must be on disk
diff --git a/internal/lsp/source/view.go b/internal/lsp/source/view.go
index 036da6c..7fa1077 100644
--- a/internal/lsp/source/view.go
+++ b/internal/lsp/source/view.go
@@ -516,4 +516,7 @@
return fmt.Sprintf("%s:%s: %s", e.URI, e.Range, e.Message)
}
-var InconsistentVendoring = errors.New("inconsistent vendoring")
+var (
+ InconsistentVendoring = errors.New("inconsistent vendoring")
+ PackagesLoadError = errors.New("packages.Load error")
+)