golang.org/x/exp@v0.0.0-20240506185415-9bf2ced13842/cmd/modgraphviz/main.go (about) 1 // Copyright 2019 The Go Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 // Modgraphviz converts “go mod graph” output into Graphviz's DOT language, 6 // for use with Graphviz visualization and analysis tools like dot, dotty, and sccmap. 7 // 8 // Usage: 9 // 10 // go mod graph | modgraphviz > graph.dot 11 // go mod graph | modgraphviz | dot -Tpng -o graph.png 12 // 13 // Modgraphviz takes no options or arguments; it reads a graph in the format 14 // generated by “go mod graph” on standard input and writes DOT language 15 // on standard output. 16 // 17 // For each module, the node representing the greatest version (i.e., the 18 // version chosen by Go's minimal version selection algorithm) is colored green. 19 // Other nodes, which aren't in the final build list, are colored grey. 20 // 21 // See http://www.graphviz.org/doc/info/lang.html for details of the DOT language 22 // and http://www.graphviz.org/about/ for Graphviz itself. 23 // 24 // See also golang.org/x/tools/cmd/digraph for general queries and analysis 25 // of “go mod graph” output. 26 package main 27 28 import ( 29 "bufio" 30 "bytes" 31 "flag" 32 "fmt" 33 "io" 34 "log" 35 "os" 36 "sort" 37 "strings" 38 39 "golang.org/x/mod/semver" 40 ) 41 42 func usage() { 43 fmt.Fprintf(os.Stderr, `Usage: go mod graph | modgraphviz | dot -Tpng -o graph.png 44 45 For each module, the node representing the greatest version (i.e., the 46 version chosen by Go's minimal version selection algorithm) is colored green. 47 Other nodes, which aren't in the final build list, are colored grey. 48 `) 49 os.Exit(2) 50 } 51 52 func main() { 53 log.SetFlags(0) 54 log.SetPrefix("modgraphviz: ") 55 56 flag.Usage = usage 57 flag.Parse() 58 if flag.NArg() != 0 { 59 usage() 60 } 61 62 if err := modgraphviz(os.Stdin, os.Stdout); err != nil { 63 log.Fatal(err) 64 } 65 } 66 67 func modgraphviz(in io.Reader, out io.Writer) error { 68 graph, err := convert(in) 69 if err != nil { 70 return err 71 } 72 73 fmt.Fprintf(out, "digraph gomodgraph {\n") 74 fmt.Fprintf(out, "\tnode [ shape=rectangle fontsize=12 ]\n") 75 out.Write(graph.edgesAsDOT()) 76 for _, n := range graph.mvsPicked { 77 fmt.Fprintf(out, "\t%q [style = filled, fillcolor = green]\n", n) 78 } 79 for _, n := range graph.mvsUnpicked { 80 fmt.Fprintf(out, "\t%q [style = filled, fillcolor = gray]\n", n) 81 } 82 fmt.Fprintf(out, "}\n") 83 84 return nil 85 } 86 87 type edge struct{ from, to string } 88 type graph struct { 89 edges []edge 90 mvsPicked []string 91 mvsUnpicked []string 92 } 93 94 // convert reads “go mod graph” output from r and returns a graph, recording 95 // MVS picked and unpicked nodes along the way. 96 func convert(r io.Reader) (*graph, error) { 97 scanner := bufio.NewScanner(r) 98 var g graph 99 seen := map[string]bool{} 100 mvsPicked := map[string]string{} // module name -> module version 101 102 for scanner.Scan() { 103 l := scanner.Text() 104 if l == "" { 105 continue 106 } 107 parts := strings.Fields(l) 108 if len(parts) != 2 { 109 return nil, fmt.Errorf("expected 2 words in line, but got %d: %s", len(parts), l) 110 } 111 from := parts[0] 112 to := parts[1] 113 g.edges = append(g.edges, edge{from: from, to: to}) 114 115 for _, node := range []string{from, to} { 116 if _, ok := seen[node]; ok { 117 // Skip over nodes we've already seen. 118 continue 119 } 120 seen[node] = true 121 122 var m, v string 123 if i := strings.IndexByte(node, '@'); i >= 0 { 124 m, v = node[:i], node[i+1:] 125 } else { 126 // Root node doesn't have a version. 127 continue 128 } 129 130 if maxV, ok := mvsPicked[m]; ok { 131 if semver.Compare(maxV, v) < 0 { 132 // This version is higher - replace it and consign the old 133 // max to the unpicked list. 134 g.mvsUnpicked = append(g.mvsUnpicked, m+"@"+maxV) 135 mvsPicked[m] = v 136 } else { 137 // Other version is higher - stick this version in the 138 // unpicked list. 139 g.mvsUnpicked = append(g.mvsUnpicked, node) 140 } 141 } else { 142 mvsPicked[m] = v 143 } 144 } 145 } 146 if err := scanner.Err(); err != nil { 147 return nil, err 148 } 149 150 for m, v := range mvsPicked { 151 g.mvsPicked = append(g.mvsPicked, m+"@"+v) 152 } 153 154 // Make this function deterministic. 155 sort.Strings(g.mvsPicked) 156 return &g, nil 157 } 158 159 // edgesAsDOT returns the edges in DOT notation. 160 func (g *graph) edgesAsDOT() []byte { 161 var buf bytes.Buffer 162 for _, e := range g.edges { 163 fmt.Fprintf(&buf, "\t%q -> %q\n", e.from, e.to) 164 } 165 return buf.Bytes() 166 }