mirror of
https://github.com/golang/go
synced 2024-11-18 13:34:41 -07:00
e08a7ae6bc
The -modified flag causes guru to read a simple archive file from stdin. This archive specifies alternative contents for one or more file names. The build.Context checks this table before delegating to the usual behavior. This will not work for files that import "C" since cgo accesses the file system directly. Added end-to-end test via Emacs. Simplify findQueryPos (now: fileOffsetToPos) Credit: Daniel Morsing, for the prototype of this feature. Change-Id: I5ae818ed5e8bb81001781893dded2d085e9cf8d6 Reviewed-on: https://go-review.googlesource.com/19498 Reviewed-by: Daniel Morsing <daniel.morsing@gmail.com>
261 lines
6.5 KiB
Go
261 lines
6.5 KiB
Go
// 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 (
|
|
"bytes"
|
|
"fmt"
|
|
"go/ast"
|
|
"go/build"
|
|
"go/token"
|
|
"go/types"
|
|
"io"
|
|
"sort"
|
|
|
|
"golang.org/x/tools/cmd/guru/serial"
|
|
"golang.org/x/tools/go/buildutil"
|
|
"golang.org/x/tools/go/loader"
|
|
"golang.org/x/tools/refactor/importgraph"
|
|
)
|
|
|
|
// Referrers reports all identifiers that resolve to the same object
|
|
// as the queried identifier, within any package in the analysis scope.
|
|
func referrers(q *Query) error {
|
|
lconf := loader.Config{Build: q.Build}
|
|
allowErrors(&lconf)
|
|
|
|
if _, err := importQueryPackage(q.Pos, &lconf); err != nil {
|
|
return err
|
|
}
|
|
|
|
var id *ast.Ident
|
|
var obj types.Object
|
|
var lprog *loader.Program
|
|
var pass2 bool
|
|
var qpos *queryPos
|
|
for {
|
|
// Load/parse/type-check the program.
|
|
var err error
|
|
lprog, err = lconf.Load()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
q.Fset = lprog.Fset
|
|
|
|
qpos, err = parseQueryPos(lprog, q.Pos, false)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
id, _ = qpos.path[0].(*ast.Ident)
|
|
if id == nil {
|
|
return fmt.Errorf("no identifier here")
|
|
}
|
|
|
|
obj = qpos.info.ObjectOf(id)
|
|
if obj == nil {
|
|
// Happens for y in "switch y := x.(type)",
|
|
// the package declaration,
|
|
// and unresolved identifiers.
|
|
if _, ok := qpos.path[1].(*ast.File); ok { // package decl?
|
|
pkg := qpos.info.Pkg
|
|
obj = types.NewPkgName(id.Pos(), pkg, pkg.Name(), pkg)
|
|
} else {
|
|
return fmt.Errorf("no object for identifier: %T", qpos.path[1])
|
|
}
|
|
}
|
|
|
|
if pass2 {
|
|
break
|
|
}
|
|
|
|
// If the identifier is exported, we must load all packages that
|
|
// depend transitively upon the package that defines it.
|
|
// Treat PkgNames as exported, even though they're lowercase.
|
|
if _, isPkg := obj.(*types.PkgName); !(isPkg || obj.Exported()) {
|
|
break // not exported
|
|
}
|
|
|
|
// Scan the workspace and build the import graph.
|
|
// Ignore broken packages.
|
|
_, rev, _ := importgraph.Build(q.Build)
|
|
|
|
// Re-load the larger program.
|
|
// Create a new file set so that ...
|
|
// External test packages are never imported,
|
|
// so they will never appear in the graph.
|
|
// (We must reset the Config here, not just reset the Fset field.)
|
|
lconf = loader.Config{
|
|
Fset: token.NewFileSet(),
|
|
Build: q.Build,
|
|
}
|
|
allowErrors(&lconf)
|
|
for path := range rev.Search(obj.Pkg().Path()) {
|
|
lconf.ImportWithTests(path)
|
|
}
|
|
pass2 = true
|
|
}
|
|
|
|
// Iterate over all go/types' Uses facts for the entire program.
|
|
var refs []*ast.Ident
|
|
for _, info := range lprog.AllPackages {
|
|
for id2, obj2 := range info.Uses {
|
|
if sameObj(obj, obj2) {
|
|
refs = append(refs, id2)
|
|
}
|
|
}
|
|
}
|
|
sort.Sort(byNamePos{q.Fset, refs})
|
|
|
|
q.result = &referrersResult{
|
|
build: q.Build,
|
|
qpos: qpos,
|
|
query: id,
|
|
obj: obj,
|
|
refs: refs,
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// same reports whether x and y are identical, or both are PkgNames
|
|
// that import the same Package.
|
|
//
|
|
func sameObj(x, y types.Object) bool {
|
|
if x == y {
|
|
return true
|
|
}
|
|
if x, ok := x.(*types.PkgName); ok {
|
|
if y, ok := y.(*types.PkgName); ok {
|
|
return x.Imported() == y.Imported()
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// -------- utils --------
|
|
|
|
// An deterministic ordering for token.Pos that doesn't
|
|
// depend on the order in which packages were loaded.
|
|
func lessPos(fset *token.FileSet, x, y token.Pos) bool {
|
|
fx := fset.File(x)
|
|
fy := fset.File(y)
|
|
if fx != fy {
|
|
return fx.Name() < fy.Name()
|
|
}
|
|
return x < y
|
|
}
|
|
|
|
type byNamePos struct {
|
|
fset *token.FileSet
|
|
ids []*ast.Ident
|
|
}
|
|
|
|
func (p byNamePos) Len() int { return len(p.ids) }
|
|
func (p byNamePos) Swap(i, j int) { p.ids[i], p.ids[j] = p.ids[j], p.ids[i] }
|
|
func (p byNamePos) Less(i, j int) bool {
|
|
return lessPos(p.fset, p.ids[i].NamePos, p.ids[j].NamePos)
|
|
}
|
|
|
|
type referrersResult struct {
|
|
build *build.Context
|
|
qpos *queryPos
|
|
query *ast.Ident // identifier of query
|
|
obj types.Object // object it denotes
|
|
refs []*ast.Ident // set of all other references to it
|
|
}
|
|
|
|
func (r *referrersResult) display(printf printfFunc) {
|
|
printf(r.obj, "%d references to %s", len(r.refs), r.qpos.objectString(r.obj))
|
|
|
|
// Show referring lines, like grep.
|
|
type fileinfo struct {
|
|
refs []*ast.Ident
|
|
linenums []int // line number of refs[i]
|
|
data chan interface{} // file contents or error
|
|
}
|
|
var fileinfos []*fileinfo
|
|
fileinfosByName := make(map[string]*fileinfo)
|
|
|
|
// First pass: start the file reads concurrently.
|
|
sema := make(chan struct{}, 20) // counting semaphore to limit I/O concurrency
|
|
for _, ref := range r.refs {
|
|
posn := r.qpos.fset.Position(ref.Pos())
|
|
fi := fileinfosByName[posn.Filename]
|
|
if fi == nil {
|
|
fi = &fileinfo{data: make(chan interface{})}
|
|
fileinfosByName[posn.Filename] = fi
|
|
fileinfos = append(fileinfos, fi)
|
|
|
|
// First request for this file:
|
|
// start asynchronous read.
|
|
go func() {
|
|
sema <- struct{}{} // acquire token
|
|
content, err := readFile(r.build, posn.Filename)
|
|
<-sema // release token
|
|
if err != nil {
|
|
fi.data <- err
|
|
} else {
|
|
fi.data <- content
|
|
}
|
|
}()
|
|
}
|
|
fi.refs = append(fi.refs, ref)
|
|
fi.linenums = append(fi.linenums, posn.Line)
|
|
}
|
|
|
|
// Second pass: print refs in original order.
|
|
// One line may have several refs at different columns.
|
|
for _, fi := range fileinfos {
|
|
v := <-fi.data // wait for I/O completion
|
|
|
|
// Print one item for all refs in a file that could not
|
|
// be loaded (perhaps due to //line directives).
|
|
if err, ok := v.(error); ok {
|
|
var suffix string
|
|
if more := len(fi.refs) - 1; more > 0 {
|
|
suffix = fmt.Sprintf(" (+ %d more refs in this file)", more)
|
|
}
|
|
printf(fi.refs[0], "%v%s", err, suffix)
|
|
continue
|
|
}
|
|
|
|
lines := bytes.Split(v.([]byte), []byte("\n"))
|
|
for i, ref := range fi.refs {
|
|
printf(ref, "%s", lines[fi.linenums[i]-1])
|
|
}
|
|
}
|
|
}
|
|
|
|
// readFile is like ioutil.ReadFile, but
|
|
// it goes through the virtualized build.Context.
|
|
func readFile(ctxt *build.Context, filename string) ([]byte, error) {
|
|
rc, err := buildutil.OpenFile(ctxt, filename)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rc.Close()
|
|
var buf bytes.Buffer
|
|
if _, err := io.Copy(&buf, rc); err != nil {
|
|
return nil, err
|
|
}
|
|
return buf.Bytes(), nil
|
|
}
|
|
|
|
// TODO(adonovan): encode extent, not just Pos info, in Serial form.
|
|
|
|
func (r *referrersResult) toSerial(res *serial.Result, fset *token.FileSet) {
|
|
referrers := &serial.Referrers{
|
|
Pos: fset.Position(r.query.Pos()).String(),
|
|
Desc: r.obj.String(),
|
|
}
|
|
if pos := r.obj.Pos(); pos != token.NoPos { // Package objects have no Pos()
|
|
referrers.ObjPos = fset.Position(pos).String()
|
|
}
|
|
for _, ref := range r.refs {
|
|
referrers.Refs = append(referrers.Refs, fset.Position(ref.NamePos).String())
|
|
}
|
|
res.Referrers = referrers
|
|
}
|