1
0
mirror of https://github.com/golang/go synced 2024-11-18 12:44:49 -07:00

internal/lsp/source: speed up completion candidate formatting

Completion could be slow due to calls to astutil.PathEnclosingInterval
for every candidate during formatting. There were two reasons we
called PEI:

1. To properly render type alias names, we must refer to the AST
   because the alias name is not available in the typed world.
   Previously we would call PEI to find the *type.Var's
   corresponding *ast.Field, but now we have a PosToField cache that
   lets us jump straight from the types.Object's token.Pos to the
   corresponding *ast.Field.

2. To display an object's documentation we must refer to the AST. We
   need the object's declaring node and any containing ast.Decl. We
   now maintain a special PosToDecl cache so we can avoid the PEI call
   in this case as well.

We can't use a single cache for both because the *ast.Field's position
is present in both caches (but points to different nodes). The caches
are memoized to defer generation until they are needed and to save
work creating them if the *ast.Files haven't changed.

These changes speed up completing the fields of
github.com/aws/aws-sdk-go/service/ec2 from 18.5s to 45ms on my laptop.

Fixes golang/go#37450.

Change-Id: I25cc5ea39551db728a2348f346342ebebeddd049
Reviewed-on: https://go-review.googlesource.com/c/tools/+/221021
Run-TryBot: Muir Manders <muir@mnd.rs>
Reviewed-by: Rebecca Stambler <rstambler@golang.org>
This commit is contained in:
Muir Manders 2020-02-25 20:28:00 -08:00 committed by Rebecca Stambler
parent 9a0e069805
commit 1837592efa
11 changed files with 221 additions and 96 deletions

View File

@ -92,8 +92,10 @@ func (s *snapshot) buildPackageHandle(ctx context.Context, id packageID, mode so
dep.check(ctx) dep.check(ctx)
}(dep) }(dep)
} }
data := &packageData{} data := &packageData{}
data.pkg, data.err = typeCheck(ctx, fset, m, mode, goFiles, compiledGoFiles, deps) data.pkg, data.err = typeCheck(ctx, fset, m, mode, goFiles, compiledGoFiles, deps)
return data return data
}) })
ph.handle = h ph.handle = h
@ -413,6 +415,7 @@ func typeCheck(ctx context.Context, fset *token.FileSet, m *metadata, mode sourc
} }
} }
} }
return pkg, nil return pkg, nil
} }

View File

