mirror of
https://github.com/opentofu/opentofu.git
synced 2024-12-26 08:51:02 -06:00
c8df3e5995
Fixes #11052 It appears that historically nodes did not expect DotOpts to ever be nil. To avoid nil panics in general I'm in agreement with this behavior so this modifies dag to always pass in a non-nil DotOpts. Tests included.
283 lines
6.1 KiB
Go
283 lines
6.1 KiB
Go
package dag
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"sort"
|
|
"strings"
|
|
)
|
|
|
|
// DotOpts are the options for generating a dot formatted Graph.
|
|
type DotOpts struct {
|
|
// Allows some nodes to decide to only show themselves when the user has
|
|
// requested the "verbose" graph.
|
|
Verbose bool
|
|
|
|
// Highlight Cycles
|
|
DrawCycles bool
|
|
|
|
// How many levels to expand modules as we draw
|
|
MaxDepth int
|
|
|
|
// use this to keep the cluster_ naming convention from the previous dot writer
|
|
cluster bool
|
|
}
|
|
|
|
// GraphNodeDotter can be implemented by a node to cause it to be included
|
|
// in the dot graph. The Dot method will be called which is expected to
|
|
// return a representation of this node.
|
|
type GraphNodeDotter interface {
|
|
// Dot is called to return the dot formatting for the node.
|
|
// The first parameter is the title of the node.
|
|
// The second parameter includes user-specified options that affect the dot
|
|
// graph. See GraphDotOpts below for details.
|
|
DotNode(string, *DotOpts) *DotNode
|
|
}
|
|
|
|
// DotNode provides a structure for Vertices to return in order to specify their
|
|
// dot format.
|
|
type DotNode struct {
|
|
Name string
|
|
Attrs map[string]string
|
|
}
|
|
|
|
// Returns the DOT representation of this Graph.
|
|
func (g *marshalGraph) Dot(opts *DotOpts) []byte {
|
|
if opts == nil {
|
|
opts = &DotOpts{
|
|
DrawCycles: true,
|
|
MaxDepth: -1,
|
|
Verbose: true,
|
|
}
|
|
}
|
|
|
|
var w indentWriter
|
|
w.WriteString("digraph {\n")
|
|
w.Indent()
|
|
|
|
// some dot defaults
|
|
w.WriteString(`compound = "true"` + "\n")
|
|
w.WriteString(`newrank = "true"` + "\n")
|
|
|
|
// the top level graph is written as the first subgraph
|
|
w.WriteString(`subgraph "root" {` + "\n")
|
|
g.writeBody(opts, &w)
|
|
|
|
// cluster isn't really used other than for naming purposes in some graphs
|
|
opts.cluster = opts.MaxDepth != 0
|
|
maxDepth := opts.MaxDepth
|
|
if maxDepth == 0 {
|
|
maxDepth = -1
|
|
}
|
|
|
|
for _, s := range g.Subgraphs {
|
|
g.writeSubgraph(s, opts, maxDepth, &w)
|
|
}
|
|
|
|
w.Unindent()
|
|
w.WriteString("}\n")
|
|
return w.Bytes()
|
|
}
|
|
|
|
func (v *marshalVertex) dot(g *marshalGraph, opts *DotOpts) []byte {
|
|
var buf bytes.Buffer
|
|
graphName := g.Name
|
|
if graphName == "" {
|
|
graphName = "root"
|
|
}
|
|
|
|
name := v.Name
|
|
attrs := v.Attrs
|
|
if v.graphNodeDotter != nil {
|
|
node := v.graphNodeDotter.DotNode(name, opts)
|
|
if node == nil {
|
|
return []byte{}
|
|
}
|
|
|
|
newAttrs := make(map[string]string)
|
|
for k, v := range attrs {
|
|
newAttrs[k] = v
|
|
}
|
|
for k, v := range node.Attrs {
|
|
newAttrs[k] = v
|
|
}
|
|
|
|
name = node.Name
|
|
attrs = newAttrs
|
|
}
|
|
|
|
buf.WriteString(fmt.Sprintf(`"[%s] %s"`, graphName, name))
|
|
writeAttrs(&buf, attrs)
|
|
buf.WriteByte('\n')
|
|
|
|
return buf.Bytes()
|
|
}
|
|
|
|
func (e *marshalEdge) dot(g *marshalGraph) string {
|
|
var buf bytes.Buffer
|
|
graphName := g.Name
|
|
if graphName == "" {
|
|
graphName = "root"
|
|
}
|
|
|
|
sourceName := g.vertexByID(e.Source).Name
|
|
targetName := g.vertexByID(e.Target).Name
|
|
s := fmt.Sprintf(`"[%s] %s" -> "[%s] %s"`, graphName, sourceName, graphName, targetName)
|
|
buf.WriteString(s)
|
|
writeAttrs(&buf, e.Attrs)
|
|
|
|
return buf.String()
|
|
}
|
|
|
|
func cycleDot(e *marshalEdge, g *marshalGraph) string {
|
|
return e.dot(g) + ` [color = "red", penwidth = "2.0"]`
|
|
}
|
|
|
|
// Write the subgraph body. The is recursive, and the depth argument is used to
|
|
// record the current depth of iteration.
|
|
func (g *marshalGraph) writeSubgraph(sg *marshalGraph, opts *DotOpts, depth int, w *indentWriter) {
|
|
if depth == 0 {
|
|
return
|
|
}
|
|
depth--
|
|
|
|
name := sg.Name
|
|
if opts.cluster {
|
|
// we prefix with cluster_ to match the old dot output
|
|
name = "cluster_" + name
|
|
sg.Attrs["label"] = sg.Name
|
|
}
|
|
w.WriteString(fmt.Sprintf("subgraph %q {\n", name))
|
|
sg.writeBody(opts, w)
|
|
|
|
for _, sg := range sg.Subgraphs {
|
|
g.writeSubgraph(sg, opts, depth, w)
|
|
}
|
|
}
|
|
|
|
func (g *marshalGraph) writeBody(opts *DotOpts, w *indentWriter) {
|
|
w.Indent()
|
|
|
|
for _, as := range attrStrings(g.Attrs) {
|
|
w.WriteString(as + "\n")
|
|
}
|
|
|
|
// list of Vertices that aren't to be included in the dot output
|
|
skip := map[string]bool{}
|
|
|
|
for _, v := range g.Vertices {
|
|
if v.graphNodeDotter == nil {
|
|
skip[v.ID] = true
|
|
continue
|
|
}
|
|
|
|
w.Write(v.dot(g, opts))
|
|
}
|
|
|
|
var dotEdges []string
|
|
|
|
if opts.DrawCycles {
|
|
for _, c := range g.Cycles {
|
|
if len(c) < 2 {
|
|
continue
|
|
}
|
|
|
|
for i, j := 0, 1; i < len(c); i, j = i+1, j+1 {
|
|
if j >= len(c) {
|
|
j = 0
|
|
}
|
|
src := c[i]
|
|
tgt := c[j]
|
|
|
|
if skip[src.ID] || skip[tgt.ID] {
|
|
continue
|
|
}
|
|
|
|
e := &marshalEdge{
|
|
Name: fmt.Sprintf("%s|%s", src.Name, tgt.Name),
|
|
Source: src.ID,
|
|
Target: tgt.ID,
|
|
Attrs: make(map[string]string),
|
|
}
|
|
|
|
dotEdges = append(dotEdges, cycleDot(e, g))
|
|
src = tgt
|
|
}
|
|
}
|
|
}
|
|
|
|
for _, e := range g.Edges {
|
|
dotEdges = append(dotEdges, e.dot(g))
|
|
}
|
|
|
|
// srot these again to match the old output
|
|
sort.Strings(dotEdges)
|
|
|
|
for _, e := range dotEdges {
|
|
w.WriteString(e + "\n")
|
|
}
|
|
|
|
w.Unindent()
|
|
w.WriteString("}\n")
|
|
}
|
|
|
|
func writeAttrs(buf *bytes.Buffer, attrs map[string]string) {
|
|
if len(attrs) > 0 {
|
|
buf.WriteString(" [")
|
|
buf.WriteString(strings.Join(attrStrings(attrs), ", "))
|
|
buf.WriteString("]")
|
|
}
|
|
}
|
|
|
|
func attrStrings(attrs map[string]string) []string {
|
|
strings := make([]string, 0, len(attrs))
|
|
for k, v := range attrs {
|
|
strings = append(strings, fmt.Sprintf("%s = %q", k, v))
|
|
}
|
|
sort.Strings(strings)
|
|
return strings
|
|
}
|
|
|
|
// Provide a bytes.Buffer like structure, which will indent when starting a
|
|
// newline.
|
|
type indentWriter struct {
|
|
bytes.Buffer
|
|
level int
|
|
}
|
|
|
|
func (w *indentWriter) indent() {
|
|
newline := []byte("\n")
|
|
if !bytes.HasSuffix(w.Bytes(), newline) {
|
|
return
|
|
}
|
|
for i := 0; i < w.level; i++ {
|
|
w.Buffer.WriteString("\t")
|
|
}
|
|
}
|
|
|
|
// Indent increases indentation by 1
|
|
func (w *indentWriter) Indent() { w.level++ }
|
|
|
|
// Unindent decreases indentation by 1
|
|
func (w *indentWriter) Unindent() { w.level-- }
|
|
|
|
// the following methods intercecpt the byte.Buffer writes and insert the
|
|
// indentation when starting a new line.
|
|
func (w *indentWriter) Write(b []byte) (int, error) {
|
|
w.indent()
|
|
return w.Buffer.Write(b)
|
|
}
|
|
|
|
func (w *indentWriter) WriteString(s string) (int, error) {
|
|
w.indent()
|
|
return w.Buffer.WriteString(s)
|
|
}
|
|
func (w *indentWriter) WriteByte(b byte) error {
|
|
w.indent()
|
|
return w.Buffer.WriteByte(b)
|
|
}
|
|
func (w *indentWriter) WriteRune(r rune) (int, error) {
|
|
w.indent()
|
|
return w.Buffer.WriteRune(r)
|
|
}
|