blob: 422c6c1095028486f12fc0ea5bb07330502993e9 [file] [log] [blame]
// Copyright 2013 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 main
import (
"fmt"
"go/ast"
"go/build"
"go/token"
"os"
"path"
"path/filepath"
"sort"
"strings"
"golang.org/x/tools/cmd/guru/serial"
"golang.org/x/tools/go/ast/astutil"
)
// what reports all the information about the query selection that can be
// obtained from parsing only its containing source file.
// It is intended to be a very low-latency query callable from GUI
// tools, e.g. to populate a menu of options of slower queries about
// the selected location.
func what(q *Query) error {
qpos, err := fastQueryPos(q.Build, q.Pos)
if err != nil {
return err
}
// (ignore errors)
srcdir, importPath, _ := guessImportPath(qpos.fset.File(qpos.start).Name(), q.Build)
// Determine which query modes are applicable to the selection.
enable := map[string]bool{
"describe": true, // any syntax; always enabled
}
if qpos.end > qpos.start {
enable["freevars"] = true // nonempty selection?
}
for _, n := range qpos.path {
switch n.(type) {
case *ast.Ident:
enable["definition"] = true
enable["referrers"] = true
enable["implements"] = true
}
// For implements, we approximate findInterestingNode.
if _, ok := enable["implements"]; !ok {
switch n.(type) {
case *ast.ArrayType,
*ast.StructType,
*ast.FuncType,
*ast.InterfaceType,
*ast.MapType,
*ast.ChanType:
enable["implements"] = true
}
}
}
// If we don't have an exact selection, disable modes that need one.
if !qpos.exact {
enable["describe"] = false
}
var modes []string
for mode := range enable {
modes = append(modes, mode)
}
sort.Strings(modes)
// Find the object referred to by the selection (if it's an
// identifier) and report the position of each identifier
// that refers to the same object.
//
// This may return spurious matches (e.g. struct fields) because
// it uses the best-effort name resolution done by go/parser.
var sameids []token.Pos
var object string
if id, ok := qpos.path[0].(*ast.Ident); ok {
if id.Obj == nil {
// An unresolved identifier is potentially a package name.
// Resolve them with a simple importer (adds ~100µs).
importer := func(imports map[string]*ast.Object, path string) (*ast.Object, error) {
pkg, ok := imports[path]
if !ok {
pkg = &ast.Object{
Kind: ast.Pkg,
Name: filepath.Base(path), // a guess
}
imports[path] = pkg
}
return pkg, nil
}
f := qpos.path[len(qpos.path)-1].(*ast.File)
ast.NewPackage(qpos.fset, map[string]*ast.File{"": f}, importer, nil)
}
if id.Obj != nil {
object = id.Obj.Name
decl := qpos.path[len(qpos.path)-1]
ast.Inspect(decl, func(n ast.Node) bool {
if n, ok := n.(*ast.Ident); ok && n.Obj == id.Obj {
sameids = append(sameids, n.Pos())
}
return true
})
}
}
q.Output(qpos.fset, &whatResult{
path: qpos.path,
srcdir: srcdir,
importPath: importPath,
modes: modes,
object: object,
sameids: sameids,
})
return nil
}
// guessImportPath finds the package containing filename, and returns
// its source directory (an element of $GOPATH) and its import path
// relative to it.
//
// TODO(adonovan): what about _test.go files that are not part of the
// package?
func guessImportPath(filename string, buildContext *build.Context) (srcdir, importPath string, err error) {
absFile, err := filepath.Abs(filename)
if err != nil {
return "", "", fmt.Errorf("can't form absolute path of %s: %v", filename, err)
}
absFileDir := filepath.Dir(absFile)
resolvedAbsFileDir, err := filepath.EvalSymlinks(absFileDir)
if err != nil {
return "", "", fmt.Errorf("can't evaluate symlinks of %s: %v", absFileDir, err)
}
segmentedAbsFileDir := segments(resolvedAbsFileDir)
// Find the innermost directory in $GOPATH that encloses filename.
minD := 1024
for _, gopathDir := range buildContext.SrcDirs() {
absDir, err := filepath.Abs(gopathDir)
if err != nil {
continue // e.g. non-existent dir on $GOPATH
}
resolvedAbsDir, err := filepath.EvalSymlinks(absDir)
if err != nil {
continue // e.g. non-existent dir on $GOPATH
}
d := prefixLen(segments(resolvedAbsDir), segmentedAbsFileDir)
// If there are multiple matches,
// prefer the innermost enclosing directory
// (smallest d).
if d >= 0 && d < minD {
minD = d
srcdir = gopathDir
importPath = path.Join(segmentedAbsFileDir[len(segmentedAbsFileDir)-minD:]...)
}
}
if srcdir == "" {
return "", "", fmt.Errorf("directory %s is not beneath any of these GOROOT/GOPATH directories: %s",
filepath.Dir(absFile), strings.Join(buildContext.SrcDirs(), ", "))
}
if importPath == "" {
// This happens for e.g. $GOPATH/src/a.go, but
// "" is not a valid path for (*go/build).Import.
return "", "", fmt.Errorf("cannot load package in root of source directory %s", srcdir)
}
return srcdir, importPath, nil
}
func segments(path string) []string {
return strings.Split(path, string(os.PathSeparator))
}
// prefixLen returns the length of the remainder of y if x is a prefix
// of y, a negative number otherwise.
func prefixLen(x, y []string) int {
d := len(y) - len(x)
if d >= 0 {
for i := range x {
if y[i] != x[i] {
return -1 // not a prefix
}
}
}
return d
}
type whatResult struct {
path []ast.Node
modes []string
srcdir string
importPath string
object string
sameids []token.Pos
}
func (r *whatResult) PrintPlain(printf printfFunc) {
for _, n := range r.path {
printf(n, "%s", astutil.NodeDescription(n))
}
printf(nil, "modes: %s", r.modes)
printf(nil, "srcdir: %s", r.srcdir)
printf(nil, "import path: %s", r.importPath)
for _, pos := range r.sameids {
printf(pos, "%s", r.object)
}
}
func (r *whatResult) JSON(fset *token.FileSet) []byte {
var enclosing []serial.SyntaxNode
for _, n := range r.path {
enclosing = append(enclosing, serial.SyntaxNode{
Description: astutil.NodeDescription(n),
Start: fset.Position(n.Pos()).Offset,
End: fset.Position(n.End()).Offset,
})
}
var sameids []string
for _, pos := range r.sameids {
sameids = append(sameids, fset.Position(pos).String())
}
return toJSON(&serial.What{
Modes: r.modes,
SrcDir: r.srcdir,
ImportPath: r.importPath,
Enclosing: enclosing,
Object: r.object,
SameIDs: sameids,
})
}