use crate::protocol::TreeNode;
#[derive(Debug, Default, Clone)]
pub struct WalkCtx {
pub scope: String,
pub window_id: String,
pub warnings: Vec<crate::Diagnostic>,
pub depth: usize,
}
pub const MAX_TREE_DEPTH: usize = 256;
pub trait TreeTransform {
fn enter(&mut self, node: &mut TreeNode, ctx: &mut WalkCtx);
fn exit(&mut self, _node: &mut TreeNode, _ctx: &mut WalkCtx) {}
fn skip_children(&self, _node: &TreeNode, _ctx: &WalkCtx) -> bool {
false
}
}
pub fn walk(node: &mut TreeNode, transforms: &mut [&mut dyn TreeTransform], ctx: &mut WalkCtx) {
for t in transforms.iter_mut() {
t.enter(node, ctx);
}
let transform_skip = transforms.iter().any(|t| t.skip_children(node, ctx));
let depth_cap_hit = ctx.depth >= MAX_TREE_DEPTH && !node.children.is_empty();
if depth_cap_hit {
ctx.warnings.push(crate::Diagnostic::TreeDepthExceeded {
id: node.id.clone(),
max_depth: MAX_TREE_DEPTH,
});
}
if !transform_skip && !depth_cap_hit {
ctx.depth += 1;
let child_count = node.children.len();
for i in 0..child_count {
walk(&mut node.children[i], transforms, ctx);
}
ctx.depth -= 1;
}
for t in transforms.iter_mut().rev() {
t.exit(node, ctx);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::protocol::TreeNode;
fn node(id: &str, children: Vec<TreeNode>) -> TreeNode {
TreeNode {
id: id.to_string(),
type_name: "test".to_string(),
props: crate::protocol::Props::default(),
children,
}
}
struct Recorder {
name: &'static str,
trace: std::rc::Rc<std::cell::RefCell<Vec<String>>>,
}
impl TreeTransform for Recorder {
fn enter(&mut self, node: &mut TreeNode, _ctx: &mut WalkCtx) {
self.trace
.borrow_mut()
.push(format!("{}:enter:{}", self.name, node.id));
}
fn exit(&mut self, node: &mut TreeNode, _ctx: &mut WalkCtx) {
self.trace
.borrow_mut()
.push(format!("{}:exit:{}", self.name, node.id));
}
}
struct Pruner {
skip_id: &'static str,
}
impl TreeTransform for Pruner {
fn enter(&mut self, _node: &mut TreeNode, _ctx: &mut WalkCtx) {}
fn skip_children(&self, node: &TreeNode, _ctx: &WalkCtx) -> bool {
node.id == self.skip_id
}
}
#[test]
fn enter_is_called_in_slice_order() {
let trace = std::rc::Rc::new(std::cell::RefCell::new(Vec::new()));
let mut a = Recorder {
name: "A",
trace: trace.clone(),
};
let mut b = Recorder {
name: "B",
trace: trace.clone(),
};
let mut tree = node("root", vec![]);
let mut ctx = WalkCtx::default();
walk(&mut tree, &mut [&mut a, &mut b], &mut ctx);
let t = trace.borrow();
assert_eq!(t[0], "A:enter:root");
assert_eq!(t[1], "B:enter:root");
}
#[test]
fn exit_is_called_in_reverse_slice_order() {
let trace = std::rc::Rc::new(std::cell::RefCell::new(Vec::new()));
let mut a = Recorder {
name: "A",
trace: trace.clone(),
};
let mut b = Recorder {
name: "B",
trace: trace.clone(),
};
let mut tree = node("root", vec![]);
let mut ctx = WalkCtx::default();
walk(&mut tree, &mut [&mut a, &mut b], &mut ctx);
let t = trace.borrow();
assert_eq!(t[t.len() - 2], "B:exit:root");
assert_eq!(t[t.len() - 1], "A:exit:root");
}
#[test]
fn depth_first_traversal_enters_before_descending() {
let trace = std::rc::Rc::new(std::cell::RefCell::new(Vec::new()));
let mut rec = Recorder {
name: "R",
trace: trace.clone(),
};
let mut tree = node(
"root",
vec![node("a", vec![node("a1", vec![])]), node("b", vec![])],
);
let mut ctx = WalkCtx::default();
walk(&mut tree, &mut [&mut rec], &mut ctx);
let t = trace.borrow();
let expected = vec![
"R:enter:root",
"R:enter:a",
"R:enter:a1",
"R:exit:a1",
"R:exit:a",
"R:enter:b",
"R:exit:b",
"R:exit:root",
];
assert_eq!(*t, expected);
}
#[test]
fn skip_children_prunes_the_subtree_but_still_runs_exit() {
let trace = std::rc::Rc::new(std::cell::RefCell::new(Vec::new()));
let mut rec = Recorder {
name: "R",
trace: trace.clone(),
};
let mut pruner = Pruner { skip_id: "a" };
let mut tree = node(
"root",
vec![node("a", vec![node("a1", vec![])]), node("b", vec![])],
);
let mut ctx = WalkCtx::default();
walk(&mut tree, &mut [&mut rec, &mut pruner], &mut ctx);
let t = trace.borrow();
assert!(!t.iter().any(|line| line.contains("a1")));
assert!(t.contains(&"R:enter:a".to_string()));
assert!(t.contains(&"R:exit:a".to_string()));
assert!(t.contains(&"R:enter:b".to_string()));
}
#[test]
fn transforms_can_mutate_nodes_in_enter() {
struct Renamer;
impl TreeTransform for Renamer {
fn enter(&mut self, node: &mut TreeNode, _ctx: &mut WalkCtx) {
node.id = format!("x:{}", node.id);
}
}
let mut tree = node("root", vec![node("child", vec![])]);
let mut r = Renamer;
let mut ctx = WalkCtx::default();
walk(&mut tree, &mut [&mut r], &mut ctx);
assert_eq!(tree.id, "x:root");
assert_eq!(tree.children[0].id, "x:child");
}
#[test]
fn depth_cap_skips_subtree_with_diagnostic() {
let mut leaf = node("leaf", vec![]);
for i in 0..(MAX_TREE_DEPTH + 20) {
leaf = node(&format!("n{i}"), vec![leaf]);
}
let trace = std::rc::Rc::new(std::cell::RefCell::new(Vec::new()));
let mut rec = Recorder {
name: "R",
trace: trace.clone(),
};
let mut ctx = WalkCtx::default();
walk(&mut leaf, &mut [&mut rec], &mut ctx);
assert!(
ctx.warnings
.iter()
.any(|w| matches!(w.kind(), crate::DiagnosticKind::TreeDepthExceeded)),
"expected tree_depth_exceeded warning, got {:?}",
ctx.warnings,
);
assert!(
!trace.borrow().iter().any(|line| line == "R:enter:leaf"),
"walker descended past the cap"
);
}
#[test]
fn warnings_accumulate_across_transforms() {
struct Warner(&'static str);
impl TreeTransform for Warner {
fn enter(&mut self, node: &mut TreeNode, ctx: &mut WalkCtx) {
ctx.warnings.push(crate::Diagnostic::EmptyId {
type_name: format!("{}@{}", self.0, node.id),
});
}
}
let mut tree = node("root", vec![node("child", vec![])]);
let mut a = Warner("A");
let mut b = Warner("B");
let mut ctx = WalkCtx::default();
walk(&mut tree, &mut [&mut a, &mut b], &mut ctx);
assert_eq!(ctx.warnings.len(), 4);
let tags: Vec<&str> = ctx
.warnings
.iter()
.filter_map(|d| match d {
crate::Diagnostic::EmptyId { type_name } => Some(type_name.as_str()),
_ => None,
})
.collect();
assert!(tags.contains(&"A@root"));
assert!(tags.contains(&"B@root"));
assert!(tags.contains(&"A@child"));
assert!(tags.contains(&"B@child"));
}
}