Skip to content

Commit a940ec7

Browse files
Merge pull request #18247 from juanvallejo/jvallejo/deptool-graph-pieces
Automatic merge from submit-queue (batch tested with PRs 17953, 18218, 18247). add graph-building pieces to tools/depcheck cc @deads2k
2 parents 90ef2db + 698a5d3 commit a940ec7

File tree

3 files changed

+385
-0
lines changed

3 files changed

+385
-0
lines changed

tools/depcheck/pkg/cmd/trace/pkg.go

+125
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
package trace
2+
3+
import (
4+
"fmt"
5+
"regexp"
6+
"strings"
7+
8+
"github.com/gonum/graph/concrete"
9+
10+
depgraph "github.com/openshift/origin/tools/depcheck/pkg/graph"
11+
)
12+
13+
var (
14+
// Matches standard goimport format for a package.
15+
//
16+
// The following formats will successfully match a valid import path:
17+
// - host.tld/repo/pkg
18+
// - foo.bar/baz
19+
//
20+
// The following formats will fail to match an import path:
21+
// - company.com
22+
// - company/missing/tld
23+
// - fmt
24+
// - encoding/json
25+
baseRepoRegex = regexp.MustCompile("[a-zA-Z0-9]+\\.([a-z0-9])+\\/.+")
26+
)
27+
28+
type Package struct {
29+
Dir string
30+
ImportPath string
31+
Imports []string
32+
TestImports []string
33+
}
34+
35+
type PackageList struct {
36+
Packages []Package
37+
}
38+
39+
func (p *PackageList) Add(pkg Package) {
40+
p.Packages = append(p.Packages, pkg)
41+
}
42+
43+
// BuildGraph receives a list of Go packages and constructs a dependency graph from it.
44+
// Any core library dependencies (fmt, strings, etc.) are not added to the graph.
45+
// Any packages whose import path is contained within a list of "excludes" are not added to the graph.
46+
// Returns a directed graph and a map of package import paths to node ids, or an error.
47+
func BuildGraph(packages *PackageList, excludes []string) (*depgraph.MutableDirectedGraph, error) {
48+
g := depgraph.NewMutableDirectedGraph(concrete.NewDirectedGraph())
49+
50+
// contains the subset of packages from the set of given packages (and their immediate dependencies)
51+
// that will actually be included in our graph - any packages in the excludes slice, or that do not
52+
// do not match the baseRepoRegex pattern will be filtered out from this collection.
53+
filteredPackages := []Package{}
54+
55+
// add nodes to graph
56+
for _, pkg := range packages.Packages {
57+
if isExcludedPath(pkg.ImportPath, excludes) {
58+
continue
59+
}
60+
if !isValidPackagePath(pkg.ImportPath) {
61+
continue
62+
}
63+
64+
n := &depgraph.Node{
65+
Id: g.NewNodeID(),
66+
UniqueName: pkg.ImportPath,
67+
LabelName: labelNameForNode(pkg.ImportPath),
68+
}
69+
g.AddNode(n)
70+
filteredPackages = append(filteredPackages, pkg)
71+
}
72+
73+
// add edges
74+
for _, pkg := range filteredPackages {
75+
from, exists := g.NodeByName(pkg.ImportPath)
76+
if !exists {
77+
return nil, fmt.Errorf("expected node for package %q was not found in graph", pkg.ImportPath)
78+
}
79+
80+
for _, dependency := range append(pkg.Imports, pkg.TestImports...) {
81+
if isExcludedPath(dependency, excludes) {
82+
continue
83+
}
84+
if !isValidPackagePath(dependency) {
85+
continue
86+
}
87+
88+
to, exists := g.NodeByName(dependency)
89+
if !exists {
90+
return nil, fmt.Errorf("expected child node for dependency %q was not found in graph", dependency)
91+
}
92+
93+
g.SetEdge(concrete.Edge{
94+
F: from,
95+
T: to,
96+
}, 0)
97+
}
98+
}
99+
100+
return g, nil
101+
}
102+
103+
func isExcludedPath(path string, excludes []string) bool {
104+
for _, exclude := range excludes {
105+
if strings.HasPrefix(path, exclude) {
106+
return true
107+
}
108+
}
109+
110+
return false
111+
}
112+
113+
// labelNameForNode trims vendored paths of their full /vendor/ path
114+
func labelNameForNode(importPath string) string {
115+
segs := strings.Split(importPath, "/vendor/")
116+
if len(segs) > 1 {
117+
return segs[1]
118+
}
119+
120+
return importPath
121+
}
122+
123+
func isValidPackagePath(path string) bool {
124+
return baseRepoRegex.Match([]byte(path))
125+
}
+204
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
package trace
2+
3+
import (
4+
"testing"
5+
6+
depgraph "github.com/openshift/origin/tools/depcheck/pkg/graph"
7+
)
8+
9+
var pkgs = &PackageList{
10+
Packages: []Package{
11+
{
12+
Dir: "/path/to/github.com/test/repo/root",
13+
ImportPath: "github.com/test/repo/root",
14+
Imports: []string{
15+
"github.com/test/repo/pkg/one",
16+
},
17+
},
18+
{
19+
Dir: "/path/to/github.com/test/repo/pkg/one",
20+
ImportPath: "github.com/test/repo/pkg/one",
21+
Imports: []string{
22+
"github.com/test/repo/pkg/two",
23+
"github.com/test/repo/pkg/three",
24+
"github.com/test/repo/pkg/depends_on_fmt",
25+
},
26+
},
27+
{
28+
Dir: "/path/to/github.com/test/repo/pkg/two",
29+
ImportPath: "github.com/test/repo/pkg/two",
30+
Imports: []string{
31+
"github.com/test/repo/vendor/github.com/testvendor/vendor_one",
32+
},
33+
},
34+
{
35+
Dir: "/path/to/github.com/test/repo/pkg/three",
36+
ImportPath: "github.com/test/repo/pkg/three",
37+
Imports: []string{
38+
"github.com/test/repo/shared/shared_one",
39+
},
40+
},
41+
{
42+
Dir: "/path/to/github.com/test/repo/pkg/depends_on_fmt",
43+
ImportPath: "github.com/test/repo/pkg/depends_on_fmt",
44+
Imports: []string{
45+
"fmt",
46+
"github.com/test/repo/unique/unique_nonvendored_one",
47+
},
48+
},
49+
{
50+
Dir: "/path/to/github.com/test/repo/unique/unique_nonvendored_one",
51+
ImportPath: "github.com/test/repo/unique/unique_nonvendored_one",
52+
Imports: []string{},
53+
},
54+
{
55+
Dir: "/path/to/github.com/test/repo/shared/shared_one",
56+
ImportPath: "github.com/test/repo/shared/shared_one",
57+
Imports: []string{},
58+
},
59+
{
60+
Dir: "/path/to/github.com/test/repo/vendor/github.com/testvendor/vendor_one",
61+
ImportPath: "github.com/test/repo/vendor/github.com/testvendor/vendor_one",
62+
Imports: []string{
63+
"github.com/test/repo/unique/unique_vendor_one",
64+
"github.com/test/repo/shared/shared_one",
65+
},
66+
},
67+
{
68+
Dir: "/path/to/github.com/test/repo/unique/unique_vendor_one",
69+
ImportPath: "github.com/test/repo/unique/unique_vendor_one",
70+
Imports: []string{},
71+
},
72+
},
73+
}
74+
75+
func TestBuildGraphCreatesExpectedNodesAndEdges(t *testing.T) {
76+
invalidImports := map[string]bool{
77+
"fmt": true,
78+
}
79+
80+
g, err := BuildGraph(pkgs, []string{})
81+
if err != nil {
82+
t.Fatalf("unexpected error: %v", err)
83+
}
84+
85+
if len(g.Nodes()) != len(pkgs.Packages) {
86+
t.Fatalf("node count mismatch. Expected %v nodes but got %v.", len(pkgs.Packages), len(g.Nodes()))
87+
}
88+
89+
for _, pkg := range pkgs.Packages {
90+
from, exists := g.NodeByName(pkg.ImportPath)
91+
if !exists || !g.Has(from) {
92+
t.Fatalf("expected node with name to exist for given package with ImportPath %q", pkg.ImportPath)
93+
}
94+
95+
for _, dep := range pkg.Imports {
96+
if _, skip := invalidImports[dep]; skip {
97+
continue
98+
}
99+
100+
to, exists := g.NodeByName(dep)
101+
if !exists || !g.Has(to) {
102+
t.Fatalf("expected node with name ")
103+
}
104+
105+
if !g.HasEdgeFromTo(from, to) {
106+
t.Fatalf("expected edge to exist between nodes %v and %v", from, to)
107+
}
108+
}
109+
}
110+
}
111+
112+
func TestBuildGraphExcludesNodes(t *testing.T) {
113+
excludes := []string{
114+
"github.com/test/repo/pkg/three",
115+
"github.com/test/repo/pkg/depends_on_fmt",
116+
}
117+
118+
g, err := BuildGraph(pkgs, excludes)
119+
if err != nil {
120+
t.Fatalf("unexpected error: %v", err)
121+
}
122+
123+
for _, n := range g.Nodes() {
124+
node, ok := n.(*depgraph.Node)
125+
if !ok {
126+
t.Fatalf("expected node to be of type *depgraph.Node")
127+
}
128+
129+
for _, exclude := range excludes {
130+
if node.UniqueName == exclude {
131+
t.Fatalf("expected node with unique name %q to have been excluded from the graph", node.UniqueName)
132+
}
133+
}
134+
}
135+
136+
}
137+
138+
func TestPackagesWithInvalidPathsAreOmitted(t *testing.T) {
139+
pkgList := &PackageList{
140+
Packages: []Package{
141+
{
142+
Dir: "/path/to/github.com/test/repo/invalid",
143+
ImportPath: "invalid/import/path1",
144+
Imports: []string{
145+
"fmt",
146+
"invalid.import.path2",
147+
"invalid.import.path3",
148+
},
149+
},
150+
{
151+
Dir: "/path/to/github.com/test/repo/invalid",
152+
ImportPath: "invalid.import.path2",
153+
Imports: []string{
154+
"net",
155+
"encoding/json",
156+
},
157+
},
158+
{
159+
Dir: "/path/to/github.com/test/repo/invalid",
160+
ImportPath: "invalid3",
161+
},
162+
},
163+
}
164+
165+
g, err := BuildGraph(pkgList, []string{})
166+
if err != nil {
167+
t.Fatalf("unexpected error: %v", err)
168+
}
169+
170+
if len(g.Nodes()) != 0 {
171+
t.Fatalf("expected no nodes to have been created for an invalid package list. Saw %v unexpected nodes.", len(g.Nodes()))
172+
}
173+
}
174+
175+
func TestLabelNamesForVendoredNodes(t *testing.T) {
176+
pkgList := &PackageList{
177+
Packages: []Package{
178+
{
179+
Dir: "/path/to/github.com/test/repo/vendor/github.com/testvendor/vendor_one",
180+
ImportPath: "github.com/test/repo/vendor/github.com/testvendor/vendor_one",
181+
},
182+
},
183+
}
184+
185+
expectedLabelName := "github.com/testvendor/vendor_one"
186+
187+
g, err := BuildGraph(pkgList, []string{})
188+
if err != nil {
189+
t.Fatalf("unexpected error: %v", err)
190+
}
191+
192+
if len(g.Nodes()) != 1 {
193+
t.Fatalf("expected graph of size 1, but got graph with %v nodes", len(g.Nodes()))
194+
}
195+
196+
node, ok := g.Nodes()[0].(*depgraph.Node)
197+
if !ok {
198+
t.Fatalf("expected node %v to be of type *depgraph.Node", node)
199+
}
200+
201+
if node.LabelName != expectedLabelName {
202+
t.Fatalf("expected node label name to be %q but was %q", expectedLabelName, node.LabelName)
203+
}
204+
}

