--- /dev/null
+// 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/internal/lsp/protocol"
+ "golang.org/x/tools/internal/lsp/snippet"
+ "golang.org/x/tools/internal/lsp/source"
+)
+
+// addStatementCandidates adds full statement completion candidates
+// appropriate for the current context.
+func (c *completer) addStatementCandidates() {
+ c.addErrCheckAndReturn()
+ c.addAssignAppend()
+}
+
+// addAssignAppend offers a completion candidate of the form:
+//
+// someSlice = append(someSlice, )
+//
+// It will offer the "append" completion in 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, )".
+//
+// Or
+//
+// 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.snapshot.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.obj == nil || bestItem.obj.Type() == nil {
+ return
+ }
+
+ if _, isSlice := bestItem.obj.Type().Underlying().(*types.Slice); !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
+}
+
+// addErrCheckAndReturn offers a completion candidate of the form:
+//
+// if err != nil {
+// return nil, 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. addErrCheckAndReturn will synthesize
+// zero values as necessary to make the return statement valid.
+func (c *completer) addErrCheckAndReturn() {
+ if len(c.path) < 2 || c.enclosingFunc == nil || !c.opts.placeholders {
+ return
+ }
+
+ var (
+ errorType = types.Universe.Lookup("error").Type()
+ result = c.enclosingFunc.sig.Results()
+ )
+ // Make sure our enclosing function returns an error.
+ if result.Len() == 0 || !types.Identical(result.At(result.Len()-1).Type(), errorType) {
+ 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 (
+ // errText is e.g. "err" in "foo, err := bar()".
+ errText = source.FormatNode(c.snapshot.FileSet(), lastAssignee)
+
+ // Whether we need to include the "if" keyword in our candidate.
+ needsIf = true
+ )
+
+ // "_" isn't a real object.
+ if errText == "_" {
+ 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(errText) <= 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\treturn ", errText))
+
+ 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(errText)
+ })
+
+ snip.WriteText("\n}")
+
+ label := fmt.Sprintf("%[1]s != nil { return %[1]s }", errText)
+ 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,
+ })
+}