mirror of
https://github.com/golang/go
synced 2024-11-05 20:36:10 -07:00
207d3de1fa
Updates golang/go#35718 Change-Id: I10bfd5421cd44bb58b8bcaa6e9205040c25f51be Reviewed-on: https://go-review.googlesource.com/c/tools/+/208257 Run-TryBot: Rebecca Stambler <rstambler@golang.org> TryBot-Result: Gobot Gobot <gobot@golang.org> Reviewed-by: Heschi Kreinick <heschi@google.com>
658 lines
15 KiB
Go
658 lines
15 KiB
Go
// Copyright 2019 The Go Authors. All rights reserved.
|
|
// Use of this source code is governed by a BSD-style
|
|
// license that can be found in the LICENSE file.
|
|
|
|
/*
|
|
The digraph command performs queries over unlabelled directed graphs
|
|
represented in text form. It is intended to integrate nicely with
|
|
typical UNIX command pipelines.
|
|
|
|
Usage:
|
|
|
|
your-application | digraph [command]
|
|
|
|
The support commands are:
|
|
|
|
nodes
|
|
the set of all nodes
|
|
degree
|
|
the in-degree and out-degree of each node
|
|
transpose
|
|
the reverse of the input edges
|
|
preds <node> ...
|
|
the set of immediate predecessors of the specified nodes
|
|
succs <node> ...
|
|
the set of immediate successors of the specified nodes
|
|
forward <node> ...
|
|
the set of nodes transitively reachable from the specified nodes
|
|
reverse <node> ...
|
|
the set of nodes that transitively reach the specified nodes
|
|
somepath <node> <node>
|
|
the list of nodes on some arbitrary path from the first node to the second
|
|
allpaths <node> <node>
|
|
the set of nodes on all paths from the first node to the second
|
|
sccs
|
|
all strongly connected components (one per line)
|
|
scc <node>
|
|
the set of nodes nodes strongly connected to the specified one
|
|
focus <node>
|
|
the subgraph containing all directed paths that pass through the specified node
|
|
|
|
Input format:
|
|
|
|
Each line contains zero or more words. Words are separated by unquoted
|
|
whitespace; words may contain Go-style double-quoted portions, allowing spaces
|
|
and other characters to be expressed.
|
|
|
|
Each word declares a node, and if there are more than one, an edge from the
|
|
first to each subsequent one. The graph is provided on the standard input.
|
|
|
|
For instance, the following (acyclic) graph specifies a partial order among the
|
|
subtasks of getting dressed:
|
|
|
|
$ cat clothes.txt
|
|
socks shoes
|
|
"boxer shorts" pants
|
|
pants belt shoes
|
|
shirt tie sweater
|
|
sweater jacket
|
|
hat
|
|
|
|
The line "shirt tie sweater" indicates the two edges shirt -> tie and
|
|
shirt -> sweater, not shirt -> tie -> sweater.
|
|
|
|
Example usage:
|
|
|
|
Using digraph with existing Go tools:
|
|
|
|
$ go mod graph | digraph nodes # Operate on the Go module graph.
|
|
$ go list -m all | digraph nodes # Operate on the Go package graph.
|
|
|
|
Show the transitive closure of imports of the digraph tool itself:
|
|
$ go list -f '{{.ImportPath}} {{join .Imports " "}}' ... | digraph forward golang.org/x/tools/cmd/digraph
|
|
|
|
Show which clothes (see above) must be donned before a jacket:
|
|
$ digraph reverse jacket
|
|
|
|
*/
|
|
package main // import "golang.org/x/tools/cmd/digraph"
|
|
|
|
// TODO(adonovan):
|
|
// - support input files other than stdin
|
|
// - support alternative formats (AT&T GraphViz, CSV, etc),
|
|
// a comment syntax, etc.
|
|
// - allow queries to nest, like Blaze query language.
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"errors"
|
|
"flag"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"unicode"
|
|
"unicode/utf8"
|
|
)
|
|
|
|
func usage() {
|
|
fmt.Fprintf(os.Stderr, `Usage: your-application | digraph [command]
|
|
|
|
The support commands are:
|
|
nodes
|
|
the set of all nodes
|
|
degree
|
|
the in-degree and out-degree of each node
|
|
transpose
|
|
the reverse of the input edges
|
|
preds <node> ...
|
|
the set of immediate predecessors of the specified nodes
|
|
succs <node> ...
|
|
the set of immediate successors of the specified nodes
|
|
forward <node> ...
|
|
the set of nodes transitively reachable from the specified nodes
|
|
reverse <node> ...
|
|
the set of nodes that transitively reach the specified nodes
|
|
somepath <node> <node>
|
|
the list of nodes on some arbitrary path from the first node to the second
|
|
allpaths <node> <node>
|
|
the set of nodes on all paths from the first node to the second
|
|
sccs
|
|
all strongly connected components (one per line)
|
|
scc <node>
|
|
the set of nodes nodes strongly connected to the specified one
|
|
focus <node>
|
|
the subgraph containing all directed paths that pass through the specified node
|
|
`)
|
|
os.Exit(2)
|
|
}
|
|
|
|
func main() {
|
|
flag.Usage = usage
|
|
flag.Parse()
|
|
|
|
args := flag.Args()
|
|
if len(args) == 0 {
|
|
usage()
|
|
}
|
|
|
|
if err := digraph(args[0], args[1:]); err != nil {
|
|
fmt.Fprintf(os.Stderr, "digraph: %s\n", err)
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
|
|
type nodelist []string
|
|
|
|
func (l nodelist) println(sep string) {
|
|
for i, node := range l {
|
|
if i > 0 {
|
|
fmt.Fprint(stdout, sep)
|
|
}
|
|
fmt.Fprint(stdout, node)
|
|
}
|
|
fmt.Fprintln(stdout)
|
|
}
|
|
|
|
type nodeset map[string]bool // TODO(deklerk): change bool to struct to reduce memory footprint
|
|
|
|
func (s nodeset) sort() nodelist {
|
|
nodes := make(nodelist, len(s))
|
|
var i int
|
|
for node := range s {
|
|
nodes[i] = node
|
|
i++
|
|
}
|
|
sort.Strings(nodes)
|
|
return nodes
|
|
}
|
|
|
|
func (s nodeset) addAll(x nodeset) {
|
|
for node := range x {
|
|
s[node] = true
|
|
}
|
|
}
|
|
|
|
// A graph maps nodes to the non-nil set of their immediate successors.
|
|
type graph map[string]nodeset
|
|
|
|
func (g graph) addNode(node string) nodeset {
|
|
edges := g[node]
|
|
if edges == nil {
|
|
edges = make(nodeset)
|
|
g[node] = edges
|
|
}
|
|
return edges
|
|
}
|
|
|
|
func (g graph) addEdges(from string, to ...string) {
|
|
edges := g.addNode(from)
|
|
for _, to := range to {
|
|
g.addNode(to)
|
|
edges[to] = true
|
|
}
|
|
}
|
|
|
|
func (g graph) reachableFrom(roots nodeset) nodeset {
|
|
seen := make(nodeset)
|
|
var visit func(node string)
|
|
visit = func(node string) {
|
|
if !seen[node] {
|
|
seen[node] = true
|
|
for e := range g[node] {
|
|
visit(e)
|
|
}
|
|
}
|
|
}
|
|
for root := range roots {
|
|
visit(root)
|
|
}
|
|
return seen
|
|
}
|
|
|
|
func (g graph) transpose() graph {
|
|
rev := make(graph)
|
|
for node, edges := range g {
|
|
rev.addNode(node)
|
|
for succ := range edges {
|
|
rev.addEdges(succ, node)
|
|
}
|
|
}
|
|
return rev
|
|
}
|
|
|
|
func (g graph) sccs() []nodeset {
|
|
// Kosaraju's algorithm---Tarjan is overkill here.
|
|
|
|
// Forward pass.
|
|
S := make(nodelist, 0, len(g)) // postorder stack
|
|
seen := make(nodeset)
|
|
var visit func(node string)
|
|
visit = func(node string) {
|
|
if !seen[node] {
|
|
seen[node] = true
|
|
for e := range g[node] {
|
|
visit(e)
|
|
}
|
|
S = append(S, node)
|
|
}
|
|
}
|
|
for node := range g {
|
|
visit(node)
|
|
}
|
|
|
|
// Reverse pass.
|
|
rev := g.transpose()
|
|
var scc nodeset
|
|
seen = make(nodeset)
|
|
var rvisit func(node string)
|
|
rvisit = func(node string) {
|
|
if !seen[node] {
|
|
seen[node] = true
|
|
scc[node] = true
|
|
for e := range rev[node] {
|
|
rvisit(e)
|
|
}
|
|
}
|
|
}
|
|
var sccs []nodeset
|
|
for len(S) > 0 {
|
|
top := S[len(S)-1]
|
|
S = S[:len(S)-1] // pop
|
|
if !seen[top] {
|
|
scc = make(nodeset)
|
|
rvisit(top)
|
|
sccs = append(sccs, scc)
|
|
}
|
|
}
|
|
return sccs
|
|
}
|
|
|
|
func (g graph) allpaths(from, to string) error {
|
|
// Mark all nodes to "to".
|
|
seen := make(nodeset) // value of seen[x] indicates whether x is on some path to "to"
|
|
var visit func(node string) bool
|
|
visit = func(node string) bool {
|
|
reachesTo, ok := seen[node]
|
|
if !ok {
|
|
reachesTo = node == to
|
|
seen[node] = reachesTo
|
|
for e := range g[node] {
|
|
if visit(e) {
|
|
reachesTo = true
|
|
}
|
|
}
|
|
if reachesTo && node != to {
|
|
seen[node] = true
|
|
}
|
|
}
|
|
return reachesTo
|
|
}
|
|
visit(from)
|
|
|
|
// For each marked node, collect its marked successors.
|
|
var edges []string
|
|
for n := range seen {
|
|
for succ := range g[n] {
|
|
if seen[succ] {
|
|
edges = append(edges, n+" "+succ)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Sort (so that this method is deterministic) and print edges.
|
|
sort.Strings(edges)
|
|
for _, e := range edges {
|
|
fmt.Fprintln(stdout, e)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (g graph) somepath(from, to string) error {
|
|
type edge struct{ from, to string }
|
|
seen := make(nodeset)
|
|
var dfs func(path []edge, from string) bool
|
|
dfs = func(path []edge, from string) bool {
|
|
if !seen[from] {
|
|
seen[from] = true
|
|
if from == to {
|
|
// fmt.Println(path, len(path), cap(path))
|
|
// Print and unwind.
|
|
for _, e := range path {
|
|
fmt.Fprintln(stdout, e.from+" "+e.to)
|
|
}
|
|
return true
|
|
}
|
|
for e := range g[from] {
|
|
if dfs(append(path, edge{from: from, to: e}), e) {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
maxEdgesInGraph := len(g) * (len(g) - 1)
|
|
if !dfs(make([]edge, 0, maxEdgesInGraph), from) {
|
|
return fmt.Errorf("no path from %q to %q", from, to)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func parse(rd io.Reader) (graph, error) {
|
|
g := make(graph)
|
|
|
|
var linenum int
|
|
in := bufio.NewScanner(rd)
|
|
for in.Scan() {
|
|
linenum++
|
|
// Split into words, honoring double-quotes per Go spec.
|
|
words, err := split(in.Text())
|
|
if err != nil {
|
|
return nil, fmt.Errorf("at line %d: %v", linenum, err)
|
|
}
|
|
if len(words) > 0 {
|
|
g.addEdges(words[0], words[1:]...)
|
|
}
|
|
}
|
|
if err := in.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return g, nil
|
|
}
|
|
|
|
// Overridable for testing purposes.
|
|
var stdin io.Reader = os.Stdin
|
|
var stdout io.Writer = os.Stdout
|
|
|
|
func digraph(cmd string, args []string) error {
|
|
// Parse the input graph.
|
|
g, err := parse(stdin)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Parse the command line.
|
|
switch cmd {
|
|
case "nodes":
|
|
if len(args) != 0 {
|
|
return fmt.Errorf("usage: digraph nodes")
|
|
}
|
|
nodes := make(nodeset)
|
|
for node := range g {
|
|
nodes[node] = true
|
|
}
|
|
nodes.sort().println("\n")
|
|
|
|
case "degree":
|
|
if len(args) != 0 {
|
|
return fmt.Errorf("usage: digraph degree")
|
|
}
|
|
nodes := make(nodeset)
|
|
for node := range g {
|
|
nodes[node] = true
|
|
}
|
|
rev := g.transpose()
|
|
for _, node := range nodes.sort() {
|
|
fmt.Fprintf(stdout, "%d\t%d\t%s\n", len(rev[node]), len(g[node]), node)
|
|
}
|
|
|
|
case "transpose":
|
|
if len(args) != 0 {
|
|
return fmt.Errorf("usage: digraph transpose")
|
|
}
|
|
var revEdges []string
|
|
for node, succs := range g.transpose() {
|
|
for succ := range succs {
|
|
revEdges = append(revEdges, fmt.Sprintf("%s %s", node, succ))
|
|
}
|
|
}
|
|
sort.Strings(revEdges) // make output deterministic
|
|
for _, e := range revEdges {
|
|
fmt.Fprintln(stdout, e)
|
|
}
|
|
|
|
case "succs", "preds":
|
|
if len(args) == 0 {
|
|
return fmt.Errorf("usage: digraph %s <node> ... ", cmd)
|
|
}
|
|
g := g
|
|
if cmd == "preds" {
|
|
g = g.transpose()
|
|
}
|
|
result := make(nodeset)
|
|
for _, root := range args {
|
|
edges := g[root]
|
|
if edges == nil {
|
|
return fmt.Errorf("no such node %q", root)
|
|
}
|
|
result.addAll(edges)
|
|
}
|
|
result.sort().println("\n")
|
|
|
|
case "forward", "reverse":
|
|
if len(args) == 0 {
|
|
return fmt.Errorf("usage: digraph %s <node> ... ", cmd)
|
|
}
|
|
roots := make(nodeset)
|
|
for _, root := range args {
|
|
if g[root] == nil {
|
|
return fmt.Errorf("no such node %q", root)
|
|
}
|
|
roots[root] = true
|
|
}
|
|
g := g
|
|
if cmd == "reverse" {
|
|
g = g.transpose()
|
|
}
|
|
g.reachableFrom(roots).sort().println("\n")
|
|
|
|
case "somepath":
|
|
if len(args) != 2 {
|
|
return fmt.Errorf("usage: digraph somepath <from> <to>")
|
|
}
|
|
from, to := args[0], args[1]
|
|
if g[from] == nil {
|
|
return fmt.Errorf("no such 'from' node %q", from)
|
|
}
|
|
if g[to] == nil {
|
|
return fmt.Errorf("no such 'to' node %q", to)
|
|
}
|
|
if err := g.somepath(from, to); err != nil {
|
|
return err
|
|
}
|
|
|
|
case "allpaths":
|
|
if len(args) != 2 {
|
|
return fmt.Errorf("usage: digraph allpaths <from> <to>")
|
|
}
|
|
from, to := args[0], args[1]
|
|
if g[from] == nil {
|
|
return fmt.Errorf("no such 'from' node %q", from)
|
|
}
|
|
if g[to] == nil {
|
|
return fmt.Errorf("no such 'to' node %q", to)
|
|
}
|
|
if err := g.allpaths(from, to); err != nil {
|
|
return err
|
|
}
|
|
|
|
case "sccs":
|
|
if len(args) != 0 {
|
|
return fmt.Errorf("usage: digraph sccs")
|
|
}
|
|
for _, scc := range g.sccs() {
|
|
scc.sort().println(" ")
|
|
}
|
|
|
|
case "scc":
|
|
if len(args) != 1 {
|
|
return fmt.Errorf("usage: digraph scc <node>")
|
|
}
|
|
node := args[0]
|
|
if g[node] == nil {
|
|
return fmt.Errorf("no such node %q", node)
|
|
}
|
|
for _, scc := range g.sccs() {
|
|
if scc[node] {
|
|
scc.sort().println("\n")
|
|
break
|
|
}
|
|
}
|
|
|
|
case "focus":
|
|
if len(args) != 1 {
|
|
return fmt.Errorf("usage: digraph focus <node>")
|
|
}
|
|
node := args[0]
|
|
if g[node] == nil {
|
|
return fmt.Errorf("no such node %q", node)
|
|
}
|
|
|
|
edges := make(map[string]struct{})
|
|
for from := range g.reachableFrom(nodeset{node: true}) {
|
|
for to := range g[from] {
|
|
edges[fmt.Sprintf("%s %s", from, to)] = struct{}{}
|
|
}
|
|
}
|
|
|
|
gtrans := g.transpose()
|
|
for from := range gtrans.reachableFrom(nodeset{node: true}) {
|
|
for to := range gtrans[from] {
|
|
edges[fmt.Sprintf("%s %s", to, from)] = struct{}{}
|
|
}
|
|
}
|
|
|
|
edgesSorted := make([]string, 0, len(edges))
|
|
for e := range edges {
|
|
edgesSorted = append(edgesSorted, e)
|
|
}
|
|
sort.Strings(edgesSorted)
|
|
fmt.Fprintln(stdout, strings.Join(edgesSorted, "\n"))
|
|
|
|
default:
|
|
return fmt.Errorf("no such command %q", cmd)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// -- Utilities --------------------------------------------------------
|
|
|
|
// split splits a line into words, which are generally separated by
|
|
// spaces, but Go-style double-quoted string literals are also supported.
|
|
// (This approximates the behaviour of the Bourne shell.)
|
|
//
|
|
// `one "two three"` -> ["one" "two three"]
|
|
// `a"\n"b` -> ["a\nb"]
|
|
//
|
|
func split(line string) ([]string, error) {
|
|
var (
|
|
words []string
|
|
inWord bool
|
|
current bytes.Buffer
|
|
)
|
|
|
|
for len(line) > 0 {
|
|
r, size := utf8.DecodeRuneInString(line)
|
|
if unicode.IsSpace(r) {
|
|
if inWord {
|
|
words = append(words, current.String())
|
|
current.Reset()
|
|
inWord = false
|
|
}
|
|
} else if r == '"' {
|
|
var ok bool
|
|
size, ok = quotedLength(line)
|
|
if !ok {
|
|
return nil, errors.New("invalid quotation")
|
|
}
|
|
s, err := strconv.Unquote(line[:size])
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
current.WriteString(s)
|
|
inWord = true
|
|
} else {
|
|
current.WriteRune(r)
|
|
inWord = true
|
|
}
|
|
line = line[size:]
|
|
}
|
|
if inWord {
|
|
words = append(words, current.String())
|
|
}
|
|
return words, nil
|
|
}
|
|
|
|
// quotedLength returns the length in bytes of the prefix of input that
|
|
// contain a possibly-valid double-quoted Go string literal.
|
|
//
|
|
// On success, n is at least two (""); input[:n] may be passed to
|
|
// strconv.Unquote to interpret its value, and input[n:] contains the
|
|
// rest of the input.
|
|
//
|
|
// On failure, quotedLength returns false, and the entire input can be
|
|
// passed to strconv.Unquote if an informative error message is desired.
|
|
//
|
|
// quotedLength does not and need not detect all errors, such as
|
|
// invalid hex or octal escape sequences, since it assumes
|
|
// strconv.Unquote will be applied to the prefix. It guarantees only
|
|
// that if there is a prefix of input containing a valid string literal,
|
|
// its length is returned.
|
|
//
|
|
// TODO(adonovan): move this into a strconv-like utility package.
|
|
//
|
|
func quotedLength(input string) (n int, ok bool) {
|
|
var offset int
|
|
|
|
// next returns the rune at offset, or -1 on EOF.
|
|
// offset advances to just after that rune.
|
|
next := func() rune {
|
|
if offset < len(input) {
|
|
r, size := utf8.DecodeRuneInString(input[offset:])
|
|
offset += size
|
|
return r
|
|
}
|
|
return -1
|
|
}
|
|
|
|
if next() != '"' {
|
|
return // error: not a quotation
|
|
}
|
|
|
|
for {
|
|
r := next()
|
|
if r == '\n' || r < 0 {
|
|
return // error: string literal not terminated
|
|
}
|
|
if r == '"' {
|
|
return offset, true // success
|
|
}
|
|
if r == '\\' {
|
|
var skip int
|
|
switch next() {
|
|
case 'a', 'b', 'f', 'n', 'r', 't', 'v', '\\', '"':
|
|
skip = 0
|
|
case '0', '1', '2', '3', '4', '5', '6', '7':
|
|
skip = 2
|
|
case 'x':
|
|
skip = 2
|
|
case 'u':
|
|
skip = 4
|
|
case 'U':
|
|
skip = 8
|
|
default:
|
|
return // error: invalid escape
|
|
}
|
|
|
|
for i := 0; i < skip; i++ {
|
|
next()
|
|
}
|
|
}
|
|
}
|
|
}
|