diff options
Diffstat (limited to 'gopls/internal/lsp/source/completion/statements.go')
-rw-r--r-- | gopls/internal/lsp/source/completion/statements.go | 361 |
1 files changed, 361 insertions, 0 deletions
diff --git a/gopls/internal/lsp/source/completion/statements.go b/gopls/internal/lsp/source/completion/statements.go new file mode 100644 index 000000000..707375fa1 --- /dev/null +++ b/gopls/internal/lsp/source/completion/statements.go @@ -0,0 +1,361 @@ +// 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 completion + +import ( + "fmt" + "go/ast" + "go/token" + "go/types" + + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/snippet" + "golang.org/x/tools/gopls/internal/lsp/source" +) + +// addStatementCandidates adds full statement completion candidates +// appropriate for the current context. +func (c *completer) addStatementCandidates() { + c.addErrCheck() + c.addAssignAppend() +} + +// addAssignAppend offers a completion candidate of the form: +// +// someSlice = append(someSlice, ) +// +// It will offer the "append" completion in either of two situations: +// +// 1. Position is in RHS of assign, prefix matches "append", and +// corresponding LHS object is a slice. For example, +// "foo = ap<>" completes to "foo = append(foo, )". +// +// 2. Prefix is an ident or selector in an *ast.ExprStmt (i.e. +// beginning of statement), and our best matching candidate is a +// slice. For example: "foo.ba" completes to "foo.bar = append(foo.bar, )". +func (c *completer) addAssignAppend() { + if len(c.path) < 3 { + return + } + + ident, _ := c.path[0].(*ast.Ident) + if ident == nil { + return + } + + var ( + // sliceText is the full name of our slice object, e.g. "s.abc" in + // "s.abc = app<>". + sliceText string + // needsLHS is true if we need to prepend the LHS slice name and + // "=" to our candidate. + needsLHS = false + fset = c.pkg.FileSet() + ) + + switch n := c.path[1].(type) { + case *ast.AssignStmt: + // We are already in an assignment. Make sure our prefix matches "append". + if c.matcher.Score("append") <= 0 { + return + } + + exprIdx := exprAtPos(c.pos, n.Rhs) + if exprIdx == len(n.Rhs) || exprIdx > len(n.Lhs)-1 { + return + } + + lhsType := c.pkg.GetTypesInfo().TypeOf(n.Lhs[exprIdx]) + if lhsType == nil { + return + } + + // Make sure our corresponding LHS object is a slice. + if _, isSlice := lhsType.Underlying().(*types.Slice); !isSlice { + return + } + + // The name or our slice is whatever's in the LHS expression. + sliceText = source.FormatNode(fset, n.Lhs[exprIdx]) + case *ast.SelectorExpr: + // Make sure we are a selector at the beginning of a statement. + if _, parentIsExprtStmt := c.path[2].(*ast.ExprStmt); !parentIsExprtStmt { + return + } + + // So far we only know the first part of our slice name. For + // example in "s.a<>" we only know our slice begins with "s." + // since the user could still be typing. + sliceText = source.FormatNode(fset, n.X) + "." + needsLHS = true + case *ast.ExprStmt: + needsLHS = true + default: + return + } + + var ( + label string + snip snippet.Builder + score = highScore + ) + + if needsLHS { + // Offer the long form assign + append candidate if our best + // candidate is a slice. + bestItem := c.topCandidate() + if bestItem == nil || !bestItem.isSlice { + return + } + + // Don't rank the full form assign + append candidate above the + // slice itself. + score = bestItem.Score - 0.01 + + // Fill in rest of sliceText now that we have the object name. + sliceText += bestItem.Label + + // Fill in the candidate's LHS bits. + label = fmt.Sprintf("%s = ", bestItem.Label) + snip.WriteText(label) + } + + snip.WriteText(fmt.Sprintf("append(%s, ", sliceText)) + snip.WritePlaceholder(nil) + snip.WriteText(")") + + c.items = append(c.items, CompletionItem{ + Label: label + fmt.Sprintf("append(%s, )", sliceText), + Kind: protocol.FunctionCompletion, + Score: score, + snippet: &snip, + }) +} + +// topCandidate returns the strictly highest scoring candidate +// collected so far. If the top two candidates have the same score, +// nil is returned. +func (c *completer) topCandidate() *CompletionItem { + var bestItem, secondBestItem *CompletionItem + for i := range c.items { + if bestItem == nil || c.items[i].Score > bestItem.Score { + bestItem = &c.items[i] + } else if secondBestItem == nil || c.items[i].Score > secondBestItem.Score { + secondBestItem = &c.items[i] + } + } + + // If secondBestItem has the same score, bestItem isn't + // the strict best. + if secondBestItem != nil && secondBestItem.Score == bestItem.Score { + return nil + } + + return bestItem +} + +// addErrCheck offers a completion candidate of the form: +// +// if err != nil { +// return nil, err +// } +// +// In the case of test functions, it offers a completion candidate of the form: +// +// if err != nil { +// t.Fatal(err) +// } +// +// The position must be in a function that returns an error, and the +// statement preceding the position must be an assignment where the +// final LHS object is an error. addErrCheck will synthesize +// zero values as necessary to make the return statement valid. +func (c *completer) addErrCheck() { + if len(c.path) < 2 || c.enclosingFunc == nil || !c.opts.placeholders { + return + } + + var ( + errorType = types.Universe.Lookup("error").Type() + result = c.enclosingFunc.sig.Results() + testVar = getTestVar(c.enclosingFunc, c.pkg) + isTest = testVar != "" + doesNotReturnErr = result.Len() == 0 || !types.Identical(result.At(result.Len()-1).Type(), errorType) + ) + // Make sure our enclosing function is a Test func or returns an error. + if !isTest && doesNotReturnErr { + return + } + + prevLine := prevStmt(c.pos, c.path) + if prevLine == nil { + return + } + + // Make sure our preceding statement was as assignment. + assign, _ := prevLine.(*ast.AssignStmt) + if assign == nil || len(assign.Lhs) == 0 { + return + } + + lastAssignee := assign.Lhs[len(assign.Lhs)-1] + + // Make sure the final assignee is an error. + if !types.Identical(c.pkg.GetTypesInfo().TypeOf(lastAssignee), errorType) { + return + } + + var ( + // errVar is e.g. "err" in "foo, err := bar()". + errVar = source.FormatNode(c.pkg.FileSet(), lastAssignee) + + // Whether we need to include the "if" keyword in our candidate. + needsIf = true + ) + + // If the returned error from the previous statement is "_", it is not a real object. + // If we don't have an error, and the function signature takes a testing.TB that is either ignored + // or an "_", then we also can't call t.Fatal(err). + if errVar == "_" { + return + } + + // Below we try to detect if the user has already started typing "if + // err" so we can replace what they've typed with our complete + // statement. + switch n := c.path[0].(type) { + case *ast.Ident: + switch c.path[1].(type) { + case *ast.ExprStmt: + // This handles: + // + // f, err := os.Open("foo") + // i<> + + // Make sure they are typing "if". + if c.matcher.Score("if") <= 0 { + return + } + case *ast.IfStmt: + // This handles: + // + // f, err := os.Open("foo") + // if er<> + + // Make sure they are typing the error's name. + if c.matcher.Score(errVar) <= 0 { + return + } + + needsIf = false + default: + return + } + case *ast.IfStmt: + // This handles: + // + // f, err := os.Open("foo") + // if <> + + // Avoid false positives by ensuring the if's cond is a bad + // expression. For example, don't offer the completion in cases + // like "if <> somethingElse". + if _, bad := n.Cond.(*ast.BadExpr); !bad { + return + } + + // If "if" is our direct prefix, we need to include it in our + // candidate since the existing "if" will be overwritten. + needsIf = c.pos == n.Pos()+token.Pos(len("if")) + } + + // Build up a snippet that looks like: + // + // if err != nil { + // return <zero value>, ..., ${1:err} + // } + // + // We make the error a placeholder so it is easy to alter the error. + var snip snippet.Builder + if needsIf { + snip.WriteText("if ") + } + snip.WriteText(fmt.Sprintf("%s != nil {\n\t", errVar)) + + var label string + if isTest { + snip.WriteText(fmt.Sprintf("%s.Fatal(%s)", testVar, errVar)) + label = fmt.Sprintf("%[1]s != nil { %[2]s.Fatal(%[1]s) }", errVar, testVar) + } else { + snip.WriteText("return ") + for i := 0; i < result.Len()-1; i++ { + snip.WriteText(formatZeroValue(result.At(i).Type(), c.qf)) + snip.WriteText(", ") + } + snip.WritePlaceholder(func(b *snippet.Builder) { + b.WriteText(errVar) + }) + label = fmt.Sprintf("%[1]s != nil { return %[1]s }", errVar) + } + + snip.WriteText("\n}") + + if needsIf { + label = "if " + label + } + + c.items = append(c.items, CompletionItem{ + Label: label, + // There doesn't seem to be a more appropriate kind. + Kind: protocol.KeywordCompletion, + Score: highScore, + snippet: &snip, + }) +} + +// getTestVar checks the function signature's input parameters and returns +// the name of the first parameter that implements "testing.TB". For example, +// func someFunc(t *testing.T) returns the string "t", func someFunc(b *testing.B) +// returns "b" etc. An empty string indicates that the function signature +// does not take a testing.TB parameter or does so but is ignored such +// as func someFunc(*testing.T). +func getTestVar(enclosingFunc *funcInfo, pkg source.Package) string { + if enclosingFunc == nil || enclosingFunc.sig == nil { + return "" + } + + var testingPkg *types.Package + for _, p := range pkg.GetTypes().Imports() { + if p.Path() == "testing" { + testingPkg = p + break + } + } + if testingPkg == nil { + return "" + } + tbObj := testingPkg.Scope().Lookup("TB") + if tbObj == nil { + return "" + } + iface, ok := tbObj.Type().Underlying().(*types.Interface) + if !ok { + return "" + } + + sig := enclosingFunc.sig + for i := 0; i < sig.Params().Len(); i++ { + param := sig.Params().At(i) + if param.Name() == "_" { + continue + } + if !types.Implements(param.Type(), iface) { + continue + } + return param.Name() + } + + return "" +} |