// Copyright 2018 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 source import ( "bytes" "context" "fmt" "go/ast" "go/token" "go/types" "golang.org/x/tools/go/ast/astutil" ) // IdentifierInfo holds information about an identifier in Go source. type IdentifierInfo struct { Name string Range Range File File Type struct { Range Range Object types.Object } Declaration struct { Range Range Object types.Object } ident *ast.Ident wasEmbeddedField bool } // Identifier returns identifier information for a position // in a file, accounting for a potentially incomplete selector. func Identifier(ctx context.Context, v View, f File, pos token.Pos) (*IdentifierInfo, error) { if result, err := identifier(ctx, v, f, pos); err != nil || result != nil { return result, err } // If the position is not an identifier but immediately follows // an identifier or selector period (as is common when // requesting a completion), use the path to the preceding node. return identifier(ctx, v, f, pos-1) } func (i *IdentifierInfo) Hover(q types.Qualifier) (string, error) { if q == nil { fAST, err := i.File.GetAST() if err != nil { return "", err } pkg, err := i.File.GetPackage() if err != nil { return "", err } q = qualifier(fAST, pkg.Types, pkg.TypesInfo) } return types.ObjectString(i.Declaration.Object, q), nil } // identifier checks a single position for a potential identifier. func identifier(ctx context.Context, v View, f File, pos token.Pos) (*IdentifierInfo, error) { fAST, err := f.GetAST() if err != nil { return nil, err } pkg, err := f.GetPackage() if err != nil { return nil, err } path, _ := astutil.PathEnclosingInterval(fAST, pos, pos) result := &IdentifierInfo{ File: f, } if path == nil { return nil, fmt.Errorf("can't find node enclosing position") } switch node := path[0].(type) { case *ast.Ident: result.ident = node case *ast.SelectorExpr: result.ident = node.Sel } if result.ident == nil { return nil, nil } for _, n := range path[1:] { if field, ok := n.(*ast.Field); ok { result.wasEmbeddedField = len(field.Names) == 0 } } result.Name = result.ident.Name result.Range = Range{Start: result.ident.Pos(), End: result.ident.End()} result.Declaration.Object = pkg.TypesInfo.ObjectOf(result.ident) if result.Declaration.Object == nil { return nil, fmt.Errorf("no object for ident %v", result.Name) } if result.wasEmbeddedField { // The original position was on the embedded field declaration, so we // try to dig out the type and jump to that instead. if v, ok := result.Declaration.Object.(*types.Var); ok { if n, ok := v.Type().(*types.Named); ok { result.Declaration.Object = n.Obj() } } } if result.Declaration.Range, err = objToRange(ctx, v, result.Declaration.Object); err != nil { return nil, err } typ := pkg.TypesInfo.TypeOf(result.ident) if typ == nil { return nil, fmt.Errorf("no type for %s", result.Name) } result.Type.Object = typeToObject(typ) if result.Type.Object != nil { if result.Type.Range, err = objToRange(ctx, v, result.Type.Object); err != nil { return nil, err } } return result, nil } func typeToObject(typ types.Type) types.Object { switch typ := typ.(type) { case *types.Named: return typ.Obj() case *types.Pointer: return typeToObject(typ.Elem()) default: return nil } } func objToRange(ctx context.Context, v View, obj types.Object) (Range, error) { p := obj.Pos() if !p.IsValid() { return Range{}, fmt.Errorf("invalid position for %v", obj.Name()) } tok := v.FileSet().File(p) pos := tok.Position(p) if pos.Column == 1 { // We do not have full position information because exportdata does not // store the column. For now, we attempt to read the original source // and find the identifier within the line. If we find it, we patch the // column to match its offset. // // TODO: If we parse from source, we will never need this hack. f, err := v.GetFile(ctx, ToURI(pos.Filename)) if err != nil { goto Return } src, err := f.Read() if err != nil { goto Return } tok, err := f.GetToken() if err != nil { goto Return } start := lineStart(tok, pos.Line) offset := tok.Offset(start) col := bytes.Index(src[offset:], []byte(obj.Name())) p = tok.Pos(offset + col) } Return: return Range{ Start: p, End: p + token.Pos(identifierLen(obj.Name())), }, nil } // TODO: This needs to be fixed to address golang.org/issue/29149. func identifierLen(ident string) int { return len([]byte(ident)) } // this functionality was borrowed from the analysisutil package func lineStart(f *token.File, line int) token.Pos { // Use binary search to find the start offset of this line. // // TODO(adonovan): eventually replace this function with the // simpler and more efficient (*go/token.File).LineStart, added // in go1.12. min := 0 // inclusive max := f.Size() // exclusive for { offset := (min + max) / 2 pos := f.Pos(offset) posn := f.Position(pos) if posn.Line == line { return pos - (token.Pos(posn.Column) - 1) } if min+1 >= max { return token.NoPos } if posn.Line < line { min = offset } else { max = offset } } }