blob: b52d993e1e62b9e8e6d86b28f6e91011247dd266 [file] [log] [blame]
// 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
}