internal/lsp: add go get quick fix on failing imports

With -mod=readonly set, we no longer automatically add new requires to
go.mod, even the temporary one. We have the go mod tidy code lens, but
that only works on saved files, even in 1.16 due to golang/go#42491.
Plus we may remove the code lens's network access in the future.

Add a simple quick fix for import errors that runs (the moral equivalent
of) go get on the missing import.

Change-Id: Id5764a37ce7db0dce5370da9d648462aefa2042b
Reviewed-on: https://go-review.googlesource.com/c/tools/+/274121
Trust: Heschi Kreinick <heschi@google.com>
Run-TryBot: Heschi Kreinick <heschi@google.com>
gopls-CI: kokoro <noreply+kokoro@google.com>
TryBot-Result: Go Bot <gobot@golang.org>
Reviewed-by: Rebecca Stambler <rstambler@golang.org>
Reviewed-by: Robert Findley <rfindley@google.com>
This commit is contained in:
Heschi Kreinick 2020-11-10 18:20:37 -05:00
parent 43adb69d7e
commit bd5d160bec
7 changed files with 134 additions and 3 deletions

View File

@ -47,6 +47,12 @@ undeclared_name adds a variable declaration for an undeclared
name.
### **go get package**
Identifier: `gopls.go_get_package`
go_get_package runs `go get` to fetch a package.
### **Add dependency**
Identifier: `gopls.add_dependency`

View File