tools/depcheck/pkg/graph/graph.go

+56
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
package graph
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/gonum/graph"
7+
"github.com/gonum/graph/concrete"
8+
"github.com/gonum/graph/encoding/dot"
9+
)
10+
11+
type Node struct {
12+
Id int
13+
UniqueName string
14+
LabelName string
15+
Color string
16+
}
17+
18+
func (n Node) ID() int {
19+
return n.Id
20+
}
21+
22+
// DOTAttributes implements an attribute getter for the DOT encoding
23+
func (n Node) DOTAttributes() []dot.Attribute {
24+
color := n.Color
25+
if len(color) == 0 {
26+
color = "black"
27+
}
28+
29+
return []dot.Attribute{
30+
{Key: "label", Value: fmt.Sprintf("%q", n.LabelName)},
31+
{Key: "color", Value: color},
32+
}
33+
}
34+
35+
func NewMutableDirectedGraph(g *concrete.DirectedGraph) *MutableDirectedGraph {
36+
return &MutableDirectedGraph{
37+
DirectedGraph: concrete.NewDirectedGraph(),
38+
nodesByName: make(map[string]graph.Node),
39+
}
40+
}
41+
42+
type MutableDirectedGraph struct {
43+
*concrete.DirectedGraph
44+
45+
nodesByName map[string]graph.Node
46+
}
47+
48+
func (g *MutableDirectedGraph) AddNode(n *Node) {
49+
g.nodesByName[n.UniqueName] = n
50+
g.DirectedGraph.AddNode(n)
51+
}
52+
53+
func (g *MutableDirectedGraph) NodeByName(name string) (graph.Node, bool) {
54+
n, exists := g.nodesByName[name]
55+
return n, exists && g.DirectedGraph.Has(n)
56+
}

0 commit comments

Comments
 (0)