1
0
mirror of https://github.com/golang/go synced 2024-09-30 20:28:32 -06:00

internal/lsp: support taking address for completion candidates

We now support taking the address of objects to make better completion
candidates. For example:

i := 123
var p *int = <> // now you get a candidate for "&i"

This required that we track addressability better, particularly when
searching for deep candidates. Now each candidate knows if it is
addressable, and the deep search propagates addressability to child
candidates appropriately.

The basic propagation logic is:

- In-scope *types.Var candidates are addressable. This handles your
  basic "foo" variable whose address if "&foo".

- Surrounding selector is addressable based on type checker info. This
  knows "foo.bar.<>" is addressable but "foo.bar().<>" isn't

- When evaluating deep completions, fields after a function call lose
  addressability, but fields after a pointer regain addressability. For
  example, "foo.bar()" isn't addressable, but "foo.bar().baz" is
  addressable if "bar()" returns a pointer.

Fixes golang/go#36132.

Change-Id: I6a8659eb8c203262aedf86844ac39a2d1e81ecc4
Reviewed-on: https://go-review.googlesource.com/c/tools/+/212399
Run-TryBot: Muir Manders <muir@mnd.rs>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Rebecca Stambler <rstambler@golang.org>
This commit is contained in:
Muir Manders 2019-12-22 09:58:14 -08:00 committed by Rebecca Stambler
parent 918115ff85
commit 3721262b3e
10 changed files with 167 additions and 76 deletions

View File

