mirror of
https://github.com/golang/go
synced 2024-11-18 12:54:44 -07: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:
parent
0a5cd10191
commit
b42efcd11c
2
internal/lsp/cache/load.go
vendored
2
internal/lsp/cache/load.go
vendored
@ -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 {
|
||||||
|
@ -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:
|
|
||||||
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
|
||||||
|
}
|
||||||
|
@ -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))
|
||||||
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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")
|
||||||
|
)
|
||||||
|
Loading…
Reference in New Issue
Block a user