#[cfg(test)]
mod tests {
use rowan::GreenNodeBuilder;
use crate::cst::SyntaxKind::{
ACCOUNT, BOM, COMMENT, DATE, DIRECTIVE, EMACS_DIRECTIVE, NEWLINE, OPEN_KW, PERCENT_COMMENT,
SHEBANG, SOURCE_FILE, WHITESPACE,
};
use crate::cst::SyntaxNode;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Element {
Tok(crate::cst::SyntaxKind),
Node(crate::cst::SyntaxKind),
}
fn elements_of(node: &SyntaxNode) -> Vec<Element> {
node.children_with_tokens()
.map(|el| match el {
rowan::NodeOrToken::Token(t) => Element::Tok(t.kind()),
rowan::NodeOrToken::Node(n) => Element::Node(n.kind()),
})
.collect()
}
fn tok_seq(kinds: &[crate::cst::SyntaxKind]) -> Vec<Element> {
kinds.iter().copied().map(Element::Tok).collect()
}
fn top_level_directives(root: &SyntaxNode) -> Vec<SyntaxNode> {
root.children().filter(|c| c.kind() == DIRECTIVE).collect()
}
fn build_open_directive(
b: &mut GreenNodeBuilder<'_>,
date: &str,
account: &str,
same_line_trailing: &[(crate::cst::SyntaxKind, &str)],
terminator: Option<&str>,
) {
b.start_node(DIRECTIVE.into());
b.token(DATE.into(), date);
b.token(WHITESPACE.into(), " ");
b.token(OPEN_KW.into(), "open");
b.token(WHITESPACE.into(), " ");
b.token(ACCOUNT.into(), account);
for (kind, text) in same_line_trailing {
b.token((*kind).into(), text);
}
if let Some(nl) = terminator {
b.token(NEWLINE.into(), nl);
}
b.finish_node();
}
fn build_open_directive_with_leading(
b: &mut GreenNodeBuilder<'_>,
leading: &[(crate::cst::SyntaxKind, &str)],
date: &str,
account: &str,
same_line_trailing: &[(crate::cst::SyntaxKind, &str)],
terminator: Option<&str>,
) {
b.start_node(DIRECTIVE.into());
for (kind, text) in leading {
b.token((*kind).into(), text);
}
b.token(DATE.into(), date);
b.token(WHITESPACE.into(), " ");
b.token(OPEN_KW.into(), "open");
b.token(WHITESPACE.into(), " ");
b.token(ACCOUNT.into(), account);
for (kind, text) in same_line_trailing {
b.token((*kind).into(), text);
}
if let Some(nl) = terminator {
b.token(NEWLINE.into(), nl);
}
b.finish_node();
}
#[test]
fn rule_1_same_line_trailing_inside_preceding_directive() {
let source = "2024-01-01 open Assets:Cash ; EOL comment\n\
2024-01-02 open Assets:Bank";
let mut b = GreenNodeBuilder::new();
b.start_node(SOURCE_FILE.into());
build_open_directive(
&mut b,
"2024-01-01",
"Assets:Cash",
&[(WHITESPACE, " "), (COMMENT, "; EOL comment")],
Some("\n"),
);
build_open_directive(&mut b, "2024-01-02", "Assets:Bank", &[], None);
b.finish_node();
let tree = SyntaxNode::new_root(b.finish());
assert_eq!(tree.text().to_string(), source);
let directives = top_level_directives(&tree);
assert_eq!(directives.len(), 2);
assert_eq!(
elements_of(&directives[0]),
tok_seq(&[
DATE, WHITESPACE, OPEN_KW, WHITESPACE, ACCOUNT, WHITESPACE, COMMENT, NEWLINE,
]),
"rule 1: d1 owns its same-line trailing + terminator NEWLINE",
);
assert_eq!(
elements_of(&directives[1]),
tok_seq(&[DATE, WHITESPACE, OPEN_KW, WHITESPACE, ACCOUNT]),
"d2 has no leading trivia (none exists between d1 terminator and d2 first content)",
);
assert_eq!(
elements_of(&tree),
vec![Element::Node(DIRECTIVE), Element::Node(DIRECTIVE)],
"SOURCE_FILE owns exactly the two directives — no trivia leaks",
);
}
#[test]
fn rule_2_blank_line_leads_following_directive() {
let source = "2024-01-01 open Assets:Cash\n\
\n\
2024-01-02 open Assets:Bank\n";
let mut b = GreenNodeBuilder::new();
b.start_node(SOURCE_FILE.into());
build_open_directive(&mut b, "2024-01-01", "Assets:Cash", &[], Some("\n"));
build_open_directive_with_leading(
&mut b,
&[(NEWLINE, "\n")], "2024-01-02",
"Assets:Bank",
&[],
Some("\n"),
);
b.finish_node();
let tree = SyntaxNode::new_root(b.finish());
assert_eq!(tree.text().to_string(), source);
let directives = top_level_directives(&tree);
assert_eq!(directives.len(), 2);
assert_eq!(
elements_of(&directives[0]),
tok_seq(&[DATE, WHITESPACE, OPEN_KW, WHITESPACE, ACCOUNT, NEWLINE]),
"rule 1: d1 owns its terminator NEWLINE",
);
assert_eq!(
elements_of(&directives[1]),
tok_seq(&[
NEWLINE, DATE, WHITESPACE, OPEN_KW, WHITESPACE, ACCOUNT, NEWLINE
]),
"rule 2: blank line leads d2; rule 1: d2 owns its terminator NEWLINE",
);
}
#[test]
fn rule_3_copyright_header_under_source_file() {
let source = ";; Copyright 2024\n\
;; All rights reserved\n\
2024-01-01 open Assets:Cash\n";
let mut b = GreenNodeBuilder::new();
b.start_node(SOURCE_FILE.into());
b.token(COMMENT.into(), ";; Copyright 2024");
b.token(NEWLINE.into(), "\n");
b.token(COMMENT.into(), ";; All rights reserved");
b.token(NEWLINE.into(), "\n");
build_open_directive(&mut b, "2024-01-01", "Assets:Cash", &[], Some("\n"));
b.finish_node();
let tree = SyntaxNode::new_root(b.finish());
assert_eq!(tree.text().to_string(), source);
assert_eq!(
elements_of(&tree),
vec![
Element::Tok(COMMENT),
Element::Tok(NEWLINE),
Element::Tok(COMMENT),
Element::Tok(NEWLINE),
Element::Node(DIRECTIVE),
],
"rule 3: copyright header is direct under SOURCE_FILE; directive follows",
);
let directives = top_level_directives(&tree);
assert_eq!(
elements_of(&directives[0]),
tok_seq(&[DATE, WHITESPACE, OPEN_KW, WHITESPACE, ACCOUNT, NEWLINE]),
"d1 has no leading trivia (header is under SOURCE_FILE) and owns its terminator",
);
}
#[test]
fn rule_3_bom_and_shebang_under_source_file() {
let source = "\u{FEFF}#!/usr/bin/env bean-check\n\
2024-01-01 open Assets:Cash\n";
let mut b = GreenNodeBuilder::new();
b.start_node(SOURCE_FILE.into());
b.token(BOM.into(), "\u{FEFF}");
b.token(SHEBANG.into(), "#!/usr/bin/env bean-check");
b.token(NEWLINE.into(), "\n");
build_open_directive(&mut b, "2024-01-01", "Assets:Cash", &[], Some("\n"));
b.finish_node();
let tree = SyntaxNode::new_root(b.finish());
assert_eq!(tree.text().to_string(), source);
assert_eq!(
elements_of(&tree),
vec![
Element::Tok(BOM),
Element::Tok(SHEBANG),
Element::Tok(NEWLINE),
Element::Node(DIRECTIVE),
],
);
}
#[test]
fn rule_4_trailing_comment_block_under_source_file() {
let source = "2024-01-01 open Assets:Cash\n\
;; closing remarks\n";
let mut b = GreenNodeBuilder::new();
b.start_node(SOURCE_FILE.into());
build_open_directive(&mut b, "2024-01-01", "Assets:Cash", &[], Some("\n"));
b.token(COMMENT.into(), ";; closing remarks");
b.token(NEWLINE.into(), "\n");
b.finish_node();
let tree = SyntaxNode::new_root(b.finish());
assert_eq!(tree.text().to_string(), source);
assert_eq!(
elements_of(&tree),
vec![
Element::Node(DIRECTIVE),
Element::Tok(COMMENT),
Element::Tok(NEWLINE),
],
"rule 4: closing remarks are direct under SOURCE_FILE, NOT inside d1",
);
let directives = top_level_directives(&tree);
assert_eq!(
elements_of(&directives[0]),
tok_seq(&[DATE, WHITESPACE, OPEN_KW, WHITESPACE, ACCOUNT, NEWLINE]),
"d1 owns its terminator but NOT the closing remarks",
);
}
#[test]
fn rule_5_unterminated_final_directive() {
let source = "2024-01-01 open Assets:Cash";
let mut b = GreenNodeBuilder::new();
b.start_node(SOURCE_FILE.into());
build_open_directive(&mut b, "2024-01-01", "Assets:Cash", &[], None);
b.finish_node();
let tree = SyntaxNode::new_root(b.finish());
assert_eq!(tree.text().to_string(), source);
assert_eq!(elements_of(&tree), vec![Element::Node(DIRECTIVE)],);
let directives = top_level_directives(&tree);
assert_eq!(
elements_of(&directives[0]),
tok_seq(&[DATE, WHITESPACE, OPEN_KW, WHITESPACE, ACCOUNT]),
"rule 5: no terminator means directive range ends at last content",
);
}
#[test]
fn rule_1_plus_rule_5_unterminated_directive_with_same_line_trailing() {
let source = "2024-01-01 open Assets:Cash ; eol-no-nl";
let mut b = GreenNodeBuilder::new();
b.start_node(SOURCE_FILE.into());
build_open_directive(
&mut b,
"2024-01-01",
"Assets:Cash",
&[(WHITESPACE, " "), (COMMENT, "; eol-no-nl")],
None,
);
b.finish_node();
let tree = SyntaxNode::new_root(b.finish());
assert_eq!(tree.text().to_string(), source);
assert_eq!(elements_of(&tree), vec![Element::Node(DIRECTIVE)]);
let directives = top_level_directives(&tree);
assert_eq!(
elements_of(&directives[0]),
tok_seq(&[
DATE, WHITESPACE, OPEN_KW, WHITESPACE, ACCOUNT, WHITESPACE, COMMENT,
]),
"rules 1+5: same-line trailing trivia stays INSIDE the directive \
even when there's no terminator NEWLINE",
);
}
#[test]
fn percent_comment_obeys_directive_terminator_rule() {
let source = "2024-01-01 open Assets:Cash % EOL\n\
2024-01-02 open Assets:Bank";
let mut b = GreenNodeBuilder::new();
b.start_node(SOURCE_FILE.into());
build_open_directive(
&mut b,
"2024-01-01",
"Assets:Cash",
&[(WHITESPACE, " "), (PERCENT_COMMENT, "% EOL")],
Some("\n"),
);
build_open_directive(&mut b, "2024-01-02", "Assets:Bank", &[], None);
b.finish_node();
let tree = SyntaxNode::new_root(b.finish());
assert_eq!(tree.text().to_string(), source);
let directives = top_level_directives(&tree);
assert_eq!(
elements_of(&directives[0]),
tok_seq(&[
DATE,
WHITESPACE,
OPEN_KW,
WHITESPACE,
ACCOUNT,
WHITESPACE,
PERCENT_COMMENT,
NEWLINE,
]),
"PERCENT_COMMENT obeys rule 1 the same as COMMENT",
);
}
#[test]
fn emacs_directive_obeys_file_leading_rule() {
let source = "#+OPTIONS toc:nil\n\
2024-01-01 open Assets:Cash\n";
let mut b = GreenNodeBuilder::new();
b.start_node(SOURCE_FILE.into());
b.token(EMACS_DIRECTIVE.into(), "#+OPTIONS toc:nil");
b.token(NEWLINE.into(), "\n");
build_open_directive(&mut b, "2024-01-01", "Assets:Cash", &[], Some("\n"));
b.finish_node();
let tree = SyntaxNode::new_root(b.finish());
assert_eq!(tree.text().to_string(), source);
assert_eq!(
elements_of(&tree),
vec![
Element::Tok(EMACS_DIRECTIVE),
Element::Tok(NEWLINE),
Element::Node(DIRECTIVE),
],
"rule 3: EMACS_DIRECTIVE before any content is under SOURCE_FILE",
);
}
#[test]
fn adjacent_directives_no_blank_line() {
let source = "2024-01-01 open Assets:Cash\n\
2024-01-02 open Assets:Bank\n";
let mut b = GreenNodeBuilder::new();
b.start_node(SOURCE_FILE.into());
build_open_directive(&mut b, "2024-01-01", "Assets:Cash", &[], Some("\n"));
build_open_directive(&mut b, "2024-01-02", "Assets:Bank", &[], Some("\n"));
b.finish_node();
let tree = SyntaxNode::new_root(b.finish());
assert_eq!(tree.text().to_string(), source);
let directives = top_level_directives(&tree);
assert_eq!(directives.len(), 2);
assert_eq!(
elements_of(&directives[0]),
tok_seq(&[DATE, WHITESPACE, OPEN_KW, WHITESPACE, ACCOUNT, NEWLINE]),
);
assert_eq!(
elements_of(&directives[1]),
tok_seq(&[DATE, WHITESPACE, OPEN_KW, WHITESPACE, ACCOUNT, NEWLINE]),
"Two adjacent directives have IDENTICAL child shape — full symmetry",
);
}
#[test]
fn file_with_only_trivia() {
let source = ";; only a comment\n\n";
let mut b = GreenNodeBuilder::new();
b.start_node(SOURCE_FILE.into());
b.token(COMMENT.into(), ";; only a comment");
b.token(NEWLINE.into(), "\n");
b.token(NEWLINE.into(), "\n");
b.finish_node();
let tree = SyntaxNode::new_root(b.finish());
assert_eq!(tree.text().to_string(), source);
assert!(top_level_directives(&tree).is_empty());
assert_eq!(
elements_of(&tree),
vec![
Element::Tok(COMMENT),
Element::Tok(NEWLINE),
Element::Tok(NEWLINE),
],
);
}
#[test]
fn empty_file() {
let mut b = GreenNodeBuilder::new();
b.start_node(SOURCE_FILE.into());
b.finish_node();
let tree = SyntaxNode::new_root(b.finish());
assert_eq!(tree.text().to_string(), "");
assert!(top_level_directives(&tree).is_empty());
assert!(elements_of(&tree).is_empty());
}
#[test]
fn all_rules_combined() {
let source = ";; copyright\n\
2024-01-01 open Assets:Cash ; eol1\n\
\n\
2024-01-02 open Assets:Bank\n\
;; footer\n";
let mut b = GreenNodeBuilder::new();
b.start_node(SOURCE_FILE.into());
b.token(COMMENT.into(), ";; copyright");
b.token(NEWLINE.into(), "\n");
build_open_directive(
&mut b,
"2024-01-01",
"Assets:Cash",
&[(WHITESPACE, " "), (COMMENT, "; eol1")],
Some("\n"),
);
build_open_directive_with_leading(
&mut b,
&[(NEWLINE, "\n")],
"2024-01-02",
"Assets:Bank",
&[],
Some("\n"),
);
b.token(COMMENT.into(), ";; footer");
b.token(NEWLINE.into(), "\n");
b.finish_node();
let tree = SyntaxNode::new_root(b.finish());
assert_eq!(tree.text().to_string(), source);
assert_eq!(
elements_of(&tree),
vec![
Element::Tok(COMMENT),
Element::Tok(NEWLINE),
Element::Node(DIRECTIVE),
Element::Node(DIRECTIVE),
Element::Tok(COMMENT),
Element::Tok(NEWLINE),
],
"SOURCE_FILE owns file-leading copyright + 2 directives + file-trailing footer",
);
let directives = top_level_directives(&tree);
assert_eq!(directives.len(), 2);
assert_eq!(
elements_of(&directives[0]),
tok_seq(&[
DATE, WHITESPACE, OPEN_KW, WHITESPACE, ACCOUNT, WHITESPACE, COMMENT, NEWLINE,
]),
"d1: rule 1 (same-line + terminator)",
);
assert_eq!(
elements_of(&directives[1]),
tok_seq(&[
NEWLINE, DATE, WHITESPACE, OPEN_KW, WHITESPACE, ACCOUNT, NEWLINE,
]),
"d2: rule 2 leading + rule 1 terminator",
);
}
}