Skip to content

Commit 85146f5

Browse files
findleyrgopherbot
authored andcommitted
gopls/internal/lsp/source: highlight returns correctly
Rewrite the control flow highlighting logic to be more understandable, and to fix a bug where multi-name result parameters were not properly counted. Fixes golang/go#60589 Change-Id: Id12ada78852be0a8376fbd482326322fc1b87fcf Reviewed-on: https://go-review.googlesource.com/c/tools/+/503439 Auto-Submit: Robert Findley <[email protected]> Reviewed-by: Alan Donovan <[email protected]> LUCI-TryBot-Result: Go LUCI <[email protected]>
1 parent e211e0f commit 85146f5

File tree

2 files changed

+171
-87
lines changed

2 files changed

+171
-87
lines changed

gopls/internal/golang/highlight.go

Lines changed: 141 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,8 @@ func Highlight(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, po
6565
return ranges, nil
6666
}
6767

68+
// highlightPath returns ranges to highlight for the given enclosing path,
69+
// which should be the result of astutil.PathEnclosingInterval.
6870
func highlightPath(path []ast.Node, file *ast.File, info *types.Info) (map[posRange]struct{}, error) {
6971
result := make(map[posRange]struct{})
7072
switch node := path[0].(type) {
@@ -133,115 +135,167 @@ type posRange struct {
133135
start, end token.Pos
134136
}
135137

136-
func highlightFuncControlFlow(path []ast.Node, result map[posRange]struct{}) {
137-
var enclosingFunc ast.Node
138-
var returnStmt *ast.ReturnStmt
139-
var resultsList *ast.FieldList
140-
inReturnList := false
141-
142-
Outer:
143-
// Reverse walk the path till we get to the func block.
138+
// highlightFuncControlFlow adds highlight ranges to the result map to
139+
// associate results and result parameters.
140+
//
141+
// Specifically, if the cursor is in a result or result parameter, all
142+
// results and result parameters with the same index are highlighted. If the
143+
// cursor is in a 'func' or 'return' keyword, the func keyword as well as all
144+
// returns from that func are highlighted.
145+
//
146+
// As a special case, if the cursor is within a complicated expression, control
147+
// flow highlighting is disabled, as it would highlight too much.
148+
func highlightFuncControlFlow(path []ast.Node, result map[posRange]unit) {
149+
150+
var (
151+
funcType *ast.FuncType // type of enclosing func, or nil
152+
funcBody *ast.BlockStmt // body of enclosing func, or nil
153+
returnStmt *ast.ReturnStmt // enclosing ReturnStmt within the func, or nil
154+
)
155+
156+
findEnclosingFunc:
144157
for i, n := range path {
145-
switch node := n.(type) {
158+
switch n := n.(type) {
159+
// TODO(rfindley, low priority): these pre-existing cases for KeyValueExpr
160+
// and CallExpr appear to avoid highlighting when the cursor is in a
161+
// complicated expression. However, the basis for this heuristic is
162+
// unclear. Can we formalize a rationale?
146163
case *ast.KeyValueExpr:
147-
// If cursor is in a key: value expr, we don't want control flow highlighting
164+
// If cursor is in a key: value expr, we don't want control flow highlighting.
148165
return
166+
149167
case *ast.CallExpr:
150168
// If cursor is an arg in a callExpr, we don't want control flow highlighting.
151169
if i > 0 {
152-
for _, arg := range node.Args {
170+
for _, arg := range n.Args {
153171
if arg == path[i-1] {
154172
return
155173
}
156174
}
157175
}
158-
case *ast.Field:
159-
inReturnList = true
176+
160177
case *ast.FuncLit:
161-
enclosingFunc = n
162-
resultsList = node.Type.Results
163-
break Outer
178+
funcType = n.Type
179+
funcBody = n.Body
180+
break findEnclosingFunc
181+
164182
case *ast.FuncDecl:
165-
enclosingFunc = n
166-
resultsList = node.Type.Results
167-
break Outer
183+
funcType = n.Type
184+
funcBody = n.Body
185+
break findEnclosingFunc
186+
168187
case *ast.ReturnStmt:
169-
returnStmt = node
170-
// If the cursor is not directly in a *ast.ReturnStmt, then
171-
// we need to know if it is within one of the values that is being returned.
172-
inReturnList = inReturnList || path[0] != returnStmt
173-
}
174-
}
175-
// Cursor is not in a function.
176-
if enclosingFunc == nil {
177-
return
178-
}
179-
// If the cursor is on a "return" or "func" keyword, we should highlight all of the exit
180-
// points of the function, including the "return" and "func" keywords.
181-
highlightAllReturnsAndFunc := path[0] == returnStmt || path[0] == enclosingFunc
182-
switch path[0].(type) {
183-
case *ast.Ident, *ast.BasicLit:
184-
// Cursor is in an identifier and not in a return statement or in the results list.
185-
if returnStmt == nil && !inReturnList {
186-
return
188+
returnStmt = n
187189
}
188-
case *ast.FuncType:
189-
highlightAllReturnsAndFunc = true
190190
}
191-
// The user's cursor may be within the return statement of a function,
192-
// or within the result section of a function's signature.
193-
// index := -1
194-
var nodes []ast.Node
195-
if returnStmt != nil {
196-
for _, n := range returnStmt.Results {
197-
nodes = append(nodes, n)
198-
}
199-
} else if resultsList != nil {
200-
for _, n := range resultsList.List {
201-
nodes = append(nodes, n)
202-
}
203-
}
204-
_, index := nodeAtPos(nodes, path[0].Pos())
205191

206-
// Highlight the correct argument in the function declaration return types.
207-
if resultsList != nil && -1 < index && index < len(resultsList.List) {
208-
rng := posRange{
209-
start: resultsList.List[index].Pos(),
210-
end: resultsList.List[index].End(),
211-
}
212-
result[rng] = struct{}{}
192+
if funcType == nil {
193+
return // cursor is not in a function
213194
}
214-
// Add the "func" part of the func declaration.
215-
if highlightAllReturnsAndFunc {
216-
r := posRange{
217-
start: enclosingFunc.Pos(),
218-
end: enclosingFunc.Pos() + token.Pos(len("func")),
195+
196+
// Helper functions for inspecting the current location.
197+
var (
198+
pos = path[0].Pos()
199+
inSpan = func(start, end token.Pos) bool { return start <= pos && pos < end }
200+
inNode = func(n ast.Node) bool { return inSpan(n.Pos(), n.End()) }
201+
)
202+
203+
inResults := funcType.Results != nil && inNode(funcType.Results)
204+
205+
// If the cursor is on a "return" or "func" keyword, but not highlighting any
206+
// specific field or expression, we should highlight all of the exit points
207+
// of the function, including the "return" and "func" keywords.
208+
funcEnd := funcType.Func + token.Pos(len("func"))
209+
highlightAll := path[0] == returnStmt || inSpan(funcType.Func, funcEnd)
210+
var highlightIndexes map[int]bool
211+
212+
if highlightAll {
213+
// Add the "func" part of the func declaration.
214+
result[posRange{
215+
start: funcType.Func,
216+
end: funcEnd,
217+
}] = unit{}
218+
} else if returnStmt == nil && !inResults {
219+
return // nothing to highlight
220+
} else {
221+
// If we're not highighting the entire return statement, we need to collect
222+
// specific result indexes to highlight. This may be more than one index if
223+
// the cursor is on a multi-name result field, but not in any specific name.
224+
if !highlightAll {
225+
highlightIndexes = make(map[int]bool)
226+
if returnStmt != nil {
227+
for i, n := range returnStmt.Results {
228+
if inNode(n) {
229+
highlightIndexes[i] = true
230+
break
231+
}
232+
}
233+
}
234+
235+
// Scan fields, either adding highlights according to the highlightIndexes
236+
// computed above, or accounting for the cursor position within the result
237+
// list.
238+
// (We do both at once to avoid repeating the cumbersome field traversal.)
239+
i := 0
240+
findField:
241+
for _, field := range funcType.Results.List {
242+
for j, name := range field.Names {
243+
if inNode(name) || highlightIndexes[i+j] {
244+
result[posRange{name.Pos(), name.End()}] = unit{}
245+
highlightIndexes[i+j] = true
246+
break findField // found/highlighted the specific name
247+
}
248+
}
249+
// If the cursor is in a field but not in a name (e.g. in the space, or
250+
// the type), highlight the whole field.
251+
//
252+
// Note that this may not be ideal if we're at e.g.
253+
//
254+
// (x,‸y int, z int8)
255+
//
256+
// ...where it would make more sense to highlight only y. But we don't
257+
// reach this function if not in a func, return, ident, or basiclit.
258+
if inNode(field) || highlightIndexes[i] {
259+
result[posRange{field.Pos(), field.End()}] = unit{}
260+
highlightIndexes[i] = true
261+
if inNode(field) {
262+
for j := range field.Names {
263+
highlightIndexes[i+j] = true
264+
}
265+
}
266+
break findField // found/highlighted the field
267+
}
268+
269+
n := len(field.Names)
270+
if n == 0 {
271+
n = 1
272+
}
273+
i += n
274+
}
219275
}
220-
result[r] = struct{}{}
221276
}
222-
ast.Inspect(enclosingFunc, func(n ast.Node) bool {
223-
// Don't traverse any other functions.
224-
switch n.(type) {
277+
278+
ast.Inspect(funcBody, func(n ast.Node) bool {
279+
switch n := n.(type) {
225280
case *ast.FuncDecl, *ast.FuncLit:
226-
return enclosingFunc == n
227-
}
228-
ret, ok := n.(*ast.ReturnStmt)
229-
if !ok {
230-
return true
231-
}
232-
var toAdd ast.Node
233-
// Add the entire return statement, applies when highlight the word "return" or "func".
234-
if highlightAllReturnsAndFunc {
235-
toAdd = n
236-
}
237-
// Add the relevant field within the entire return statement.
238-
if -1 < index && index < len(ret.Results) {
239-
toAdd = ret.Results[index]
240-
}
241-
if toAdd != nil {
242-
result[posRange{start: toAdd.Pos(), end: toAdd.End()}] = struct{}{}
281+
// Don't traverse into any functions other than enclosingFunc.
282+
return false
283+
case *ast.ReturnStmt:
284+
if highlightAll {
285+
// Add the entire return statement.
286+
result[posRange{n.Pos(), n.End()}] = unit{}
287+
} else {
288+
// Add the highlighted indexes.
289+
for i, expr := range n.Results {
290+
if highlightIndexes[i] {
291+
result[posRange{expr.Pos(), expr.End()}] = unit{}
292+
}
293+
}
294+
}
295+
return false
296+
243297
}
244-
return false
298+
return true
245299
})
246300
}
247301

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
This test verifies that control flow lighlighting correctly accounts for
2+
multi-name result parameters. In golang/go#60589, it did not.
3+
4+
-- go.mod --
5+
module mod.com
6+
7+
go 1.18
8+
9+
-- p.go --
10+
package p
11+
12+
func _() (foo int, bar, baz string) { //@ loc(func, "func"), loc(foo, "foo"), loc(fooint, "foo int"), loc(int, "int"), loc(bar, "bar"), loc(beforebaz, " baz"), loc(baz, "baz"), loc(barbazstring, "bar, baz string"), loc(beforestring, re`() string`), loc(string, "string")
13+
return 0, "1", "2" //@ loc(return, `return 0, "1", "2"`), loc(l0, "0"), loc(l1, `"1"`), loc(l2, `"2"`)
14+
}
15+
16+
// Assertions, expressed here to avoid clutter above.
17+
// Note that when the cursor is over the field type, there is some
18+
// (likely harmless) redundancy.
19+
20+
//@ highlight(func, func, return)
21+
//@ highlight(foo, foo, l0)
22+
//@ highlight(int, fooint, int, l0)
23+
//@ highlight(bar, bar, l1)
24+
//@ highlight(beforebaz)
25+
//@ highlight(baz, baz, l2)
26+
//@ highlight(beforestring, baz, l2)
27+
//@ highlight(string, barbazstring, string, l1, l2)
28+
//@ highlight(l0, foo, l0)
29+
//@ highlight(l1, bar, l1)
30+
//@ highlight(l2, baz, l2)

0 commit comments

Comments
 (0)