github.com/opentofu/opentofu@v1.7.1/internal/dag/dot.go (about) 1 // Copyright (c) The OpenTofu Authors 2 // SPDX-License-Identifier: MPL-2.0 3 // Copyright (c) 2023 HashiCorp, Inc. 4 // SPDX-License-Identifier: MPL-2.0 5 6 package dag 7 8 import ( 9 "bytes" 10 "fmt" 11 "sort" 12 "strings" 13 ) 14 15 // DotOpts are the options for generating a dot formatted Graph. 16 type DotOpts struct { 17 // Allows some nodes to decide to only show themselves when the user has 18 // requested the "verbose" graph. 19 Verbose bool 20 21 // Highlight Cycles 22 DrawCycles bool 23 24 // How many levels to expand modules as we draw 25 MaxDepth int 26 27 // use this to keep the cluster_ naming convention from the previous dot writer 28 cluster bool 29 } 30 31 // GraphNodeDotter can be implemented by a node to cause it to be included 32 // in the dot graph. The Dot method will be called which is expected to 33 // return a representation of this node. 34 type GraphNodeDotter interface { 35 // Dot is called to return the dot formatting for the node. 36 // The first parameter is the title of the node. 37 // The second parameter includes user-specified options that affect the dot 38 // graph. See GraphDotOpts below for details. 39 DotNode(string, *DotOpts) *DotNode 40 } 41 42 // DotNode provides a structure for Vertices to return in order to specify their 43 // dot format. 44 type DotNode struct { 45 Name string 46 Attrs map[string]string 47 } 48 49 // Returns the DOT representation of this Graph. 50 func (g *marshalGraph) Dot(opts *DotOpts) []byte { 51 if opts == nil { 52 opts = &DotOpts{ 53 DrawCycles: true, 54 MaxDepth: -1, 55 Verbose: true, 56 } 57 } 58 59 var w indentWriter 60 w.WriteString("digraph {\n") 61 w.Indent() 62 63 // some dot defaults 64 w.WriteString(`compound = "true"` + "\n") 65 w.WriteString(`newrank = "true"` + "\n") 66 67 // the top level graph is written as the first subgraph 68 w.WriteString(`subgraph "root" {` + "\n") 69 g.writeBody(opts, &w) 70 71 // cluster isn't really used other than for naming purposes in some graphs 72 opts.cluster = opts.MaxDepth != 0 73 maxDepth := opts.MaxDepth 74 if maxDepth == 0 { 75 maxDepth = -1 76 } 77 78 for _, s := range g.Subgraphs { 79 g.writeSubgraph(s, opts, maxDepth, &w) 80 } 81 82 w.Unindent() 83 w.WriteString("}\n") 84 return w.Bytes() 85 } 86 87 func (v *marshalVertex) dot(g *marshalGraph, opts *DotOpts) []byte { 88 var buf bytes.Buffer 89 graphName := g.Name 90 if graphName == "" { 91 graphName = "root" 92 } 93 94 name := v.Name 95 attrs := v.Attrs 96 if v.graphNodeDotter != nil { 97 node := v.graphNodeDotter.DotNode(name, opts) 98 if node == nil { 99 return []byte{} 100 } 101 102 newAttrs := make(map[string]string) 103 for k, v := range attrs { 104 newAttrs[k] = v 105 } 106 for k, v := range node.Attrs { 107 newAttrs[k] = v 108 } 109 110 name = node.Name 111 attrs = newAttrs 112 } 113 114 buf.WriteString(fmt.Sprintf(`"[%s] %s"`, graphName, name)) 115 writeAttrs(&buf, attrs) 116 buf.WriteByte('\n') 117 118 return buf.Bytes() 119 } 120 121 func (e *marshalEdge) dot(g *marshalGraph) string { 122 var buf bytes.Buffer 123 graphName := g.Name 124 if graphName == "" { 125 graphName = "root" 126 } 127 128 sourceName := g.vertexByID(e.Source).Name 129 targetName := g.vertexByID(e.Target).Name 130 s := fmt.Sprintf(`"[%s] %s" -> "[%s] %s"`, graphName, sourceName, graphName, targetName) 131 buf.WriteString(s) 132 writeAttrs(&buf, e.Attrs) 133 134 return buf.String() 135 } 136 137 func cycleDot(e *marshalEdge, g *marshalGraph) string { 138 return e.dot(g) + ` [color = "red", penwidth = "2.0"]` 139 } 140 141 // Write the subgraph body. The is recursive, and the depth argument is used to 142 // record the current depth of iteration. 143 func (g *marshalGraph) writeSubgraph(sg *marshalGraph, opts *DotOpts, depth int, w *indentWriter) { 144 if depth == 0 { 145 return 146 } 147 depth-- 148 149 name := sg.Name 150 if opts.cluster { 151 // we prefix with cluster_ to match the old dot output 152 name = "cluster_" + name 153 sg.Attrs["label"] = sg.Name 154 } 155 w.WriteString(fmt.Sprintf("subgraph %q {\n", name)) 156 sg.writeBody(opts, w) 157 158 for _, sg := range sg.Subgraphs { 159 g.writeSubgraph(sg, opts, depth, w) 160 } 161 } 162 163 func (g *marshalGraph) writeBody(opts *DotOpts, w *indentWriter) { 164 w.Indent() 165 166 for _, as := range attrStrings(g.Attrs) { 167 w.WriteString(as + "\n") 168 } 169 170 // list of Vertices that aren't to be included in the dot output 171 skip := map[string]bool{} 172 173 for _, v := range g.Vertices { 174 if v.graphNodeDotter == nil { 175 skip[v.ID] = true 176 continue 177 } 178 179 w.Write(v.dot(g, opts)) 180 } 181 182 var dotEdges []string 183 184 if opts.DrawCycles { 185 for _, c := range g.Cycles { 186 if len(c) < 2 { 187 continue 188 } 189 190 for i, j := 0, 1; i < len(c); i, j = i+1, j+1 { 191 if j >= len(c) { 192 j = 0 193 } 194 src := c[i] 195 tgt := c[j] 196 197 if skip[src.ID] || skip[tgt.ID] { 198 continue 199 } 200 201 e := &marshalEdge{ 202 Name: fmt.Sprintf("%s|%s", src.Name, tgt.Name), 203 Source: src.ID, 204 Target: tgt.ID, 205 Attrs: make(map[string]string), 206 } 207 208 dotEdges = append(dotEdges, cycleDot(e, g)) 209 src = tgt 210 } 211 } 212 } 213 214 for _, e := range g.Edges { 215 dotEdges = append(dotEdges, e.dot(g)) 216 } 217 218 // srot these again to match the old output 219 sort.Strings(dotEdges) 220 221 for _, e := range dotEdges { 222 w.WriteString(e + "\n") 223 } 224 225 w.Unindent() 226 w.WriteString("}\n") 227 } 228 229 func writeAttrs(buf *bytes.Buffer, attrs map[string]string) { 230 if len(attrs) > 0 { 231 buf.WriteString(" [") 232 buf.WriteString(strings.Join(attrStrings(attrs), ", ")) 233 buf.WriteString("]") 234 } 235 } 236 237 func attrStrings(attrs map[string]string) []string { 238 strings := make([]string, 0, len(attrs)) 239 for k, v := range attrs { 240 strings = append(strings, fmt.Sprintf("%s = %q", k, v)) 241 } 242 sort.Strings(strings) 243 return strings 244 } 245 246 // Provide a bytes.Buffer like structure, which will indent when starting a 247 // newline. 248 type indentWriter struct { 249 bytes.Buffer 250 level int 251 } 252 253 func (w *indentWriter) indent() { 254 newline := []byte("\n") 255 if !bytes.HasSuffix(w.Bytes(), newline) { 256 return 257 } 258 for i := 0; i < w.level; i++ { 259 w.Buffer.WriteString("\t") 260 } 261 } 262 263 // Indent increases indentation by 1 264 func (w *indentWriter) Indent() { w.level++ } 265 266 // Unindent decreases indentation by 1 267 func (w *indentWriter) Unindent() { w.level-- } 268 269 // the following methods intercecpt the byte.Buffer writes and insert the 270 // indentation when starting a new line. 271 func (w *indentWriter) Write(b []byte) (int, error) { 272 w.indent() 273 return w.Buffer.Write(b) 274 } 275 276 func (w *indentWriter) WriteString(s string) (int, error) { 277 w.indent() 278 return w.Buffer.WriteString(s) 279 } 280 func (w *indentWriter) WriteByte(b byte) error { 281 w.indent() 282 return w.Buffer.WriteByte(b) 283 } 284 func (w *indentWriter) WriteRune(r rune) (int, error) { 285 w.indent() 286 return w.Buffer.WriteRune(r) 287 }