| // Copyright 2022 The Dawn Authors |
| // |
| // 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 query |
| |
| import ( |
| "fmt" |
| "io" |
| "sort" |
| ) |
| |
| // Tree holds a tree structure of Query to generic Data type. |
| // Each separate suite, file, test of the query produces a separate tree node. |
| // All cases of the query produce a single leaf tree node. |
| type Tree[Data any] struct { |
| TreeNode[Data] |
| } |
| |
| // TreeNode is a single node in the Tree |
| type TreeNode[Data any] struct { |
| // The full query of the node |
| Query Query |
| // The data associated with this node. nil is used to represent no-data. |
| Data *Data |
| // Children of the node. Keyed by query.Target and name. |
| Children TreeNodeChildren[Data] |
| } |
| |
| // TreeNodeChildKey is the key used by TreeNode for the Children map |
| type TreeNodeChildKey struct { |
| // The child name. This is the string between `:` and `,` delimiters. |
| // Note: that all test cases are held by a single TreeNode. |
| Name string |
| // The target type of the child. Examples: |
| // Query | Target of 'child' |
| // -----------------+-------------------- |
| // parent:child | Files |
| // parent:x,child | Files |
| // parent:x:child | Test |
| // parent:x:y,child | Test |
| // parent:x:y:child | Cases |
| // |
| // It's possible to have a directory and '.spec.ts' share the same name, |
| // hence why we include the Target as part of the child key. |
| Target Target |
| } |
| |
| // TreeNodeChildren is a map of TreeNodeChildKey to TreeNode pointer. |
| // Data is the data type held by a TreeNode. |
| type TreeNodeChildren[Data any] map[TreeNodeChildKey]*TreeNode[Data] |
| |
| // sortedChildKeys returns all the sorted children keys. |
| func (n *TreeNode[Data]) sortedChildKeys() []TreeNodeChildKey { |
| keys := make([]TreeNodeChildKey, 0, len(n.Children)) |
| for key := range n.Children { |
| keys = append(keys, key) |
| } |
| sort.Slice(keys, func(i, j int) bool { |
| a, b := keys[i], keys[j] |
| switch { |
| case a.Name < b.Name: |
| return true |
| case a.Name > b.Name: |
| return false |
| case a.Target < b.Target: |
| return true |
| case a.Target > b.Target: |
| return false |
| } |
| return false |
| }) |
| return keys |
| } |
| |
| // traverse performs a depth-first-search of the tree calling f for each visited |
| // node, starting with n, then visiting each of children in sorted order |
| // (pre-order traversal). |
| func (n *TreeNode[Data]) traverse(f func(n *TreeNode[Data]) error) error { |
| if err := f(n); err != nil { |
| return err |
| } |
| for _, key := range n.sortedChildKeys() { |
| if err := n.Children[key].traverse(f); err != nil { |
| return err |
| } |
| } |
| return nil |
| } |
| |
| // Merger is a function used to merge the children nodes of a tree. |
| // Merger is called with the Data of each child node. If the function returns a |
| // non-nil Data pointer, then this is used as the merged result. If the function |
| // returns nil, then the node will not be merged. |
| type Merger[Data any] func([]Data) *Data |
| |
| // merge collapses tree nodes based on child node data, using the function f. |
| // merge operates on the leaf nodes first, working its way towards the root of |
| // the tree. |
| // Returns the merged target data for this node, or nil if the node is not a |
| // leaf and its children has non-uniform data. |
| func (n *TreeNode[Data]) merge(f Merger[Data]) *Data { |
| // If the node is a leaf, then simply return the node's data. |
| if len(n.Children) == 0 { |
| return n.Data |
| } |
| |
| // Build a map of child target to merged child data. |
| // A nil for the value indicates that one or more children could not merge. |
| mergedChildren := map[Target][]Data{} |
| for key, child := range n.Children { |
| // Call merge() on the child. Even if we cannot merge this node, we want |
| // to do this for all children so they can merge their sub-graphs. |
| childData := child.merge(f) |
| |
| if childData == nil { |
| // If merge() returned nil, then the data could not be merged. |
| // Mark the entire target as unmergeable. |
| mergedChildren[key.Target] = nil |
| continue |
| } |
| |
| // Fetch the merge list for this child's target. |
| list, found := mergedChildren[key.Target] |
| if !found { |
| // First child with the given target? |
| mergedChildren[key.Target] = []Data{*childData} |
| continue |
| } |
| if list != nil { |
| mergedChildren[key.Target] = append(list, *childData) |
| } |
| } |
| |
| merge := func(in []Data) *Data { |
| switch len(in) { |
| case 0: |
| return nil // nothing to merge. |
| case 1: |
| return &in[0] // merge of a single item results in that item |
| default: |
| return f(in) |
| } |
| } |
| |
| // Might it possible to merge this node? |
| maybeMergeable := true |
| |
| // The merged data, per target |
| mergedTargets := map[Target]Data{} |
| |
| // Attempt to merge each of the target's data |
| for target, list := range mergedChildren { |
| if list != nil { // nil == unmergeable target |
| if data := merge(list); data != nil { |
| // Merge success! |
| mergedTargets[target] = *data |
| continue |
| } |
| } |
| maybeMergeable = false // Merge of this node is not possible |
| } |
| |
| // Remove all children that have been merged |
| for key := range n.Children { |
| if _, merged := mergedTargets[key.Target]; merged { |
| delete(n.Children, key) |
| } |
| } |
| |
| // Add wildcards for merged targets |
| for target, data := range mergedTargets { |
| data := data // Don't take address of iterator |
| n.getOrCreateChild(TreeNodeChildKey{"*", target}).Data = &data |
| } |
| |
| // If any of the targets are unmergeable, then we cannot merge the node itself. |
| if !maybeMergeable { |
| return nil |
| } |
| |
| // All targets were merged. Attempt to merge each of the targets. |
| data := make([]Data, 0, len(mergedTargets)) |
| for _, d := range mergedTargets { |
| data = append(data, d) |
| } |
| return merge(data) |
| } |
| |
| // print writes a textual representation of this node and its children to w. |
| // prefix is used as the line prefix for each node, which is appended with |
| // whitespace for each child node. |
| func (n *TreeNode[Data]) print(w io.Writer, prefix string) { |
| fmt.Fprintf(w, "%v{\n", prefix) |
| fmt.Fprintf(w, "%v query: '%v'\n", prefix, n.Query) |
| fmt.Fprintf(w, "%v data: '%v'\n", prefix, n.Data) |
| for _, key := range n.sortedChildKeys() { |
| n.Children[key].print(w, prefix+" ") |
| } |
| fmt.Fprintf(w, "%v}\n", prefix) |
| } |
| |
| // Format implements the io.Formatter interface. |
| // See https://pkg.go.dev/fmt#Formatter |
| func (n *TreeNode[Data]) Format(f fmt.State, verb rune) { |
| n.print(f, "") |
| } |
| |
| // getOrCreateChild returns the child with the given key if it exists, |
| // otherwise the child node is created and added to n and is returned. |
| func (n *TreeNode[Data]) getOrCreateChild(key TreeNodeChildKey) *TreeNode[Data] { |
| if n.Children == nil { |
| child := &TreeNode[Data]{Query: n.Query.Append(key.Target, key.Name)} |
| n.Children = TreeNodeChildren[Data]{key: child} |
| return child |
| } |
| if child, ok := n.Children[key]; ok { |
| return child |
| } |
| child := &TreeNode[Data]{Query: n.Query.Append(key.Target, key.Name)} |
| n.Children[key] = child |
| return child |
| } |
| |
| // QueryData is a pair of a Query and a generic Data type. |
| // Used by NewTree for constructing a tree with entries. |
| type QueryData[Data any] struct { |
| Query Query |
| Data Data |
| } |
| |
| // NewTree returns a new Tree populated with the given entries. |
| // If entries returns duplicate queries, then ErrDuplicateData will be returned. |
| func NewTree[Data any](entries ...QueryData[Data]) (Tree[Data], error) { |
| out := Tree[Data]{} |
| for _, qd := range entries { |
| if err := out.Add(qd.Query, qd.Data); err != nil { |
| return Tree[Data]{}, err |
| } |
| } |
| return out, nil |
| } |
| |
| // Add adds a new data to the tree. |
| // Returns ErrDuplicateData if the tree already contains a data for the given |
| func (t *Tree[Data]) Add(q Query, d Data) error { |
| node := &t.TreeNode |
| q.Walk(func(q Query, t Target, n string) error { |
| node = node.getOrCreateChild(TreeNodeChildKey{n, t}) |
| return nil |
| }) |
| if node.Data != nil { |
| return ErrDuplicateData{node.Query} |
| } |
| node.Data = &d |
| return nil |
| } |
| |
| // Reduce reduces the tree using the Merger function f. |
| // If the Merger function returns a non-nil Data value, then this will be used |
| // to replace the non-leaf node with a new leaf node holding the returned Data. |
| // This process recurses up to the tree root. |
| func (t *Tree[Data]) Reduce(f Merger[Data]) { |
| for _, root := range t.TreeNode.Children { |
| root.merge(f) |
| } |
| } |
| |
| // ReduceUnder reduces the sub-tree under the given query using the Merger |
| // function f. |
| // If the Merger function returns a non-nil Data value, then this will be used |
| // to replace the non-leaf node with a new leaf node holding the returned Data. |
| // This process recurses up to the node pointed at by the query to. |
| func (t *Tree[Data]) ReduceUnder(to Query, f Merger[Data]) error { |
| node := &t.TreeNode |
| return to.Walk(func(q Query, t Target, n string) error { |
| if n == "*" { |
| node.merge(f) |
| return nil |
| } |
| child, ok := node.Children[TreeNodeChildKey{n, t}] |
| if !ok { |
| return ErrNoDataForQuery{q} |
| } |
| node = child |
| if q == to { |
| node.merge(f) |
| } |
| return nil |
| }) |
| } |
| |
| // glob calls f for every node under the given query. |
| func (t *Tree[Data]) glob(fq Query, f func(f *TreeNode[Data]) error) error { |
| node := &t.TreeNode |
| return fq.Walk(func(q Query, t Target, n string) error { |
| if n == "*" { |
| // Wildcard reached. |
| // Glob the parent, but restrict to the wildcard target type. |
| for _, key := range node.sortedChildKeys() { |
| child := node.Children[key] |
| if child.Query.Target() == t { |
| if err := child.traverse(f); err != nil { |
| return err |
| } |
| } |
| } |
| return nil |
| } |
| switch t { |
| case Suite, Files, Tests: |
| child, ok := node.Children[TreeNodeChildKey{n, t}] |
| if !ok { |
| return ErrNoDataForQuery{q} |
| } |
| node = child |
| case Cases: |
| for _, key := range node.sortedChildKeys() { |
| child := node.Children[key] |
| if child.Query.Contains(fq) { |
| if err := f(child); err != nil { |
| return err |
| } |
| } |
| } |
| return nil |
| } |
| if q == fq { |
| return node.traverse(f) |
| } |
| return nil |
| }) |
| } |
| |
| // Replace replaces the sub-tree matching the query 'what' with the Data 'with' |
| func (t *Tree[Data]) Replace(what Query, with Data) error { |
| node := &t.TreeNode |
| return what.Walk(func(q Query, t Target, n string) error { |
| childKey := TreeNodeChildKey{n, t} |
| if q == what { |
| for key, child := range node.Children { |
| // Use Query.Contains() to handle matching of Cases |
| // (which are not split into tree nodes) |
| if q.Contains(child.Query) { |
| delete(node.Children, key) |
| } |
| } |
| node = node.getOrCreateChild(childKey) |
| node.Data = &with |
| } else { |
| child, ok := node.Children[childKey] |
| if !ok { |
| return ErrNoDataForQuery{q} |
| } |
| node = child |
| } |
| return nil |
| }) |
| } |
| |
| // List returns the tree nodes flattened as a list of QueryData |
| func (t *Tree[Data]) List() []QueryData[Data] { |
| out := []QueryData[Data]{} |
| t.traverse(func(n *TreeNode[Data]) error { |
| if n.Data != nil { |
| out = append(out, QueryData[Data]{n.Query, *n.Data}) |
| } |
| return nil |
| }) |
| return out |
| } |
| |
| // Glob returns a list of QueryData's for every node that is under the given |
| // query, which holds data. |
| // Glob handles wildcards as well as non-wildcard queries: |
| // * A non-wildcard query will match the node itself, along with every node |
| // under the query. For example: 'a:b' will match every File and Test |
| // node under 'a:b', including 'a:b' itself. |
| // * A wildcard Query will include every node under the parent node with the |
| // matching Query target. For example: 'a:b:*' will match every Test |
| // node (excluding File nodes) under 'a:b', 'a:b' will not be included. |
| func (t *Tree[Data]) Glob(q Query) ([]QueryData[Data], error) { |
| out := []QueryData[Data]{} |
| err := t.glob(q, func(n *TreeNode[Data]) error { |
| if n.Data != nil { |
| out = append(out, QueryData[Data]{n.Query, *n.Data}) |
| } |
| return nil |
| }) |
| if err != nil { |
| return nil, err |
| } |
| return out, nil |
| } |