1
0
mirror of https://github.com/golang/go synced 2024-09-30 22:48:32 -06:00
go/internal/lsp/mod/diagnostics.go
Rebecca Stambler 1745ac5bc6 internal/lsp: refactor various module-specific handles in cache
This change separates out different functions of mod handles.
Previously, we had ModHandle and ModTidyHandle. ModHandle was used to
parse go.mod files and get the results of `go mod why` and possible
dependency upgrades.

Now, we factor this out into 4 handles: ParseModHandle, ModWhyHandle,
ModUpgradeHandle, and ModTidyHandle. This allows each handle to be
specific to its own functionality. It also simplifies the code a bit,
as the handles can be written in terms of ParseModHandles instead of
FileHandles.

I may have some follow-up CLs to refactor the `go mod tidy` logic out of
the cache package, though I'm no longer certain that that's a good
choice.

Change-Id: I8e12299dfdda7bb61b05903d9aa474461d7f4836
Reviewed-on: https://go-review.googlesource.com/c/tools/+/239117
Run-TryBot: Rebecca Stambler <rstambler@golang.org>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Heschi Kreinick <heschi@google.com>
2020-06-23 18:31:46 +00:00

192 lines
5.2 KiB
Go

// Copyright 2019 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 mod provides core features related to go.mod file
// handling for use by Go editors and tools.
package mod
import (
"context"
"golang.org/x/mod/modfile"
"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"
)
func Diagnostics(ctx context.Context, snapshot source.Snapshot) (map[source.FileIdentity][]*source.Diagnostic, map[string]*modfile.Require, error) {
uri := snapshot.View().ModFile()
if uri == "" {
return nil, nil, nil
}
ctx, done := event.Start(ctx, "mod.Diagnostics", tag.URI.Of(uri))
defer done()
fh, err := snapshot.GetFile(ctx, uri)
if err != nil {
return nil, nil, err
}
mth, err := snapshot.ModTidyHandle(ctx)
if err == source.ErrTmpModfileUnsupported {
return nil, nil, nil
}
if err != nil {
return nil, nil, err
}
missingDeps, diagnostics, err := mth.Tidy(ctx)
if err != nil {
return nil, nil, err
}
reports := map[source.FileIdentity][]*source.Diagnostic{
fh.Identity(): {},
}
for _, e := range diagnostics {
diag := &source.Diagnostic{
Message: e.Message,
Range: e.Range,
Source: e.Category,
}
if e.Category == "syntax" {
diag.Severity = protocol.SeverityError
} else {
diag.Severity = protocol.SeverityWarning
}
reports[fh.Identity()] = append(reports[fh.Identity()], diag)
}
return reports, missingDeps, nil
}
func SuggestedFixes(ctx context.Context, snapshot source.Snapshot, fh source.FileHandle, diags []protocol.Diagnostic) ([]protocol.CodeAction, error) {
mth, err := snapshot.ModTidyHandle(ctx)
if err == source.ErrTmpModfileUnsupported {
return nil, nil
}
if err != nil {
return nil, err
}
_, diagnostics, err := mth.Tidy(ctx)
if err != nil {
return nil, err
}
errorsMap := make(map[string][]source.Error)
for _, e := range diagnostics {
if errorsMap[e.Message] == nil {
errorsMap[e.Message] = []source.Error{}
}
errorsMap[e.Message] = append(errorsMap[e.Message], e)
}
var actions []protocol.CodeAction
for _, diag := range diags {
for _, e := range errorsMap[diag.Message] {
if !sameDiagnostic(diag, e) {
continue
}
for _, fix := range e.SuggestedFixes {
action := protocol.CodeAction{
Title: fix.Title,
Kind: protocol.QuickFix,
Diagnostics: []protocol.Diagnostic{diag},
Edit: protocol.WorkspaceEdit{},
}
for uri, edits := range fix.Edits {
fh, err := snapshot.GetFile(ctx, uri)
if err != nil {
return nil, err
}
action.Edit.DocumentChanges = append(action.Edit.DocumentChanges, protocol.TextDocumentEdit{
TextDocument: protocol.VersionedTextDocumentIdentifier{
Version: fh.Version(),
TextDocumentIdentifier: protocol.TextDocumentIdentifier{
URI: protocol.URIFromSpanURI(fh.URI()),
},
},
Edits: edits,
})
}
actions = append(actions, action)
}
}
}
return actions, nil
}
func SuggestedGoFixes(ctx context.Context, snapshot source.Snapshot) (map[string]protocol.TextDocumentEdit, error) {
uri := snapshot.View().ModFile()
if uri == "" {
return nil, nil
}
ctx, done := event.Start(ctx, "mod.SuggestedGoFixes", tag.URI.Of(uri))
defer done()
fh, err := snapshot.GetFile(ctx, uri)
if err != nil {
return nil, err
}
mth, err := snapshot.ModTidyHandle(ctx)
if err == source.ErrTmpModfileUnsupported {
return nil, nil
}
if err != nil {
return nil, err
}
missingDeps, _, err := mth.Tidy(ctx)
if err != nil {
return nil, err
}
if len(missingDeps) == 0 {
return nil, nil
}
pmh, err := snapshot.ParseModHandle(ctx, fh)
if err != nil {
return nil, err
}
file, m, _, err := pmh.Parse(ctx)
if err != nil {
return nil, err
}
// Get the contents of the go.mod file before we make any changes.
oldContents, err := fh.Read()
if err != nil {
return nil, err
}
textDocumentEdits := make(map[string]protocol.TextDocumentEdit)
for dep, req := range missingDeps {
// Calculate the quick fix edits that need to be made to the go.mod file.
if err := file.AddRequire(req.Mod.Path, req.Mod.Version); err != nil {
return nil, err
}
file.Cleanup()
newContents, err := file.Format()
if err != nil {
return nil, err
}
// Reset the *modfile.File back to before we added the dependency.
if err := file.DropRequire(req.Mod.Path); err != nil {
return nil, err
}
// Calculate the edits to be made due to the change.
diff := snapshot.View().Options().ComputeEdits(fh.URI(), string(oldContents), string(newContents))
edits, err := source.ToProtocolEdits(m, diff)
if err != nil {
return nil, err
}
textDocumentEdits[dep] = protocol.TextDocumentEdit{
TextDocument: protocol.VersionedTextDocumentIdentifier{
Version: fh.Version(),
TextDocumentIdentifier: protocol.TextDocumentIdentifier{
URI: protocol.URIFromSpanURI(fh.URI()),
},
},
Edits: edits,
}
}
return textDocumentEdits, nil
}
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
}