1
0
mirror of https://github.com/golang/go synced 2024-09-30 16:28:32 -06:00

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>
This commit is contained in:
Rebecca Stambler 2020-07-14 01:50:57 -04:00
parent 0a5cd10191
commit b42efcd11c
5 changed files with 164 additions and 28 deletions

View File

@ -124,7 +124,7 @@ func (s *snapshot) load(ctx context.Context, scopes ...interface{}) error {
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))) 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 { if len(pkgs) == 0 {
return err return errors.Errorf("%v: %w", err, source.PackagesLoadError)
} }
for _, pkg := range pkgs { for _, pkg := range pkgs {

View File

@ -16,6 +16,7 @@ import (
"golang.org/x/tools/internal/lsp/protocol" "golang.org/x/tools/internal/lsp/protocol"
"golang.org/x/tools/internal/lsp/source" "golang.org/x/tools/internal/lsp/source"
"golang.org/x/tools/internal/xcontext" "golang.org/x/tools/internal/xcontext"
"golang.org/x/xerrors"
) )
type diagnosticKey struct { type diagnosticKey struct {
@ -61,13 +62,13 @@ func (s *Server) diagnose(ctx context.Context, snapshot source.Snapshot, alwaysA
var wg sync.WaitGroup var wg sync.WaitGroup
// Diagnose the go.mod file. // Diagnose the go.mod file.
reports, err := mod.Diagnostics(ctx, snapshot) reports, modErr := mod.Diagnostics(ctx, snapshot)
if err != nil {
event.Error(ctx, "warning: diagnose go.mod", err, tag.Directory.Of(snapshot.View().Folder().Filename()))
}
if ctx.Err() != nil { if ctx.Err() != nil {
return nil, 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 { for id, diags := range reports {
if id.URI == "" { if id.URI == "" {
event.Error(ctx, "missing URI for module diagnostics", fmt.Errorf("empty URI"), tag.Directory.Of(snapshot.View().Folder().Filename())) event.Error(ctx, "missing URI for module diagnostics", fmt.Errorf("empty URI"), tag.Directory.Of(snapshot.View().Folder().Filename()))
@ -83,7 +84,19 @@ func (s *Server) diagnose(ctx context.Context, snapshot source.Snapshot, alwaysA
// Diagnose all of the packages in the workspace. // Diagnose all of the packages in the workspace.
wsPackages, err := snapshot.WorkspacePackages(ctx) wsPackages, err := snapshot.WorkspacePackages(ctx)
if err != nil { 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 return nil, nil
} }
var shows *protocol.ShowMessageParams var shows *protocol.ShowMessageParams
@ -229,8 +242,15 @@ func toProtocolDiagnostics(diagnostics []*source.Diagnostic) []protocol.Diagnost
return reports return reports
} }
func (s *Server) handleFatalErrors(ctx context.Context, snapshot source.Snapshot, err error) { func (s *Server) handleFatalErrors(ctx context.Context, snapshot source.Snapshot, modErr, loadErr error) bool {
switch err { modURI := snapshot.View().ModFile()
// We currently only have workarounds for errors associated with modules.
if modURI == "" {
return false
}
switch loadErr {
case source.InconsistentVendoring: case source.InconsistentVendoring:
item, err := s.client.ShowMessageRequest(ctx, &protocol.ShowMessageRequestParams{ item, err := s.client.ShowMessageRequest(ctx, &protocol.ShowMessageRequestParams{
Type: protocol.Error, Type: protocol.Error,
@ -240,27 +260,45 @@ See https://github.com/golang/go/issues/39164 for more detail on this issue.`,
{Title: "go mod vendor"}, {Title: "go mod vendor"},
}, },
}) })
if item == nil || err != nil { // If the user closes the pop-up, don't show them further errors.
event.Error(ctx, "go mod vendor ShowMessageRequest failed", err, tag.Directory.Of(snapshot.View().Folder())) if item == nil {
return return true
}
if err != nil {
event.Error(ctx, "go mod vendor ShowMessageRequest failed", err, tag.Directory.Of(snapshot.View().Folder()))
return true
} }
modURI := snapshot.View().ModFile()
if err := s.directGoModCommand(ctx, protocol.URIFromSpanURI(modURI), "mod", []string{"vendor"}...); err != nil { if err := s.directGoModCommand(ctx, protocol.URIFromSpanURI(modURI), "mod", []string{"vendor"}...); err != nil {
if err := s.client.ShowMessage(ctx, &protocol.ShowMessageParams{ if err := s.client.ShowMessage(ctx, &protocol.ShowMessageParams{
Type: protocol.Error, Type: protocol.Error,
Message: fmt.Sprintf(`"go mod vendor" failed with %v`, err), Message: fmt.Sprintf(`"go mod vendor" failed with %v`, err),
}); err != nil { }); 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: return true
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()))
}
} }
// 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
} }

View File

@ -8,11 +8,17 @@ package mod
import ( import (
"context" "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/event"
"golang.org/x/tools/internal/lsp/debug/tag" "golang.org/x/tools/internal/lsp/debug/tag"
"golang.org/x/tools/internal/lsp/protocol" "golang.org/x/tools/internal/lsp/protocol"
"golang.org/x/tools/internal/lsp/source" "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) { func Diagnostics(ctx context.Context, snapshot source.Snapshot) (map[source.FileIdentity][]*source.Diagnostic, error) {
@ -32,6 +38,9 @@ func Diagnostics(ctx context.Context, snapshot source.Snapshot) (map[source.File
if err == source.ErrTmpModfileUnsupported { if err == source.ErrTmpModfileUnsupported {
return nil, nil return nil, nil
} }
reports := map[source.FileIdentity][]*source.Diagnostic{
fh.Identity(): {},
}
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -39,9 +48,6 @@ func Diagnostics(ctx context.Context, snapshot source.Snapshot) (map[source.File
if err != nil { if err != nil {
return nil, err return nil, err
} }
reports := map[source.FileIdentity][]*source.Diagnostic{
fh.Identity(): {},
}
for _, e := range diagnostics { for _, e := range diagnostics {
diag := &source.Diagnostic{ diag := &source.Diagnostic{
Message: e.Message, Message: e.Message,
@ -119,3 +125,90 @@ func SuggestedFixes(ctx context.Context, snapshot source.Snapshot, diags []proto
func sameDiagnostic(d protocol.Diagnostic, e source.Error) bool { 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 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))
}

View File

@ -325,6 +325,8 @@ require (
// Reproduces golang/go#38232. // Reproduces golang/go#38232.
func TestUnknownRevision(t *testing.T) { func TestUnknownRevision(t *testing.T) {
testenv.NeedsGo1Point(t, 14)
const unknown = ` const unknown = `
-- go.mod -- -- go.mod --
module mod.com module mod.com
@ -350,7 +352,7 @@ func main() {
) )
env.OpenFile("go.mod") env.OpenFile("go.mod")
env.Await( 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.RegexpReplace("go.mod", "v1.2.2", "v1.2.3")
env.Editor.SaveBufferWithoutActions(env.Ctx, "go.mod") // go.mod changes must be on disk env.Editor.SaveBufferWithoutActions(env.Ctx, "go.mod") // go.mod changes must be on disk
@ -387,7 +389,7 @@ func main() {
env.RegexpReplace("go.mod", "v1.2.3", "v1.2.2") 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.Editor.SaveBufferWithoutActions(env.Ctx, "go.mod") // go.mod changes must be on disk
env.Await( 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.RegexpReplace("go.mod", "v1.2.2", "v1.2.3")
env.Editor.SaveBufferWithoutActions(env.Ctx, "go.mod") // go.mod changes must be on disk env.Editor.SaveBufferWithoutActions(env.Ctx, "go.mod") // go.mod changes must be on disk

View File

@ -516,4 +516,7 @@ func (e *Error) Error() string {
return fmt.Sprintf("%s:%s: %s", e.URI, e.Range, e.Message) 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")
)