1
0
mirror of https://github.com/golang/go synced 2024-11-18 11:24:41 -07:00
go/cmd/guru/referrers.go
Koichi Shiraishi be3ddffac4 cmd/guru: fix several functions godoc comment prefix
The several functions not exported, Use "The xxx function ..." rule.

Change-Id: Iaba2490b4a9675213f70d055bd9ded9e5a245aa5
Reviewed-on: https://go-review.googlesource.com/c/47872
Reviewed-by: Alan Donovan <adonovan@google.com>
2018-10-11 18:05:12 +00:00

803 lines
24 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/parser"
"go/token"
"go/types"
"io"
"log"
"os"
"sort"
"strconv"
"strings"
"sync"
"golang.org/x/tools/cmd/guru/serial"
"golang.org/x/tools/go/buildutil"
"golang.org/x/tools/go/loader"
"golang.org/x/tools/imports"
"golang.org/x/tools/refactor/importgraph"
)
// The referrers function reports all identifiers that resolve to the same object
// as the queried identifier, within any package in the workspace.
func referrers(q *Query) error {
fset := token.NewFileSet()
lconf := loader.Config{Fset: fset, Build: q.Build}
allowErrors(&lconf)
if _, err := importQueryPackage(q.Pos, &lconf); err != nil {
return err
}
// Load tests of the query package
// even if the query location is not in the tests.
for path := range lconf.ImportPkgs {
lconf.ImportPkgs[path] = true
}
// Load/parse/type-check the query package.
lprog, err := lconf.Load()
if err != nil {
return err
}
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?
return packageReferrers(q, qpos.info.Pkg.Path())
}
return fmt.Errorf("no object for identifier: %T", qpos.path[1])
}
// Imported package name?
if pkgname, ok := obj.(*types.PkgName); ok {
return packageReferrers(q, pkgname.Imported().Path())
}
if obj.Pkg() == nil {
return fmt.Errorf("references to predeclared %q are everywhere!", obj.Name())
}
q.Output(fset, &referrersInitialResult{
qinfo: qpos.info,
obj: obj,
})
// For a globally accessible object defined in package P, we
// must load packages that depend on P. Specifically, for a
// package-level object, we need load only direct importers
// of P, but for a field or method, we must load
// any package that transitively imports P.
if global, pkglevel := classify(obj); global {
if pkglevel {
return globalReferrersPkgLevel(q, obj, fset)
}
// We'll use the the object's position to identify it in the larger program.
objposn := fset.Position(obj.Pos())
defpkg := obj.Pkg().Path() // defining package
return globalReferrers(q, qpos.info.Pkg.Path(), defpkg, objposn)
}
outputUses(q, fset, usesOf(obj, qpos.info), obj.Pkg())
return nil // success
}
// classify classifies objects by how far
// we have to look to find references to them.
func classify(obj types.Object) (global, pkglevel bool) {
if obj.Exported() {
if obj.Parent() == nil {
// selectable object (field or method)
return true, false
}
if obj.Parent() == obj.Pkg().Scope() {
// lexical object (package-level var/const/func/type)
return true, true
}
}
// object with unexported named or defined in local scope
return false, false
}
// packageReferrers reports all references to the specified package
// throughout the workspace.
func packageReferrers(q *Query, path string) error {
// Scan the workspace and build the import graph.
// Ignore broken packages.
_, rev, _ := importgraph.Build(q.Build)
// Find the set of packages that directly import the query package.
// Only those packages need typechecking of function bodies.
users := rev[path]
// Load the larger program.
fset := token.NewFileSet()
lconf := loader.Config{
Fset: fset,
Build: q.Build,
TypeCheckFuncBodies: func(p string) bool {
return users[strings.TrimSuffix(p, "_test")]
},
}
allowErrors(&lconf)
// The importgraph doesn't treat external test packages
// as separate nodes, so we must use ImportWithTests.
for path := range users {
lconf.ImportWithTests(path)
}
// Subtle! AfterTypeCheck needs no mutex for qpkg because the
// topological import order gives us the necessary happens-before edges.
// TODO(adonovan): what about import cycles?
var qpkg *types.Package
// For efficiency, we scan each package for references
// just after it has been type-checked. The loader calls
// AfterTypeCheck (concurrently), providing us with a stream of
// packages.
lconf.AfterTypeCheck = func(info *loader.PackageInfo, files []*ast.File) {
// AfterTypeCheck may be called twice for the same package due to augmentation.
if info.Pkg.Path() == path && qpkg == nil {
// Found the package of interest.
qpkg = info.Pkg
fakepkgname := types.NewPkgName(token.NoPos, qpkg, qpkg.Name(), qpkg)
q.Output(fset, &referrersInitialResult{
qinfo: info,
obj: fakepkgname, // bogus
})
}
// Only inspect packages that directly import the
// declaring package (and thus were type-checked).
if lconf.TypeCheckFuncBodies(info.Pkg.Path()) {
// Find PkgNames that refer to qpkg.
// TODO(adonovan): perhaps more useful would be to show imports
// of the package instead of qualified identifiers.
var refs []*ast.Ident
for id, obj := range info.Uses {
if obj, ok := obj.(*types.PkgName); ok && obj.Imported() == qpkg {
refs = append(refs, id)
}
}
outputUses(q, fset, refs, info.Pkg)
}
clearInfoFields(info) // save memory
}
lconf.Load() // ignore error
if qpkg == nil {
log.Fatalf("query package %q not found during reloading", path)
}
return nil
}
func usesOf(queryObj types.Object, info *loader.PackageInfo) []*ast.Ident {
var refs []*ast.Ident
for id, obj := range info.Uses {
if sameObj(queryObj, obj) {
refs = append(refs, id)
}
}
return refs
}
// outputUses outputs a result describing refs, which appear in the package denoted by info.
func outputUses(q *Query, fset *token.FileSet, refs []*ast.Ident, pkg *types.Package) {
if len(refs) > 0 {
sort.Sort(byNamePos{fset, refs})
q.Output(fset, &referrersPackageResult{
pkg: pkg,
build: q.Build,
fset: fset,
refs: refs,
})
}
}
// globalReferrers reports references throughout the entire workspace to the
// object (a field or method) at the specified source position.
// Its defining package is defpkg, and the query package is qpkg.
func globalReferrers(q *Query, qpkg, defpkg string, objposn token.Position) error {
// Scan the workspace and build the import graph.
// Ignore broken packages.
_, rev, _ := importgraph.Build(q.Build)
// Find the set of packages that depend on defpkg.
// Only function bodies in those packages need type-checking.
users := rev.Search(defpkg) // transitive importers
// Prepare to load the larger program.
fset := token.NewFileSet()
lconf := loader.Config{
Fset: fset,
Build: q.Build,
TypeCheckFuncBodies: func(p string) bool {
return users[strings.TrimSuffix(p, "_test")]
},
}
allowErrors(&lconf)
// The importgraph doesn't treat external test packages
// as separate nodes, so we must use ImportWithTests.
for path := range users {
lconf.ImportWithTests(path)
}
// The remainder of this function is somewhat tricky because it
// operates on the concurrent stream of packages observed by the
// loader's AfterTypeCheck hook. Most of guru's helper
// functions assume the entire program has already been loaded,
// so we can't use them here.
// TODO(adonovan): smooth things out once the other changes have landed.
// Results are reported concurrently from within the
// AfterTypeCheck hook. The program may provide a useful stream
// of information even if the user doesn't let the program run
// to completion.
var (
mu sync.Mutex
qobj types.Object
)
// For efficiency, we scan each package for references
// just after it has been type-checked. The loader calls
// AfterTypeCheck (concurrently), providing us with a stream of
// packages.
lconf.AfterTypeCheck = func(info *loader.PackageInfo, files []*ast.File) {
// AfterTypeCheck may be called twice for the same package due to augmentation.
// Only inspect packages that depend on the declaring package
// (and thus were type-checked).
if lconf.TypeCheckFuncBodies(info.Pkg.Path()) {
// Record the query object and its package when we see it.
mu.Lock()
if qobj == nil && info.Pkg.Path() == defpkg {
// Find the object by its position (slightly ugly).
qobj = findObject(fset, &info.Info, objposn)
if qobj == nil {
// It really ought to be there;
// we found it once already.
log.Fatalf("object at %s not found in package %s",
objposn, defpkg)
}
}
obj := qobj
mu.Unlock()
// Look for references to the query object.
if obj != nil {
outputUses(q, fset, usesOf(obj, info), info.Pkg)
}
}
clearInfoFields(info) // save memory
}
lconf.Load() // ignore error
if qobj == nil {
log.Fatal("query object not found during reloading")
}
return nil // success
}
// globalReferrersPkgLevel reports references throughout the entire workspace to the package-level object obj.
// It assumes that the query object itself has already been reported.
func globalReferrersPkgLevel(q *Query, obj types.Object, fset *token.FileSet) error {
// globalReferrersPkgLevel uses go/ast and friends instead of go/types.
// This affords a considerable performance benefit.
// It comes at the cost of some code complexity.
//
// Here's a high level summary.
//
// The goal is to find references to the query object p.Q.
// There are several possible scenarios, each handled differently.
//
// 1. We are looking in a package other than p, and p is not dot-imported.
// This is the simplest case. Q must be referred to as n.Q,
// where n is the name under which p is imported.
// We look at all imports of p to gather all names under which it is imported.
// (In the typical case, it is imported only once, under its default name.)
// Then we look at all selector expressions and report any matches.
//
// 2. We are looking in a package other than p, and p is dot-imported.
// In this case, Q will be referred to just as Q.
// Furthermore, go/ast's object resolution will not be able to resolve
// Q to any other object, unlike any local (file- or function- or block-scoped) object.
// So we look at all matching identifiers and report all unresolvable ones.
//
// 3. We are looking in package p.
// (Care must be taken to separate p and p_test (an xtest package),
// and make sure that they are treated as separate packages.)
// In this case, we give go/ast the entire package for object resolution,
// instead of going file by file.
// We then iterate over all identifiers that resolve to the query object.
// (The query object itself has already been reported, so we don't re-report it.)
//
// We always skip all files that don't contain the string Q, as they cannot be
// relevant to finding references to Q.
//
// We parse all files leniently. In the presence of parsing errors, results are best-effort.
// Scan the workspace and build the import graph.
// Ignore broken packages.
_, rev, _ := importgraph.Build(q.Build)
// Find the set of packages that directly import defpkg.
defpkg := obj.Pkg().Path()
defpkg = strings.TrimSuffix(defpkg, "_test") // package x_test actually has package name x
defpkg = imports.VendorlessPath(defpkg) // remove vendor goop
users := rev[defpkg]
if len(users) == 0 {
users = make(map[string]bool)
}
// We also need to check defpkg itself, and its xtests.
// For the reverse graph packages, we process xtests with the main package.
// defpkg gets special handling; we must distinguish between in-package vs out-of-package.
// To make the control flow below simpler, add defpkg and defpkg xtest placeholders.
// Use "!test" instead of "_test" because "!" is not a valid character in an import path.
// (More precisely, it is not guaranteed to be a valid character in an import path,
// so it is unlikely that it will be in use. See https://golang.org/ref/spec#Import_declarations.)
users[defpkg] = true
users[defpkg+"!test"] = true
cwd, err := os.Getwd()
if err != nil {
return err
}
defname := obj.Pkg().Name() // name of defining package, used for imports using import path only
isxtest := strings.HasSuffix(defname, "_test") // indicates whether the query object is defined in an xtest package
name := obj.Name()
namebytes := []byte(name) // byte slice version of query object name, for early filtering
objpos := fset.Position(obj.Pos()) // position of query object, used to prevent re-emitting original decl
sema := make(chan struct{}, 20) // counting semaphore to limit I/O concurrency
var wg sync.WaitGroup
for u := range users {
u := u
wg.Add(1)
go func() {
defer wg.Done()
uIsXTest := strings.HasSuffix(u, "!test") // indicates whether this package is the special defpkg xtest package
u = strings.TrimSuffix(u, "!test")
// Resolve package.
sema <- struct{}{} // acquire token
pkg, err := q.Build.Import(u, cwd, build.IgnoreVendor)
<-sema // release token
if err != nil {
return
}
// If we're not in the query package,
// the object is in another package regardless,
// so we want to process all files.
// If we are in the query package,
// we want to only process the files that are
// part of that query package;
// that set depends on whether the query package itself is an xtest.
inQueryPkg := u == defpkg && isxtest == uIsXTest
var files []string
if !inQueryPkg || !isxtest {
files = append(files, pkg.GoFiles...)
files = append(files, pkg.TestGoFiles...)
files = append(files, pkg.CgoFiles...) // use raw cgo files, as we're only parsing
}
if !inQueryPkg || isxtest {
files = append(files, pkg.XTestGoFiles...)
}
if len(files) == 0 {
return
}
var deffiles map[string]*ast.File
if inQueryPkg {
deffiles = make(map[string]*ast.File)
}
buf := new(bytes.Buffer) // reusable buffer for reading files
for _, file := range files {
if !buildutil.IsAbsPath(q.Build, file) {
file = buildutil.JoinPath(q.Build, pkg.Dir, file)
}
buf.Reset()
sema <- struct{}{} // acquire token
src, err := readFile(q.Build, file, buf)
<-sema // release token
if err != nil {
continue
}
// Fast path: If the object's name isn't present anywhere in the source, ignore the file.
if !bytes.Contains(src, namebytes) {
continue
}
if inQueryPkg {
// If we're in the query package, we defer final processing until we have
// parsed all of the candidate files in the package.
// Best effort; allow errors and use what we can from what remains.
f, _ := parser.ParseFile(fset, file, src, parser.AllErrors)
if f != nil {
deffiles[file] = f
}
continue
}
// We aren't in the query package. Go file by file.
// Parse out only the imports, to check whether the defining package
// was imported, and if so, under what names.
// Best effort; allow errors and use what we can from what remains.
f, _ := parser.ParseFile(fset, file, src, parser.ImportsOnly|parser.AllErrors)
if f == nil {
continue
}
// pkgnames is the set of names by which defpkg is imported in this file.
// (Multiple imports in the same file are legal but vanishingly rare.)
pkgnames := make([]string, 0, 1)
var isdotimport bool
for _, imp := range f.Imports {
path, err := strconv.Unquote(imp.Path.Value)
if err != nil || path != defpkg {
continue
}
switch {
case imp.Name == nil:
pkgnames = append(pkgnames, defname)
case imp.Name.Name == ".":
isdotimport = true
default:
pkgnames = append(pkgnames, imp.Name.Name)
}
}
if len(pkgnames) == 0 && !isdotimport {
// Defining package not imported, bail.
continue
}
// Re-parse the entire file.
// Parse errors are ok; we'll do the best we can with a partial AST, if we have one.
f, _ = parser.ParseFile(fset, file, src, parser.AllErrors)
if f == nil {
continue
}
// Walk the AST looking for references.
var refs []*ast.Ident
ast.Inspect(f, func(n ast.Node) bool {
// Check selector expressions.
// If the selector matches the target name,
// and the expression is one of the names
// that the defining package was imported under,
// then we have a match.
if sel, ok := n.(*ast.SelectorExpr); ok && sel.Sel.Name == name {
if id, ok := sel.X.(*ast.Ident); ok {
for _, n := range pkgnames {
if n == id.Name {
refs = append(refs, sel.Sel)
// Don't recurse further, to avoid duplicate entries
// from the dot import check below.
return false
}
}
}
}
// Dot imports are special.
// Objects imported from the defining package are placed in the package scope.
// go/ast does not resolve them to an object.
// At all other scopes (file, local), go/ast can do the resolution.
// So we're looking for object-free idents with the right name.
// The only other way to get something with the right name at the package scope
// is to *be* the defining package. We handle that case separately (inQueryPkg).
if isdotimport {
if id, ok := n.(*ast.Ident); ok && id.Obj == nil && id.Name == name {
refs = append(refs, id)
return false
}
}
return true
})
// Emit any references we found.
if len(refs) > 0 {
q.Output(fset, &referrersPackageResult{
pkg: types.NewPackage(pkg.ImportPath, pkg.Name),
build: q.Build,
fset: fset,
refs: refs,
})
}
}
// If we're in the query package, we've now collected all the files in the package.
// (Or at least the ones that might contain references to the object.)
// Find and emit refs.
if inQueryPkg {
// Bundle the files together into a package.
// This does package-level object resolution.
qpkg, _ := ast.NewPackage(fset, deffiles, nil, nil)
// Look up the query object; we know that it is defined in the package scope.
pkgobj := qpkg.Scope.Objects[name]
if pkgobj == nil {
panic("missing defpkg object for " + defpkg + "." + name)
}
// Find all references to the query object.
var refs []*ast.Ident
ast.Inspect(qpkg, func(n ast.Node) bool {
if id, ok := n.(*ast.Ident); ok {
// Check both that this is a reference to the query object
// and that it is not the query object itself;
// the query object itself was already emitted.
if id.Obj == pkgobj && objpos != fset.Position(id.Pos()) {
refs = append(refs, id)
return false
}
}
return true
})
if len(refs) > 0 {
q.Output(fset, &referrersPackageResult{
pkg: types.NewPackage(pkg.ImportPath, pkg.Name),
build: q.Build,
fset: fset,
refs: refs,
})
}
deffiles = nil // allow GC
}
}()
}
wg.Wait()
return nil
}
// findObject returns the object defined at the specified position.
func findObject(fset *token.FileSet, info *types.Info, objposn token.Position) types.Object {
good := func(obj types.Object) bool {
if obj == nil {
return false
}
posn := fset.Position(obj.Pos())
return posn.Filename == objposn.Filename && posn.Offset == objposn.Offset
}
for _, obj := range info.Defs {
if good(obj) {
return obj
}
}
for _, obj := range info.Implicits {
if good(obj) {
return obj
}
}
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
}
func clearInfoFields(info *loader.PackageInfo) {
// TODO(adonovan): opt: save memory by eliminating unneeded scopes/objects.
// (Requires go/types change for Go 1.7.)
// info.Pkg.Scope().ClearChildren()
// Discard the file ASTs and their accumulated type
// information to save memory.
info.Files = nil
info.Defs = make(map[*ast.Ident]types.Object)
info.Uses = make(map[*ast.Ident]types.Object)
info.Implicits = make(map[ast.Node]types.Object)
// Also, disable future collection of wholly unneeded
// type information for the package in case there is
// more type-checking to do (augmentation).
info.Types = nil
info.Scopes = nil
info.Selections = nil
}
// -------- 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)
}
// referrersInitialResult is the initial result of a "referrers" query.
type referrersInitialResult struct {
qinfo *loader.PackageInfo
obj types.Object // object it denotes
}
func (r *referrersInitialResult) PrintPlain(printf printfFunc) {
printf(r.obj, "references to %s",
types.ObjectString(r.obj, types.RelativeTo(r.qinfo.Pkg)))
}
func (r *referrersInitialResult) JSON(fset *token.FileSet) []byte {
var objpos string
if pos := r.obj.Pos(); pos.IsValid() {
objpos = fset.Position(pos).String()
}
return toJSON(&serial.ReferrersInitial{
Desc: r.obj.String(),
ObjPos: objpos,
})
}
// referrersPackageResult is the streaming result for one package of a "referrers" query.
type referrersPackageResult struct {
pkg *types.Package
build *build.Context
fset *token.FileSet
refs []*ast.Ident // set of all other references to it
}
// forEachRef calls f(id, text) for id in r.refs, in order.
// Text is the text of the line on which id appears.
func (r *referrersPackageResult) foreachRef(f func(id *ast.Ident, text string)) {
// 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.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, nil)
<-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)
}
f(fi.refs[0], err.Error()+suffix)
continue
}
lines := bytes.Split(v.([]byte), []byte("\n"))
for i, ref := range fi.refs {
f(ref, string(lines[fi.linenums[i]-1]))
}
}
}
// readFile is like ioutil.ReadFile, but
// it goes through the virtualized build.Context.
// If non-nil, buf must have been reset.
func readFile(ctxt *build.Context, filename string, buf *bytes.Buffer) ([]byte, error) {
rc, err := buildutil.OpenFile(ctxt, filename)
if err != nil {
return nil, err
}
defer rc.Close()
if buf == nil {
buf = new(bytes.Buffer)
}
if _, err := io.Copy(buf, rc); err != nil {
return nil, err
}
return buf.Bytes(), nil
}
func (r *referrersPackageResult) PrintPlain(printf printfFunc) {
r.foreachRef(func(id *ast.Ident, text string) {
printf(id, "%s", text)
})
}
func (r *referrersPackageResult) JSON(fset *token.FileSet) []byte {
refs := serial.ReferrersPackage{Package: r.pkg.Path()}
r.foreachRef(func(id *ast.Ident, text string) {
refs.Refs = append(refs.Refs, serial.Ref{
Pos: fset.Position(id.NamePos).String(),
Text: text,
})
})
return toJSON(refs)
}