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  }