From c6e168a53a35369b91c68a46cb59a0ff7e9ad77c Mon Sep 17 00:00:00 2001 From: Pontus Leitzler Date: Wed, 29 Jul 2020 11:56:51 +0200 Subject: [PATCH] deps: upgrade x/tools and gopls to 77f530d86f9a (#913) * internal/lsp: handle deletion of file content 77f530d8 * internal/lsp: move fillstruct suggested fixes out of analysis 4025ed84 * internal/lsp: support extract function 6d307edf * internal/lsp: split only on the first = for Go environment variables 9cbb971a * internal/lsp: support go1.12 cf97b7f4 * internal/lsp: don't re-run `go env` for every call to WriteEnv e6549b8d The update also updates the fillstruct logic according to recent changes in gopls where it now return Commands instead of direct edits. As gopls went from edits to commands, it will not be possible to fill more than one struct per fillstruct call. This also adds support for incoming LSP ApplyEdit calls, to be able to use LSP ExecuteCommand. --- cmd/govim/fillstruct.go | 124 +++- cmd/govim/gopls_client.go | 32 +- .../analysisinternal/analysis.go | 93 ++- .../golang_org_x_tools/jsonrpc2/messages.go | 3 +- .../golang_org_x_tools/jsonrpc2/serve.go | 2 +- .../lsp/analysis/fillstruct/fillstruct.go | 254 ++++---- .../golang_org_x_tools/lsp/cache/session.go | 5 +- .../golang_org_x_tools/lsp/cache/view.go | 29 +- .../lsp/cmd/test/cmdtest.go | 4 + .../golang_org_x_tools/lsp/code_action.go | 71 ++- .../golang_org_x_tools/lsp/command.go | 40 ++ .../golang_org_x_tools/lsp/fake/client.go | 4 +- .../golang_org_x_tools/lsp/fake/editor.go | 35 +- .../lsp/regtest/wrappers.go | 4 +- .../golang_org_x_tools/lsp/source/extract.go | 542 +++++++++++++++++- .../lsp/source/fill_struct.go | 69 +++ .../golang_org_x_tools/lsp/source/options.go | 7 +- .../golang_org_x_tools/lsp/tests/tests.go | 26 + cmd/govim/main.go | 7 + .../scenario_fillstruct/fillstruct.txt | 2 + .../scenario_fillstruct/fillstruct_single.txt | 53 ++ go.mod | 4 +- go.sum | 8 +- 23 files changed, 1198 insertions(+), 220 deletions(-) create mode 100644 cmd/govim/internal/golang_org_x_tools/lsp/source/fill_struct.go create mode 100644 cmd/govim/testdata/scenario_fillstruct/fillstruct_single.txt diff --git a/cmd/govim/fillstruct.go b/cmd/govim/fillstruct.go index d124a3a3f..39ce6c617 100644 --- a/cmd/govim/fillstruct.go +++ b/cmd/govim/fillstruct.go @@ -8,6 +8,7 @@ import ( "github.com/govim/govim" "github.com/govim/govim/cmd/govim/internal/golang_org_x_tools/lsp/protocol" "github.com/govim/govim/cmd/govim/internal/golang_org_x_tools/span" + "github.com/govim/govim/cmd/govim/internal/types" ) func (v *vimstate) fillStruct(flags govim.CommandFlags, args ...string) error { @@ -34,33 +35,114 @@ func (v *vimstate) fillStruct(flags govim.CommandFlags, args ...string) error { return nil } - buri := b.URI() - var edits []protocol.TextEdit - for _, ca := range codeActions { - // there should be just a single file - dcs := ca.Edit.DocumentChanges - switch len(dcs) { - case 1: - dc := dcs[0] - // verify that the URI and version of the edits match the buffer - euri := span.URI(dc.TextDocument.TextDocumentIdentifier.URI) - if euri != buri { - return fmt.Errorf("got edits for file %v, but buffer is %v", euri, buri) + // TODO: revisit this logic when gopls enables us to distingush two different + // code action responses. + // + // The response from gopls contain Commands, but gopls currently responds + // with all code actions at the current line (instead of the exact range + // passed as parameter). + // + // If there are more than one command returned, we can't apply them all since + // each one of them contains unspecified parameters that are bound to current + // version of the document. + + // The gopls ExecuteCommand is blocking, and gopls will call back to govim + // using ApplyEdit that must be handled before the blocking is released. + // Since fillstruct is ordered by the user (and the single threaded nature + // of vim), we are effectively blocking ApplyEdit from modifying buffers. + // + // To prevent a deadlock, we create a channel that ApplyEdit can use pass + // edits to this thread (if needed). And then call ExecuteCommand in a + // separate goroutine so that this thread can go on updating buffers + // until the ExecuteCommand is released. When it is, we implicitly know + // that ApplyEdit has been processed. + editsCh := make(chan applyEditCall) + v.govimplugin.applyEditsLock.Lock() + v.govimplugin.applyEditsCh = editsCh + v.govimplugin.applyEditsLock.Unlock() + done := make(chan struct{}) + + var ecErr error + v.tomb.Go(func() error { + // We can only apply one command at the moment since they all target the same document + // version. Let's go for the first one and let the user call fillstruct again if they + // want to fill several structs on the same line. + ca := codeActions[0] + _, ecErr = v.server.ExecuteCommand(context.Background(), &protocol.ExecuteCommandParams{ + Command: ca.Command.Command, + Arguments: ca.Command.Arguments, + WorkDoneProgressParams: protocol.WorkDoneProgressParams{}, + }) + + v.govimplugin.applyEditsLock.Lock() + v.govimplugin.applyEditsCh = nil + v.govimplugin.applyEditsLock.Unlock() + close(done) + return nil + }) + + for { + select { + case <-done: + if ecErr != nil { + return fmt.Errorf("executeCommand failed: %v", ecErr) + } + return nil + case c := <-editsCh: + res, err := v.applyWorkspaceEdit(c.params) + c.responseCh <- applyEditResponse{res, err} + } + } +} + +// applyEditCall represents a single LSP ApplyEdit call including a channel used +// to pass a response back. +type applyEditCall struct { + params *protocol.ApplyWorkspaceEditParams + responseCh chan applyEditResponse +} + +// applyEditResponse represents a LSP ApplyEdit response +type applyEditResponse struct { + res *protocol.ApplyWorkspaceEditResponse + err error +} + +func (v *vimstate) applyWorkspaceEdit(params *protocol.ApplyWorkspaceEditParams) (*protocol.ApplyWorkspaceEditResponse, error) { + res := &protocol.ApplyWorkspaceEditResponse{Applied: true} + + edits := make(map[*types.Buffer][]protocol.TextEdit) + for _, dc := range params.Edit.DocumentChanges { + // verify that the version of the edits matches a buffer + var buf *types.Buffer + for _, b := range v.buffers { + if b.URI() != span.URI(dc.TextDocument.URI) { + continue } + if ev := int(math.Round(dc.TextDocument.Version)); ev > 0 && ev != b.Version { - return fmt.Errorf("got edits for version %v, but current buffer version is %v", ev, b.Version) + return nil, fmt.Errorf("got edits for buffer version %v, found matching buffer with version %v", ev, b.Version) } - edits = append(edits, dc.Edits...) - default: - return fmt.Errorf("expected single file, saw: %v", len(dcs)) + + buf = b } - } - if len(edits) != 0 { - if err := v.applyProtocolTextEdits(b, edits); err != nil { - return err + if buf == nil { + // TODO: we might get edits for files that we don't have open so we need to support that + // as well. For fillstruct this isn't an issue since the user calls it within an open file. + res.FailureReason = fmt.Sprintf("got edits for buffer %v, but didn't find it", dc.TextDocument.URI) + res.Applied = false + return res, nil } + edits[buf] = append(edits[buf], dc.Edits...) } - return nil + for b, e := range edits { + if err := v.applyProtocolTextEdits(b, e); err != nil { + res.FailureReason = err.Error() + res.Applied = false + return res, nil + } + } + return res, nil } diff --git a/cmd/govim/gopls_client.go b/cmd/govim/gopls_client.go index de5eaf425..c3d528237 100644 --- a/cmd/govim/gopls_client.go +++ b/cmd/govim/gopls_client.go @@ -151,9 +151,37 @@ func (g *govimplugin) Configuration(ctxt context.Context, params *protocol.Param return res, nil } -func (g *govimplugin) ApplyEdit(context.Context, *protocol.ApplyWorkspaceEditParams) (*protocol.ApplyWorkspaceEditResponse, error) { +func (g *govimplugin) ApplyEdit(ctxt context.Context, params *protocol.ApplyWorkspaceEditParams) (*protocol.ApplyWorkspaceEditResponse, error) { defer absorbShutdownErr() - panic("ApplyEdit not implemented yet") + g.logGoplsClientf("ApplyEdit: %v", pretty.Sprint(params)) + + var err error + var res *protocol.ApplyWorkspaceEditResponse + g.applyEditsLock.Lock() + if g.applyEditsCh == nil { + // ApplyEdit wasn't send by another blocking call so it's fine to just schedule the edits here + g.applyEditsLock.Unlock() + done := make(chan struct{}) + g.Schedule(func(govim.Govim) error { + v := g.vimstate + res, err = v.applyWorkspaceEdit(params) + close(done) + return nil + }) + <-done + } else { + // There is an ongoing call that can apply edits on the vim thread. A Schedule here would deadlock so + // pass the edits to the vim thread instead. + e := applyEditCall{params: params, responseCh: make(chan applyEditResponse)} + g.applyEditsCh <- e + aer := <-e.responseCh + g.applyEditsLock.Unlock() + res = aer.res + err = aer.err + } + + g.logGoplsClientf("ApplyEdit response: %v", pretty.Sprint(res)) + return res, err } func (g *govimplugin) Event(context.Context, *interface{}) error { diff --git a/cmd/govim/internal/golang_org_x_tools/analysisinternal/analysis.go b/cmd/govim/internal/golang_org_x_tools/analysisinternal/analysis.go index 098298c0e..a194f5339 100644 --- a/cmd/govim/internal/golang_org_x_tools/analysisinternal/analysis.go +++ b/cmd/govim/internal/golang_org_x_tools/analysisinternal/analysis.go @@ -84,9 +84,95 @@ func TypeExpr(fset *token.FileSet, f *ast.File, pkg *types.Package, typ types.Ty default: return ast.NewIdent(t.Name()) } + case *types.Pointer: + x := TypeExpr(fset, f, pkg, t.Elem()) + if x == nil { + return nil + } + return &ast.UnaryExpr{ + Op: token.MUL, + X: x, + } + case *types.Array: + elt := TypeExpr(fset, f, pkg, t.Elem()) + if elt == nil { + return nil + } + return &ast.ArrayType{ + Len: &ast.BasicLit{ + Kind: token.INT, + Value: fmt.Sprintf("%d", t.Len()), + }, + Elt: elt, + } + case *types.Slice: + elt := TypeExpr(fset, f, pkg, t.Elem()) + if elt == nil { + return nil + } + return &ast.ArrayType{ + Elt: elt, + } + case *types.Map: + key := TypeExpr(fset, f, pkg, t.Key()) + value := TypeExpr(fset, f, pkg, t.Elem()) + if key == nil || value == nil { + return nil + } + return &ast.MapType{ + Key: key, + Value: value, + } + case *types.Chan: + dir := ast.ChanDir(t.Dir()) + if t.Dir() == types.SendRecv { + dir = ast.SEND | ast.RECV + } + value := TypeExpr(fset, f, pkg, t.Elem()) + if value == nil { + return nil + } + return &ast.ChanType{ + Dir: dir, + Value: value, + } + case *types.Signature: + var params []*ast.Field + for i := 0; i < t.Params().Len(); i++ { + p := TypeExpr(fset, f, pkg, t.Params().At(i).Type()) + if p == nil { + return nil + } + params = append(params, &ast.Field{ + Type: p, + Names: []*ast.Ident{ + { + Name: t.Params().At(i).Name(), + }, + }, + }) + } + var returns []*ast.Field + for i := 0; i < t.Results().Len(); i++ { + r := TypeExpr(fset, f, pkg, t.Results().At(i).Type()) + if r == nil { + return nil + } + returns = append(returns, &ast.Field{ + Type: r, + }) + } + return &ast.FuncType{ + Params: &ast.FieldList{ + List: params, + }, + Results: &ast.FieldList{ + List: returns, + }, + } case *types.Named: if t.Obj().Pkg() == nil { - return nil + return ast.NewIdent(t.Obj().Name()) } if t.Obj().Pkg() == pkg { return ast.NewIdent(t.Obj().Name()) @@ -109,11 +195,6 @@ func TypeExpr(fset *token.FileSet, f *ast.File, pkg *types.Package, typ types.Ty X: ast.NewIdent(pkgName), Sel: ast.NewIdent(t.Obj().Name()), } - case *types.Pointer: - return &ast.UnaryExpr{ - Op: token.MUL, - X: TypeExpr(fset, f, pkg, t.Elem()), - } default: return nil // TODO: anonymous structs, but who does that } diff --git a/cmd/govim/internal/golang_org_x_tools/jsonrpc2/messages.go b/cmd/govim/internal/golang_org_x_tools/jsonrpc2/messages.go index 58d285d99..c29a0e851 100644 --- a/cmd/govim/internal/golang_org_x_tools/jsonrpc2/messages.go +++ b/cmd/govim/internal/golang_org_x_tools/jsonrpc2/messages.go @@ -6,8 +6,9 @@ package jsonrpc2 import ( "encoding/json" - "errors" "fmt" + + errors "golang.org/x/xerrors" ) // Message is the interface to all jsonrpc2 message types. diff --git a/cmd/govim/internal/golang_org_x_tools/jsonrpc2/serve.go b/cmd/govim/internal/golang_org_x_tools/jsonrpc2/serve.go index 6e5dec393..b1f070bce 100644 --- a/cmd/govim/internal/golang_org_x_tools/jsonrpc2/serve.go +++ b/cmd/govim/internal/golang_org_x_tools/jsonrpc2/serve.go @@ -6,7 +6,6 @@ package jsonrpc2 import ( "context" - "errors" "fmt" "io" "net" @@ -14,6 +13,7 @@ import ( "time" "github.com/govim/govim/cmd/govim/internal/golang_org_x_tools/event" + errors "golang.org/x/xerrors" ) // NOTE: This file provides an experimental API for serving multiple remote diff --git a/cmd/govim/internal/golang_org_x_tools/lsp/analysis/fillstruct/fillstruct.go b/cmd/govim/internal/golang_org_x_tools/lsp/analysis/fillstruct/fillstruct.go index e4944d1d0..ddfaeaab9 100644 --- a/cmd/govim/internal/golang_org_x_tools/lsp/analysis/fillstruct/fillstruct.go +++ b/cmd/govim/internal/golang_org_x_tools/lsp/analysis/fillstruct/fillstruct.go @@ -11,34 +11,24 @@ import ( "fmt" "go/ast" "go/format" - "go/printer" "go/token" "go/types" "log" - "strings" "unicode" "golang.org/x/tools/go/analysis" "golang.org/x/tools/go/analysis/passes/inspect" + "golang.org/x/tools/go/ast/astutil" "golang.org/x/tools/go/ast/inspector" "github.com/govim/govim/cmd/govim/internal/golang_org_x_tools/analysisinternal" ) -const Doc = `suggested input for incomplete struct initializations +const Doc = `note incomplete struct initializations -This analyzer provides the appropriate zero values for all -uninitialized fields of an empty struct. For example, given the following struct: - type Foo struct { - ID int64 - Name string - } -the initialization - var _ = Foo{} -will turn into - var _ = Foo{ - ID: 0, - Name: "", - } +This analyzer provides diagnostics for any struct literals that do not have +any fields initialized. Because the suggested fix for this analysis is +expensive to compute, callers should compute it separately, using the +SuggestedFix function below. ` var Analyzer = &analysis.Analyzer{ @@ -95,11 +85,24 @@ func run(pass *analysis.Pass) (interface{}, error) { return } fieldCount := obj.NumFields() + // Skip any struct that is already populated or that has no fields. if fieldCount == 0 || fieldCount == len(expr.Elts) { return } + var fillable bool + for i := 0; i < fieldCount; i++ { + field := obj.Field(i) + // Ignore fields that are not accessible in the current package. + if field.Pkg() != nil && field.Pkg() != pass.Pkg && !field.Exported() { + continue + } + fillable = true + } + if !fillable { + return + } var name string switch typ := expr.Type.(type) { case *ast.Ident: @@ -109,117 +112,151 @@ func run(pass *analysis.Pass) (interface{}, error) { default: name = "anonymous struct" } + pass.Report(analysis.Diagnostic{ + Message: fmt.Sprintf("Fill %s with default values", name), + Pos: expr.Lbrace, + End: expr.Rbrace, + }) + }) + return nil, nil +} - // Use a new fileset to build up a token.File for the new composite - // literal. We need one line for foo{, one line for }, and one line for - // each field we're going to set. format.Node only cares about line - // numbers, so we don't need to set columns, and each line can be - // 1 byte long. - fset := token.NewFileSet() - tok := fset.AddFile("", -1, fieldCount+2) +func SuggestedFix(fset *token.FileSet, pos token.Pos, content []byte, file *ast.File, pkg *types.Package, info *types.Info) (*analysis.SuggestedFix, error) { + // TODO(rstambler): Using ast.Inspect would probably be more efficient than + // calling PathEnclosingInterval. Switch this approach. + path, _ := astutil.PathEnclosingInterval(file, pos, pos) + if len(path) == 0 { + return nil, fmt.Errorf("no enclosing ast.Node") + } + var expr *ast.CompositeLit + for _, n := range path { + if node, ok := n.(*ast.CompositeLit); ok { + expr = node + break + } + } + if info == nil { + return nil, fmt.Errorf("nil types.Info") + } + typ := info.TypeOf(expr) + if typ == nil { + return nil, fmt.Errorf("no composite literal") + } - line := 2 // account for 1-based lines and the left brace - var elts []ast.Expr - for i := 0; i < fieldCount; i++ { - field := obj.Field(i) + // Find reference to the type declaration of the struct being initialized. + for { + p, ok := typ.Underlying().(*types.Pointer) + if !ok { + break + } + typ = p.Elem() + } + typ = typ.Underlying() - // Ignore fields that are not accessible in the current package. - if field.Pkg() != nil && field.Pkg() != pass.Pkg && !field.Exported() { - continue - } + obj, ok := typ.(*types.Struct) + if !ok { + return nil, fmt.Errorf("unexpected type %v (%T), expected *types.Struct", typ, typ) + } + fieldCount := obj.NumFields() - value := populateValue(pass.Fset, file, pass.Pkg, field.Type()) - if value == nil { - continue - } + // Use a new fileset to build up a token.File for the new composite + // literal. We need one line for foo{, one line for }, and one line for + // each field we're going to set. format.Node only cares about line + // numbers, so we don't need to set columns, and each line can be + // 1 byte long. + fakeFset := token.NewFileSet() + tok := fakeFset.AddFile("", -1, fieldCount+2) - tok.AddLine(line - 1) // add 1 byte per line - if line > tok.LineCount() { - panic(fmt.Sprintf("invalid line number %v (of %v) for fillstruct %s", line, tok.LineCount(), name)) - } - pos := tok.LineStart(line) + line := 2 // account for 1-based lines and the left brace + var elts []ast.Expr + for i := 0; i < fieldCount; i++ { + field := obj.Field(i) - kv := &ast.KeyValueExpr{ - Key: &ast.Ident{ - NamePos: pos, - Name: field.Name(), - }, - Colon: pos, - Value: value, - } - elts = append(elts, kv) - line++ + // Ignore fields that are not accessible in the current package. + if field.Pkg() != nil && field.Pkg() != pkg && !field.Exported() { + continue } - // If all of the struct's fields are unexported, we have nothing to do. - if len(elts) == 0 { - return + value := populateValue(fset, file, pkg, field.Type()) + if value == nil { + continue } - // Add the final line for the right brace. Offset is the number of - // bytes already added plus 1. - tok.AddLine(len(elts) + 1) - line = len(elts) + 2 + tok.AddLine(line - 1) // add 1 byte per line if line > tok.LineCount() { - panic(fmt.Sprintf("invalid line number %v (of %v) for fillstruct %s", line, tok.LineCount(), name)) + panic(fmt.Sprintf("invalid line number %v (of %v) for fillstruct", line, tok.LineCount())) } + pos := tok.LineStart(line) - cl := &ast.CompositeLit{ - Type: expr.Type, - Lbrace: tok.LineStart(1), - Elts: elts, - Rbrace: tok.LineStart(line), + kv := &ast.KeyValueExpr{ + Key: &ast.Ident{ + NamePos: pos, + Name: field.Name(), + }, + Colon: pos, + Value: value, } + elts = append(elts, kv) + line++ + } - // Print the AST to get the the original source code. - var b bytes.Buffer - if err := printer.Fprint(&b, pass.Fset, file); err != nil { - log.Printf("failed to print original file: %s", err) - return - } + // If all of the struct's fields are unexported, we have nothing to do. + if len(elts) == 0 { + return nil, fmt.Errorf("no elements to fill") + } - // Find the line on which the composite literal is declared. - split := strings.Split(b.String(), "\n") - lineNumber := pass.Fset.Position(expr.Lbrace).Line - firstLine := split[lineNumber-1] // lines are 1-indexed + // Add the final line for the right brace. Offset is the number of + // bytes already added plus 1. + tok.AddLine(len(elts) + 1) + line = len(elts) + 2 + if line > tok.LineCount() { + panic(fmt.Sprintf("invalid line number %v (of %v) for fillstruct", line, tok.LineCount())) + } - // Trim the whitespace from the left of the line, and use the index - // to get the amount of whitespace on the left. - trimmed := strings.TrimLeftFunc(firstLine, unicode.IsSpace) - index := strings.Index(firstLine, trimmed) - whitespace := firstLine[:index] + cl := &ast.CompositeLit{ + Type: expr.Type, + Lbrace: tok.LineStart(1), + Elts: elts, + Rbrace: tok.LineStart(line), + } - var newExpr bytes.Buffer - if err := format.Node(&newExpr, fset, cl); err != nil { - log.Printf("failed to format %s: %v", cl.Type, err) - return + // Find the line on which the composite literal is declared. + split := bytes.Split(content, []byte("\n")) + lineNumber := fset.Position(expr.Lbrace).Line + firstLine := split[lineNumber-1] // lines are 1-indexed + + // Trim the whitespace from the left of the line, and use the index + // to get the amount of whitespace on the left. + trimmed := bytes.TrimLeftFunc(firstLine, unicode.IsSpace) + index := bytes.Index(firstLine, trimmed) + whitespace := firstLine[:index] + + var newExpr bytes.Buffer + if err := format.Node(&newExpr, fakeFset, cl); err != nil { + log.Printf("failed to format %s: %v", cl.Type, err) + return nil, err + } + split = bytes.Split(newExpr.Bytes(), []byte("\n")) + newText := bytes.NewBuffer(nil) + for i, s := range split { + // Don't add the extra indentation to the first line. + if i != 0 { + newText.Write(whitespace) } - split = strings.Split(newExpr.String(), "\n") - var newText strings.Builder - for i, s := range split { - // Don't add the extra indentation to the first line. - if i != 0 { - newText.WriteString(whitespace) - } - newText.WriteString(s) - if i < len(split)-1 { - newText.WriteByte('\n') - } + newText.Write(s) + if i < len(split)-1 { + newText.WriteByte('\n') } - pass.Report(analysis.Diagnostic{ - Pos: expr.Lbrace, - End: expr.Rbrace, - SuggestedFixes: []analysis.SuggestedFix{{ - Message: fmt.Sprintf("Fill %s with default values", name), - TextEdits: []analysis.TextEdit{{ - Pos: expr.Pos(), - End: expr.End(), - NewText: []byte(newText.String()), - }}, - }}, - }) - }) - return nil, nil + } + return &analysis.SuggestedFix{ + TextEdits: []analysis.TextEdit{ + { + Pos: expr.Pos(), + End: expr.End(), + NewText: newText.Bytes(), + }, + }, + }, nil } // populateValue constructs an expression to fill the value of a struct field. @@ -278,7 +315,8 @@ func populateValue(fset *token.FileSet, f *ast.File, pkg *types.Package, typ typ Type: &ast.ArrayType{ Elt: a, Len: &ast.BasicLit{ - Kind: token.INT, Value: fmt.Sprintf("%v", u.Len())}, + Kind: token.INT, Value: fmt.Sprintf("%v", u.Len()), + }, }, } case *types.Chan: diff --git a/cmd/govim/internal/golang_org_x_tools/lsp/cache/session.go b/cmd/govim/internal/golang_org_x_tools/lsp/cache/session.go index 1825a5543..4d8177656 100644 --- a/cmd/govim/internal/golang_org_x_tools/lsp/cache/session.go +++ b/cmd/govim/internal/golang_org_x_tools/lsp/cache/session.go @@ -407,9 +407,10 @@ func (s *Session) updateOverlays(ctx context.Context, changes []source.FileModif continue } - // If the file is on disk, check if its content is the same as the overlay. + // If the file is on disk, check if its content is the same as in the + // overlay. Saves don't necessarily come with the file's content. text := c.Text - if text == nil { + if text == nil && c.Action == source.Save { text = o.text } hash := hashContents(text) diff --git a/cmd/govim/internal/golang_org_x_tools/lsp/cache/view.go b/cmd/govim/internal/golang_org_x_tools/lsp/cache/view.go index b63a3e0bf..b9000fe09 100644 --- a/cmd/govim/internal/golang_org_x_tools/lsp/cache/view.go +++ b/cmd/govim/internal/golang_org_x_tools/lsp/cache/view.go @@ -119,6 +119,8 @@ type View struct { // `go env` variables that need to be tracked by gopls. gocache, gomodcache, gopath, goprivate string + // goEnv is the `go env` output collected when a view is created. + // It includes the values of the environment variables above. goEnv map[string]string } @@ -343,21 +345,24 @@ func (v *View) WriteEnv(ctx context.Context, w io.Writer) error { env, buildFlags := v.envLocked() v.optionsMu.Unlock() - // TODO(rstambler): We could probably avoid running this by saving the - // output on original create, but I'm not sure if it's worth it. - inv := gocommand.Invocation{ - Verb: "env", - Env: env, - WorkingDir: v.Folder().Filename(), + fullEnv := make(map[string]string) + for k, v := range v.goEnv { + fullEnv[k] = v } - // Don't go through runGoCommand, as we don't need a temporary go.mod to - // run `go env`. - stdout, err := v.session.gocmdRunner.Run(ctx, inv) - if err != nil { - return err + for _, v := range env { + s := strings.SplitN(v, "=", 2) + if len(s) != 2 { + continue + } + if _, ok := fullEnv[s[0]]; ok { + fullEnv[s[0]] = s[1] + } + } fmt.Fprintf(w, "go env for %v\n(valid build configuration = %v)\n(build flags: %v)\n", v.folder.Filename(), v.hasValidBuildConfiguration, buildFlags) - fmt.Fprint(w, stdout) + for k, v := range fullEnv { + fmt.Fprintf(w, "%s=%s\n", k, v) + } return nil } diff --git a/cmd/govim/internal/golang_org_x_tools/lsp/cmd/test/cmdtest.go b/cmd/govim/internal/golang_org_x_tools/lsp/cmd/test/cmdtest.go index 65fa12a37..4265c84bd 100644 --- a/cmd/govim/internal/golang_org_x_tools/lsp/cmd/test/cmdtest.go +++ b/cmd/govim/internal/golang_org_x_tools/lsp/cmd/test/cmdtest.go @@ -133,6 +133,10 @@ func (r *runner) RankCompletion(t *testing.T, src span.Span, test tests.Completi //TODO: add command line completions tests when it works } +func (r *runner) FunctionExtraction(t *testing.T, start span.Span, end span.Span) { + //TODO: function extraction not supported on command line +} + func (r *runner) runGoplsCmd(t testing.TB, args ...string) (string, string) { rStdout, wStdout, err := os.Pipe() if err != nil { diff --git a/cmd/govim/internal/golang_org_x_tools/lsp/code_action.go b/cmd/govim/internal/golang_org_x_tools/lsp/code_action.go index d21d1a6ab..c5a74f4ba 100644 --- a/cmd/govim/internal/golang_org_x_tools/lsp/code_action.go +++ b/cmd/govim/internal/golang_org_x_tools/lsp/code_action.go @@ -6,6 +6,7 @@ package lsp import ( "context" + "encoding/json" "fmt" "sort" "strings" @@ -13,6 +14,7 @@ import ( "golang.org/x/tools/go/analysis" "github.com/govim/govim/cmd/govim/internal/golang_org_x_tools/event" "github.com/govim/govim/cmd/govim/internal/golang_org_x_tools/imports" + "github.com/govim/govim/cmd/govim/internal/golang_org_x_tools/lsp/analysis/fillstruct" "github.com/govim/govim/cmd/govim/internal/golang_org_x_tools/lsp/debug/tag" "github.com/govim/govim/cmd/govim/internal/golang_org_x_tools/lsp/mod" "github.com/govim/govim/cmd/govim/internal/golang_org_x_tools/lsp/protocol" @@ -51,24 +53,13 @@ func (s *Server) codeAction(ctx context.Context, params *protocol.CodeActionPara var codeActions []protocol.CodeAction switch fh.Kind() { case source.Mod: - if diagnostics := params.Context.Diagnostics; len(diagnostics) > 0 { + if diagnostics := params.Context.Diagnostics; wanted[protocol.SourceOrganizeImports] || len(diagnostics) > 0 { modFixes, err := mod.SuggestedFixes(ctx, snapshot, diagnostics) if err != nil { return nil, err } codeActions = append(codeActions, modFixes...) } - if wanted[protocol.SourceOrganizeImports] { - codeActions = append(codeActions, protocol.CodeAction{ - Title: "Tidy", - Kind: protocol.SourceOrganizeImports, - Command: &protocol.Command{ - Title: "Tidy", - Command: "tidy", - Arguments: []interface{}{fh.URI()}, - }, - }) - } case source.Go: // Don't suggest fixes for generated files, since they are generally // not useful and some editors may apply them automatically on save. @@ -336,22 +327,30 @@ func convenienceFixes(ctx context.Context, snapshot source.Snapshot, ph source.P if d.Range.Start.Line != rng.Start.Line { continue } - for _, fix := range d.SuggestedFixes { + // The fix depends on the category of the analyzer. + switch d.Category { + case fillstruct.Analyzer.Name: + arg, err := json.Marshal(CommandRangeArgument{ + URI: protocol.URIFromSpanURI(d.URI), + Range: rng, + }) + if err != nil { + return nil, err + } action := protocol.CodeAction{ - Title: fix.Title, + Title: d.Message, Kind: protocol.RefactorRewrite, - Edit: protocol.WorkspaceEdit{}, - } - for uri, edits := range fix.Edits { - fh, err := snapshot.GetFile(ctx, uri) - if err != nil { - return nil, err - } - docChanges := documentChanges(fh, edits) - action.Edit.DocumentChanges = append(action.Edit.DocumentChanges, docChanges...) + Command: &protocol.Command{ + Command: source.CommandFillStruct, + Title: d.Message, + Arguments: []interface{}{ + string(arg), + }, + }, } codeActions = append(codeActions, action) } + } return codeActions, nil } @@ -361,22 +360,34 @@ func extractionFixes(ctx context.Context, snapshot source.Snapshot, ph source.Pa if err != nil { return nil, nil } + var actions []protocol.CodeAction edits, err := source.ExtractVariable(ctx, snapshot, fh, rng) if err != nil { return nil, err } - if len(edits) == 0 { - return nil, nil - } - return []protocol.CodeAction{ - { + if len(edits) > 0 { + actions = append(actions, protocol.CodeAction{ Title: "Extract to variable", Kind: protocol.RefactorExtract, Edit: protocol.WorkspaceEdit{ DocumentChanges: documentChanges(fh, edits), }, - }, - }, nil + }) + } + edits, err = source.ExtractFunction(ctx, snapshot, fh, rng) + if err != nil { + return nil, err + } + if len(edits) > 0 { + actions = append(actions, protocol.CodeAction{ + Title: "Extract to function", + Kind: protocol.RefactorExtract, + Edit: protocol.WorkspaceEdit{ + DocumentChanges: documentChanges(fh, edits), + }, + }) + } + return actions, nil } func documentChanges(fh source.FileHandle, edits []protocol.TextEdit) []protocol.TextDocumentEdit { diff --git a/cmd/govim/internal/golang_org_x_tools/lsp/command.go b/cmd/govim/internal/golang_org_x_tools/lsp/command.go index 9264dc10f..4ec7ac37b 100644 --- a/cmd/govim/internal/golang_org_x_tools/lsp/command.go +++ b/cmd/govim/internal/golang_org_x_tools/lsp/command.go @@ -6,6 +6,7 @@ package lsp import ( "context" + "encoding/json" "fmt" "io" "strings" @@ -19,6 +20,11 @@ import ( errors "golang.org/x/xerrors" ) +type CommandRangeArgument struct { + URI protocol.DocumentURI `json:"uri,omitempty"` + Range protocol.Range `json:"range,omitempty"` +} + func (s *Server) executeCommand(ctx context.Context, params *protocol.ExecuteCommandParams) (interface{}, error) { var found bool for _, command := range s.session.Options().SupportedCommands { @@ -89,6 +95,40 @@ func (s *Server) executeCommand(ctx context.Context, params *protocol.ExecuteCom deps := params.Arguments[1].(string) err := s.directGoModCommand(ctx, uri, "get", strings.Split(deps, " ")...) return nil, err + case source.CommandFillStruct: + if len(params.Arguments) != 1 { + return nil, fmt.Errorf("expected 1 arguments, got %v: %v", len(params.Arguments), params.Arguments) + } + var arg CommandRangeArgument + str, ok := params.Arguments[0].(string) + if !ok { + return nil, fmt.Errorf("expected string, got %v (%T)", params.Arguments[0], params.Arguments[0]) + } + if err := json.Unmarshal([]byte(str), &arg); err != nil { + return nil, err + } + snapshot, fh, ok, err := s.beginFileRequest(ctx, arg.URI, source.Go) + if !ok { + return nil, err + } + edits, err := source.FillStruct(ctx, snapshot, fh, arg.Range) + if err != nil { + return nil, err + } + r, err := s.client.ApplyEdit(ctx, &protocol.ApplyWorkspaceEditParams{ + Edit: protocol.WorkspaceEdit{ + DocumentChanges: edits, + }, + }) + if err != nil { + return nil, err + } + if !r.Applied { + return nil, s.client.ShowMessage(ctx, &protocol.ShowMessageParams{ + Type: protocol.Error, + Message: fmt.Sprintf("fillstruct failed: %v", r.FailureReason), + }) + } } return nil, nil } diff --git a/cmd/govim/internal/golang_org_x_tools/lsp/fake/client.go b/cmd/govim/internal/golang_org_x_tools/lsp/fake/client.go index d78b2d45e..0099b27d4 100644 --- a/cmd/govim/internal/golang_org_x_tools/lsp/fake/client.go +++ b/cmd/govim/internal/golang_org_x_tools/lsp/fake/client.go @@ -111,7 +111,9 @@ func (c *Client) ApplyEdit(ctx context.Context, params *protocol.ApplyWorkspaceE for _, change := range params.Edit.DocumentChanges { path := c.editor.sandbox.Workdir.URIToPath(change.TextDocument.URI) edits := convertEdits(change.Edits) - c.editor.EditBuffer(ctx, path, edits) + if err := c.editor.EditBuffer(ctx, path, edits); err != nil { + return nil, err + } } return &protocol.ApplyWorkspaceEditResponse{Applied: true}, nil } diff --git a/cmd/govim/internal/golang_org_x_tools/lsp/fake/editor.go b/cmd/govim/internal/golang_org_x_tools/lsp/fake/editor.go index c6ee5a010..45106d9ba 100644 --- a/cmd/govim/internal/golang_org_x_tools/lsp/fake/editor.go +++ b/cmd/govim/internal/golang_org_x_tools/lsp/fake/editor.go @@ -595,15 +595,20 @@ func (e *Editor) Symbol(ctx context.Context, query string) ([]SymbolInformation, // OrganizeImports requests and performs the source.organizeImports codeAction. func (e *Editor) OrganizeImports(ctx context.Context, path string) error { - return e.codeAction(ctx, path, nil, protocol.SourceOrganizeImports) + return e.codeAction(ctx, path, nil, nil, protocol.SourceOrganizeImports) +} + +// RefactorRewrite requests and performs the source.refactorRewrite codeAction. +func (e *Editor) RefactorRewrite(ctx context.Context, path string, rng *protocol.Range) error { + return e.codeAction(ctx, path, rng, nil, protocol.RefactorRewrite) } // ApplyQuickFixes requests and performs the quickfix codeAction. -func (e *Editor) ApplyQuickFixes(ctx context.Context, path string, diagnostics []protocol.Diagnostic) error { - return e.codeAction(ctx, path, diagnostics, protocol.QuickFix, protocol.SourceFixAll) +func (e *Editor) ApplyQuickFixes(ctx context.Context, path string, rng *protocol.Range, diagnostics []protocol.Diagnostic) error { + return e.codeAction(ctx, path, rng, diagnostics, protocol.QuickFix, protocol.SourceFixAll) } -func (e *Editor) codeAction(ctx context.Context, path string, diagnostics []protocol.Diagnostic, only ...protocol.CodeActionKind) error { +func (e *Editor) codeAction(ctx context.Context, path string, rng *protocol.Range, diagnostics []protocol.Diagnostic, only ...protocol.CodeActionKind) error { if e.Server == nil { return nil } @@ -613,12 +618,13 @@ func (e *Editor) codeAction(ctx context.Context, path string, diagnostics []prot if diagnostics != nil { params.Context.Diagnostics = diagnostics } + if rng != nil { + params.Range = *rng + } actions, err := e.Server.CodeAction(ctx, params) if err != nil { return fmt.Errorf("textDocument/codeAction: %w", err) } - e.mu.Lock() - defer e.mu.Unlock() for _, action := range actions { var match bool for _, o := range only { @@ -637,10 +643,20 @@ func (e *Editor) codeAction(ctx context.Context, path string, diagnostics []prot continue } edits := convertEdits(change.Edits) - if err := e.editBufferLocked(ctx, path, edits); err != nil { + if err := e.EditBuffer(ctx, path, edits); err != nil { return fmt.Errorf("editing buffer %q: %w", path, err) } } + // Execute any commands. The specification says that commands are + // executed after edits are applied. + if action.Command != nil { + if _, err := e.Server.ExecuteCommand(ctx, &protocol.ExecuteCommandParams{ + Command: action.Command.Command, + Arguments: action.Command.Arguments, + }); err != nil { + return err + } + } } return nil } @@ -734,7 +750,7 @@ func (e *Editor) CodeLens(ctx context.Context, path string) ([]protocol.CodeLens } // CodeAction executes a codeAction request on the server. -func (e *Editor) CodeAction(ctx context.Context, path string) ([]protocol.CodeAction, error) { +func (e *Editor) CodeAction(ctx context.Context, path string, rng *protocol.Range) ([]protocol.CodeAction, error) { if e.Server == nil { return nil, nil } @@ -747,6 +763,9 @@ func (e *Editor) CodeAction(ctx context.Context, path string) ([]protocol.CodeAc params := &protocol.CodeActionParams{ TextDocument: e.textDocumentIdentifier(path), } + if rng != nil { + params.Range = *rng + } lens, err := e.Server.CodeAction(ctx, params) if err != nil { return nil, err diff --git a/cmd/govim/internal/golang_org_x_tools/lsp/regtest/wrappers.go b/cmd/govim/internal/golang_org_x_tools/lsp/regtest/wrappers.go index a7736cd87..2e745a30c 100644 --- a/cmd/govim/internal/golang_org_x_tools/lsp/regtest/wrappers.go +++ b/cmd/govim/internal/golang_org_x_tools/lsp/regtest/wrappers.go @@ -165,7 +165,7 @@ func (e *Env) OrganizeImports(name string) { // ApplyQuickFixes processes the quickfix codeAction, calling t.Fatal on any error. func (e *Env) ApplyQuickFixes(path string, diagnostics []protocol.Diagnostic) { e.T.Helper() - if err := e.Editor.ApplyQuickFixes(e.Ctx, path, diagnostics); err != nil { + if err := e.Editor.ApplyQuickFixes(e.Ctx, path, nil, diagnostics); err != nil { e.T.Fatal(err) } } @@ -242,7 +242,7 @@ func (e *Env) CodeLens(path string) []protocol.CodeLens { // t.Fatal if there are errors. func (e *Env) CodeAction(path string) []protocol.CodeAction { e.T.Helper() - actions, err := e.Editor.CodeAction(e.Ctx, path) + actions, err := e.Editor.CodeAction(e.Ctx, path, nil) if err != nil { e.T.Fatal(err) } diff --git a/cmd/govim/internal/golang_org_x_tools/lsp/source/extract.go b/cmd/govim/internal/golang_org_x_tools/lsp/source/extract.go index afb60e0d3..9e92e9b0e 100644 --- a/cmd/govim/internal/golang_org_x_tools/lsp/source/extract.go +++ b/cmd/govim/internal/golang_org_x_tools/lsp/source/extract.go @@ -10,8 +10,11 @@ import ( "fmt" "go/ast" "go/format" + "go/parser" "go/token" "go/types" + "strings" + "unicode" "golang.org/x/tools/go/ast/astutil" "github.com/govim/govim/cmd/govim/internal/golang_org_x_tools/analysisinternal" @@ -36,6 +39,9 @@ func ExtractVariable(ctx context.Context, snapshot Snapshot, fh FileHandle, prot if err != nil { return nil, err } + if rng.Start == rng.End { + return nil, nil + } path, _ := astutil.PathEnclosingInterval(file, rng.Start, rng.End) if len(path) == 0 { return nil, nil @@ -53,25 +59,17 @@ func ExtractVariable(ctx context.Context, snapshot Snapshot, fh FileHandle, prot if rng.Start != node.Pos() || rng.End != node.End() { return nil, nil } - - // Adjust new variable name until no collisons in scope. - scopes := collectScopes(pkg, path, node.Pos()) - name := "x0" - idx := 0 - for !isValidName(name, scopes) { - idx++ - name = fmt.Sprintf("x%d", idx) - } + name := generateAvailableIdentifier(node.Pos(), pkg, path, file) var assignment string expr, ok := node.(ast.Expr) if !ok { return nil, nil } - // Create new AST node for extracted code + // Create new AST node for extracted code. switch expr.(type) { case *ast.BasicLit, *ast.CompositeLit, *ast.IndexExpr, - *ast.SliceExpr, *ast.UnaryExpr, *ast.BinaryExpr, *ast.SelectorExpr: // TODO: stricter rules for selectorExpr + *ast.SliceExpr, *ast.UnaryExpr, *ast.BinaryExpr, *ast.SelectorExpr: // TODO: stricter rules for selectorExpr. assignStmt := &ast.AssignStmt{ Lhs: []ast.Expr{ast.NewIdent(name)}, Tok: token.DEFINE, @@ -93,7 +91,7 @@ func ExtractVariable(ctx context.Context, snapshot Snapshot, fh FileHandle, prot return nil, nil } - // Convert token.Pos to protcol.Position + // Convert token.Pos to protocol.Position. rng = span.NewRange(fset, insertBeforeStmt.Pos(), insertBeforeStmt.End()) spn, err = rng.Span() if err != nil { @@ -107,17 +105,12 @@ func ExtractVariable(ctx context.Context, snapshot Snapshot, fh FileHandle, prot Start: beforeStmtStart, End: beforeStmtStart, } - - // Calculate indentation for insertion - line := tok.Line(insertBeforeStmt.Pos()) - lineOffset := tok.Offset(tok.LineStart(line)) - stmtOffset := tok.Offset(insertBeforeStmt.Pos()) - indent := content[lineOffset:stmtOffset] // space between these is indentation. + indent := calculateIndentation(content, tok, insertBeforeStmt) return []protocol.TextEdit{ { Range: stmtBeforeRng, - NewText: assignment + "\n" + string(indent), + NewText: assignment + "\n" + indent, }, { Range: protoRng, @@ -126,6 +119,17 @@ func ExtractVariable(ctx context.Context, snapshot Snapshot, fh FileHandle, prot }, nil } +// Calculate indentation for insertion. +// When inserting lines of code, we must ensure that the lines have consistent +// formatting (i.e. the proper indentation). To do so, we observe the indentation on the +// line of code on which the insertion occurs. +func calculateIndentation(content []byte, tok *token.File, insertBeforeStmt ast.Node) string { + line := tok.Line(insertBeforeStmt.Pos()) + lineOffset := tok.Offset(tok.LineStart(line)) + stmtOffset := tok.Offset(insertBeforeStmt.Pos()) + return string(content[lineOffset:stmtOffset]) +} + // Check for variable collision in scope. func isValidName(name string, scopes []*types.Scope) bool { for _, scope := range scopes { @@ -138,3 +142,503 @@ func isValidName(name string, scopes []*types.Scope) bool { } return true } + +// ExtractFunction refactors the selected block of code into a new function. It also +// replaces the selected block of code with a call to the extracted function. First, we +// manually adjust the selection range. We remove trailing and leading whitespace +// characters to ensure the range is precisely bounded by AST nodes. Next, we +// determine the variables that will be the paramters and return values of the +// extracted function. Lastly, we construct the call of the function and insert +// this call as well as the extracted function into their proper locations. +func ExtractFunction(ctx context.Context, snapshot Snapshot, fh FileHandle, protoRng protocol.Range) ([]protocol.TextEdit, error) { + pkg, pgh, err := getParsedFile(ctx, snapshot, fh, NarrowestPackageHandle) + if err != nil { + return nil, fmt.Errorf("ExtractFunction: %v", err) + } + file, _, m, _, err := pgh.Cached() + if err != nil { + return nil, err + } + spn, err := m.RangeSpan(protoRng) + if err != nil { + return nil, err + } + rng, err := spn.Range(m.Converter) + if err != nil { + return nil, err + } + if rng.Start == rng.End { + return nil, nil + } + content, err := fh.Read() + if err != nil { + return nil, err + } + fset := snapshot.View().Session().Cache().FileSet() + tok := fset.File(file.Pos()) + if tok == nil { + return nil, fmt.Errorf("ExtractFunction: no token.File for %s", fh.URI()) + } + rng = adjustRangeForWhitespace(content, tok, rng) + path, _ := astutil.PathEnclosingInterval(file, rng.Start, rng.End) + if len(path) == 0 { + return nil, nil + } + // Node that encloses selection must be a statement. + // TODO: Support function extraction for an expression. + if _, ok := path[0].(ast.Stmt); !ok { + return nil, nil + } + info := pkg.GetTypesInfo() + if info == nil { + return nil, fmt.Errorf("nil TypesInfo") + } + fileScope := info.Scopes[file] + if fileScope == nil { + return nil, nil + } + pkgScope := fileScope.Parent() + if pkgScope == nil { + return nil, nil + } + // Find function enclosing the selection. + var outer *ast.FuncDecl + for _, p := range path { + if p, ok := p.(*ast.FuncDecl); ok { + outer = p + break + } + } + if outer == nil { + return nil, nil + } + // At the moment, we don't extract selections containing return statements, + // as they are more complex and need to be adjusted to maintain correctness. + // TODO: Support extracting and rewriting code with return statements. + var containsReturn bool + ast.Inspect(outer, func(n ast.Node) bool { + if n == nil { + return true + } + if rng.Start <= n.Pos() && n.End() <= rng.End { + if _, ok := n.(*ast.ReturnStmt); ok { + containsReturn = true + return false + } + } + return n.Pos() <= rng.End + }) + if containsReturn { + return nil, nil + } + // Find the nodes at the start and end of the selection. + var start, end ast.Node + ast.Inspect(outer, func(n ast.Node) bool { + if n == nil { + return true + } + if n.Pos() == rng.Start && n.End() <= rng.End { + start = n + } + if n.End() == rng.End && n.Pos() >= rng.Start { + end = n + } + return n.Pos() <= rng.End + }) + if start == nil || end == nil { + return nil, nil + } + + // Now that we have determined the correct range for the selection block, + // we must determine the signature of the extracted function. We will then replace + // the block with an assignment statement that calls the extracted function with + // the appropriate parameters and return values. + free, vars, assigned := collectFreeVars(info, file, fileScope, pkgScope, rng, path[0]) + + var ( + params, returns []ast.Expr // used when calling the extracted function + paramTypes, returnTypes []*ast.Field // used in the signature of the extracted function + uninitialized []types.Object // vars we will need to initialize before the call + ) + + // Avoid duplicates while traversing vars and uninitialzed. + seenVars := make(map[types.Object]ast.Expr) + seenUninitialized := make(map[types.Object]struct{}) + + // Each identifier in the selected block must become (1) a parameter to the + // extracted function, (2) a return value of the extracted function, or (3) a local + // variable in the extracted function. Determine the outcome(s) for each variable + // based on whether it is free, altered within the selected block, and used outside + // of the selected block. + for _, obj := range vars { + if _, ok := seenVars[obj]; ok { + continue + } + typ := analysisinternal.TypeExpr(fset, file, pkg.GetTypes(), obj.Type()) + if typ == nil { + return nil, fmt.Errorf("nil AST expression for type: %v", obj.Name()) + } + seenVars[obj] = typ + identifier := ast.NewIdent(obj.Name()) + // An identifier must meet two conditions to become a return value of the + // extracted function. (1) it must be used at least once after the + // selection (isUsed), and (2) its value must be initialized or reassigned + // within the selection (isAssigned). + isUsed := objUsed(obj, info, rng.End, obj.Parent().End()) + _, isAssigned := assigned[obj] + _, isFree := free[obj] + if isUsed && isAssigned { + returnTypes = append(returnTypes, &ast.Field{Type: typ}) + returns = append(returns, identifier) + if !isFree { + uninitialized = append(uninitialized, obj) + } + } + // All free variables are parameters of and passed as arguments to the + // extracted function. + if isFree { + params = append(params, identifier) + paramTypes = append(paramTypes, &ast.Field{ + Names: []*ast.Ident{identifier}, + Type: typ, + }) + } + } + + // Our preference is to replace the selected block with an "x, y, z := fn()" style + // assignment statement. We can use this style when none of the variables in the + // extracted function's return statement have already be initialized outside of the + // selected block. However, for example, if z is already defined elsewhere, we + // replace the selected block with: + // + // var x int + // var y string + // x, y, z = fn() + // + var initializations string + if len(uninitialized) > 0 && len(uninitialized) != len(returns) { + var declarations []ast.Stmt + for _, obj := range uninitialized { + if _, ok := seenUninitialized[obj]; ok { + continue + } + seenUninitialized[obj] = struct{}{} + valSpec := &ast.ValueSpec{ + Names: []*ast.Ident{ast.NewIdent(obj.Name())}, + Type: seenVars[obj], + } + genDecl := &ast.GenDecl{ + Tok: token.VAR, + Specs: []ast.Spec{valSpec}, + } + declarations = append(declarations, &ast.DeclStmt{Decl: genDecl}) + } + var declBuf bytes.Buffer + if err = format.Node(&declBuf, fset, declarations); err != nil { + return nil, err + } + indent := calculateIndentation(content, tok, start) + // Add proper indentation to each declaration. Also add formatting to + // the line following the last initialization to ensure that subsequent + // edits begin at the proper location. + initializations = strings.ReplaceAll(declBuf.String(), "\n", "\n"+indent) + + "\n" + indent + } + + name := generateAvailableIdentifier(start.Pos(), pkg, path, file) + var replace ast.Node + if len(returns) > 0 { + // If none of the variables on the left-hand side of the function call have + // been initialized before the selection, we can use := instead of =. + assignTok := token.ASSIGN + if len(uninitialized) == len(returns) { + assignTok = token.DEFINE + } + callExpr := &ast.CallExpr{ + Fun: ast.NewIdent(name), + Args: params, + } + replace = &ast.AssignStmt{ + Lhs: returns, + Tok: assignTok, + Rhs: []ast.Expr{callExpr}, + } + } else { + replace = &ast.CallExpr{ + Fun: ast.NewIdent(name), + Args: params, + } + } + + startOffset := tok.Offset(rng.Start) + endOffset := tok.Offset(rng.End) + selection := content[startOffset:endOffset] + // Put selection in constructed file to parse and produce block statement. We can + // then use the block statement to traverse and edit extracted function without + // altering the original file. + text := "package main\nfunc _() { " + string(selection) + " }" + extract, err := parser.ParseFile(fset, "", text, 0) + if err != nil { + return nil, err + } + if len(extract.Decls) == 0 { + return nil, fmt.Errorf("parsed file does not contain any declarations") + } + decl, ok := extract.Decls[0].(*ast.FuncDecl) + if !ok { + return nil, fmt.Errorf("parsed file does not contain expected function declaration") + } + // Add return statement to the end of the new function. + if len(returns) > 0 { + decl.Body.List = append(decl.Body.List, + &ast.ReturnStmt{Results: returns}, + ) + } + funcDecl := &ast.FuncDecl{ + Name: ast.NewIdent(name), + Type: &ast.FuncType{ + Params: &ast.FieldList{List: paramTypes}, + Results: &ast.FieldList{List: returnTypes}, + }, + Body: decl.Body, + } + + var replaceBuf, newFuncBuf bytes.Buffer + if err := format.Node(&replaceBuf, fset, replace); err != nil { + return nil, err + } + if err := format.Node(&newFuncBuf, fset, funcDecl); err != nil { + return nil, err + } + + outerStart := tok.Offset(outer.Pos()) + outerEnd := tok.Offset(outer.End()) + // We're going to replace the whole enclosing function, + // so preserve the text before and after the selected block. + before := content[outerStart:startOffset] + after := content[endOffset:outerEnd] + var fullReplacement strings.Builder + fullReplacement.Write(before) + fullReplacement.WriteString(initializations) // add any initializations, if needed + fullReplacement.Write(replaceBuf.Bytes()) // call the extracted function + fullReplacement.Write(after) + fullReplacement.WriteString("\n\n") // add newlines after the enclosing function + fullReplacement.Write(newFuncBuf.Bytes()) // insert the extracted function + + // Convert enclosing function's span.Range to protocol.Range. + rng = span.NewRange(fset, outer.Pos(), outer.End()) + spn, err = rng.Span() + if err != nil { + return nil, nil + } + startFunc, err := m.Position(spn.Start()) + if err != nil { + return nil, nil + } + endFunc, err := m.Position(spn.End()) + if err != nil { + return nil, nil + } + funcLoc := protocol.Range{ + Start: startFunc, + End: endFunc, + } + return []protocol.TextEdit{ + { + Range: funcLoc, + NewText: fullReplacement.String(), + }, + }, nil +} + +// collectFreeVars maps each identifier in the given range to whether it is "free." +// Given a range, a variable in that range is defined as "free" if it is declared +// outside of the range and neither at the file scope nor package scope. These free +// variables will be used as arguments in the extracted function. It also returns a +// list of identifiers that may need to be returned by the extracted function. +// Some of the code in this function has been adapted from tools/cmd/guru/freevars.go. +func collectFreeVars(info *types.Info, file *ast.File, fileScope *types.Scope, + pkgScope *types.Scope, rng span.Range, node ast.Node) (map[types.Object]struct{}, []types.Object, map[types.Object]struct{}) { + // id returns non-nil if n denotes an object that is referenced by the span + // and defined either within the span or in the lexical environment. The bool + // return value acts as an indicator for where it was defined. + id := func(n *ast.Ident) (types.Object, bool) { + obj := info.Uses[n] + if obj == nil { + return info.Defs[n], false + } + if _, ok := obj.(*types.PkgName); ok { + return nil, false // imported package + } + if !(file.Pos() <= obj.Pos() && obj.Pos() <= file.End()) { + return nil, false // not defined in this file + } + scope := obj.Parent() + if scope == nil { + return nil, false // e.g. interface method, struct field + } + if scope == fileScope || scope == pkgScope { + return nil, false // defined at file or package scope + } + if rng.Start <= obj.Pos() && obj.Pos() <= rng.End { + return obj, false // defined within selection => not free + } + return obj, true + } + // sel returns non-nil if n denotes a selection o.x.y that is referenced by the + // span and defined either within the span or in the lexical environment. The bool + // return value acts as an indicator for where it was defined. + var sel func(n *ast.SelectorExpr) (types.Object, bool) + sel = func(n *ast.SelectorExpr) (types.Object, bool) { + switch x := astutil.Unparen(n.X).(type) { + case *ast.SelectorExpr: + return sel(x) + case *ast.Ident: + return id(x) + } + return nil, false + } + free := make(map[types.Object]struct{}) + var vars []types.Object + ast.Inspect(node, func(n ast.Node) bool { + if n == nil { + return true + } + if rng.Start <= n.Pos() && n.End() <= rng.End { + var obj types.Object + var isFree, prune bool + switch n := n.(type) { + case *ast.Ident: + obj, isFree = id(n) + case *ast.SelectorExpr: + obj, isFree = sel(n) + prune = true + } + if obj != nil && obj.Name() != "_" { + if isFree { + free[obj] = struct{}{} + } + vars = append(vars, obj) + if prune { + return false + } + } + } + return n.Pos() <= rng.End + }) + + // Find identifiers that are initialized or whose values are altered at some + // point in the selected block. For example, in a selected block from lines 2-4, + // variables x, y, and z are included in assigned. However, in a selected block + // from lines 3-4, only variables y and z are included in assigned. + // + // 1: var a int + // 2: var x int + // 3: y := 3 + // 4: z := x + a + // + assigned := make(map[types.Object]struct{}) + ast.Inspect(node, func(n ast.Node) bool { + if n == nil { + return true + } + if n.Pos() < rng.Start || n.End() > rng.End { + return n.Pos() <= rng.End + } + switch n := n.(type) { + case *ast.AssignStmt: + for _, assignment := range n.Lhs { + if assignment, ok := assignment.(*ast.Ident); ok { + obj, _ := id(assignment) + if obj == nil { + continue + } + assigned[obj] = struct{}{} + } + } + return false + case *ast.DeclStmt: + gen, ok := n.Decl.(*ast.GenDecl) + if !ok { + return true + } + for _, spec := range gen.Specs { + vSpecs, ok := spec.(*ast.ValueSpec) + if !ok { + continue + } + for _, vSpec := range vSpecs.Names { + obj, _ := id(vSpec) + if obj == nil { + continue + } + assigned[obj] = struct{}{} + } + } + return false + } + return true + }) + return free, vars, assigned +} + +// Adjust new function name until no collisons in scope. Possible collisions include +// other function and variable names. +func generateAvailableIdentifier(pos token.Pos, pkg Package, path []ast.Node, file *ast.File) string { + scopes := collectScopes(pkg, path, pos) + var idx int + name := "x0" + for file.Scope.Lookup(name) != nil || !isValidName(name, scopes) { + idx++ + name = fmt.Sprintf("x%d", idx) + } + return name +} + +// adjustRangeForWhitespace adjusts the given range to exclude unnecessary leading or +// trailing whitespace characters from selection. In the following example, each line +// of the if statement is indented once. There are also two extra spaces after the +// closing bracket before the line break. +// +// \tif (true) { +// \t _ = 1 +// \t} \n +// +// By default, a valid range begins at 'if' and ends at the first whitespace character +// after the '}'. But, users are likely to highlight full lines rather than adjusting +// their cursors for whitespace. To support this use case, we must manually adjust the +// ranges to match the correct AST node. In this particular example, we would adjust +// rng.Start forward by one byte, and rng.End backwards by two bytes. +func adjustRangeForWhitespace(content []byte, tok *token.File, rng span.Range) span.Range { + offset := tok.Offset(rng.Start) + for offset < len(content) { + if !unicode.IsSpace(rune(content[offset])) { + break + } + // Move forwards one byte to find a non-whitespace character. + offset += 1 + } + rng.Start = tok.Pos(offset) + + offset = tok.Offset(rng.End) + for offset-1 >= 0 { + if !unicode.IsSpace(rune(content[offset-1])) { + break + } + // Move backwards one byte to find a non-whitespace character. + offset -= 1 + } + rng.End = tok.Pos(offset) + return rng +} + +// objUsed checks if the object is used after the selection but within +// the scope of the enclosing function. +func objUsed(obj types.Object, info *types.Info, endSel token.Pos, endScope token.Pos) bool { + for id, ob := range info.Uses { + if obj == ob && endSel < id.Pos() && id.End() <= endScope { + return true + } + } + return false +} diff --git a/cmd/govim/internal/golang_org_x_tools/lsp/source/fill_struct.go b/cmd/govim/internal/golang_org_x_tools/lsp/source/fill_struct.go new file mode 100644 index 000000000..e56561718 --- /dev/null +++ b/cmd/govim/internal/golang_org_x_tools/lsp/source/fill_struct.go @@ -0,0 +1,69 @@ +// Copyright 2020 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 source + +import ( + "context" + "fmt" + + "github.com/govim/govim/cmd/govim/internal/golang_org_x_tools/lsp/analysis/fillstruct" + "github.com/govim/govim/cmd/govim/internal/golang_org_x_tools/lsp/protocol" + "github.com/govim/govim/cmd/govim/internal/golang_org_x_tools/span" +) + +func FillStruct(ctx context.Context, snapshot Snapshot, fh FileHandle, pRng protocol.Range) ([]protocol.TextDocumentEdit, error) { + pkg, pgh, err := getParsedFile(ctx, snapshot, fh, NarrowestPackageHandle) + if err != nil { + return nil, fmt.Errorf("getting file for Identifier: %w", err) + } + file, _, m, _, err := pgh.Cached() + if err != nil { + return nil, err + } + spn, err := m.RangeSpan(pRng) + if err != nil { + return nil, err + } + rng, err := spn.Range(m.Converter) + if err != nil { + return nil, err + } + content, err := fh.Read() + if err != nil { + return nil, err + } + fset := snapshot.View().Session().Cache().FileSet() + fix, err := fillstruct.SuggestedFix(fset, rng.Start, content, file, pkg.GetTypes(), pkg.GetTypesInfo()) + if err != nil { + return nil, err + } + var edits []protocol.TextDocumentEdit + for _, edit := range fix.TextEdits { + rng := span.NewRange(fset, edit.Pos, edit.End) + spn, err = rng.Span() + if err != nil { + return nil, nil + } + clRng, err := m.Range(spn) + if err != nil { + return nil, nil + } + edits = append(edits, protocol.TextDocumentEdit{ + TextDocument: protocol.VersionedTextDocumentIdentifier{ + Version: fh.Version(), + TextDocumentIdentifier: protocol.TextDocumentIdentifier{ + URI: protocol.URIFromSpanURI(fh.URI()), + }, + }, + Edits: []protocol.TextEdit{ + { + Range: clRng, + NewText: string(edit.NewText), + }, + }, + }) + } + return edits, nil +} diff --git a/cmd/govim/internal/golang_org_x_tools/lsp/source/options.go b/cmd/govim/internal/golang_org_x_tools/lsp/source/options.go index 20b120525..2780de2cd 100644 --- a/cmd/govim/internal/golang_org_x_tools/lsp/source/options.go +++ b/cmd/govim/internal/golang_org_x_tools/lsp/source/options.go @@ -72,6 +72,10 @@ const ( // CommandRegenerateCfgo is a gopls command to regenerate cgo definitions. CommandRegenerateCgo = "regenerate_cgo" + + // CommandFillStruct is a gopls command to fill a struct with default + // values. + CommandFillStruct = "fill_struct" ) // DefaultOptions is the options that are used for Gopls execution independent @@ -104,6 +108,7 @@ func DefaultOptions() Options { }, SupportedCommands: []string{ CommandGenerate, + CommandFillStruct, CommandRegenerateCgo, CommandTest, CommandTidy, @@ -708,7 +713,7 @@ func convenienceAnalyzers() map[string]Analyzer { return map[string]Analyzer{ fillstruct.Analyzer.Name: { Analyzer: fillstruct.Analyzer, - enabled: false, + enabled: true, }, } } diff --git a/cmd/govim/internal/golang_org_x_tools/lsp/tests/tests.go b/cmd/govim/internal/golang_org_x_tools/lsp/tests/tests.go index 0ca2ddd49..52f3518d9 100644 --- a/cmd/govim/internal/golang_org_x_tools/lsp/tests/tests.go +++ b/cmd/govim/internal/golang_org_x_tools/lsp/tests/tests.go @@ -57,6 +57,7 @@ type FoldingRanges []span.Span type Formats []span.Span type Imports []span.Span type SuggestedFixes map[span.Span][]string +type FunctionExtractions map[span.Span]span.Span type Definitions map[span.Span]Definition type Implementations map[span.Span][]span.Span type Highlights map[span.Span][]span.Span @@ -87,6 +88,7 @@ type Data struct { Formats Formats Imports Imports SuggestedFixes SuggestedFixes + FunctionExtractions FunctionExtractions Definitions Definitions Implementations Implementations Highlights Highlights @@ -128,6 +130,7 @@ type Tests interface { Format(*testing.T, span.Span) Import(*testing.T, span.Span) SuggestedFix(*testing.T, span.Span, []string) + FunctionExtraction(*testing.T, span.Span, span.Span) Definition(*testing.T, span.Span, Definition) Implementation(*testing.T, span.Span, []span.Span) Highlight(*testing.T, span.Span, []span.Span) @@ -288,6 +291,7 @@ func Load(t testing.TB, exporter packagestest.Exporter, dir string) []*Data { Renames: make(Renames), PrepareRenames: make(PrepareRenames), SuggestedFixes: make(SuggestedFixes), + FunctionExtractions: make(FunctionExtractions), Symbols: make(Symbols), symbolsChildren: make(SymbolsChildren), symbolInformation: make(SymbolInformation), @@ -420,6 +424,7 @@ func Load(t testing.TB, exporter packagestest.Exporter, dir string) []*Data { "signature": datum.collectSignatures, "link": datum.collectLinks, "suggestedfix": datum.collectSuggestedFixes, + "extractfunc": datum.collectFunctionExtractions, }); err != nil { t.Fatal(err) } @@ -611,6 +616,20 @@ func Run(t *testing.T, tests Tests, data *Data) { } }) + t.Run("FunctionExtraction", func(t *testing.T) { + t.Helper() + for start, end := range data.FunctionExtractions { + // Check if we should skip this spn if the -modfile flag is not available. + if shouldSkip(data, start.URI()) { + continue + } + t.Run(SpanName(start), func(t *testing.T) { + t.Helper() + tests.FunctionExtraction(t, start, end) + }) + } + }) + t.Run("Definition", func(t *testing.T) { t.Helper() for spn, d := range data.Definitions { @@ -801,6 +820,7 @@ func checkData(t *testing.T, data *Data) { fmt.Fprintf(buf, "FormatCount = %v\n", len(data.Formats)) fmt.Fprintf(buf, "ImportCount = %v\n", len(data.Imports)) fmt.Fprintf(buf, "SuggestedFixCount = %v\n", len(data.SuggestedFixes)) + fmt.Fprintf(buf, "FunctionExtractionCount = %v\n", len(data.FunctionExtractions)) fmt.Fprintf(buf, "DefinitionsCount = %v\n", definitionCount) fmt.Fprintf(buf, "TypeDefinitionsCount = %v\n", typeDefinitionCount) fmt.Fprintf(buf, "HighlightsCount = %v\n", len(data.Highlights)) @@ -1023,6 +1043,12 @@ func (data *Data) collectSuggestedFixes(spn span.Span, actionKind string) { data.SuggestedFixes[spn] = append(data.SuggestedFixes[spn], actionKind) } +func (data *Data) collectFunctionExtractions(start span.Span, end span.Span) { + if _, ok := data.FunctionExtractions[start]; !ok { + data.FunctionExtractions[start] = end + } +} + func (data *Data) collectDefinitions(src, target span.Span) { data.Definitions[src] = Definition{ Src: src, diff --git a/cmd/govim/main.go b/cmd/govim/main.go index b54ea6749..49f0b68ec 100644 --- a/cmd/govim/main.go +++ b/cmd/govim/main.go @@ -190,6 +190,13 @@ type govimplugin struct { cancelDocHighlight context.CancelFunc cancelDocHighlightLock sync.Mutex + // applyEditsCh is used to pass incoming edit requests (ApplyEdit) to the main thread. + // Incoming ApplyEdit calls will use this channel if set (not nil) instead of schedule + // edits directly. It is used to allow process edits during a blocking call on the vim + // thread. Setting and unsetting this channel is protected by applyEditsLock. + applyEditsCh chan applyEditCall + applyEditsLock sync.Mutex + bufferUpdates chan *bufferUpdate // inShutdown is closed when govim is told to Shutdown diff --git a/cmd/govim/testdata/scenario_fillstruct/fillstruct.txt b/cmd/govim/testdata/scenario_fillstruct/fillstruct.txt index 17be14128..34da0f15f 100644 --- a/cmd/govim/testdata/scenario_fillstruct/fillstruct.txt +++ b/cmd/govim/testdata/scenario_fillstruct/fillstruct.txt @@ -1,5 +1,7 @@ # Test that code action "fill struct" works +skip 'Temporary disabled until gopls support filling multiple structs on one row' + vim ex 'e main.go' vim ex 'call cursor(14,1)' vim ex 'call execute(\"GOVIMFillStruct\")' diff --git a/cmd/govim/testdata/scenario_fillstruct/fillstruct_single.txt b/cmd/govim/testdata/scenario_fillstruct/fillstruct_single.txt new file mode 100644 index 000000000..ecd5f0449 --- /dev/null +++ b/cmd/govim/testdata/scenario_fillstruct/fillstruct_single.txt @@ -0,0 +1,53 @@ +# Temporary test case that only tests the most basic case of fill struct +# Remove as soon as the ordinary fillstruct test is enabled. + +vim ex 'e main.go' +vim ex 'call cursor(12,10)' +vim ex 'call execute(\"GOVIMFillStruct\")' +vim ex 'w' +cmp main.go main.go.golden + +# Assert that we have received no error (Type: 1) or warning (Type: 2) log messages +# Disabled pending resolution to https://github.com/golang/go/issues/34103 +# errlogmatch -start -count=0 'LogMessage callback: &protocol\.LogMessageParams\{Type:(1|2), Message:".*' + +-- go.mod -- +module mod.com + +go 1.12 +-- main.go -- +package main + +type foo struct { + b bool + s string + i int +} + +func fn(a, b foo) {} + +func main() { + _ = foo{} + + fn(foo{}, foo{}) +} +-- main.go.golden -- +package main + +type foo struct { + b bool + s string + i int +} + +func fn(a, b foo) {} + +func main() { + _ = foo{ + b: false, + s: "", + i: 0, + } + + fn(foo{}, foo{}) +} diff --git a/go.mod b/go.mod index b3e3485d6..119ea62ce 100644 --- a/go.mod +++ b/go.mod @@ -12,8 +12,8 @@ require ( github.com/rogpeppe/go-internal v1.6.0 golang.org/x/mod v0.3.0 golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208 - golang.org/x/tools v0.0.0-20200717024301-6ddee64345a6 - golang.org/x/tools/gopls v0.0.0-20200717024301-6ddee64345a6 + golang.org/x/tools v0.0.0-20200721032237-77f530d86f9a + golang.org/x/tools/gopls v0.0.0-20200721032237-77f530d86f9a golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 gopkg.in/retry.v1 v1.0.3 gopkg.in/tomb.v2 v2.0.0-20161208151619-d5d1b5820637 diff --git a/go.sum b/go.sum index d084cd269..f949aebad 100644 --- a/go.sum +++ b/go.sum @@ -75,10 +75,10 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200626171337-aa94e735be7f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200717024301-6ddee64345a6 h1:nULzSsKgihxFGLnQFv2T7lE5vIhOtg8ZPpJHapEt7o0= -golang.org/x/tools v0.0.0-20200717024301-6ddee64345a6/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools/gopls v0.0.0-20200717024301-6ddee64345a6 h1:A0aRY1yTQmgk2zdQzknhpcj9mGpTlmWe/hyToYhvO3Y= -golang.org/x/tools/gopls v0.0.0-20200717024301-6ddee64345a6/go.mod h1:5G2oxsLBLUxd2vJXEacLdfPOY4UsG7h9HzRa1DYxQJ4= +golang.org/x/tools v0.0.0-20200721032237-77f530d86f9a h1:kVMPw4f6EVqYdfGQTedjrpw1dbE2PEMfw4jwXsNdn9s= +golang.org/x/tools v0.0.0-20200721032237-77f530d86f9a/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools/gopls v0.0.0-20200721032237-77f530d86f9a h1:BjzW87jHNh1asc5uhZfXNsmeQgLiTS+JDwrk6kKCyRo= +golang.org/x/tools/gopls v0.0.0-20200721032237-77f530d86f9a/go.mod h1:5G2oxsLBLUxd2vJXEacLdfPOY4UsG7h9HzRa1DYxQJ4= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=