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