| // 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 ( |
| "fmt" |
| "go/ast" |
| "go/token" |
| "go/types" |
| |
| "golang.org/x/tools/internal/lsp/protocol" |
| "golang.org/x/tools/internal/lsp/snippet" |
| ) |
| |
| // 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 = 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 = 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 = 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, |
| }) |
| } |