mirror of
https://github.com/golang/go
synced 2024-11-19 02:34:44 -07:00
f344c7530c
Added a View interface to the source package, which allows for reading of other files (in the same package or in other packages). We were already reading files in jump to definition (to handle the lack of column information in export data), but now we can also read files in diagnostics, which allows us to determine the end of an identifier so that we can report ranges in diagnostic messages. Updates golang/go#29150 Change-Id: I7958d860dea8f41f2df88a467b5e2946bba4d1c5 Reviewed-on: https://go-review.googlesource.com/c/154742 Reviewed-by: Ian Cottrell <iancottrell@google.com>
206 lines
4.9 KiB
Go
206 lines
4.9 KiB
Go
// 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"
|
|
)
|
|
|
|
func Definition(ctx context.Context, v View, f File, pos token.Pos) (Range, error) {
|
|
fAST, err := f.GetAST()
|
|
if err != nil {
|
|
return Range{}, err
|
|
}
|
|
pkg, err := f.GetPackage()
|
|
if err != nil {
|
|
return Range{}, err
|
|
}
|
|
i, err := findIdentifier(fAST, pos)
|
|
if err != nil {
|
|
return Range{}, err
|
|
}
|
|
if i.ident == nil {
|
|
return Range{}, fmt.Errorf("not a valid identifier")
|
|
}
|
|
obj := pkg.TypesInfo.ObjectOf(i.ident)
|
|
if obj == nil {
|
|
return Range{}, fmt.Errorf("no object")
|
|
}
|
|
if i.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 := obj.(*types.Var); ok {
|
|
if n, ok := v.Type().(*types.Named); ok {
|
|
obj = n.Obj()
|
|
}
|
|
}
|
|
}
|
|
fset, err := f.GetFileSet()
|
|
if err != nil {
|
|
return Range{}, err
|
|
}
|
|
return objToRange(v, fset, obj), nil
|
|
}
|
|
|
|
func TypeDefinition(ctx context.Context, v View, f File, pos token.Pos) (Range, error) {
|
|
fAST, err := f.GetAST()
|
|
if err != nil {
|
|
return Range{}, err
|
|
}
|
|
pkg, err := f.GetPackage()
|
|
if err != nil {
|
|
return Range{}, err
|
|
}
|
|
i, err := findIdentifier(fAST, pos)
|
|
if err != nil {
|
|
return Range{}, err
|
|
}
|
|
if i.ident == nil {
|
|
return Range{}, fmt.Errorf("not a valid identifier")
|
|
}
|
|
typ := pkg.TypesInfo.TypeOf(i.ident)
|
|
if typ == nil {
|
|
return Range{}, fmt.Errorf("no type for %s", i.ident.Name)
|
|
}
|
|
obj := typeToObject(typ)
|
|
if obj == nil {
|
|
return Range{}, fmt.Errorf("no object for type %s", typ.String())
|
|
}
|
|
fset, err := f.GetFileSet()
|
|
if err != nil {
|
|
return Range{}, err
|
|
}
|
|
return objToRange(v, fset, obj), nil
|
|
}
|
|
|
|
func typeToObject(typ types.Type) (obj types.Object) {
|
|
switch typ := typ.(type) {
|
|
case *types.Named:
|
|
obj = typ.Obj()
|
|
case *types.Pointer:
|
|
obj = typeToObject(typ.Elem())
|
|
}
|
|
return obj
|
|
}
|
|
|
|
// ident returns the ident plus any extra information needed
|
|
type ident struct {
|
|
ident *ast.Ident
|
|
wasEmbeddedField bool
|
|
}
|
|
|
|
// findIdentifier returns the ast.Ident for a position
|
|
// in a file, accounting for a potentially incomplete selector.
|
|
func findIdentifier(f *ast.File, pos token.Pos) (ident, error) {
|
|
m, err := checkIdentifier(f, pos)
|
|
if err != nil {
|
|
return ident{}, err
|
|
}
|
|
if m.ident != nil {
|
|
return m, nil
|
|
}
|
|
// 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 checkIdentifier(f, pos-1)
|
|
}
|
|
|
|
// checkIdentifier checks a single position for a potential identifier.
|
|
func checkIdentifier(f *ast.File, pos token.Pos) (ident, error) {
|
|
path, _ := astutil.PathEnclosingInterval(f, pos, pos)
|
|
result := ident{}
|
|
if path == nil {
|
|
return result, 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 {
|
|
for _, n := range path[1:] {
|
|
if field, ok := n.(*ast.Field); ok {
|
|
result.wasEmbeddedField = len(field.Names) == 0
|
|
}
|
|
}
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
func objToRange(v View, fset *token.FileSet, obj types.Object) Range {
|
|
p := obj.Pos()
|
|
f := fset.File(p)
|
|
pos := f.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 := v.GetFile(ToURI(pos.Filename))
|
|
tok, err := f.GetToken()
|
|
if err != nil {
|
|
goto Return
|
|
}
|
|
src, err := f.Read()
|
|
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())),
|
|
}
|
|
}
|
|
|
|
// 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
|
|
}
|
|
}
|
|
}
|