Skip to content

Commit 2437936

Browse files
committed
adds dependency analysis
1 parent f70a013 commit 2437936

File tree

1 file changed

+129
-1
lines changed

1 file changed

+129
-1
lines changed

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

+129-1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ import (
1414

1515
"github.com/gonum/graph"
1616
"github.com/gonum/graph/encoding/dot"
17+
18+
depgraph "github.com/openshift/origin/tools/depcheck/pkg/graph"
1719
)
1820

1921
var traceImportsExample = `# create a dependency graph
@@ -30,6 +32,7 @@ type TraceImportsOpts struct {
3032
excludes []string
3133
filters []string
3234

35+
Analyze string
3336
ExcludeFileName string
3437
CollapseFileName string
3538

@@ -44,13 +47,15 @@ type TraceImportsFlags struct {
4447
OutputFormat string
4548
Exclude string
4649
Collapse string
50+
Analyze string
4751
}
4852

4953
func (o *TraceImportsFlags) ToOptions(out, errout io.Writer) (*TraceImportsOpts, error) {
5054
return &TraceImportsOpts{
5155
Roots: o.Roots,
5256
ExcludeFileName: o.Exclude,
5357
CollapseFileName: o.Collapse,
58+
Analyze: o.Analyze,
5459

5560
OutputFormat: o.OutputFormat,
5661

@@ -91,6 +96,7 @@ func NewCmdTraceImports(parent string, out, errout io.Writer) *cobra.Command {
9196
cmd.Flags().StringVarP(&flags.Exclude, "exclude", "e", "", "json file containing a list of import-paths of packages to recursively exclude when traversing the set of given entrypoints specified through --root.")
9297
cmd.Flags().StringVarP(&flags.OutputFormat, "output", "o", "", "output generated dependency graph in specified format. One of: dot.")
9398
cmd.Flags().StringVarP(&flags.Collapse, "collapse", "c", "", "json file containing a list of import-paths of packages to collapse sub-packages into.")
99+
cmd.Flags().StringVarP(&flags.Analyze, "analyze", "a", "", "output a summary report on the dependency set of a given repository. Mutually exclusive with --output")
94100
return cmd
95101
}
96102

@@ -151,7 +157,10 @@ func (o *TraceImportsOpts) Validate() error {
151157
if len(o.Roots) == 0 {
152158
return errors.New("at least one root package must be provided")
153159
}
154-
if len(o.OutputFormat) != 0 && o.OutputFormat != "dot" {
160+
if len(o.OutputFormat) > 0 && len(o.Analyze) > 0 {
161+
return fmt.Errorf("--output and --analyze are mutually exclusive")
162+
}
163+
if len(o.OutputFormat) > 0 && o.OutputFormat != "dot" {
155164
return fmt.Errorf("invalid output format provided: %s", o.OutputFormat)
156165
}
157166

@@ -209,6 +218,10 @@ func (o *TraceImportsOpts) Run() error {
209218
return o.outputGraph(g)
210219
}
211220

221+
if len(o.Analyze) > 0 {
222+
return o.analyzeGraph(g)
223+
}
224+
212225
return nil
213226
}
214227

@@ -225,3 +238,118 @@ func (o *TraceImportsOpts) outputGraph(g graph.Directed) error {
225238
fmt.Fprintf(o.Out, "%v\n", string(data))
226239
return nil
227240
}
241+
242+
// analyzeGraph receives a MutableDirectedGraph and outputs
243+
// - "Yours": a list of every node in the set unique to the dependency tree of a given target node.
244+
// - "Mine": a list of every node in the set unique to the dependency tree of the root nodes (set non-overlapping with given target node)
245+
// - "Ours": a list of every node in the overlapping set between the dependency tree of the root nodes and a given target node
246+
func (o *TraceImportsOpts) analyzeGraph(g *depgraph.MutableDirectedGraph) error {
247+
remove, exists := g.NodeByName(o.Analyze)
248+
if !exists {
249+
return fmt.Errorf("unable to find package with import path %q", o.Analyze)
250+
}
251+
252+
// determine existing orphans and roots
253+
keep := []graph.Node{}
254+
roots := []graph.Node{}
255+
for _, n := range g.Nodes() {
256+
node, ok := n.(*depgraph.Node)
257+
if !ok {
258+
continue
259+
}
260+
261+
to := g.To(n)
262+
from := g.From(n)
263+
264+
// found an existing orphaned node to exclude
265+
if len(to) == 0 && len(from) == 0 {
266+
continue
267+
}
268+
269+
// found a root
270+
if len(from) > 0 && len(to) == 0 {
271+
roots = append(roots, n)
272+
continue
273+
}
274+
275+
// skip package we are analyzing
276+
if node.UniqueName == o.Analyze {
277+
continue
278+
}
279+
280+
keep = append(keep, n)
281+
}
282+
283+
// remove package we are analyzing from the graph
284+
g.RemoveNode(remove)
285+
286+
// find nodes not reachable from our given roots
287+
yours := findOrphans(g, roots, keep)
288+
289+
fmt.Printf("\"Yours\" packages exclusive to %q\n", o.Analyze)
290+
for _, n := range yours {
291+
pkg, ok := n.(*depgraph.Node)
292+
if !ok {
293+
continue
294+
}
295+
296+
fmt.Printf(" - %s\n", pkg.UniqueName)
297+
}
298+
299+
return nil
300+
}
301+
302+
// findOrphans receives a set of root nodes and a set of child nodes and
303+
// returns the set of child nodes not reachable from any root node.
304+
func findOrphans(g *depgraph.MutableDirectedGraph, roots []graph.Node, nodes []graph.Node) []graph.Node {
305+
orphans := []graph.Node{}
306+
307+
for _, n := range nodes {
308+
isOrphan := true
309+
for _, root := range roots {
310+
if hasPathFromTo(g, root, n) {
311+
isOrphan = false
312+
break
313+
}
314+
}
315+
316+
if !isOrphan {
317+
continue
318+
}
319+
320+
orphans = append(orphans, n)
321+
}
322+
323+
return orphans
324+
}
325+
326+
// hasPathFromTo receives a node A and determines
327+
// if a node B can be reached from it.
328+
// Returns false if the source and destination are the same.
329+
func hasPathFromTo(g graph.Directed, A graph.Node, B graph.Node) bool {
330+
if A.ID() == B.ID() {
331+
return false
332+
}
333+
if !g.Has(A) || !g.Has(B) {
334+
return false
335+
}
336+
337+
seen := map[int]bool{}
338+
unseen := []graph.Node{B}
339+
for len(unseen) > 0 {
340+
n := unseen[0]
341+
unseen = unseen[1:]
342+
343+
if _, isSeen := seen[n.ID()]; isSeen {
344+
continue
345+
}
346+
if n.ID() == A.ID() {
347+
return true
348+
}
349+
350+
seen[n.ID()] = true
351+
unseen = append(unseen, g.To(n)...)
352+
}
353+
354+
return false
355+
}

0 commit comments

Comments
 (0)