1// Copyright 2020 The Go Authors. All rights reserved. 2// Use of this source code is governed by a BSD-style 3// license that can be found in the LICENSE file. 4 5package completion 6 7import ( 8 "fmt" 9 "go/ast" 10 "go/token" 11 "go/types" 12 13 "golang.org/x/tools/internal/lsp/protocol" 14 "golang.org/x/tools/internal/lsp/snippet" 15 "golang.org/x/tools/internal/lsp/source" 16) 17 18// addStatementCandidates adds full statement completion candidates 19// appropriate for the current context. 20func (c *completer) addStatementCandidates() { 21 c.addErrCheckAndReturn() 22 c.addAssignAppend() 23} 24 25// addAssignAppend offers a completion candidate of the form: 26// 27// someSlice = append(someSlice, ) 28// 29// It will offer the "append" completion in two situations: 30// 31// 1. Position is in RHS of assign, prefix matches "append", and 32// corresponding LHS object is a slice. For example, 33// "foo = ap<>" completes to "foo = append(foo, )". 34// 35// Or 36// 37// 2. Prefix is an ident or selector in an *ast.ExprStmt (i.e. 38// beginning of statement), and our best matching candidate is a 39// slice. For example: "foo.ba" completes to "foo.bar = append(foo.bar, )". 40func (c *completer) addAssignAppend() { 41 if len(c.path) < 3 { 42 return 43 } 44 45 ident, _ := c.path[0].(*ast.Ident) 46 if ident == nil { 47 return 48 } 49 50 var ( 51 // sliceText is the full name of our slice object, e.g. "s.abc" in 52 // "s.abc = app<>". 53 sliceText string 54 // needsLHS is true if we need to prepend the LHS slice name and 55 // "=" to our candidate. 56 needsLHS = false 57 fset = c.snapshot.FileSet() 58 ) 59 60 switch n := c.path[1].(type) { 61 case *ast.AssignStmt: 62 // We are already in an assignment. Make sure our prefix matches "append". 63 if c.matcher.Score("append") <= 0 { 64 return 65 } 66 67 exprIdx := exprAtPos(c.pos, n.Rhs) 68 if exprIdx == len(n.Rhs) || exprIdx > len(n.Lhs)-1 { 69 return 70 } 71 72 lhsType := c.pkg.GetTypesInfo().TypeOf(n.Lhs[exprIdx]) 73 if lhsType == nil { 74 return 75 } 76 77 // Make sure our corresponding LHS object is a slice. 78 if _, isSlice := lhsType.Underlying().(*types.Slice); !isSlice { 79 return 80 } 81 82 // The name or our slice is whatever's in the LHS expression. 83 sliceText = source.FormatNode(fset, n.Lhs[exprIdx]) 84 case *ast.SelectorExpr: 85 // Make sure we are a selector at the beginning of a statement. 86 if _, parentIsExprtStmt := c.path[2].(*ast.ExprStmt); !parentIsExprtStmt { 87 return 88 } 89 90 // So far we only know the first part of our slice name. For 91 // example in "s.a<>" we only know our slice begins with "s." 92 // since the user could still be typing. 93 sliceText = source.FormatNode(fset, n.X) + "." 94 needsLHS = true 95 case *ast.ExprStmt: 96 needsLHS = true 97 default: 98 return 99 } 100 101 var ( 102 label string 103 snip snippet.Builder 104 score = highScore 105 ) 106 107 if needsLHS { 108 // Offer the long form assign + append candidate if our best 109 // candidate is a slice. 110 bestItem := c.topCandidate() 111 if bestItem == nil || bestItem.obj == nil || bestItem.obj.Type() == nil { 112 return 113 } 114 115 if _, isSlice := bestItem.obj.Type().Underlying().(*types.Slice); !isSlice { 116 return 117 } 118 119 // Don't rank the full form assign + append candidate above the 120 // slice itself. 121 score = bestItem.Score - 0.01 122 123 // Fill in rest of sliceText now that we have the object name. 124 sliceText += bestItem.Label 125 126 // Fill in the candidate's LHS bits. 127 label = fmt.Sprintf("%s = ", bestItem.Label) 128 snip.WriteText(label) 129 } 130 131 snip.WriteText(fmt.Sprintf("append(%s, ", sliceText)) 132 snip.WritePlaceholder(nil) 133 snip.WriteText(")") 134 135 c.items = append(c.items, CompletionItem{ 136 Label: label + fmt.Sprintf("append(%s, )", sliceText), 137 Kind: protocol.FunctionCompletion, 138 Score: score, 139 snippet: &snip, 140 }) 141} 142 143// topCandidate returns the strictly highest scoring candidate 144// collected so far. If the top two candidates have the same score, 145// nil is returned. 146func (c *completer) topCandidate() *CompletionItem { 147 var bestItem, secondBestItem *CompletionItem 148 for i := range c.items { 149 if bestItem == nil || c.items[i].Score > bestItem.Score { 150 bestItem = &c.items[i] 151 } else if secondBestItem == nil || c.items[i].Score > secondBestItem.Score { 152 secondBestItem = &c.items[i] 153 } 154 } 155 156 // If secondBestItem has the same score, bestItem isn't 157 // the strict best. 158 if secondBestItem != nil && secondBestItem.Score == bestItem.Score { 159 return nil 160 } 161 162 return bestItem 163} 164 165// addErrCheckAndReturn offers a completion candidate of the form: 166// 167// if err != nil { 168// return nil, err 169// } 170// 171// The position must be in a function that returns an error, and the 172// statement preceding the position must be an assignment where the 173// final LHS object is an error. addErrCheckAndReturn will synthesize 174// zero values as necessary to make the return statement valid. 175func (c *completer) addErrCheckAndReturn() { 176 if len(c.path) < 2 || c.enclosingFunc == nil || !c.opts.placeholders { 177 return 178 } 179 180 var ( 181 errorType = types.Universe.Lookup("error").Type() 182 result = c.enclosingFunc.sig.Results() 183 ) 184 // Make sure our enclosing function returns an error. 185 if result.Len() == 0 || !types.Identical(result.At(result.Len()-1).Type(), errorType) { 186 return 187 } 188 189 prevLine := prevStmt(c.pos, c.path) 190 if prevLine == nil { 191 return 192 } 193 194 // Make sure our preceding statement was as assignment. 195 assign, _ := prevLine.(*ast.AssignStmt) 196 if assign == nil || len(assign.Lhs) == 0 { 197 return 198 } 199 200 lastAssignee := assign.Lhs[len(assign.Lhs)-1] 201 202 // Make sure the final assignee is an error. 203 if !types.Identical(c.pkg.GetTypesInfo().TypeOf(lastAssignee), errorType) { 204 return 205 } 206 207 var ( 208 // errText is e.g. "err" in "foo, err := bar()". 209 errText = source.FormatNode(c.snapshot.FileSet(), lastAssignee) 210 211 // Whether we need to include the "if" keyword in our candidate. 212 needsIf = true 213 ) 214 215 // "_" isn't a real object. 216 if errText == "_" { 217 return 218 } 219 220 // Below we try to detect if the user has already started typing "if 221 // err" so we can replace what they've typed with our complete 222 // statement. 223 switch n := c.path[0].(type) { 224 case *ast.Ident: 225 switch c.path[1].(type) { 226 case *ast.ExprStmt: 227 // This handles: 228 // 229 // f, err := os.Open("foo") 230 // i<> 231 232 // Make sure they are typing "if". 233 if c.matcher.Score("if") <= 0 { 234 return 235 } 236 case *ast.IfStmt: 237 // This handles: 238 // 239 // f, err := os.Open("foo") 240 // if er<> 241 242 // Make sure they are typing the error's name. 243 if c.matcher.Score(errText) <= 0 { 244 return 245 } 246 247 needsIf = false 248 default: 249 return 250 } 251 case *ast.IfStmt: 252 // This handles: 253 // 254 // f, err := os.Open("foo") 255 // if <> 256 257 // Avoid false positives by ensuring the if's cond is a bad 258 // expression. For example, don't offer the completion in cases 259 // like "if <> somethingElse". 260 if _, bad := n.Cond.(*ast.BadExpr); !bad { 261 return 262 } 263 264 // If "if" is our direct prefix, we need to include it in our 265 // candidate since the existing "if" will be overwritten. 266 needsIf = c.pos == n.Pos()+token.Pos(len("if")) 267 } 268 269 // Build up a snippet that looks like: 270 // 271 // if err != nil { 272 // return <zero value>, ..., ${1:err} 273 // } 274 // 275 // We make the error a placeholder so it is easy to alter the error. 276 var snip snippet.Builder 277 if needsIf { 278 snip.WriteText("if ") 279 } 280 snip.WriteText(fmt.Sprintf("%s != nil {\n\treturn ", errText)) 281 282 for i := 0; i < result.Len()-1; i++ { 283 snip.WriteText(formatZeroValue(result.At(i).Type(), c.qf)) 284 snip.WriteText(", ") 285 } 286 287 snip.WritePlaceholder(func(b *snippet.Builder) { 288 b.WriteText(errText) 289 }) 290 291 snip.WriteText("\n}") 292 293 label := fmt.Sprintf("%[1]s != nil { return %[1]s }", errText) 294 if needsIf { 295 label = "if " + label 296 } 297 298 c.items = append(c.items, CompletionItem{ 299 Label: label, 300 // There doesn't seem to be a more appropriate kind. 301 Kind: protocol.KeywordCompletion, 302 Score: highScore, 303 snippet: &snip, 304 }) 305} 306