1
0
mirror of https://github.com/golang/go synced 2024-09-30 14:28:33 -06:00

refactor/importgraph: a utility for computing the import graph.

Refactoring tools can use this to determine an upper bound on
the set of packages potentially affected by a refactoring.

LGTM=gri
R=gri
CC=golang-codereviews
https://golang.org/cl/141310043
This commit is contained in:
Alan Donovan 2014-09-09 18:39:26 -04:00
parent 77b9ff6df3
commit 3cded4a933
2 changed files with 206 additions and 0 deletions

View File

@ -0,0 +1,125 @@
// Copyright 2014 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 importgraph computes the forward and reverse import
// dependency graphs for all packages in a Go workspace.
package importgraph
import (
"go/build"
"sync"
"code.google.com/p/go.tools/go/buildutil"
)
// A Graph is an import dependency graph, either forward or reverse.
//
// The graph maps each node (a package import path) to the set of its
// successors in the graph. For a forward graph, this is the set of
// imported packages (prerequisites); for a reverse graph, it is the set
// of importing packages (clients).
//
type Graph map[string]map[string]bool
func (g Graph) addEdge(from, to string) {
edges := g[from]
if edges == nil {
edges = make(map[string]bool)
g[from] = edges
}
edges[to] = true
}
// Search returns all the nodes of the graph reachable from
// any of the specified roots, by following edges forwards.
// Relationally, this is the reflexive transitive closure.
func (g Graph) Search(roots ...string) map[string]bool {
seen := make(map[string]bool)
var visit func(x string)
visit = func(x string) {
if !seen[x] {
seen[x] = true
for y := range g[x] {
visit(y)
}
}
}
for _, root := range roots {
visit(root)
}
return seen
}
// Builds scans the specified Go workspace and builds the forward and
// reverse import dependency graphs for all its packages.
// It also returns a mapping from import paths to errors for packages
// that could not be loaded.
func Build(ctxt *build.Context) (forward, reverse Graph, errors map[string]error) {
// The (sole) graph builder goroutine receives a stream of import
// edges from the package loading goroutine.
forward = make(Graph)
reverse = make(Graph)
edgec := make(chan [2]string)
go func() {
for edge := range edgec {
if edge[1] == "C" {
continue // "C" is fake
}
forward.addEdge(edge[0], edge[1])
reverse.addEdge(edge[1], edge[0])
}
}()
// The (sole) error goroutine receives a stream of ReadDir and
// Import errors.
type pathError struct {
path string
err error
}
errorc := make(chan pathError)
go func() {
for e := range errorc {
if errors == nil {
errors = make(map[string]error)
}
errors[e.path] = e.err
}
}()
var wg sync.WaitGroup
buildutil.AllPackages(ctxt, func(path string, err error) {
if err != nil {
errorc <- pathError{path, err}
return
}
wg.Add(1)
// The import goroutines load the metadata for each package.
go func(path string) {
defer wg.Done()
bp, err := ctxt.Import(path, "", 0)
if _, ok := err.(*build.NoGoError); ok {
return // empty directory is not an error
}
if err != nil {
errorc <- pathError{path, err}
return
}
for _, imp := range bp.Imports {
edgec <- [2]string{path, imp}
}
for _, imp := range bp.TestImports {
edgec <- [2]string{path, imp}
}
for _, imp := range bp.XTestImports {
edgec <- [2]string{path, imp}
}
}(path)
})
wg.Wait()
close(edgec)
close(errorc)
return forward, reverse, errors
}

View File

@ -0,0 +1,81 @@
package importgraph_test
import (
"go/build"
"runtime"
"sort"
"testing"
"code.google.com/p/go.tools/refactor/importgraph"
_ "crypto/hmac" // just for test, below
)
const this = "code.google.com/p/go.tools/refactor/importgraph"
func TestBuild(t *testing.T) {
saved := runtime.GOMAXPROCS(8) // Build is highly parallel
defer runtime.GOMAXPROCS(saved)
forward, reverse, errors := importgraph.Build(&build.Default)
// Test direct edges.
// We throw in crypto/hmac to prove that external test files
// (such as this one) are inspected.
for _, p := range []string{"go/build", "runtime", "testing", "crypto/hmac"} {
if !forward[this][p] {
t.Errorf("forward[importgraph][%s] not found", p)
}
if !reverse[p][this] {
t.Errorf("reverse[%s][importgraph] not found", p)
}
}
// Test non-existent direct edges
for _, p := range []string{"fmt", "errors", "reflect"} {
if forward[this][p] {
t.Errorf("unexpected: forward[importgraph][%s] found", p)
}
if reverse[p][this] {
t.Errorf("unexpected: reverse[%s][importgraph] found", p)
}
}
// Test Search is reflexive.
if !forward.Search(this)[this] {
t.Errorf("irreflexive: forward.Search(importgraph)[importgraph] not found")
}
if !reverse.Search(this)[this] {
t.Errorf("irrefexive: reverse.Search(importgraph)[importgraph] not found")
}
// Test Search is transitive. (There is no direct edge to these packages.)
for _, p := range []string{"errors", "reflect", "unsafe"} {
if !forward.Search(this)[p] {
t.Errorf("intransitive: forward.Search(importgraph)[%s] not found", p)
}
if !reverse.Search(p)[this] {
t.Errorf("intransitive: reverse.Search(%s)[importgraph] not found", p)
}
}
// debugging
if false {
for path, err := range errors {
t.Logf("%s: %s", path, err)
}
printSorted := func(direction string, g importgraph.Graph, start string) {
t.Log(direction)
var pkgs []string
for pkg := range g.Search(start) {
pkgs = append(pkgs, pkg)
}
sort.Strings(pkgs)
for _, pkg := range pkgs {
t.Logf("\t%s", pkg)
}
}
printSorted("forward", forward, this)
printSorted("forward", reverse, this)
}
}