Files
releases/v1/ast/mermaid.go
T
Anders Eknert 8a6000dd8c Enable static check of consistent receiver names (#9008)
Style thing really but I think one that makes sense. I renamed only by
what was already the most popular option.

Tested building against Go 1.27 to make sure I didn't mess that up.

Signed-off-by: Anders Eknert <anders.eknert@apple.com>
2026-08-12 13:19:42 -05:00

432 lines
10 KiB
Go

// Copyright 2026 The OPA Authors. All rights reserved.
// Use of this source code is governed by an Apache2
// license that can be found in the LICENSE file.
package ast
import (
"fmt"
"strconv"
"strings"
)
// TODO: move/rename?
// mermaidFormatter is implemented by AST nodes that can render themselves as a
// Mermaid flowchart fragment. Each call writes node and edge declarations to b
// and returns the ID of the node it emitted.
type mermaidFormatter interface {
mermaidFormat(b *mermaidBuilder) string
}
// mermaidBuilder accumulates Mermaid flowchart lines and issues unique node IDs.
type mermaidBuilder struct {
buf strings.Builder
counter int
}
func (b *mermaidBuilder) newID() string {
b.counter++
return fmt.Sprintf("n%d", b.counter)
}
// node writes a Mermaid node declaration and returns its ID.
// Supported shapes: "rect" (default), "round", "hex", "cyl", "stadium", "trap".
func (b *mermaidBuilder) node(shape, label string) string {
id := b.newID()
label = mermaidEscapeLabel(label)
b.buf.WriteString(" ")
b.buf.WriteString(id)
switch shape {
case "round":
b.buf.WriteString("(\"")
b.buf.WriteString(label)
b.buf.WriteString("\")\n")
case "hex":
b.buf.WriteString("{{\"")
b.buf.WriteString(label)
b.buf.WriteString("\"}}\n")
case "cyl":
b.buf.WriteString("[(\"")
b.buf.WriteString(label)
b.buf.WriteString("\")]\n")
case "stadium":
b.buf.WriteString("([\"")
b.buf.WriteString(label)
b.buf.WriteString("\"])\n")
case "trap":
b.buf.WriteString("[/\"")
b.buf.WriteString(label)
b.buf.WriteString("\"/]\n")
default: // "rect"
b.buf.WriteString("[\"")
b.buf.WriteString(label)
b.buf.WriteString("\"]\n")
}
return id
}
// edge writes a plain directed edge.
func (b *mermaidBuilder) edge(from, to string) {
b.buf.WriteString(" ")
b.buf.WriteString(from)
b.buf.WriteString(" --> ")
b.buf.WriteString(to)
b.buf.WriteString("\n")
}
// edgeLabeled writes a directed edge with a text label.
func (b *mermaidBuilder) edgeLabeled(from, to, label string) {
b.buf.WriteString(" ")
b.buf.WriteString(from)
b.buf.WriteString(" -->|")
b.buf.WriteString(label)
b.buf.WriteString("| ")
b.buf.WriteString(to)
b.buf.WriteString("\n")
}
// mermaidEscapeLabel replaces characters that break Mermaid quoted node labels.
func mermaidEscapeLabel(s string) string {
return strings.ReplaceAll(s, `"`, "#quot;")
}
// mermaidGraph returns a Mermaid flowchart string representing the structure of
// the given module.
func mermaidGraph(module *Module) string {
b := &mermaidBuilder{}
module.mermaidFormat(b)
var out strings.Builder
out.WriteString("flowchart TD\n")
out.WriteString(b.buf.String())
return out.String()
}
// --- Module ---
func (mod *Module) mermaidFormat(b *mermaidBuilder) string {
id := b.node("rect", "Module")
pkgID := mod.Package.mermaidFormat(b)
b.edgeLabeled(id, pkgID, "package")
for _, imp := range mod.Imports {
impID := imp.mermaidFormat(b)
b.edgeLabeled(id, impID, "import")
}
for _, rule := range mod.Rules {
ruleID := rule.mermaidFormat(b)
b.edgeLabeled(id, ruleID, "rule")
}
return id
}
// --- Package ---
func (pkg *Package) mermaidFormat(b *mermaidBuilder) string {
return b.node("rect", "Package: "+pkg.Path.String())
}
// --- Import ---
func (imp *Import) mermaidFormat(b *mermaidBuilder) string {
label := "Import: " + imp.Path.String()
if imp.Alias != "" {
label += " as " + string(imp.Alias)
}
return b.node("rect", label)
}
// --- Rule ---
func (rule *Rule) mermaidFormat(b *mermaidBuilder) string {
ref := rule.Head.Ref().String()
label := "Rule: " + ref
if rule.Default {
label = "Rule: default " + ref
}
id := b.node("rect", label)
headID := mermaidFormatHead(rule.Head, b)
b.edge(id, headID)
if len(rule.Body) > 0 {
bodyID := b.node("rect", "Body")
b.edge(id, bodyID)
for i, expr := range rule.Body {
exprID := mermaidFormatExpr(expr, b)
b.edgeLabeled(bodyID, exprID, strconv.Itoa(i))
}
}
if rule.Else != nil {
elseID := rule.Else.mermaidFormat(b)
b.edgeLabeled(id, elseID, "else")
}
return id
}
func mermaidFormatHead(head *Head, b *mermaidBuilder) string {
id := b.node("rect", "Head")
refId := head.Ref().mermaidFormat(b)
b.edgeLabeled(id, refId, "ref")
for i, arg := range head.Args {
argID := arg.mermaidFormat(b)
b.edgeLabeled(id, argID, fmt.Sprintf(`"arg[%d]"`, i))
}
if head.Key != nil {
keyID := head.Key.mermaidFormat(b)
b.edgeLabeled(id, keyID, "key")
}
if head.Value != nil {
valID := head.Value.mermaidFormat(b)
b.edgeLabeled(id, valID, "value")
}
return id
}
func mermaidFormatExpr(expr *Expr, b *mermaidBuilder) string {
label := expr.String()
id := b.node("hex", label)
switch terms := expr.Terms.(type) {
case *Term:
termID := terms.mermaidFormat(b)
b.edge(id, termID)
case []*Term:
// terms[0] is the operator; remaining are arguments.
for i, t := range terms {
tID := t.mermaidFormat(b)
if i == 0 {
b.edgeLabeled(id, tID, "op")
} else {
b.edgeLabeled(id, tID, fmt.Sprintf(`"arg[%d]"`, i-1))
}
}
case *SomeDecl:
for _, sym := range terms.Symbols {
symID := sym.mermaidFormat(b)
b.edgeLabeled(id, symID, "symbol")
}
case *Every:
everyID := mermaidFormatEvery(terms, b)
b.edge(id, everyID)
case *Not:
notID := terms.mermaidFormat(b)
b.edge(id, notID)
case *LogicalAnd:
andID := mermaidFormatLogical("and", terms.Lhs, terms.Rhs, b)
b.edge(id, andID)
case *LogicalOr:
orID := mermaidFormatLogical("or", terms.Lhs, terms.Rhs, b)
b.edge(id, orID)
}
for _, w := range expr.With {
withID := mermaidFormatWith(w, b)
b.edgeLabeled(id, withID, "with")
}
return id
}
func mermaidFormatEvery(every *Every, b *mermaidBuilder) string {
id := b.node("rect", "every")
if every.Key != nil {
keyID := every.Key.mermaidFormat(b)
b.edgeLabeled(id, keyID, "key")
}
valID := every.Value.mermaidFormat(b)
b.edgeLabeled(id, valID, "value")
domainID := every.Domain.mermaidFormat(b)
b.edgeLabeled(id, domainID, "domain")
bodyID := b.node("rect", "Body")
b.edge(id, bodyID)
for _, expr := range every.Body {
exprID := mermaidFormatExpr(expr, b)
b.edge(bodyID, exprID)
}
return id
}
func mermaidFormatLogical(op string, lhs, rhs Body, b *mermaidBuilder) string {
id := b.node("rect", op)
lhsID := b.node("rect", "Lhs")
b.edge(id, lhsID)
for _, expr := range lhs {
exprID := mermaidFormatExpr(expr, b)
b.edge(lhsID, exprID)
}
rhsID := b.node("rect", "Rhs")
b.edge(id, rhsID)
for _, expr := range rhs {
exprID := mermaidFormatExpr(expr, b)
b.edge(rhsID, exprID)
}
return id
}
func mermaidFormatWith(w *With, b *mermaidBuilder) string {
id := b.node("rect", "with")
targetID := w.Target.mermaidFormat(b)
b.edgeLabeled(id, targetID, "target")
valID := w.Value.mermaidFormat(b)
b.edgeLabeled(id, valID, "value")
return id
}
// --- Not ---
func (n *Not) mermaidFormat(b *mermaidBuilder) string {
id := b.node("stadium", "not")
for i, expr := range n.Body {
exprID := mermaidFormatExpr(expr, b)
b.edgeLabeled(id, exprID, strconv.Itoa(i))
}
return id
}
// --- Term ---
// mermaidFormat delegates to the underlying Value. Term itself does not emit a
// node; the Value determines the node shape and label.
func (term *Term) mermaidFormat(b *mermaidBuilder) string {
switch v := term.Value.(type) {
case mermaidFormatter:
return v.mermaidFormat(b)
case Set:
return mermaidFormatSet(v, b)
case Object:
return mermaidFormatObject(v, b)
default:
return b.node("round", term.String())
}
}
// --- Scalar Values ---
func (Null) mermaidFormat(b *mermaidBuilder) string {
return b.node("round", "null")
}
func (bol Boolean) mermaidFormat(b *mermaidBuilder) string {
return b.node("round", bol.String())
}
func (num Number) mermaidFormat(b *mermaidBuilder) string {
return b.node("round", num.String())
}
func (str String) mermaidFormat(b *mermaidBuilder) string {
return b.node("round", str.String())
}
func (v Var) mermaidFormat(b *mermaidBuilder) string {
return b.node("round", string(v))
}
// --- Ref ---
func (ref Ref) mermaidFormat(b *mermaidBuilder) string {
return b.node("round", "ref: "+ref.String())
}
// --- Array ---
func (arr *Array) mermaidFormat(b *mermaidBuilder) string {
id := b.node("cyl", fmt.Sprintf("array[%d]", arr.Len()))
arr.Foreach(func(t *Term) {
elemID := t.mermaidFormat(b)
b.edge(id, elemID)
})
return id
}
// --- Set ---
func mermaidFormatSet(s Set, b *mermaidBuilder) string {
id := b.node("cyl", fmt.Sprintf("set{%d}", s.Len()))
s.Foreach(func(t *Term) {
elemID := t.mermaidFormat(b)
b.edge(id, elemID)
})
return id
}
// --- Object ---
func mermaidFormatObject(o Object, b *mermaidBuilder) string {
id := b.node("cyl", fmt.Sprintf("object{%d}", o.Len()))
_ = o.Iter(func(k, v *Term) error {
kvID := b.node("rect", "kv")
b.edge(id, kvID)
kID := k.mermaidFormat(b)
b.edgeLabeled(kvID, kID, "key")
vID := v.mermaidFormat(b)
b.edgeLabeled(kvID, vID, "value")
return nil
})
return id
}
// --- Comprehensions ---
func (ac *ArrayComprehension) mermaidFormat(b *mermaidBuilder) string {
id := b.node("stadium", "Array Comprehension")
termID := ac.Term.mermaidFormat(b)
b.edgeLabeled(id, termID, "term")
bodyID := b.node("rect", "Body")
b.edge(id, bodyID)
for i, expr := range ac.Body {
exprID := mermaidFormatExpr(expr, b)
b.edgeLabeled(bodyID, exprID, strconv.Itoa(i))
}
return id
}
func (oc *ObjectComprehension) mermaidFormat(b *mermaidBuilder) string {
id := b.node("stadium", "Object Comprehension")
kID := oc.Key.mermaidFormat(b)
b.edgeLabeled(id, kID, "key")
vID := oc.Value.mermaidFormat(b)
b.edgeLabeled(id, vID, "value")
bodyID := b.node("rect", "Body")
b.edge(id, bodyID)
for i, expr := range oc.Body {
exprID := mermaidFormatExpr(expr, b)
b.edgeLabeled(bodyID, exprID, strconv.Itoa(i))
}
return id
}
func (sc *SetComprehension) mermaidFormat(b *mermaidBuilder) string {
id := b.node("stadium", "Set Comprehension")
termID := sc.Term.mermaidFormat(b)
b.edgeLabeled(id, termID, "term")
bodyID := b.node("rect", "Body")
b.edge(id, bodyID)
for i, expr := range sc.Body {
exprID := mermaidFormatExpr(expr, b)
b.edgeLabeled(bodyID, exprID, strconv.Itoa(i))
}
return id
}
// --- Call ---
func (c Call) mermaidFormat(b *mermaidBuilder) string {
opLabel := "call"
if len(c) > 0 {
opLabel = "call: " + c[0].String()
}
id := b.node("trap", opLabel)
for i, arg := range c[1:] {
argID := arg.mermaidFormat(b)
b.edgeLabeled(id, argID, fmt.Sprintf(`"arg[%d]"`, i))
}
return id
}