mirror of
https://github.com/open-policy-agent/opa.git
synced 2026-08-12 19:32:48 -06:00
8a6000dd8c
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>
432 lines
10 KiB
Go
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
|
|
}
|