use super::*;
use crate::cst::Kind;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Event {
Enter(Kind),
Token,
Exit(Kind),
}
#[derive(Default)]
struct Recorder {
events: Vec<Event>,
abort_on_enter: Option<Kind>,
stop_on_enter: Option<Kind>,
skip_on_enter: Option<Kind>,
abort_on_exit: Option<Kind>,
stop_on_exit: Option<Kind>,
skip_on_exit: Option<Kind>,
abort_on_token: bool,
stop_on_token: bool,
skip_on_token_in: Option<Kind>,
}
impl Recorder {
fn walk(mut self, text: &str) -> Vec<Event> {
let cst = Cst::parse(text);
assert!(cst.errors.is_empty(), "parse errors: {:#?}", cst.errors);
cst.walk(&mut self);
self.events
}
}
impl Visitor for Recorder {
fn enter_tree(&mut self, ctx: &VisitCtx<'_>, tree: NodeId) -> Visit {
let kind = ctx.node(tree).unwrap().kind;
self.events.push(Event::Enter(kind));
if self.abort_on_enter == Some(kind) {
return Visit::Abort;
}
if self.stop_on_enter == Some(kind) {
return Visit::Stop;
}
if self.skip_on_enter == Some(kind) {
return Visit::Skip;
}
Visit::Continue
}
fn exit_tree(&mut self, ctx: &VisitCtx<'_>, tree: NodeId) -> Visit {
let kind = ctx.node(tree).unwrap().kind;
self.events.push(Event::Exit(kind));
if self.abort_on_exit == Some(kind) {
return Visit::Abort;
}
if self.stop_on_exit == Some(kind) {
return Visit::Stop;
}
if self.skip_on_exit == Some(kind) {
return Visit::Skip;
}
Visit::Continue
}
fn visit_token(&mut self, ctx: &VisitCtx<'_>, _token: TokenId, parent: NodeId) -> Visit {
self.events.push(Event::Token);
if self.abort_on_token {
return Visit::Abort;
}
if self.stop_on_token {
return Visit::Stop;
}
if self.skip_on_token_in == Some(ctx.node(parent).unwrap().kind) {
return Visit::Skip;
}
Visit::Continue
}
}
const TEXT: &str = "a: list[u32] = { 1 2 }\nb: u32 = 3";
fn assert_balanced(events: &[Event]) {
let mut stack = Vec::new();
for event in events {
match event {
Event::Enter(kind) => stack.push(*kind),
Event::Exit(kind) => {
assert_eq!(stack.pop(), Some(*kind), "mismatched exit in {events:#?}")
}
Event::Token => {}
}
}
assert!(stack.is_empty(), "nodes never exited: {stack:?}");
}
fn count(events: &[Event], event: Event) -> usize {
events.iter().filter(|e| **e == event).count()
}
#[test]
fn full_walk_is_balanced() {
let events = Recorder::default().walk(TEXT);
assert_balanced(&events);
assert_eq!(events.first(), Some(&Event::Enter(Kind::File)));
assert_eq!(events.last(), Some(&Event::Exit(Kind::File)));
assert_eq!(count(&events, Event::Enter(Kind::Entry)), 2);
}
#[test]
fn stop_from_enter_exits_open_ancestors() {
let events = Recorder {
stop_on_enter: Some(Kind::EntryValue),
..Default::default()
}
.walk(TEXT);
assert_balanced(&events);
let stop_at = events
.iter()
.position(|e| *e == Event::Enter(Kind::EntryValue))
.unwrap();
assert!(
events[stop_at + 1..]
.iter()
.all(|e| matches!(e, Event::Exit(_))),
"walk continued after Stop: {events:#?}"
);
}
#[test]
fn stop_from_token_exits_open_ancestors() {
let events = Recorder {
stop_on_token: true,
..Default::default()
}
.walk(TEXT);
assert_balanced(&events);
assert_eq!(count(&events, Event::Token), 1);
}
#[test]
fn stop_from_exit_exits_open_ancestors() {
let events = Recorder {
stop_on_exit: Some(Kind::EntryKey),
..Default::default()
}
.walk(TEXT);
assert_balanced(&events);
let stop_at = events
.iter()
.position(|e| *e == Event::Exit(Kind::EntryKey))
.unwrap();
assert!(
events[stop_at + 1..]
.iter()
.all(|e| matches!(e, Event::Exit(_))),
"walk continued after Stop: {events:#?}"
);
}
#[test]
fn skip_from_enter_skips_children_but_still_exits() {
let events = Recorder {
skip_on_enter: Some(Kind::Entry),
..Default::default()
}
.walk(TEXT);
assert_balanced(&events);
assert_eq!(count(&events, Event::Enter(Kind::EntryKey)), 0);
assert_eq!(count(&events, Event::Enter(Kind::Entry)), 2);
assert_eq!(count(&events, Event::Exit(Kind::Entry)), 2);
}
#[test]
fn skip_from_token_skips_the_nodes_remaining_children() {
let events = Recorder {
skip_on_token_in: Some(Kind::Block),
..Default::default()
}
.walk(TEXT);
assert_balanced(&events);
assert_eq!(count(&events, Event::Enter(Kind::ListItem)), 0);
assert_eq!(count(&events, Event::Exit(Kind::Block)), 1);
}
#[test]
fn skip_from_exit_prunes_later_siblings() {
let events = Recorder {
skip_on_exit: Some(Kind::Entry),
..Default::default()
}
.walk(TEXT);
assert_balanced(&events);
assert_eq!(count(&events, Event::Enter(Kind::Entry)), 1);
assert_eq!(events.last(), Some(&Event::Exit(Kind::File)));
}
#[test]
fn pruning_is_local_and_the_walk_continues_elsewhere() {
let events = Recorder {
skip_on_exit: Some(Kind::ListItem),
..Default::default()
}
.walk(TEXT);
assert_balanced(&events);
assert_eq!(count(&events, Event::Enter(Kind::ListItem)), 1);
assert_eq!(count(&events, Event::Enter(Kind::Entry)), 2);
}
#[test]
fn abort_from_enter_runs_no_more_callbacks() {
let events = Recorder {
abort_on_enter: Some(Kind::EntryValue),
..Default::default()
}
.walk(TEXT);
assert_eq!(events.last(), Some(&Event::Enter(Kind::EntryValue)));
assert_eq!(count(&events, Event::Exit(Kind::Entry)), 0);
assert_eq!(count(&events, Event::Exit(Kind::File)), 0);
}
#[test]
fn abort_from_token_runs_no_more_callbacks() {
let events = Recorder {
abort_on_token: true,
..Default::default()
}
.walk(TEXT);
assert_eq!(count(&events, Event::Token), 1);
assert_eq!(events.last(), Some(&Event::Token));
}
#[test]
fn abort_from_exit_skips_the_remaining_exits() {
let events = Recorder {
abort_on_exit: Some(Kind::EntryKey),
..Default::default()
}
.walk(TEXT);
assert_eq!(events.last(), Some(&Event::Exit(Kind::EntryKey)));
assert_eq!(count(&events, Event::Exit(Kind::Entry)), 0);
assert_eq!(count(&events, Event::Exit(Kind::File)), 0);
}
#[test]
fn abort_from_exit_while_unwinding_skips_the_remaining_exits() {
let events = Recorder {
stop_on_enter: Some(Kind::EntryValue),
abort_on_exit: Some(Kind::Entry),
..Default::default()
}
.walk(TEXT);
assert_eq!(events.last(), Some(&Event::Exit(Kind::Entry)));
assert_eq!(count(&events, Event::Exit(Kind::File)), 0);
}
#[test]
fn walk_reports_how_it_ended() {
let cst = Cst::parse(TEXT);
assert_eq!(cst.walk(&mut Recorder::default()), WalkOutcome::Completed);
assert_eq!(
cst.walk(&mut Recorder {
stop_on_token: true,
..Default::default()
}),
WalkOutcome::Stopped
);
assert_eq!(
cst.walk(&mut Recorder {
abort_on_token: true,
..Default::default()
}),
WalkOutcome::Aborted
);
assert_eq!(
cst.walk(&mut Recorder {
skip_on_exit: Some(Kind::Entry),
..Default::default()
}),
WalkOutcome::Completed
);
}