diff --git a/refactor/importgraph/graph.go b/refactor/importgraph/graph.go new file mode 100644 index 0000000000..922ff9f8b1 --- /dev/null +++ b/refactor/importgraph/graph.go @@ -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 +} diff --git a/refactor/importgraph/graph_test.go b/refactor/importgraph/graph_test.go new file mode 100644 index 0000000000..739565a9fa --- /dev/null +++ b/refactor/importgraph/graph_test.go @@ -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) + } +}