@ -363,6 +363,13 @@ type candidate struct {
// For example, expandFuncCall=true yields "foo()", expandFuncCall=false yields "foo". // For example, expandFuncCall=true yields "foo()", expandFuncCall=false yields "foo".
expandFuncCall bool expandFuncCall bool
// takeAddress is true if the completion should take a pointer to obj.
// For example, takeAddress=true yields "&foo", takeAddress=false yields "foo".
takeAddress bool
// addressable is true if a pointer can be taken to the candidate.
addressable bool
// imp is the import that needs to be added to this package in order // imp is the import that needs to be added to this package in order
// for this candidate to be valid. nil if no import needed. // for this candidate to be valid. nil if no import needed.
imp *importInfo imp *importInfo
@ -537,7 +544,8 @@ func Completion(ctx context.Context, snapshot Snapshot, fh FileHandle, pos proto
return c.items, c.getSurrounding(), nil return c.items, c.getSurrounding(), nil
} }
// populateCommentCompletions returns completions for an exported variable immediately preceeding comment // populateCommentCompletions yields completions for an exported
// variable immediately preceding comment.
func (c *completer) populateCommentCompletions(comment *ast.CommentGroup) { func (c *completer) populateCommentCompletions(comment *ast.CommentGroup) {
// Using the comment position find the line after // Using the comment position find the line after
@ -653,10 +661,12 @@ func (c *completer) selector(sel *ast.SelectorExpr) error {
func (c *completer) packageMembers(pkg *types.Package, imp *importInfo) { func (c *completer) packageMembers(pkg *types.Package, imp *importInfo) {
scope := pkg.Scope() scope := pkg.Scope()
for _, name := range scope.Names() { for _, name := range scope.Names() {
obj := scope.Lookup(name)
c.found(candidate{ c.found(candidate{
obj: scope.Lookup(name), obj: obj,
score: stdScore, score: stdScore,
imp: imp, imp: imp,
addressable: isVar(obj),
}) })
} }
} }
@ -676,18 +686,20 @@ func (c *completer) methodsAndFields(typ types.Type, addressable bool, imp *impo
for i := 0; i < mset.Len(); i++ { for i := 0; i < mset.Len(); i++ {
c.found(candidate{ c.found(candidate{
obj: mset.At(i).Obj(), obj: mset.At(i).Obj(),
score: stdScore, score: stdScore,
imp: imp, imp: imp,
addressable: addressable || isPointer(typ),
}) })
} }
// Add fields of T. // Add fields of T.
for _, f := range fieldSelections(typ) { for _, f := range fieldSelections(typ) {
c.found(candidate{ c.found(candidate{
obj: f, obj: f,
score: stdScore, score: stdScore,
imp: imp, imp: imp,
addressable: addressable || isPointer(typ),
}) })
} }
return nil return nil
@ -778,8 +790,9 @@ func (c *completer) lexical() error {
if _, ok := seen[obj.Name()]; !ok { if _, ok := seen[obj.Name()]; !ok {
seen[obj.Name()] = struct{}{} seen[obj.Name()] = struct{}{}
c.found(candidate{ c.found(candidate{
obj: obj, obj: obj,
score: score, score: score,
addressable: isVar(obj),
}) })
} }
} }
@ -1116,11 +1129,11 @@ type typeModifier struct {
type typeMod int type typeMod int
const ( const (
star typeMod = iota // dereference operator for expressions, pointer indicator for types star typeMod = iota // pointer indirection for expressions, pointer indicator for types
reference // reference ("&") operator address // address operator ("&")
chanRead // channel read ("<-") operator chanRead // channel read operator ("<-")
slice // make a slice type ("[]" in "[]int") slice // make a slice type ("[]" in "[]int")
array // make an array type ("[2]" in "[2]int") array // make an array type ("[2]" in "[2]int")
) )
// typeInference holds information we have inferred about a type that can be // typeInference holds information we have inferred about a type that can be
@ -1347,7 +1360,7 @@ Nodes:
case *ast.UnaryExpr: case *ast.UnaryExpr:
switch node.Op { switch node.Op {
case token.AND: case token.AND:
inf.modifiers = append(inf.modifiers, typeModifier{mod: reference}) inf.modifiers = append(inf.modifiers, typeModifier{mod: address})
case token.ARROW: case token.ARROW:
inf.modifiers = append(inf.modifiers, typeModifier{mod: chanRead}) inf.modifiers = append(inf.modifiers, typeModifier{mod: chanRead})
} }
@ -1362,22 +1375,36 @@ Nodes:
} }
// applyTypeModifiers applies the list of type modifiers to a type. // applyTypeModifiers applies the list of type modifiers to a type.
func (ti typeInference) applyTypeModifiers(typ types.Type) types.Type { // It returns nil if the modifiers could not be applied.
func (ti typeInference) applyTypeModifiers(typ types.Type, addressable bool) types.Type {
for _, mod := range ti.modifiers { for _, mod := range ti.modifiers {
switch mod.mod { switch mod.mod {
case star: case star:
// For every "*" deref operator, remove a pointer layer from candidate type. // For every "*" indirection operator, remove a pointer layer
typ = deref(typ) // from candidate type.
case reference: if ptr, ok := typ.Underlying().(*types.Pointer); ok {
// For every "&" ref operator, add another pointer layer to candidate type. typ = ptr.Elem()
typ = types.NewPointer(typ) } else {
return nil
}
case address:
// For every "&" address operator, add another pointer layer to
// candidate type, if the candidate is addressable.
if addressable {
typ = types.NewPointer(typ)
} else {
return nil
}
case chanRead: case chanRead:
// For every "<-" operator, remove a layer of channelness. // For every "<-" operator, remove a layer of channelness.
if ch, ok := typ.(*types.Chan); ok { if ch, ok := typ.(*types.Chan); ok {
typ = ch.Elem() typ = ch.Elem()
} else {
return nil
} }
} }
} }
return typ return typ
} }
@ -1544,10 +1571,8 @@ Nodes:
} }
} }
// matchingType reports whether a type matches the expected type. func (c *completer) fakeObj(T types.Type) *types.Var {
func (c *completer) matchingType(T types.Type) bool { return types.NewVar(token.NoPos, c.pkg.GetTypes(), "", T)
fakeObj := types.NewVar(token.NoPos, c.pkg.GetTypes(), "", T)
return c.matchingCandidate(&candidate{obj: fakeObj})
} }
// matchingCandidate reports whether a candidate matches our type // matchingCandidate reports whether a candidate matches our type
@ -1576,7 +1601,10 @@ func (c *completer) matchingCandidate(cand *candidate) bool {
} }
// Take into account any type modifiers on the expected type. // Take into account any type modifiers on the expected type.
candType = c.expectedType.applyTypeModifiers(candType) candType = c.expectedType.applyTypeModifiers(candType, cand.addressable)
if candType == nil {
return false
}
// Handle untyped values specially since AssignableTo gives false negatives // Handle untyped values specially since AssignableTo gives false negatives
// for them (see https://golang.org/issue/32146). // for them (see https://golang.org/issue/32146).
@ -1629,8 +1657,15 @@ func (c *completer) matchingCandidate(cand *candidate) bool {
} }
} }
if c.expectedType.convertibleTo != nil { if c.expectedType.convertibleTo != nil && types.ConvertibleTo(candType, c.expectedType.convertibleTo) {
return types.ConvertibleTo(candType, c.expectedType.convertibleTo) return true
}
// Check if cand is addressable and a pointer to cand matches our type inference.
if cand.addressable && c.matchingCandidate(&candidate{obj: c.fakeObj(types.NewPointer(candType))}) {
// Mark the candidate so we know to prepend "&" when formatting.
cand.takeAddress = true
return true
} }
return false return false