@ -110,6 +110,54 @@ func main() {
})
}
func TestGoGetFix(t *testing.T) {
testenv.NeedsGo1Point(t, 14)
const mod = `
-- go.mod --
module mod.com
go 1.12
-- main.go --
package main
import "example.com/blah"
var _ = blah.Name
`
const want = `module mod.com
go 1.12
require example.com v1.2.3
`
runModfileTest(t, mod, proxy, func(t *testing.T, env *Env) {
if strings.Contains(t.Name(), "workspace_module") {
t.Skip("workspace module mode doesn't set -mod=readonly")
}
env.OpenFile("main.go")
var d protocol.PublishDiagnosticsParams
env.Await(
OnceMet(
env.DiagnosticAtRegexp("main.go", `"example.com/blah"`),
ReadDiagnostics("main.go", &d),
),
)
var goGetDiag protocol.Diagnostic
for _, diag := range d.Diagnostics {
if strings.Contains(diag.Message, "could not import") {
goGetDiag = diag
}
}
env.ApplyQuickFixes("main.go", []protocol.Diagnostic{goGetDiag})
if got := env.ReadWorkspaceFile("go.mod"); got != want {
t.Fatalf("unexpected go.mod content:\n%s", tests.Diff(want, got))
}
})
}
// Tests that multiple missing dependencies gives good single fixes.
func TestMissingDependencyFixes(t *testing.T) {
testenv.NeedsGo1Point(t, 14)

View File

@ -128,7 +128,7 @@ func sourceError(ctx context.Context, snapshot *snapshot, pkg *pkg, e interface{
msg = e.Message
kind = source.Analysis
category = e.Category
fixes, err = suggestedFixes(snapshot, pkg, e)
fixes, err = suggestedAnalysisFixes(snapshot, pkg, e)
if err != nil {
return nil, err
}
@ -154,7 +154,7 @@ func sourceError(ctx context.Context, snapshot *snapshot, pkg *pkg, e interface{
}, nil
}
func suggestedFixes(snapshot *snapshot, pkg *pkg, diag *analysis.Diagnostic) ([]source.SuggestedFix, error) {
func suggestedAnalysisFixes(snapshot *snapshot, pkg *pkg, diag *analysis.Diagnostic) ([]source.SuggestedFix, error) {
var fixes []source.SuggestedFix
for _, fix := range diag.SuggestedFixes {
edits := make(map[span.URI][]protocol.TextEdit)

View File

@ -7,6 +7,7 @@ package lsp
import (
"context"
"fmt"
"regexp"
"sort"
"strings"
@ -103,6 +104,16 @@ func (s *Server) codeAction(ctx context.Context, params *protocol.CodeActionPara
})
}
}
// Fix unresolved imports with "go get". This is separate from the
// goimports fixes because goimports will not remove an import
// that appears to be used, even if currently unresolved.
actions, err := goGetFixes(ctx, snapshot, fh.URI(), diagnostics)
if err != nil {
return nil, err
}
codeActions = append(codeActions, actions...)
// Send all of the import edits as one code action if the file is
// being organized.
if wanted[protocol.SourceOrganizeImports] && len(importEdits) > 0 {
@ -349,6 +360,38 @@ func diagnosticToAnalyzer(snapshot source.Snapshot, src, msg string) (analyzer *
return nil
}
var importErrorRe = regexp.MustCompile(`could not import ([^\s]+)`)
func goGetFixes(ctx context.Context, snapshot source.Snapshot, uri span.URI, diagnostics []protocol.Diagnostic) ([]protocol.CodeAction, error) {
if snapshot.GoModForFile(ctx, uri) == "" {
// Go get only supports module mode for now.
return nil, nil
}
var actions []protocol.CodeAction
for _, diag := range diagnostics {
matches := importErrorRe.FindStringSubmatch(diag.Message)
if len(matches) == 0 {
return nil, nil
}
args, err := source.MarshalArgs(uri, matches[1])
if err != nil {
return nil, err
}
actions = append(actions, protocol.CodeAction{
Title: fmt.Sprintf("go get package %v", matches[1]),
Diagnostics: []protocol.Diagnostic{diag},
Kind: protocol.QuickFix,
Command: &protocol.Command{
Title: source.CommandGoGetPackage.Title,
Command: source.CommandGoGetPackage.ID(),
Arguments: args,
},
})
}
return actions, nil
}
func convenienceFixes(ctx context.Context, snapshot source.Snapshot, pkg source.Package, uri span.URI, rng protocol.Range) ([]protocol.CodeAction, error) {
var analyzers []*analysis.Analyzer
for _, a := range snapshot.View().Options().ConvenienceAnalyzers {

View File

@ -12,6 +12,7 @@ import (
"io"
"io/ioutil"
"path/filepath"
"strings"
"golang.org/x/tools/internal/event"
"golang.org/x/tools/internal/gocommand"
@ -228,6 +229,19 @@ func (s *Server) runCommand(ctx context.Context, work *workDone, command *source
return err
}
return s.runGoGetModule(ctx, snapshot, uri.SpanURI(), addRequire, goCmdArgs)
case source.CommandGoGetPackage:
var uri protocol.DocumentURI
var pkg string
if err := source.UnmarshalArgs(args, &uri, &pkg); err != nil {
return err
}
snapshot, _, ok, release, err := s.beginFileRequest(ctx, uri, source.UnknownKind)
defer release()
if !ok {
return err
}
return s.runGoGetPackage(ctx, snapshot, uri.SpanURI(), pkg)
case source.CommandToggleDetails:
var fileURI protocol.DocumentURI
if err := source.UnmarshalArgs(args, &fileURI); err != nil {
@ -387,6 +401,19 @@ func (s *Server) runGoGenerate(ctx context.Context, snapshot source.Snapshot, di
return nil
}
func (s *Server) runGoGetPackage(ctx context.Context, snapshot source.Snapshot, uri span.URI, pkg string) error {
stdout, err := snapshot.RunGoCommandDirect(ctx, source.WriteTemporaryModFile|source.AllowNetwork, &gocommand.Invocation{
Verb: "list",
Args: []string{"-f", "{{.Module.Path}}@{{.Module.Version}}", pkg},
WorkingDir: filepath.Dir(uri.Filename()),
})
if err != nil {
return err
}
ver := strings.TrimSpace(stdout.String())
return s.runGoGetModule(ctx, snapshot, uri, true, []string{ver})
}
func (s *Server) runGoGetModule(ctx context.Context, snapshot source.Snapshot, uri span.URI, addRequire bool, args []string) error {
if addRequire {
// Using go get to create a new dependency results in an

File diff suppressed because one or more lines are too long

View File

@ -64,6 +64,7 @@ var Commands = []*Command{
CommandTidy,
CommandUpdateGoSum,
CommandUndeclaredName,
CommandGoGetPackage,
CommandAddDependency,
CommandUpgradeDependency,
CommandRemoveDependency,
@ -100,6 +101,12 @@ var (
Title: "Run go mod vendor",
}
// CommandGoGetPackage runs `go get` to fetch a package.
CommandGoGetPackage = &Command{
Name: "go_get_package",
Title: "go get package",
}
// CommandUpdateGoSum updates the go.sum file for a module.
CommandUpdateGoSum = &Command{
Name: "update_go_sum",