├── LICENSE
├── README.md
├── code.js
├── dom.go
├── go.mod
├── go.sum
├── index.html
├── screenshot.png
├── spaghetti.go
└── style.css
/LICENSE:
--------------------------------------------------------------------------------
1 | BSD 2-Clause License
2 |
3 | Copyright (c) 2021, Alan Donovan
4 | All rights reserved.
5 |
6 | Redistribution and use in source and binary forms, with or without
7 | modification, are permitted provided that the following conditions are met:
8 |
9 | 1. Redistributions of source code must retain the above copyright notice, this
10 | list of conditions and the following disclaimer.
11 |
12 | 2. Redistributions in binary form must reproduce the above copyright notice,
13 | this list of conditions and the following disclaimer in the documentation
14 | and/or other materials provided with the distribution.
15 |
16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
17 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
18 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
20 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
21 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
22 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
23 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
24 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
25 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Spaghetti: a dependency analysis tool for Go packages
2 |
3 | Spaghetti is an interactive web-based tool to help you understand the
4 | dependencies of a Go program, and to explore and evaluate various
5 | possible efforts to eliminate dependencies.
6 |
7 | It displays the complete dependencies of the initial packages,
8 | organized into a tree based on the directory structure of the
9 | package/module namespace.
10 |
11 | Clicking on a package node displays information about it,
12 | including an arbitrary path to it from one of the initial packages.
13 | Each edge in the path may be "broken", causing it to be removed
14 | from the graph and the view recomputed.
15 | The broken edges are tabulated, and each can be restored if you change
16 | your mind or try another approach.
17 | In this manner, you can explore how your overall dependencies would
18 | change as you work to remove specific imports.
19 | Once you are happy with the overall dependencies, the set of broken
20 | edges becomes your task list for a clean-up project.
21 |
22 | Run it like so:
23 |
24 | ```shell
25 | $ go install github.com/adonovan/spaghetti@latest # install in $HOME/go/bin
26 | $ ~/go/bin/spaghetti [package] & # run the server
27 | $ open http://localhost:8080 # open in chrome, firefox, etc
28 | ```
29 |
30 | where _package_ is or more Go packages, or a pattern understood by `go
31 | list`. Then point your browser at the insecure single-user web server
32 | at `localhost:18080`.
33 |
34 | This tool is a rewrite from scratch of a tool I wrote while at Google
35 | for exploring the dependency graphs used by the Blaze build system.
36 | The basic design could be easily be generalized to support any kind of
37 | dependency graph, independent of language or domain, or turned into a
38 | secure multi-user web service that operates on graph data uploaded from
39 | the client program that generates it.
40 |
41 | You can probably tell that web UIs are not my expertise.
42 | PRs that provide cosmetic improvements are most welcome!
43 |
44 | 
45 |
--------------------------------------------------------------------------------
/code.js:
--------------------------------------------------------------------------------
1 |
2 | var packages = null // array of packages.Package JSON objects
3 | var path = null // path from root to selected package (elements are indices in 'packages')
4 | var broken = null // array of 2-arrays of int, the node ids of broken edges.
5 |
6 | function onLoad() {
7 | // Grab data from server: package graph, "directory" tree, broken edges.
8 | jQuery.ajax({url: "/data", success: onData})
9 | }
10 |
11 | // onData is called shortly after page load with the result of the /data request.
12 | function onData(data) {
13 | // Save array of Package objects.
14 | packages = data.Packages
15 |
16 | // Show initial packages.
17 | $('#initial').text(data.Initial.map(i => packages[i].PkgPath).join("\n"))
18 |
19 | // Show broken edges.
20 | broken = data.Broken
21 | var html = ""
22 | for (var i in broken) {
23 | edge = broken[i]
24 | html += " "
25 | + "" + packages[edge[0]].PkgPath + "
⟶ "
26 | + "" + packages[edge[1]].PkgPath + "
"
27 | }
28 | $('#broken').html(html)
29 |
30 | // Populate package/module "directory" tree.
31 | $('#tree').jstree({
32 | "core": {
33 | "animation": 0,
34 | "check_callback": true,
35 | 'data': data.Tree,
36 | },
37 | "types": {
38 | "#": {
39 | },
40 | "root": {
41 | "icon": "/static/3.3.11/assets/images/tree_icon.png"
42 | },
43 | "module": {
44 | "icon": "https://jstree.com/static/3.3.11/assets/images/tree_icon.png"
45 | },
46 | "default": {
47 | },
48 | "pkg": {
49 | "icon": "https://old.jstree.com//static/v.1.0pre/_demo/file.png"
50 | }
51 | },
52 | "plugins": ["search", "state", "types", "wholerow"],
53 | "search": {
54 | "case_sensitive": false,
55 | "show_only_matches": true,
56 | }
57 | })
58 |
59 | // Show package info when a node is clicked.
60 | $('#tree').on("changed.jstree", function (e, data) {
61 | if (data.node) {
62 | selectPkg(data.node.original)
63 | }
64 | })
65 |
66 | // Search the tree when the user types in the search box.
67 | $("#search").keyup(function () {
68 | var searchString = $(this).val();
69 | $('#tree').jstree('search', searchString);
70 | });
71 | }
72 |
73 | // selectPkg shows package info (if any) about the clicked node.
74 | function selectPkg(json) {
75 | if (json.Package < 0) {
76 | // Non-package "directory" node: clear the fields.
77 | $('#pkgname').text("none")
78 | $('#doc').text("")
79 | $('#imports').html("")
80 | $('#importedBy').html("")
81 | $('#path').text("")
82 | return
83 | }
84 |
85 | // A package node was selected.
86 | var pkg = packages[json.Package]
87 |
88 | // Show selected package.
89 | $('#pkgname').text(pkg.PkgPath)
90 |
91 | // Set link to Go package documentation.
92 | $('#doc').html("")
93 |
94 | // Show imports in a drop-down menu.
95 | // Selecting an import acts like clicking on that package in the tree.
96 | addImports($('#imports'), json.Imports)
97 | addImports($('#importedBy'), json.ImportedBy)
98 |
99 | // Show "break edges" buttons.
100 | var html = ""
101 | var path = [].concat(json.Path).reverse() // from root to selected package
102 | for (var i in path) {
103 | var p = packages[path[i]]
104 | if (i > 0) {
105 | html += " "
106 | + " "
107 | + "⟶ "
108 | }
109 | html += "
" + p.PkgPath + "
"
110 | }
111 | $('#path').html(html)
112 | }
113 |
114 | function breakedge(i, j, all) {
115 | // Must reload the page since the graph has changed.
116 | document.location = "/break?from=" + i + "&to=" + j + "&all=" + all
117 | }
118 |
119 | function unbreak(i, j) {
120 | // Must reload the page since the graph has changed.
121 | document.location = "/unbreak?from=" + i + "&to=" + j
122 | }
123 |
124 | // addImports adds option elements to the select element,
125 | // one per package index in the packageIDs array.
126 | function addImports(select, packageIDs) {
127 | select.html("")
128 | var option = document.createElement("option")
129 | option.textContent = "..."
130 | option.value = "-1"
131 | select.append(option)
132 | for (var i in packageIDs) {
133 | var imp = packageIDs[i]
134 | option = document.createElement("option")
135 | option.textContent = packages[imp].PkgPath
136 | option.value = "" + imp // package index, used by onSelectImport
137 | select.append(option)
138 | }
139 | }
140 |
141 | // onSelectImport is called by the imports dropdown.
142 | function onSelectImport(sel) {
143 | if (sel.value >= 0) {
144 | // Simulate a click on a tree node corresponding to the selected import.
145 | $('#tree').jstree('select_node', 'node' + sel.value);
146 | }
147 | }
148 |
--------------------------------------------------------------------------------
/dom.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | // Dominator tree construction
4 | //
5 | // This file was plundered from golang.org/x/tools/go/ssa/dom.go and
6 | // modified to support a different graph representation, multiple
7 | // roots, and unreachable nodes.
8 | //
9 | // TODO(adonovan): turn it into a generic dominance package abstracted
10 | // from representation.
11 |
12 | // LICENCE
13 | //
14 | // Copyright (c) 2009 The Go Authors. All rights reserved.
15 | //
16 | // Redistribution and use in source and binary forms, with or without
17 | // modification, are permitted provided that the following conditions are
18 | // met:
19 | //
20 | // * Redistributions of source code must retain the above copyright
21 | // notice, this list of conditions and the following disclaimer.
22 | // * Redistributions in binary form must reproduce the above
23 | // copyright notice, this list of conditions and the following disclaimer
24 | // in the documentation and/or other materials provided with the
25 | // distribution.
26 | // * Neither the name of Google Inc. nor the names of its
27 | // contributors may be used to endorse or promote products derived from
28 | // this software without specific prior written permission.
29 | //
30 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
31 | // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
32 | // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
33 | // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
34 | // OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
35 | // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
36 | // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
37 | // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
38 | // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
39 | // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
40 | // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
41 |
42 | // Dominator tree construction ----------------------------------------
43 | //
44 | // We use the algorithm described in Lengauer & Tarjan. 1979. A fast
45 | // algorithm for finding dominators in a flowgraph.
46 | // http://doi.acm.org/10.1145/357062.357071
47 | //
48 | // We also apply the optimizations to SLT described in Georgiadis et
49 | // al, Finding Dominators in Practice, JGAA 2006,
50 | // http://jgaa.info/accepted/2006/GeorgiadisTarjanWerneck2006.10.1.pdf
51 | // to avoid the need for buckets of size > 1.
52 |
53 | // Idom returns the block that immediately dominates b:
54 | // its parent in the dominator tree, if any. The root node has no parent.
55 | func (b *node) Idom() *node { return b.dom.idom }
56 |
57 | // Dominees returns the list of blocks that b immediately dominates:
58 | // its children in the dominator tree.
59 | func (b *node) Dominees() []*node { return b.dom.children }
60 |
61 | // Dominates reports whether b dominates c.
62 | func (b *node) Dominates(c *node) bool {
63 | return b.dom.pre <= c.dom.pre && c.dom.post <= b.dom.post
64 | }
65 |
66 | // domInfo contains a node's dominance information.
67 | type domInfo struct {
68 | idom *node // immediate dominator (parent in domtree)
69 | children []*node // nodes immediately dominated by this one
70 | pre, post int32 // pre- and post-order numbering within domtree
71 | index int32 // preorder index within reachable nodes; see "reachable hack"
72 | }
73 |
74 | // ltState holds the working state for Lengauer-Tarjan algorithm
75 | // (during which domInfo.pre is repurposed for CFG DFS preorder number).
76 | type ltState struct {
77 | // Each slice is indexed by domInfo.index.
78 | sdom []*node // b's semidominator
79 | parent []*node // b's parent in DFS traversal of CFG
80 | ancestor []*node // b's ancestor with least sdom
81 | }
82 |
83 | // dfs implements the depth-first search part of the LT algorithm.
84 | func (lt *ltState) dfs(v *node, i int32, preorder []*node) int32 {
85 | preorder[i] = v
86 | v.dom.pre = i // For now: DFS preorder of spanning tree of CFG
87 | i++
88 | lt.sdom[v.dom.index] = v
89 | lt.link(nil, v)
90 | for _, w := range v.imports {
91 | if lt.sdom[w.dom.index] == nil {
92 | lt.parent[w.dom.index] = v
93 | i = lt.dfs(w, i, preorder)
94 | }
95 | }
96 | return i
97 | }
98 |
99 | // eval implements the EVAL part of the LT algorithm.
100 | func (lt *ltState) eval(v *node) *node {
101 | u := v
102 | for ; lt.ancestor[v.dom.index] != nil; v = lt.ancestor[v.dom.index] {
103 | if lt.sdom[v.dom.index].dom.pre < lt.sdom[u.dom.index].dom.pre {
104 | u = v
105 | }
106 | }
107 | return u
108 | }
109 |
110 | // link implements the LINK part of the LT algorithm.
111 | func (lt *ltState) link(v, w *node) {
112 | lt.ancestor[w.dom.index] = v
113 | }
114 |
115 | // buildDomTree computes the dominator tree of f using the LT algorithm.
116 | // The first node is the distinguished root node.
117 | func buildDomTree(nodes []*node) {
118 | // The step numbers refer to the original LT paper; the
119 | // reordering is due to Georgiadis.
120 |
121 | // Clear any previous domInfo.
122 | for _, b := range nodes {
123 | b.dom = domInfo{index: -1}
124 | }
125 |
126 | root := nodes[0]
127 |
128 | // The original (ssa) implementation had the precondition
129 | // that all nodes are reachable, but because of Spaghetti's
130 | // "broken edges", some nodes may be unreachable.
131 | // We filter them out now with another graph traversal.
132 | // The domInfo.index numbering is relative to this ordering.
133 | // See other "reachable hack" comments for related parts.
134 | // We should combine this into step 1.
135 | var reachable []*node
136 | var visit func(n *node)
137 | visit = func(n *node) {
138 | if n.dom.index < 0 {
139 | n.dom.index = int32(len(reachable))
140 | reachable = append(reachable, n)
141 | for _, imp := range n.imports {
142 | visit(imp)
143 | }
144 | }
145 | }
146 | visit(root)
147 | nodes = reachable
148 |
149 | n := len(nodes)
150 | // Allocate space for 5 contiguous [n]*node arrays:
151 | // sdom, parent, ancestor, preorder, buckets.
152 | space := make([]*node, 5*n)
153 | lt := ltState{
154 | sdom: space[0:n],
155 | parent: space[n : 2*n],
156 | ancestor: space[2*n : 3*n],
157 | }
158 |
159 | // Step 1. Number vertices by depth-first preorder.
160 | preorder := space[3*n : 4*n]
161 | lt.dfs(root, 0, preorder)
162 |
163 | buckets := space[4*n : 5*n]
164 | copy(buckets, preorder)
165 |
166 | // In reverse preorder...
167 | for i := int32(n) - 1; i > 0; i-- {
168 | w := preorder[i]
169 |
170 | // Step 3. Implicitly define the immediate dominator of each node.
171 | for v := buckets[i]; v != w; v = buckets[v.dom.pre] {
172 | u := lt.eval(v)
173 | if lt.sdom[u.dom.index].dom.pre < i {
174 | v.dom.idom = u
175 | } else {
176 | v.dom.idom = w
177 | }
178 | }
179 |
180 | // Step 2. Compute the semidominators of all nodes.
181 | lt.sdom[w.dom.index] = lt.parent[w.dom.index]
182 | for _, v := range w.importedBy {
183 | if v.dom.index < 0 {
184 | continue // see "reachable hack"
185 | }
186 | u := lt.eval(v)
187 | if lt.sdom[u.dom.index].dom.pre < lt.sdom[w.dom.index].dom.pre {
188 | lt.sdom[w.dom.index] = lt.sdom[u.dom.index]
189 | }
190 | }
191 |
192 | lt.link(lt.parent[w.dom.index], w)
193 |
194 | if lt.parent[w.dom.index] == lt.sdom[w.dom.index] {
195 | w.dom.idom = lt.parent[w.dom.index]
196 | } else {
197 | buckets[i] = buckets[lt.sdom[w.dom.index].dom.pre]
198 | buckets[lt.sdom[w.dom.index].dom.pre] = w
199 | }
200 | }
201 |
202 | // The final 'Step 3' is now outside the loop.
203 | for v := buckets[0]; v != preorder[0]; v = buckets[v.dom.pre] {
204 | v.dom.idom = preorder[0]
205 | }
206 |
207 | // Step 4. Explicitly define the immediate dominator of each
208 | // node, in preorder.
209 | for _, w := range preorder[1:] {
210 | if w == root {
211 | w.dom.idom = nil
212 | } else {
213 | if w.dom.idom != lt.sdom[w.dom.index] {
214 | w.dom.idom = w.dom.idom.dom.idom
215 | }
216 | // Calculate Children relation as inverse of Idom.
217 | w.dom.idom.dom.children = append(w.dom.idom.dom.children, w)
218 | }
219 | }
220 |
221 | // Number all nodes to enable O(1) dominance queries.
222 | numberDomTree(root, 0, 0)
223 | }
224 |
225 | // numberDomTree sets the pre- and post-order numbers of a depth-first
226 | // traversal of the dominator tree rooted at v. These are used to
227 | // answer dominance queries in constant time.
228 | //
229 | func numberDomTree(v *node, pre, post int32) (int32, int32) {
230 | v.dom.pre = pre
231 | pre++
232 | for _, child := range v.dom.children {
233 | pre, post = numberDomTree(child, pre, post)
234 | }
235 | v.dom.post = post
236 | post++
237 | return pre, post
238 | }
239 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/adonovan/spaghetti
2 |
3 | go 1.16
4 |
5 | require golang.org/x/tools v0.1.1-0.20210319172145-bda8f5cee399
6 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
2 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
3 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
4 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
5 | golang.org/x/mod v0.4.1 h1:Kvvh58BN8Y9/lBi7hTekvtMpm07eUZ0ck5pRHpsMWrY=
6 | golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
7 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
8 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
9 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
10 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
11 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
12 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
13 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
14 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
15 | golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c h1:VwygUrnw9jn88c4u8GD3rZQbqrP/tgas88tPUbBxQrk=
16 | golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
17 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
18 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
19 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
20 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
21 | golang.org/x/tools v0.1.1-0.20210319172145-bda8f5cee399 h1:O5bm8buX/OaamnfcBrkjn0SPUIU30jFmaS8lP+ikkxs=
22 | golang.org/x/tools v0.1.1-0.20210319172145-bda8f5cee399/go.mod h1:9bzcO0MWcOuT0tm1iBGzDVPshzfwoVvREIui8C+MHqU=
23 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
24 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
25 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
26 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
27 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
15 | This tool displays the complete dependencies of these initial packages: 16 |
17 |...18 |
19 | Click on a package in the tree view to display information about it, 20 | including a path by which it is reached from one of the initial packages. 21 | 22 | Use the break button to remove an edge from the graph, so 23 | that you can assess what edges need to be broken to eliminate an 24 | unwanted dependency. 25 |
26 | 27 |29 |
45 | 46 |52 | ⓘThis list shows the packages 53 | directly imported by the selected package 54 | 55 | 56 |
57 |58 | ⓘThis list shows the packages 59 | that directly import the selected package 60 | 61 | 62 |
63 |
64 | ⓘ
65 | This section displays an arbitrary path from one of the
66 | initial packages to the selected package. Click
67 | the break button so see how your dependencies would
68 | change if you were to remove a single edge.
69 |
70 | Click break all to remove all inbound edges to a
71 | package, removing it from the graph. This may be useful for
72 | removing distracting packages that you don't plan to
73 | eliminate.
74 |
75 | The bold nodes are dominators: nodes that are found on
76 | every path to the selected node. One way to break a dependency
77 | on a package is to break all dependencies on any of its dominators.
78 |
79 |
80 |