View File

@ -130,6 +130,30 @@ func (c *completer) item(cand candidate) (CompletionItem, error) {
} }
} }
// Prepend "&" operator if our candidate needs address taken.
if cand.takeAddress {
var (
sel *ast.SelectorExpr
ok bool
)
if sel, ok = c.path[0].(*ast.SelectorExpr); !ok && len(c.path) > 1 {
sel, _ = c.path[1].(*ast.SelectorExpr)
}
// If we are in a selector, add an edit to place "&" before selector node.
if sel != nil {
edits, err := referenceEdit(c.snapshot.View().Session().Cache().FileSet(), c.mapper, sel)
if err != nil {
log.Error(c.ctx, "error generating reference edit", err)
} else {
protocolEdits = append(protocolEdits, edits...)
}
} else {
// If there is no selector, just stick the "&" at the start.
insert = "&" + insert
}
}
detail = strings.TrimPrefix(detail, "untyped ") detail = strings.TrimPrefix(detail, "untyped ")
item := CompletionItem{ item := CompletionItem{
Label: label, Label: label,

View File

@ -63,14 +63,13 @@ func (c *completer) literal(literalType types.Type, imp *importInfo) {
} }
} }
// Check if an object of type literalType or *literalType would // Check if an object of type literalType would match our expected type.
// match our expected type. cand := candidate{
var isPointer bool obj: c.fakeObj(literalType),
if !c.matchingType(literalType) { addressable: true,
isPointer = true }
if !c.matchingType(types.NewPointer(literalType)) { if !c.matchingCandidate(&cand) {
return return
}
} }
var ( var (
@ -105,7 +104,7 @@ func (c *completer) literal(literalType types.Type, imp *importInfo) {
// If prefix matches the type name, client may want a composite literal. // If prefix matches the type name, client may want a composite literal.
if score := c.matcher.Score(matchName); score >= 0 { if score := c.matcher.Score(matchName); score >= 0 {
if isPointer { if cand.takeAddress {
if sel != nil { if sel != nil {
// If we are in a selector we must place the "&" before the selector. // If we are in a selector we must place the "&" before the selector.
// For example, "foo.B<>" must complete to "&foo.Bar{}", not // For example, "foo.B<>" must complete to "&foo.Bar{}", not
@ -146,7 +145,7 @@ func (c *completer) literal(literalType types.Type, imp *importInfo) {
// If prefix matches "make", client may want a "make()" // If prefix matches "make", client may want a "make()"
// invocation. We also include the type name to allow for more // invocation. We also include the type name to allow for more
// flexible fuzzy matching. // flexible fuzzy matching.
if score := c.matcher.Score("make." + matchName); !isPointer && score >= 0 { if score := c.matcher.Score("make." + matchName); !cand.takeAddress && score >= 0 {
switch literalType.Underlying().(type) { switch literalType.Underlying().(type) {
case *types.Slice: case *types.Slice:
// The second argument to "make()" for slices is required, so default to "0". // The second argument to "make()" for slices is required, so default to "0".
@ -159,7 +158,7 @@ func (c *completer) literal(literalType types.Type, imp *importInfo) {
} }
// If prefix matches "func", client may want a function literal. // If prefix matches "func", client may want a function literal.
if score := c.matcher.Score("func"); !isPointer && score >= 0 && !isInterface(expType) { if score := c.matcher.Score("func"); !cand.takeAddress && score >= 0 && !isInterface(expType) {
switch t := literalType.Underlying().(type) { switch t := literalType.Underlying().(type) {
case *types.Signature: case *types.Signature:
c.functionLiteral(t, float64(score)) c.functionLiteral(t, float64(score))

View File

@ -188,9 +188,7 @@ func (c *completer) deepSearch(cand candidate) {
case *types.PkgName: case *types.PkgName:
c.packageMembers(obj.Imported(), cand.imp) c.packageMembers(obj.Imported(), cand.imp)
default: default:
// For now it is okay to assume obj is addressable since we don't search beyond c.methodsAndFields(obj.Type(), cand.addressable, cand.imp)
// function calls.
c.methodsAndFields(obj.Type(), true, cand.imp)
} }
// Pop the object off our search stack. // Pop the object off our search stack.

View File

@ -189,23 +189,12 @@ func (r *runner) FuzzyCompletion(t *testing.T, src span.Span, test tests.Complet
for _, pos := range test.CompletionItems { for _, pos := range test.CompletionItems {
want = append(want, tests.ToProtocolCompletionItem(*items[pos])) want = append(want, tests.ToProtocolCompletionItem(*items[pos]))
} }
prefix, list := r.callCompletion(t, src, source.CompletionOptions{ _, got := r.callCompletion(t, src, source.CompletionOptions{
FuzzyMatching: true, FuzzyMatching: true,
Deep: true, Deep: true,
}) })
if !strings.Contains(string(src.URI()), "builtins") { if !strings.Contains(string(src.URI()), "builtins") {
list = tests.FilterBuiltins(list) got = tests.FilterBuiltins(got)
}
var fuzzyMatcher *fuzzy.Matcher
if prefix != "" {
fuzzyMatcher = fuzzy.NewMatcher(prefix)
}
var got []protocol.CompletionItem
for _, item := range list {
if fuzzyMatcher != nil && fuzzyMatcher.Score(item.Label) <= 0 {
continue
}
got = append(got, item)
} }
if msg := tests.DiffCompletionItems(want, got); msg != "" { if msg := tests.DiffCompletionItems(want, got); msg != "" {
t.Errorf("%s: %s", src, msg) t.Errorf("%s: %s", src, msg)
@ -233,19 +222,11 @@ func (r *runner) RankCompletion(t *testing.T, src span.Span, test tests.Completi
for _, pos := range test.CompletionItems { for _, pos := range test.CompletionItems {
want = append(want, tests.ToProtocolCompletionItem(*items[pos])) want = append(want, tests.ToProtocolCompletionItem(*items[pos]))
} }
prefix, list := r.callCompletion(t, src, source.CompletionOptions{ _, got := r.callCompletion(t, src, source.CompletionOptions{
FuzzyMatching: true, FuzzyMatching: true,
Deep: true, Deep: true,
Literal: true, Literal: true,
}) })
fuzzyMatcher := fuzzy.NewMatcher(prefix)
var got []protocol.CompletionItem
for _, item := range list {
if fuzzyMatcher.Score(item.Label) <= 0 {
continue
}
got = append(got, item)
}
if msg := tests.CheckCompletionOrder(want, got, true); msg != "" { if msg := tests.CheckCompletionOrder(want, got, true); msg != "" {
t.Errorf("%s: %s", src, msg) t.Errorf("%s: %s", src, msg)
} }

View File

@ -387,6 +387,11 @@ func isPointer(T types.Type) bool {
return ok return ok
} }
func isVar(obj types.Object) bool {
_, ok := obj.(*types.Var)
return ok
}
// deref returns a pointer's element type, traversing as many levels as needed. // deref returns a pointer's element type, traversing as many levels as needed.
// Otherwise it returns typ. // Otherwise it returns typ.
func deref(typ types.Type) types.Type { func deref(typ types.Type) types.Type {

View File

@ -0,0 +1,53 @@
package address
func wantsPtr(*int) {}
type foo struct{ c int } //@item(addrFieldC, "c", "int", "field")
func _() {
var (
a string //@item(addrA, "a", "string", "var")
b int //@item(addrB, "b", "int", "var")
)
wantsPtr() //@rank(")", addrB, addrA),snippet(")", addrB, "&b", "&b")
wantsPtr(&b) //@snippet(")", addrB, "b", "b")
var s foo
s.c //@item(addrDeepC, "s.c", "int", "field")
wantsPtr() //@snippet(")", addrDeepC, "&s.c", "&s.c")
wantsPtr(s) //@snippet(")", addrDeepC, "&s.c", "&s.c")
wantsPtr(&s) //@snippet(")", addrDeepC, "s.c", "s.c")
// don't add "&" in item (it gets added as an additional edit)
wantsPtr(&s.c) //@snippet(")", addrFieldC, "c", "c")
}
func (f foo) ptr() *foo { return &f }
func _() {
getFoo := func() foo { return foo{} }
// not addressable
getFoo().c //@item(addrGetFooC, "getFoo().c", "int", "field")
// addressable
getFoo().ptr().c //@item(addrGetFooPtrC, "getFoo().ptr().c", "int", "field")
wantsPtr() //@rank(addrGetFooPtrC, addrGetFooC),snippet(")", addrGetFooPtrC, "&getFoo().ptr().c", "&getFoo().ptr().c")
wantsPtr(&g) //@rank(addrGetFooPtrC, addrGetFooC),snippet(")", addrGetFooPtrC, "getFoo().ptr().c", "getFoo().ptr().c")
}
type nested struct {
f foo
}
func _() {
getNested := func() nested { return nested{} }
getNested().f.c //@item(addrNestedC, "getNested().f.c", "int", "field")
getNested().f.ptr().c //@item(addrNestedPtrC, "getNested().f.ptr().c", "int", "field")
// addrNestedC is not addressable, so rank lower
wantsPtr(getNestedfc) //@fuzzy(")", addrNestedPtrC, addrNestedC)
}

View File

@ -20,6 +20,6 @@ func _() {
{ {
var foo chan int //@item(channelFoo, "foo", "chan int", "var") var foo chan int //@item(channelFoo, "foo", "chan int", "var")
wantsInt := func(int) {} //@item(channelWantsInt, "wantsInt", "func(int)", "var") wantsInt := func(int) {} //@item(channelWantsInt, "wantsInt", "func(int)", "var")
wantsInt(<-) //@complete(")", channelFoo, channelAB, channelWantsInt, channelAA) wantsInt(<-) //@rank(")", channelFoo, channelAB)
} }
} }

View File

@ -1,7 +1,3 @@
// Copyright 2019 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 deep package deep
import "context" import "context"

View File

@ -1,10 +1,10 @@
-- summary -- -- summary --
CompletionsCount = 224 CompletionsCount = 223
CompletionSnippetCount = 53 CompletionSnippetCount = 61
UnimportedCompletionsCount = 4 UnimportedCompletionsCount = 4
DeepCompletionsCount = 5 DeepCompletionsCount = 5
FuzzyCompletionsCount = 7 FuzzyCompletionsCount = 8
RankedCompletionsCount = 28 RankedCompletionsCount = 32
CaseSensitiveCompletionsCount = 4 CaseSensitiveCompletionsCount = 4
DiagnosticsCount = 35 DiagnosticsCount = 35
FoldingRangesCount = 2 FoldingRangesCount = 2