use crate::ast::{self, AstNode};
use crate::parser::tests::cst::{ExpectedNode, assert_equivalent};
use crate::{SyntaxKind, SyntaxNode, parse};
fn collect_text_nodes(node: &SyntaxNode) -> Vec<String> {
let mut out = Vec::new();
if node.kind() == SyntaxKind::TEXT {
out.push(node.text().to_string());
}
for child in node.children() {
out.extend(collect_text_nodes(&child));
}
out
}
fn collect_author_warnings(node: &SyntaxNode) -> Vec<String> {
let mut out = Vec::new();
if let Some(warning) = ast::AuthorWarning::cast(node.clone()) {
out.push(warning.text());
}
for child in node.children() {
out.extend(collect_author_warnings(&child));
}
out
}
fn find_knot_named(node: &SyntaxNode, name: &str) -> Option<ast::KnotDef> {
if let Some(knot) = ast::KnotDef::cast(node.clone())
&& knot.header().and_then(|h| h.name()).as_deref() == Some(name)
{
return Some(knot);
}
node.children()
.find_map(|child| find_knot_named(&child, name))
}
#[test]
fn todo_in_then_arm() {
let src = "{\n- x:\n Then branch.\n TODO: inside then branch\n- else:\n Else branch.\n}\n";
let parsed = parse(src);
assert!(parsed.errors().is_empty(), "errors: {:?}", parsed.errors());
assert_equivalent(
parse(src),
cst!(SOURCE_FILE {
MULTILINE_BLOCK {
MULTILINE_BRANCHES_COND {
MULTILINE_BRANCH_COND {
PATH
MULTILINE_BRANCH_BODY {
TEXT
AUTHOR_WARNING
}
}
MULTILINE_BRANCH_COND {
MULTILINE_BRANCH_BODY {
TEXT
}
}
}
}
}),
);
let root = parsed.syntax();
assert_eq!(
collect_author_warnings(&root),
vec!["inside then branch".to_owned()]
);
for text in collect_text_nodes(&root) {
assert!(!text.contains("TODO"), "TODO leaked into TEXT: {text:?}");
}
}
#[test]
fn todo_in_else_arm() {
let src = "{ x:\n Then branch.\n TODO: inside then branch\n- else:\n TODO: inside else branch\n}\n";
let parsed = parse(src);
assert!(parsed.errors().is_empty(), "errors: {:?}", parsed.errors());
let root = parsed.syntax();
assert_eq!(
collect_author_warnings(&root),
vec![
"inside then branch".to_owned(),
"inside else branch".to_owned()
]
);
for text in collect_text_nodes(&root) {
assert!(!text.contains("TODO"), "TODO leaked into TEXT: {text:?}");
}
assert_equivalent(
parse(src),
cst!(SOURCE_FILE {
CONTENT_LINE {
MIXED_CONTENT {
INLINE_LOGIC {
CONDITIONAL_WITH_EXPR {
PATH
BRANCHLESS_COND_BODY {
TEXT
AUTHOR_WARNING
ELSE_BRANCH {
MULTILINE_BRANCH_COND {
MULTILINE_BRANCH_BODY {
AUTHOR_WARNING
}
}
}
}
}
}
}
}
}),
);
}
#[test]
fn todo_in_nested_block() {
let src = "{ x:\n { y:\n Nested then.\n TODO: inside nested then\n - else:\n TODO: inside nested else\n }\n- else:\n Outer else.\n}\n";
let parsed = parse(src);
assert!(parsed.errors().is_empty(), "errors: {:?}", parsed.errors());
let root = parsed.syntax();
assert_eq!(
collect_author_warnings(&root),
vec![
"inside nested then".to_owned(),
"inside nested else".to_owned()
]
);
for text in collect_text_nodes(&root) {
assert!(!text.contains("TODO"), "TODO leaked into TEXT: {text:?}");
}
}
#[test]
fn todo_tag_spelling_in_branch() {
let src = "{ x:\n TODO(TAG) — tagged note\n}\n";
let parsed = parse(src);
assert!(parsed.errors().is_empty(), "errors: {:?}", parsed.errors());
let root = parsed.syntax();
let warnings = collect_author_warnings(&root);
assert_eq!(warnings.len(), 1, "warnings: {warnings:?}");
assert!(
warnings[0].contains("tagged note"),
"warning text: {:?}",
warnings[0]
);
for text in collect_text_nodes(&root) {
assert!(!text.contains("TODO"), "TODO leaked into TEXT: {text:?}");
}
}
#[test]
fn todo_indented_spelling_in_else_arm() {
let src = "{ x:\n Then.\n- else:\n TODO: deeply indented\n}\n";
let parsed = parse(src);
assert!(parsed.errors().is_empty(), "errors: {:?}", parsed.errors());
let root = parsed.syntax();
assert_eq!(
collect_author_warnings(&root),
vec!["deeply indented".to_owned()]
);
for text in collect_text_nodes(&root) {
assert!(!text.contains("TODO"), "TODO leaked into TEXT: {text:?}");
}
}
#[test]
fn todo_only_branchless_body() {
let src = "{ x:\n TODO: only line\n}\n";
let parsed = parse(src);
assert!(parsed.errors().is_empty(), "errors: {:?}", parsed.errors());
assert_equivalent(
parse(src),
cst!(SOURCE_FILE {
CONTENT_LINE {
MIXED_CONTENT {
INLINE_LOGIC {
CONDITIONAL_WITH_EXPR {
PATH
BRANCHLESS_COND_BODY {
AUTHOR_WARNING
}
}
}
}
}
}),
);
}
#[test]
fn todo_with_brace_on_same_line_closes_block_branchless() {
let src =
"VAR x = true\n{ x:\n TODO: fix }\n}\nPlain line.\n=== later ===\nKnot body.\n-> DONE\n";
let parsed = parse(src);
let root = parsed.syntax();
assert_eq!(
collect_author_warnings(&root),
vec!["fix".to_owned()],
"the closing brace must not be absorbed into the note text"
);
let later = find_knot_named(&root, "later");
assert!(
later.is_some(),
"`=== later ===` must lower to a real KNOT_DEF, not conditional prose; tree: {root:#?}"
);
}
#[test]
fn todo_with_brace_on_same_line_closes_block_multiline() {
let src = "VAR x = true\n{\n- x:\n TODO: fix }\n}\nPlain line.\n=== later ===\nKnot body.\n-> DONE\n";
let parsed = parse(src);
let root = parsed.syntax();
assert_eq!(
collect_author_warnings(&root),
vec!["fix".to_owned()],
"the closing brace must not be absorbed into the note text"
);
let later = find_knot_named(&root, "later");
assert!(
later.is_some(),
"`=== later ===` must lower to a real KNOT_DEF, not conditional prose; tree: {root:#?}"
);
}
#[test]
fn todo_mid_line_after_inline_logic_is_not_misfired() {
let src = "{\n- x:\n Value is {y} TODO fix this later\n}\n";
let parsed = parse(src);
let root = parsed.syntax();
assert!(
collect_author_warnings(&root).is_empty(),
"a mid-line TODO must not become an AUTHOR_WARNING; tree: {root:#?}"
);
let joined = collect_text_nodes(&root).concat();
assert!(
joined.contains("TODO fix this later"),
"prose must survive intact: {joined:?}"
);
}
#[test]
fn todo_mid_line_after_glue_is_not_misfired() {
let src = "{\n- x:\n Hello <> TODO not a note\n}\n";
let parsed = parse(src);
let root = parsed.syntax();
assert!(
collect_author_warnings(&root).is_empty(),
"a mid-line TODO must not become an AUTHOR_WARNING; tree: {root:#?}"
);
let joined = collect_text_nodes(&root).concat();
assert!(
joined.contains("TODO not a note"),
"prose must survive intact: {joined:?}"
);
}
#[test]
fn todo_mid_line_after_escape_is_not_misfired() {
let src = "{\n- x:\n Hello \\* TODO not a note\n}\n";
let parsed = parse(src);
let root = parsed.syntax();
assert!(
collect_author_warnings(&root).is_empty(),
"a mid-line TODO must not become an AUTHOR_WARNING; tree: {root:#?}"
);
let joined = collect_text_nodes(&root).concat();
assert!(
joined.contains("TODO not a note"),
"prose must survive intact: {joined:?}"
);
}