| // Copyright 2020-2025 Buf Technologies, Inc. |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| package dag |
| |
| import ( |
| "bytes" |
| "encoding/xml" |
| "errors" |
| "fmt" |
| |
| "buf.build/go/standard/xslices" |
| "github.com/bufbuild/buf/private/pkg/syserror" |
| ) |
| |
| // Graph is a directed acyclic graph structure. |
| type Graph[Key comparable, Value any] struct { |
| getKeyForValue func(Value) Key |
| keyToValue map[Key]Value |
| keyToNode map[Key]*node[Key] |
| // need to store order so that we can create a deterministic CycleError |
| // in the case of Walk where we have no source nodes, so that we can Walk |
| // deterministically and find the cycle. |
| keys []Key |
| } |
| |
| // NewGraph returns a new Graph for an any Value. |
| // |
| // The toKey function must convert a Value to a unique comparable key type. |
| // It is up to the caller to make sure that the key is unique on a per-value basis. |
| // |
| // This constructor must be used when initializing a Graph. |
| // |
| // TODO FUTURE: It really stinks that we have to use the constructor. We have what amounts |
| // to silent errors now below with functions that don't return an error. We should |
| // figure out a way to do this properly, or perhaps just panic if we don't use the constructor. |
| func NewGraph[Key comparable, Value any](toKey func(Value) Key) *Graph[Key, Value] { |
| return &Graph[Key, Value]{ |
| getKeyForValue: toKey, |
| keyToValue: make(map[Key]Value), |
| keyToNode: make(map[Key]*node[Key]), |
| } |
| } |
| |
| // AddNode adds a node. |
| func (g *Graph[Key, Value]) AddNode(value Value) { |
| if err := g.checkInit(); err != nil { |
| return |
| } |
| g.getOrAddNode(value) |
| } |
| |
| // AddEdge adds an edge. |
| func (g *Graph[Key, Value]) AddEdge(from Value, to Value) { |
| if err := g.checkInit(); err != nil { |
| return |
| } |
| fromNode := g.getOrAddNode(from) |
| toNode := g.getOrAddNode(to) |
| fromNode.addOutboundEdge(g.getKeyForValue(to)) |
| toNode.addInboundEdge(g.getKeyForValue(from)) |
| } |
| |
| // ContainsNode returns true if the graph contains the given node. |
| func (g *Graph[Key, Value]) ContainsNode(key Key) bool { |
| if err := g.checkInit(); err != nil { |
| return false |
| } |
| _, ok := g.keyToNode[key] |
| return ok |
| } |
| |
| // NumNodes returns the number of nodes in the graph. |
| func (g *Graph[Key, Value]) NumNodes() int { |
| if err := g.checkInit(); err != nil { |
| return 0 |
| } |
| return len(g.keys) |
| } |
| |
| // NumEdges returns the number of edges in the graph. |
| func (g *Graph[Key, Value]) NumEdges() int { |
| if err := g.checkInit(); err != nil { |
| return 0 |
| } |
| var numEdges int |
| for _, node := range g.keyToNode { |
| numEdges += len(node.outboundEdges) |
| } |
| return numEdges |
| } |
| |
| // InboundNodes returns the nodes that are inbound to the node for the key. |
| // |
| // Returns error if there is no node for the key |
| func (g *Graph[Key, Value]) InboundNodes(key Key) ([]Value, error) { |
| if err := g.checkInit(); err != nil { |
| return nil, err |
| } |
| node, ok := g.keyToNode[key] |
| if !ok { |
| return nil, fmt.Errorf("key not present: %v", key) |
| } |
| return g.getValuesForKeys(node.inboundEdges) |
| } |
| |
| // OutboundNodes returns the nodes that are outbound from the node for the key. |
| // |
| // Returns error if there is no node for the key |
| func (g *Graph[Key, Value]) OutboundNodes(key Key) ([]Value, error) { |
| if err := g.checkInit(); err != nil { |
| return nil, err |
| } |
| node, ok := g.keyToNode[key] |
| if !ok { |
| return nil, fmt.Errorf("key not present: %v", key) |
| } |
| return g.getValuesForKeys(node.outboundEdges) |
| } |
| |
| // WalkNodes visited each node in the Graph based on insertion order. |
| // |
| // f is called for each node. The first argument is the key for the node, |
| // the second argument is all inbound edges, the third argument |
| // is all outbound edges. |
| func (g *Graph[Key, Value]) WalkNodes(f func(Value, []Value, []Value) error) error { |
| if err := g.checkInit(); err != nil { |
| return err |
| } |
| for _, key := range g.keys { |
| node, ok := g.keyToNode[key] |
| if !ok { |
| return fmt.Errorf("key not present: %v", key) |
| } |
| value, err := g.getValueForKey(key) |
| if err != nil { |
| return err |
| } |
| inboundValues, err := g.getValuesForKeys(node.inboundEdges) |
| if err != nil { |
| return err |
| } |
| outboundValues, err := g.getValuesForKeys(node.outboundEdges) |
| if err != nil { |
| return err |
| } |
| if err := f(value, inboundValues, outboundValues); err != nil { |
| return err |
| } |
| } |
| return nil |
| } |
| |
| // WalkEdges visits each edge in the Graph starting at the source keys. |
| // |
| // f is called for each directed edge. The first argument is the source |
| // node, the second is the destination node. |
| // |
| // Returns a *CycleError if there is a cycle in the graph. |
| func (g *Graph[Key, Value]) WalkEdges(f func(Value, Value) error) error { |
| if err := g.checkInit(); err != nil { |
| return err |
| } |
| if g.NumEdges() == 0 { |
| // No edges, do not walk. |
| return nil |
| } |
| sourceKeys, err := g.getSourceKeys() |
| if err != nil { |
| return err |
| } |
| switch len(sourceKeys) { |
| case 0: |
| // If we have no source nodes, we have a cycle in the graph. To print the cycle, |
| // we walk starting at all keys We will hit a cycle in this process, however just to check our |
| // assumptions, we also verify the the walk returns a CycleError, and if not, |
| // return a system error. |
| allVisited := make(map[Key]struct{}) |
| for _, key := range g.keys { |
| if err := g.edgeVisit( |
| key, |
| func(Value, Value) error { return nil }, |
| newOrderedSet[Key](), |
| allVisited, |
| ); err != nil { |
| return err |
| } |
| } |
| return errors.New("graph had cycle based on source node count being zero, but this was not detected during edge walking") |
| case 1: |
| return g.edgeVisit( |
| sourceKeys[0], |
| f, |
| newOrderedSet[Key](), |
| make(map[Key]struct{}), |
| ) |
| default: |
| allVisited := make(map[Key]struct{}) |
| for _, key := range sourceKeys { |
| if err := g.edgeVisit( |
| key, |
| f, |
| newOrderedSet[Key](), |
| allVisited, |
| ); err != nil { |
| return err |
| } |
| } |
| return nil |
| } |
| } |
| |
| // TopoSort topologically sorts the nodes in the Graph starting at the given key. |
| // |
| // Returns a *CycleError if there is a cycle in the graph. |
| func (g *Graph[Key, Value]) TopoSort(start Key) ([]Value, error) { |
| if err := g.checkInit(); err != nil { |
| return nil, err |
| } |
| results := newOrderedSet[Key]() |
| if err := g.topoVisit(start, results, newOrderedSet[Key]()); err != nil { |
| return nil, err |
| } |
| return g.getValuesForKeys(results.keys) |
| } |
| |
| // DOTString returns a DOT representation of the graph. |
| // |
| // valueToString is used to print out the label for each node. |
| // |
| // https://graphviz.org/doc/info/lang.html |
| func (g *Graph[Key, Value]) DOTString(valueToString func(Value) string) (string, error) { |
| if err := g.checkInit(); err != nil { |
| return "", err |
| } |
| var edgeStrings []string |
| seenKeys := make(map[Key]struct{}) |
| if err := g.WalkEdges( |
| func(from Value, to Value) error { |
| seenKeys[g.getKeyForValue(from)] = struct{}{} |
| seenKeys[g.getKeyForValue(to)] = struct{}{} |
| fromName, err := xmlEscape(valueToString(from)) |
| if err != nil { |
| return err |
| } |
| toName, err := xmlEscape(valueToString(to)) |
| if err != nil { |
| return err |
| } |
| edgeStrings = append(edgeStrings, fmt.Sprintf("%q -> %q", fromName, toName)) |
| return nil |
| }, |
| ); err != nil { |
| return "", err |
| } |
| // We also want to pick up any nodes that do not have edges, and display them. |
| if err := g.WalkNodes( |
| func(value Value, inboundEdges []Value, outboundEdges []Value) error { |
| key := g.getKeyForValue(value) |
| if _, ok := seenKeys[key]; ok { |
| return nil |
| } |
| seenKeys[key] = struct{}{} |
| if len(inboundEdges) == 0 && len(outboundEdges) == 0 { |
| name, err := xmlEscape(valueToString(value)) |
| if err != nil { |
| return err |
| } |
| edgeStrings = append(edgeStrings, fmt.Sprintf("%q", name)) |
| return nil |
| } |
| // This is a system error. |
| return syserror.Newf( |
| "got node %v with %d inbound edges and %d outbound edges, but this was not processed during WalkEdges", |
| value, |
| len(inboundEdges), |
| len(outboundEdges), |
| ) |
| }, |
| ); err != nil { |
| return "", err |
| } |
| if len(edgeStrings) == 0 { |
| return "digraph {}", nil |
| } |
| buffer := bytes.NewBuffer(nil) |
| _, _ = buffer.WriteString("digraph {\n\n") |
| for _, edgeString := range edgeStrings { |
| _, _ = buffer.WriteString(" ") |
| _, _ = buffer.WriteString(edgeString) |
| _, _ = buffer.WriteString("\n") |
| } |
| _, _ = buffer.WriteString("\n}") |
| return buffer.String(), nil |
| } |
| |
| // *** PRIVATE *** |
| |
| func (g *Graph[Key, Value]) checkInit() error { |
| // We have to force usage of the constructor as there is no other clean way to get |
| // c.getKeyForValue into the struct. Otherwise, we could use an init function for everything, |
| // but c.getKeyForValue is required. There is no sensible default. |
| // |
| // We also do this with ComparableGraph, as otherwise, if we expose Graph as a public |
| // inheritance, we can't guarantee that an init function will be called, as even if |
| // we wrapped all the public functions specifically for ComparableGraph with init |
| // and then c.Graph.Func(), we could not guarantee that these wrapped functions |
| // would be called. |
| if g.getKeyForValue == nil || g.keyToValue == nil || g.keyToNode == nil { |
| return errors.New("graphs must be constructed with dag.NewGraph or dag.NewComparableGraph") |
| } |
| return nil |
| } |
| |
| func (g *Graph[Key, Value]) getValuesForKeys(keys []Key) ([]Value, error) { |
| return xslices.MapError(keys, g.getValueForKey) |
| } |
| |
| func (g *Graph[Key, Value]) getValueForKey(key Key) (Value, error) { |
| value, ok := g.keyToValue[key] |
| if !ok { |
| // This should never happen. |
| return value, fmt.Errorf("key not present: %v", key) |
| } |
| return value, nil |
| } |
| |
| func (g *Graph[Key, Value]) getOrAddNode(value Value) *node[Key] { |
| key := g.getKeyForValue(value) |
| node, ok := g.keyToNode[key] |
| if !ok { |
| node = newNode[Key]() |
| g.keyToValue[key] = value |
| g.keyToNode[key] = node |
| g.keys = append(g.keys, key) |
| } |
| return node |
| } |
| |
| func (g *Graph[Key, Value]) getSourceKeys() ([]Key, error) { |
| var sourceKeys []Key |
| // need to get in deterministic order |
| for _, key := range g.keys { |
| node, ok := g.keyToNode[key] |
| if !ok { |
| return nil, fmt.Errorf("key not present: %v", key) |
| } |
| if len(node.inboundEdgeMap) == 0 { |
| sourceKeys = append(sourceKeys, key) |
| } |
| } |
| return sourceKeys, nil |
| } |
| |
| func (g *Graph[Key, Value]) edgeVisit( |
| from Key, |
| f func(Value, Value) error, |
| thisSourceVisited *orderedSet[Key], |
| allSourcesVisited map[Key]struct{}, |
| ) error { |
| // this is based on this source. we want to make sure we don't |
| // have any cycles based on starting at a single source. |
| if !thisSourceVisited.add(from) { |
| index := thisSourceVisited.index(from) |
| cycle := append(thisSourceVisited.keys[index:], from) |
| return &CycleError[Key]{Keys: cycle} |
| } |
| // If we visited this from all edge visiting from other |
| // sources, do nothing, we've evaluated all cycles and visited this |
| // node properly. It's OK to return here, as we've already checked |
| // for cycles with thisSourceVisited. |
| if _, ok := allSourcesVisited[from]; ok { |
| return nil |
| } |
| // Add to the map. We'll be needing this for future iterations. |
| allSourcesVisited[from] = struct{}{} |
| |
| fromNode, ok := g.keyToNode[from] |
| if !ok { |
| return fmt.Errorf("key not present: %v", from) |
| } |
| fromValue, err := g.getValueForKey(from) |
| if err != nil { |
| return err |
| } |
| for _, to := range fromNode.outboundEdges { |
| toValue, err := g.getValueForKey(to) |
| if err != nil { |
| return err |
| } |
| if err := f(fromValue, toValue); err != nil { |
| return err |
| } |
| if err := g.edgeVisit(to, f, thisSourceVisited.copy(), allSourcesVisited); err != nil { |
| return err |
| } |
| } |
| |
| return nil |
| } |
| |
| func (g *Graph[Key, Value]) topoVisit( |
| from Key, |
| results *orderedSet[Key], |
| visited *orderedSet[Key], |
| ) error { |
| if !visited.add(from) { |
| index := visited.index(from) |
| cycle := append(visited.keys[index:], from) |
| return &CycleError[Key]{Keys: cycle} |
| } |
| |
| fromNode, ok := g.keyToNode[from] |
| if !ok { |
| return fmt.Errorf("key not present: %v", from) |
| } |
| for _, to := range fromNode.outboundEdges { |
| if err := g.topoVisit(to, results, visited.copy()); err != nil { |
| return err |
| } |
| } |
| |
| results.add(from) |
| return nil |
| } |
| |
| type node[Key comparable] struct { |
| outboundEdgeMap map[Key]struct{} |
| // need to store order for deterministic visits |
| outboundEdges []Key |
| inboundEdgeMap map[Key]struct{} |
| // need to store order for deterministic visits |
| inboundEdges []Key |
| } |
| |
| func newNode[Key comparable]() *node[Key] { |
| return &node[Key]{ |
| outboundEdgeMap: make(map[Key]struct{}), |
| inboundEdgeMap: make(map[Key]struct{}), |
| } |
| } |
| |
| func (n *node[Key]) addOutboundEdge(key Key) { |
| if _, ok := n.outboundEdgeMap[key]; !ok { |
| n.outboundEdgeMap[key] = struct{}{} |
| n.outboundEdges = append(n.outboundEdges, key) |
| } |
| } |
| |
| func (n *node[Key]) addInboundEdge(key Key) { |
| if _, ok := n.inboundEdgeMap[key]; !ok { |
| n.inboundEdgeMap[key] = struct{}{} |
| n.inboundEdges = append(n.inboundEdges, key) |
| } |
| } |
| |
| type orderedSet[Key comparable] struct { |
| keyToIndex map[Key]int |
| keys []Key |
| length int |
| } |
| |
| func newOrderedSet[Key comparable]() *orderedSet[Key] { |
| return &orderedSet[Key]{ |
| keyToIndex: make(map[Key]int), |
| } |
| } |
| |
| // returns false if already added |
| func (s *orderedSet[Key]) add(key Key) bool { |
| if _, ok := s.keyToIndex[key]; !ok { |
| s.keyToIndex[key] = s.length |
| s.keys = append(s.keys, key) |
| s.length++ |
| return true |
| } |
| return false |
| } |
| |
| func (s *orderedSet[Key]) copy() *orderedSet[Key] { |
| clone := newOrderedSet[Key]() |
| for _, item := range s.keys { |
| clone.add(item) |
| } |
| return clone |
| } |
| |
| func (s *orderedSet[Key]) index(item Key) int { |
| index, ok := s.keyToIndex[item] |
| if ok { |
| return index |
| } |
| return -1 |
| } |
| |
| func xmlEscape(s string) (string, error) { |
| buffer := bytes.NewBuffer(nil) |
| if err := xml.EscapeText(buffer, []byte(s)); err != nil { |
| return "", err |
| } |
| return buffer.String(), nil |
| } |