@ -28,10 +28,15 @@ type parseKey struct {
mode source.ParseMode mode source.ParseMode
} }
// astCacheKey is similar to parseKey, but is a distinct type because
// it is used to key a different value within the same map.
type astCacheKey parseKey
type parseGoHandle struct { type parseGoHandle struct {
handle *memoize.Handle handle *memoize.Handle
file source.FileHandle file source.FileHandle
mode source.ParseMode mode source.ParseMode
astCacheHandle *memoize.Handle
} }
type parseGoData struct { type parseGoData struct {
@ -63,10 +68,14 @@ func (c *Cache) parseGoHandle(ctx context.Context, fh source.FileHandle, mode so
h := c.store.Bind(key, func(ctx context.Context) interface{} { h := c.store.Bind(key, func(ctx context.Context) interface{} {
return parseGo(ctx, fset, fh, mode) return parseGo(ctx, fset, fh, mode)
}) })
return &parseGoHandle{ return &parseGoHandle{
handle: h, handle: h,
file: fh, file: fh,
mode: mode, mode: mode,
astCacheHandle: c.store.Bind(astCacheKey(key), func(ctx context.Context) interface{} {
return buildASTCache(ctx, h)
}),
} }
} }
@ -111,6 +120,133 @@ func (pgh *parseGoHandle) Cached() (*ast.File, []byte, *protocol.ColumnMapper, e
return data.ast, data.src, data.mapper, data.parseError, data.err return data.ast, data.src, data.mapper, data.parseError, data.err
} }
func (pgh *parseGoHandle) PosToDecl(ctx context.Context) (map[token.Pos]ast.Decl, error) {
v, err := pgh.astCacheHandle.Get(ctx)
if err != nil || v == nil {
return nil, err
}
data := v.(*astCacheData)
if data.err != nil {
return nil, data.err
}
return data.posToDecl, nil
}
func (pgh *parseGoHandle) PosToField(ctx context.Context) (map[token.Pos]*ast.Field, error) {
v, err := pgh.astCacheHandle.Get(ctx)
if err != nil || v == nil {
return nil, err
}
data := v.(*astCacheData)
if data.err != nil {
return nil, data.err
}
return data.posToField, nil
}
type astCacheData struct {
memoize.NoCopy
err error
posToDecl map[token.Pos]ast.Decl
posToField map[token.Pos]*ast.Field
}
// buildASTCache builds caches to aid in quickly going from the typed
// world to the syntactic world.
func buildASTCache(ctx context.Context, parseHandle *memoize.Handle) *astCacheData {
var (
// path contains all ancestors, including n.
path []ast.Node
// decls contains all ancestors that are decls.
decls []ast.Decl
)
v, err := parseHandle.Get(ctx)
if err != nil || v == nil || v.(*parseGoData).ast == nil {
return &astCacheData{err: err}
}
data := &astCacheData{
posToDecl: make(map[token.Pos]ast.Decl),
posToField: make(map[token.Pos]*ast.Field),
}
ast.Inspect(v.(*parseGoData).ast, func(n ast.Node) bool {
if n == nil {
lastP := path[len(path)-1]
path = path[:len(path)-1]
if len(decls) > 0 && decls[len(decls)-1] == lastP {
decls = decls[:len(decls)-1]
}
return false
}
path = append(path, n)
switch n := n.(type) {
case *ast.Field:
addField := func(f ast.Node) {
if f.Pos().IsValid() {
data.posToField[f.Pos()] = n
if len(decls) > 0 {
data.posToDecl[f.Pos()] = decls[len(decls)-1]
}
}
}
// Add mapping for *ast.Field itself. This handles embedded
// fields which have no associated *ast.Ident name.
addField(n)
// Add mapping for each field name since you can have
// multiple names for the same type expression.
for _, name := range n.Names {
addField(name)
}
// Also map "X" in "...X" to the containing *ast.Field. This
// makes it easy to format variadic signature params
// properly.
if elips, ok := n.Type.(*ast.Ellipsis); ok && elips.Elt != nil {
addField(elips.Elt)
}
case *ast.FuncDecl:
decls = append(decls, n)
if n.Name != nil && n.Name.Pos().IsValid() {
data.posToDecl[n.Name.Pos()] = n
}
case *ast.GenDecl:
decls = append(decls, n)
for _, spec := range n.Specs {
switch spec := spec.(type) {
case *ast.TypeSpec:
if spec.Name != nil && spec.Name.Pos().IsValid() {
data.posToDecl[spec.Name.Pos()] = n
}
case *ast.ValueSpec:
for _, id := range spec.Names {
if id != nil && id.Pos().IsValid() {
data.posToDecl[id.Pos()] = n
}
}
}
}
}
return true
})
return data
}
func hashParseKeys(pghs []*parseGoHandle) string { func hashParseKeys(pghs []*parseGoHandle) string {
b := bytes.NewBuffer(nil) b := bytes.NewBuffer(nil)
for _, pgh := range pghs { for _, pgh := range pghs {

View File

@ -187,15 +187,22 @@ func (c *completer) item(ctx context.Context, cand candidate) (CompletionItem, e
if cand.imp != nil && cand.imp.pkg != nil { if cand.imp != nil && cand.imp.pkg != nil {
searchPkg = cand.imp.pkg searchPkg = cand.imp.pkg
} }
file, pkg, err := findPosInPackage(c.snapshot.View(), searchPkg, obj.Pos())
ph, pkg, err := findPosInPackage(c.snapshot.View(), searchPkg, obj.Pos())
if err != nil { if err != nil {
return item, nil return item, nil
} }
ident, err := findIdentifier(ctx, c.snapshot, pkg, file, obj.Pos())
posToDecl, err := ph.PosToDecl(ctx)
if err != nil { if err != nil {
return CompletionItem{}, err
}
decl := posToDecl[obj.Pos()]
if decl == nil {
return item, nil return item, nil
} }
hover, err := HoverIdentifier(ctx, ident)
hover, err := hoverInfo(pkg, obj, decl)
if err != nil { if err != nil {
event.Error(ctx, "failed to find Hover", err, tag.URI.Of(uri)) event.Error(ctx, "failed to find Hover", err, tag.URI.Of(uri))
return item, nil return item, nil

View File

@ -102,10 +102,7 @@ func HoverIdentifier(ctx context.Context, i *IdentifierInfo) (*HoverInformation,
h.SingleLine = objectString(obj, i.qf) h.SingleLine = objectString(obj, i.qf)
} }
h.ImportPath, h.Link, h.SymbolName = pathLinkAndSymbolName(i) h.ImportPath, h.Link, h.SymbolName = pathLinkAndSymbolName(i)
if h.comment != nil {
h.FullDocumentation = h.comment.Text()
h.Synopsis = doc.Synopsis(h.FullDocumentation)
}
return h, nil return h, nil
} }
@ -217,13 +214,18 @@ func hover(ctx context.Context, fset *token.FileSet, pkg Package, d Declaration)
_, done := event.Start(ctx, "source.hover") _, done := event.Start(ctx, "source.hover")
defer done() defer done()
obj := d.obj return hoverInfo(pkg, d.obj, d.node)
switch node := d.node.(type) { }
func hoverInfo(pkg Package, obj types.Object, node ast.Node) (*HoverInformation, error) {
var info *HoverInformation
switch node := node.(type) {
case *ast.Ident: case *ast.Ident:
// The package declaration. // The package declaration.
for _, f := range pkg.GetSyntax() { for _, f := range pkg.GetSyntax() {
if f.Name == node { if f.Name == node {
return &HoverInformation{comment: f.Doc}, nil info = &HoverInformation{comment: f.Doc}
} }
} }
case *ast.ImportSpec: case *ast.ImportSpec:
@ -238,32 +240,47 @@ func hover(ctx context.Context, fset *token.FileSet, pkg Package, d Declaration)
var doc *ast.CommentGroup var doc *ast.CommentGroup
for _, file := range imp.GetSyntax() { for _, file := range imp.GetSyntax() {
if file.Doc != nil { if file.Doc != nil {
return &HoverInformation{source: obj, comment: doc}, nil info = &HoverInformation{source: obj, comment: doc}
} }
} }
} }
return &HoverInformation{source: node}, nil info = &HoverInformation{source: node}
case *ast.GenDecl: case *ast.GenDecl:
switch obj := obj.(type) { switch obj := obj.(type) {
case *types.TypeName, *types.Var, *types.Const, *types.Func: case *types.TypeName, *types.Var, *types.Const, *types.Func:
return formatGenDecl(node, obj, obj.Type()) var err error
info, err = formatGenDecl(node, obj, obj.Type())
if err != nil {
return nil, err
}
} }
case *ast.TypeSpec: case *ast.TypeSpec:
if obj.Parent() == types.Universe { if obj.Parent() == types.Universe {
if obj.Name() == "error" { if obj.Name() == "error" {
return &HoverInformation{source: node}, nil info = &HoverInformation{source: node}
} else {
info = &HoverInformation{source: node.Name} // comments not needed for builtins
} }
return &HoverInformation{source: node.Name}, nil // comments not needed for builtins
} }
case *ast.FuncDecl: case *ast.FuncDecl:
switch obj.(type) { switch obj.(type) {
case *types.Func: case *types.Func:
return &HoverInformation{source: obj, comment: node.Doc}, nil info = &HoverInformation{source: obj, comment: node.Doc}
case *types.Builtin: case *types.Builtin:
return &HoverInformation{source: node.Type, comment: node.Doc}, nil info = &HoverInformation{source: node.Type, comment: node.Doc}
} }
} }
return &HoverInformation{source: obj}, nil
if info == nil {
info = &HoverInformation{source: obj}
}
if info.comment != nil {
info.FullDocumentation = info.comment.Text()
info.Synopsis = doc.Synopsis(info.FullDocumentation)
}
return info, nil
} }
func formatGenDecl(node *ast.GenDecl, obj types.Object, typ types.Type) (*HoverInformation, error) { func formatGenDecl(node *ast.GenDecl, obj types.Object, typ types.Type) (*HoverInformation, error) {
@ -283,6 +300,7 @@ func formatGenDecl(node *ast.GenDecl, obj types.Object, typ types.Type) (*HoverI
if spec == nil { if spec == nil {
return nil, errors.Errorf("no spec for node %v at position %v", node, obj.Pos()) return nil, errors.Errorf("no spec for node %v at position %v", node, obj.Pos())
} }
// If we have a field or method. // If we have a field or method.
switch obj.(type) { switch obj.(type) {
case *types.Var, *types.Const, *types.Func: case *types.Var, *types.Const, *types.Func:

View File

@ -12,7 +12,6 @@ import (
"go/types" "go/types"
"strconv" "strconv"
"golang.org/x/tools/go/ast/astutil"
"golang.org/x/tools/internal/event" "golang.org/x/tools/internal/event"
"golang.org/x/tools/internal/lsp/protocol" "golang.org/x/tools/internal/lsp/protocol"
errors "golang.org/x/xerrors" errors "golang.org/x/xerrors"
@ -203,7 +202,7 @@ func findIdentifier(ctx context.Context, s Snapshot, pkg Package, file *ast.File
} }
result.Declaration.MappedRange = append(result.Declaration.MappedRange, rng) result.Declaration.MappedRange = append(result.Declaration.MappedRange, rng)
if result.Declaration.node, err = objToNode(s.View(), pkg, result.Declaration.obj); err != nil { if result.Declaration.node, err = objToDecl(ctx, view, pkg, result.Declaration.obj); err != nil {
return nil, err return nil, err
} }
typ := pkg.GetTypesInfo().TypeOf(result.ident) typ := pkg.GetTypesInfo().TypeOf(result.ident)
@ -261,31 +260,18 @@ func hasErrorType(obj types.Object) bool {
return types.IsInterface(obj.Type()) && obj.Pkg() == nil && obj.Name() == "error" return types.IsInterface(obj.Type()) && obj.Pkg() == nil && obj.Name() == "error"
} }
func objToNode(v View, pkg Package, obj types.Object) (ast.Decl, error) { func objToDecl(ctx context.Context, v View, srcPkg Package, obj types.Object) (ast.Decl, error) {
declAST, _, err := findPosInPackage(v, pkg, obj.Pos()) ph, _, err := findPosInPackage(v, srcPkg, obj.Pos())
if err != nil { if err != nil {
return nil, err return nil, err
} }
path, _ := astutil.PathEnclosingInterval(declAST, obj.Pos(), obj.Pos())
if path == nil { posToDecl, err := ph.PosToDecl(ctx)
return nil, errors.Errorf("no path for object %v", obj.Name()) if err != nil {
return nil, err
} }
for _, node := range path {
switch node := node.(type) { return posToDecl[obj.Pos()], nil
case *ast.GenDecl:
// Type names, fields, and methods.
switch obj.(type) {
case *types.TypeName, *types.Var, *types.Const, *types.Func:
return node, nil
}
case *ast.FuncDecl:
// Function signatures.
if _, ok := obj.(*types.Func); ok {
return node, nil
}
}
}
return nil, nil // didn't find a node, but don't fail
} }
// importSpec handles positions inside of an *ast.ImportSpec. // importSpec handles positions inside of an *ast.ImportSpec.

View File

@ -99,7 +99,7 @@ FindCall:
comment *ast.CommentGroup comment *ast.CommentGroup
) )
if obj != nil { if obj != nil {
node, err := objToNode(snapshot.View(), pkg, obj) node, err := objToDecl(ctx, snapshot.View(), pkg, obj)
if err != nil { if err != nil {
return nil, 0, err return nil, 0, err
} }

View File

@ -858,7 +858,7 @@ func (r *runner) SignatureHelp(t *testing.T, spn span.Span, want *protocol.Signa
Signatures: []protocol.SignatureInformation{*gotSignature}, Signatures: []protocol.SignatureInformation{*gotSignature},
ActiveParameter: float64(gotActiveParameter), ActiveParameter: float64(gotActiveParameter),
} }
if diff := tests.DiffSignatures(spn, got, want); diff != "" { if diff := tests.DiffSignatures(spn, want, got); diff != "" {
t.Error(diff) t.Error(diff)
} }
} }

View File

@ -195,21 +195,16 @@ func newSignature(ctx context.Context, s Snapshot, pkg Package, file *ast.File,
// To do this, it looks in the AST of the file in which the object is declared. // To do this, it looks in the AST of the file in which the object is declared.
// On any errors, it always fallbacks back to types.TypeString. // On any errors, it always fallbacks back to types.TypeString.
func formatVarType(ctx context.Context, s Snapshot, srcpkg Package, srcfile *ast.File, obj *types.Var, qf types.Qualifier) string { func formatVarType(ctx context.Context, s Snapshot, srcpkg Package, srcfile *ast.File, obj *types.Var, qf types.Qualifier) string {
file, pkg, err := findPosInPackage(s.View(), srcpkg, obj.Pos()) ph, pkg, err := findPosInPackage(s.View(), srcpkg, obj.Pos())
if err != nil { if err != nil {
return types.TypeString(obj.Type(), qf) return types.TypeString(obj.Type(), qf)
} }
// Named and unnamed variables must be handled differently.
// Unnamed variables appear in the result values of a function signature. expr, err := varType(ctx, ph, obj)
var expr ast.Expr
if obj.Name() != "" {
expr, err = namedVarType(ctx, s, pkg, file, obj)
} else {
expr, err = unnamedVarType(file, obj)
}
if err != nil { if err != nil {
return types.TypeString(obj.Type(), qf) return types.TypeString(obj.Type(), qf)
} }
// The type names in the AST may not be correctly qualified. // The type names in the AST may not be correctly qualified.
// Determine the package name to use based on the package that originated // Determine the package name to use based on the package that originated
// the query and the package in which the type is declared. // the query and the package in which the type is declared.
@ -224,43 +219,19 @@ func formatVarType(ctx context.Context, s Snapshot, srcpkg Package, srcfile *ast
return fmted return fmted
} }
// unnamedVarType finds the type for an unnamed variable. // varType returns the type expression for a *types.Var.
func unnamedVarType(file *ast.File, obj *types.Var) (ast.Expr, error) { func varType(ctx context.Context, ph ParseGoHandle, obj *types.Var) (ast.Expr, error) {
path, _ := astutil.PathEnclosingInterval(file, obj.Pos(), obj.Pos()) posToField, err := ph.PosToField(ctx)
var expr ast.Expr
for _, p := range path {
e, ok := p.(ast.Expr)
if !ok {
break
}
expr = e
}
typ, ok := expr.(ast.Expr)
if !ok {
return nil, fmt.Errorf("unexpected type for node (%T)", path[0])
}
return typ, nil
}
// namedVarType returns the type for a named variable.
func namedVarType(ctx context.Context, s Snapshot, pkg Package, file *ast.File, obj *types.Var) (ast.Expr, error) {
ident, err := findIdentifier(ctx, s, pkg, file, obj.Pos())
if err != nil { if err != nil {
return nil, err return nil, err
} }
if ident.Declaration.obj != obj { field := posToField[obj.Pos()]
return nil, fmt.Errorf("expected the ident's declaration %v to be equal to obj %v", ident.Declaration.obj, obj) if field == nil {
}
if i := ident.ident; i == nil || i.Obj == nil || i.Obj.Decl == nil {
return nil, fmt.Errorf("no declaration for object %s", obj.Name()) return nil, fmt.Errorf("no declaration for object %s", obj.Name())
} }
f, ok := ident.ident.Obj.Decl.(*ast.Field) typ, ok := field.Type.(ast.Expr)
if !ok { if !ok {
return nil, fmt.Errorf("declaration of object %v is %T, not *ast.Field", obj.Name(), ident.ident.Obj.Decl) return nil, fmt.Errorf("unexpected type for node (%T)", field.Type)
}
typ, ok := f.Type.(ast.Expr)
if !ok {
return nil, fmt.Errorf("unexpected type for node (%T)", f.Type)
} }
return typ, nil return typ, nil
} }

View File

@ -529,7 +529,7 @@ func CompareDiagnostic(a, b *Diagnostic) int {
return 1 return 1
} }
func findPosInPackage(v View, searchpkg Package, pos token.Pos) (*ast.File, Package, error) { func findPosInPackage(v View, searchpkg Package, pos token.Pos) (ParseGoHandle, Package, error) {
tok := v.Session().Cache().FileSet().File(pos) tok := v.Session().Cache().FileSet().File(pos)
if tok == nil { if tok == nil {
return nil, nil, errors.Errorf("no file for pos in package %s", searchpkg.ID()) return nil, nil, errors.Errorf("no file for pos in package %s", searchpkg.ID())
@ -540,14 +540,7 @@ func findPosInPackage(v View, searchpkg Package, pos token.Pos) (*ast.File, Pack
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
file, _, _, _, err := ph.Cached() return ph, pkg, nil
if err != nil {
return nil, nil, err
}
if !(file.Pos() <= pos && pos <= file.End()) {
return nil, nil, fmt.Errorf("pos %v, apparently in file %q, is not between %v and %v", pos, ph.File().URI(), file.Pos(), file.End())
}
return file, pkg, nil
} }
func findMapperInPackage(v View, searchpkg Package, uri span.URI) (*protocol.ColumnMapper, error) { func findMapperInPackage(v View, searchpkg Package, uri span.URI) (*protocol.ColumnMapper, error) {

View File

@ -303,6 +303,17 @@ type ParseGoHandle interface {
// Cached returns the AST for this handle, if it has already been stored. // Cached returns the AST for this handle, if it has already been stored.
Cached() (file *ast.File, src []byte, m *protocol.ColumnMapper, parseErr error, err error) Cached() (file *ast.File, src []byte, m *protocol.ColumnMapper, parseErr error, err error)
// PosToField is a cache of *ast.Fields by token.Pos. This allows us
// to quickly find corresponding *ast.Field node given a *types.Var.
// We must refer to the AST to render type aliases properly when
// formatting signatures and other types.
PosToField(context.Context) (map[token.Pos]*ast.Field, error)
// PosToDecl maps certain objects' positions to their surrounding
// ast.Decl. This mapping is used when building the documentation
// string for the objects.
PosToDecl(context.Context) (map[token.Pos]ast.Decl, error)
} }
type ParseModHandle interface { type ParseModHandle interface {

View File

@ -34,7 +34,7 @@ func _() {
*deepCircle *deepCircle
} }
var circle deepCircle //@item(deepCircle, "circle", "deepCircle", "var") var circle deepCircle //@item(deepCircle, "circle", "deepCircle", "var")
*circle.deepCircle //@item(deepCircleField, "*circle.deepCircle", "*deepCircle", "field", "deepCircle is circular.") *circle.deepCircle //@item(deepCircleField, "*circle.deepCircle", "*deepCircle", "field")
var _ deepCircle = circ //@deep(" //", deepCircle, deepCircleField) var _ deepCircle = circ //@deep(" //", deepCircle, deepCircleField)
} }