use super::*;
#[test]
fn space_after_glue_marker_survives_inside_the_text_node() {
let src = "flow f() {\n <> But surely.\n}\n";
let p = assert_lossless(src);
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
let line = p
.syntax()
.descendants()
.find(|n| n.kind() == SyntaxKind::CONTENT_LINE)
.expect("CONTENT_LINE");
assert!(has_node_kind(&line, SyntaxKind::GLUE_NODE));
assert_eq!(
text_run_concat(&line),
" But surely.",
"space after `<>` must be folded into the following TEXT node"
);
}
#[test]
fn interior_prose_whitespace_between_words_is_unchanged() {
let src = "flow f() {\n You have three gold coins.\n}\n";
let p = assert_lossless(src);
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
let line = p
.syntax()
.descendants()
.find(|n| n.kind() == SyntaxKind::CONTENT_LINE)
.expect("CONTENT_LINE");
assert_eq!(text_run_concat(&line), "You have three gold coins.");
}
#[test]
fn labeled_content_line_produces_a_label_node() {
let src = "flow f() {\n (start) You arrive at the garden.\n}\n";
let p = assert_lossless(src);
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
let content_line = p
.syntax()
.descendants()
.find(|n| n.kind() == SyntaxKind::CONTENT_LINE)
.expect("CONTENT_LINE");
assert!(has_node_kind(&content_line, SyntaxKind::LABEL));
assert!(has_node_kind(&content_line, SyntaxKind::TEXT));
}
#[test]
fn labeled_content_line_as_backward_loop_divert_target() {
let src = concat!(
"flow loop() {\n",
" (start) You spin around.\n",
" {?\n",
" * [Again] -> start\n",
" * [Stop] -> END\n",
" }\n",
"}\n",
);
let p = assert_lossless(src);
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
assert!(has_node_kind(&p.syntax(), SyntaxKind::LABEL));
assert_eq!(count_node_kind(&p.syntax(), SyntaxKind::DIVERT_STMT), 2);
}
#[test]
fn unlabeled_prose_starting_with_paren_is_unaffected() {
let src = "flow f() {\n (a very long aside) continues here.\n}\n";
let p = assert_lossless(src);
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
assert!(!has_node_kind(&p.syntax(), SyntaxKind::LABEL));
}
#[test]
fn label_inside_conditional_body_is_still_a_content_line_label() {
let src = "flow f() {\n {if hp > 0: (alive) You live.}\n}\n";
let p = assert_lossless(src);
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
assert!(has_node_kind(&p.syntax(), SyntaxKind::LABEL));
}
#[test]
fn single_word_is_one_text_node() {
let p = assert_lossless("Hello\n");
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
assert_eq!(count_node_kind(&p.syntax(), SyntaxKind::TEXT), 1);
}
#[test]
fn multiple_words_aggregate_into_one_text_node() {
let p = assert_lossless("The quick brown fox\n");
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
assert_eq!(count_node_kind(&p.syntax(), SyntaxKind::TEXT), 1);
assert_eq!(text_run_concat(&p.syntax()), "The quick brown fox");
}
#[test]
fn punctuation_characters_stay_plain_text() {
for src in [
"Hello.\n",
"Hello, world\n",
"It works!\n",
"How are you?\n",
"Name: Bob\n",
"A; B\n",
"Hello (world)\n",
"She said \"hello\"\n",
"Player 1\n",
"A = B\n",
"50% chance\n",
"a & b\n",
"x * y\n",
"1 + 1\n",
] {
let p = assert_lossless(src);
assert!(
p.errors().is_empty(),
"src {src:?} errors: {:?}",
p.errors()
);
assert_eq!(
count_node_kind(&p.syntax(), SyntaxKind::TEXT),
1,
"src {src:?} should be a single TEXT run"
);
}
}
#[test]
fn text_at_eof_without_trailing_newline() {
let p = assert_lossless("Hello");
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
assert!(has_node_kind(&p.syntax(), SyntaxKind::CONTENT_LINE));
assert_eq!(text_run_concat(&p.syntax()), "Hello");
}
#[test]
fn consecutive_content_lines_are_separate_content_line_nodes() {
let p = assert_lossless("Line one.\nLine two.\n");
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
assert_eq!(count_node_kind(&p.syntax(), SyntaxKind::CONTENT_LINE), 2);
}
#[test]
fn glue_between_two_text_runs() {
let p = assert_lossless("a<>b\n");
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
assert!(has_node_kind(&p.syntax(), SyntaxKind::GLUE_NODE));
assert_eq!(count_node_kind(&p.syntax(), SyntaxKind::TEXT), 2);
}
#[test]
fn glue_at_line_start() {
let p = assert_lossless("<>continued\n");
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
let line = p
.syntax()
.descendants()
.find(|n| n.kind() == SyntaxKind::CONTENT_LINE)
.expect("CONTENT_LINE");
let first_child_kind = line.children().next().map(|c| c.kind());
assert_eq!(first_child_kind, Some(SyntaxKind::GLUE_NODE));
}
#[test]
fn glue_at_line_end_before_newline() {
let p = assert_lossless("text<>\n");
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
assert!(has_node_kind(&p.syntax(), SyntaxKind::GLUE_NODE));
assert_eq!(count_node_kind(&p.syntax(), SyntaxKind::TEXT), 1);
}
#[test]
fn multiple_glue_operators_in_one_line() {
let p = assert_lossless("a<>b<>c\n");
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
assert_eq!(count_node_kind(&p.syntax(), SyntaxKind::GLUE_NODE), 2);
assert_eq!(count_node_kind(&p.syntax(), SyntaxKind::TEXT), 3);
}
#[test]
fn consecutive_glue_operators_with_no_text_between() {
let p = assert_lossless("<><>\n");
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
assert_eq!(count_node_kind(&p.syntax(), SyntaxKind::GLUE_NODE), 2);
assert_eq!(count_node_kind(&p.syntax(), SyntaxKind::TEXT), 0);
}
#[test]
fn glue_only_line_has_no_text_node() {
let p = assert_lossless("<>\n");
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
assert!(has_node_kind(&p.syntax(), SyntaxKind::GLUE_NODE));
assert_eq!(count_node_kind(&p.syntax(), SyntaxKind::TEXT), 0);
}
#[test]
fn glue_before_a_divert() {
let src = "text<> -> knot\n";
let p = assert_lossless(src);
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
assert!(has_node_kind(&p.syntax(), SyntaxKind::GLUE_NODE));
assert_eq!(count_node_kind(&p.syntax(), SyntaxKind::DIVERT_STMT), 1);
}
#[test]
fn lone_angle_bracket_without_its_pair_is_plain_text() {
let p = assert_lossless("a < b\n");
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
assert!(!has_node_kind(&p.syntax(), SyntaxKind::GLUE_NODE));
assert_eq!(text_run_concat(&p.syntax()), "a < b");
}
#[test]
fn interpolation_wraps_an_integer_literal() {
let p = assert_lossless("Roll: {12}\n");
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
assert!(has_node_kind(&p.syntax(), SyntaxKind::INTERPOLATION));
assert!(has_node_kind(&p.syntax(), SyntaxKind::INTEGER_LIT));
}
#[test]
fn interpolation_wraps_a_float_literal() {
let p = assert_lossless("Roll: {3.5}\n");
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
assert!(has_node_kind(&p.syntax(), SyntaxKind::INTERPOLATION));
assert!(has_node_kind(&p.syntax(), SyntaxKind::FLOAT_LIT));
}
#[test]
fn interpolation_wraps_a_string_literal() {
let p = assert_lossless("Say: {\"hi\"}\n");
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
assert!(has_node_kind(&p.syntax(), SyntaxKind::INTERPOLATION));
assert!(has_node_kind(&p.syntax(), SyntaxKind::STRING_LIT));
}
#[test]
fn interpolation_wraps_boolean_true() {
let p = assert_lossless("Flag: {true}\n");
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
assert!(has_node_kind(&p.syntax(), SyntaxKind::INTERPOLATION));
assert!(has_node_kind(&p.syntax(), SyntaxKind::BOOLEAN_LIT));
}
#[test]
fn interpolation_wraps_boolean_false() {
let p = assert_lossless("Flag: {false}\n");
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
assert!(has_node_kind(&p.syntax(), SyntaxKind::INTERPOLATION));
assert!(has_node_kind(&p.syntax(), SyntaxKind::BOOLEAN_LIT));
}
#[test]
fn interpolation_wraps_a_bare_path() {
let p = assert_lossless("Hello {name}\n");
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
assert!(has_node_kind(&p.syntax(), SyntaxKind::INTERPOLATION));
assert!(has_node_kind(&p.syntax(), SyntaxKind::PATH_EXPR));
}
#[test]
fn interpolation_wraps_a_dotted_path() {
let p = assert_lossless("HP: {player.stats.hp}\n");
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
assert!(has_node_kind(&p.syntax(), SyntaxKind::INTERPOLATION));
assert_eq!(count_node_kind(&p.syntax(), SyntaxKind::PATH_SEGMENT), 3);
}
#[test]
fn interpolation_wraps_a_paren_expr() {
let p = assert_lossless("Sum: {(1 + 2)}\n");
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
assert!(has_node_kind(&p.syntax(), SyntaxKind::INTERPOLATION));
assert!(has_node_kind(&p.syntax(), SyntaxKind::PAREN_EXPR));
}
#[test]
fn interpolation_wraps_a_prefix_minus_expr() {
let p = assert_lossless("Delta: {-x}\n");
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
assert!(has_node_kind(&p.syntax(), SyntaxKind::INTERPOLATION));
assert!(has_node_kind(&p.syntax(), SyntaxKind::PREFIX_EXPR));
}
#[test]
fn interpolation_wraps_an_infix_expr() {
let p = assert_lossless("Roll: {1 + 2}\n");
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
assert!(has_node_kind(&p.syntax(), SyntaxKind::INTERPOLATION));
assert!(has_node_kind(&p.syntax(), SyntaxKind::INFIX_EXPR));
}
#[test]
fn interpolation_wraps_a_call_expr() {
let p = assert_lossless("Result: {roll(1, 6)}\n");
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
assert!(has_node_kind(&p.syntax(), SyntaxKind::INTERPOLATION));
assert!(has_node_kind(&p.syntax(), SyntaxKind::CALL_EXPR));
}
#[test]
fn interpolation_at_start_of_content_line() {
let p = assert_lossless("{name} arrives.\n");
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
let line = p
.syntax()
.descendants()
.find(|n| n.kind() == SyntaxKind::CONTENT_LINE)
.expect("CONTENT_LINE");
let first_child_kind = line.children().next().map(|c| c.kind());
assert_eq!(first_child_kind, Some(SyntaxKind::INTERPOLATION));
}
#[test]
fn interpolation_at_end_of_content_line() {
let p = assert_lossless("You are {name}\n");
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
assert_eq!(count_node_kind(&p.syntax(), SyntaxKind::INTERPOLATION), 1);
assert_eq!(count_node_kind(&p.syntax(), SyntaxKind::TEXT), 1);
}
#[test]
fn interpolation_between_two_text_runs() {
let p = assert_lossless("before {x} after\n");
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
assert_eq!(count_node_kind(&p.syntax(), SyntaxKind::TEXT), 2);
assert_eq!(count_node_kind(&p.syntax(), SyntaxKind::INTERPOLATION), 1);
}
#[test]
fn multiple_interpolations_in_one_line() {
let p = assert_lossless("{a} and {b}\n");
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
assert_eq!(count_node_kind(&p.syntax(), SyntaxKind::INTERPOLATION), 2);
}
#[test]
fn whitespace_between_two_interpolations_survives_inside_a_text_node() {
let p = assert_lossless("{a} {b}\n");
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
assert_eq!(count_node_kind(&p.syntax(), SyntaxKind::INTERPOLATION), 2);
assert_eq!(
text_run_concat(&p.syntax()),
" ",
"the separating space must live inside a TEXT node so content lowering keeps it"
);
let line = p
.syntax()
.descendants()
.find(|n| n.kind() == SyntaxKind::CONTENT_LINE)
.expect("CONTENT_LINE");
let has_bare_whitespace_child = line
.children_with_tokens()
.any(|c| c.kind() == SyntaxKind::WHITESPACE);
assert!(
!has_bare_whitespace_child,
"the space must not show up as a bare WHITESPACE token outside any node"
);
}
#[test]
fn interpolation_flanked_by_glue() {
let p = assert_lossless("<>{x}<>\n");
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
assert_eq!(count_node_kind(&p.syntax(), SyntaxKind::GLUE_NODE), 2);
assert_eq!(count_node_kind(&p.syntax(), SyntaxKind::INTERPOLATION), 1);
}
#[test]
fn alternation_once_marker_wins_over_prefix_not_expression() {
let p = assert_lossless("Not: {!x}\n");
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
assert!(
has_node_kind(&p.syntax(), SyntaxKind::ALTERNATION_BLOCK),
"ruled behavior: {{!x}} is claimed by the alternation family"
);
assert!(!has_node_kind(&p.syntax(), SyntaxKind::INTERPOLATION));
assert!(!has_node_kind(&p.syntax(), SyntaxKind::PREFIX_EXPR));
}
#[test]
fn paren_escaped_prefix_not_expression_reaches_interpolation_despite_alternation_marker_win() {
let p = assert_lossless("Not: {(!x)}\n");
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
assert!(has_node_kind(&p.syntax(), SyntaxKind::INTERPOLATION));
assert!(has_node_kind(&p.syntax(), SyntaxKind::PAREN_EXPR));
assert!(has_node_kind(&p.syntax(), SyntaxKind::PREFIX_EXPR));
assert!(!has_node_kind(&p.syntax(), SyntaxKind::ALTERNATION_BLOCK));
}
#[test]
fn alternation_stopping_marker_wins_over_lambda_expression() {
let p = assert_lossless("Lambda: {|x| x}\n");
assert!(
p.errors().is_empty(),
"`{{|x| x}}` is a valid stopping-sequence, not an error: {:?}",
p.errors()
);
assert!(
has_node_kind(&p.syntax(), SyntaxKind::ALTERNATION_BLOCK),
"`{{|x| x}}` is claimed by the alternation family as a stopping-sequence"
);
assert!(!has_node_kind(&p.syntax(), SyntaxKind::INTERPOLATION));
assert!(!has_node_kind(&p.syntax(), SyntaxKind::LAMBDA_EXPR));
}
#[test]
fn paren_escaped_lambda_expression_reaches_interpolation_despite_alternation_marker_win() {
let p = assert_lossless("Lambda: {(|x| x)}\n");
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
assert!(has_node_kind(&p.syntax(), SyntaxKind::INTERPOLATION));
assert!(has_node_kind(&p.syntax(), SyntaxKind::PAREN_EXPR));
assert!(has_node_kind(&p.syntax(), SyntaxKind::LAMBDA_EXPR));
assert!(!has_node_kind(&p.syntax(), SyntaxKind::ALTERNATION_BLOCK));
}
#[test]
fn two_branch_pipe_alternation_parses_clean_with_or_without_space() {
for src in ["Pick: {|heads|tails}\n", "Pick: {|heads| tails}\n"] {
let p = assert_lossless(src);
assert!(p.errors().is_empty(), "{src:?} errors: {:?}", p.errors());
assert!(has_node_kind(&p.syntax(), SyntaxKind::ALTERNATION_BLOCK));
assert!(!has_node_kind(&p.syntax(), SyntaxKind::LAMBDA_EXPR));
}
}
#[test]
fn standalone_hash_line_is_tag_line_not_content_line() {
let p = assert_lossless("#tag\n");
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
assert!(has_node_kind(&p.syntax(), SyntaxKind::TAG_LINE));
assert!(!has_node_kind(&p.syntax(), SyntaxKind::CONTENT_LINE));
}
#[test]
fn content_line_with_one_trailing_tag() {
let p = assert_lossless("Hello #tag\n");
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
let line = p
.syntax()
.descendants()
.find(|n| n.kind() == SyntaxKind::CONTENT_LINE)
.expect("CONTENT_LINE");
assert!(has_node_kind(&line, SyntaxKind::TEXT));
assert_eq!(count_node_kind(&line, SyntaxKind::TAG), 1);
assert!(!has_node_kind(&p.syntax(), SyntaxKind::TAG_LINE));
}
#[test]
fn content_line_with_two_trailing_tags() {
let p = assert_lossless("Hello #tag1 #tag2\n");
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
assert_eq!(count_node_kind(&p.syntax(), SyntaxKind::TAG), 2);
}
#[test]
fn content_divert_then_trailing_tag() {
let p = assert_lossless("Hello -> knot #tag\n");
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
let line = p
.syntax()
.descendants()
.find(|n| n.kind() == SyntaxKind::CONTENT_LINE)
.expect("CONTENT_LINE");
assert!(has_node_kind(&line, SyntaxKind::TEXT));
assert!(has_node_kind(&line, SyntaxKind::DIVERT_STMT));
assert!(has_node_kind(&line, SyntaxKind::TAG));
}
#[test]
fn tags_with_no_space_between_are_two_separate_tag_nodes() {
let p = assert_lossless("Hello #a#b\n");
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
assert_eq!(count_node_kind(&p.syntax(), SyntaxKind::TAG), 2);
}
#[test]
fn tag_text_runs_raw_to_end_of_line() {
let p = assert_lossless("Hello #tag: with, punctuation!\n");
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
assert_eq!(count_node_kind(&p.syntax(), SyntaxKind::TAG), 1);
}
#[test]
fn a_tag_containing_a_balanced_interpolation_brace_does_not_end_early() {
let src = "flow f() {\n Hello #tag {gold} coins.\n The river bends.\n}\n";
let p = assert_lossless(src);
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
assert_eq!(count_node_kind(&p.syntax(), SyntaxKind::FLOW_DECL), 1);
assert_eq!(
count_node_kind(&p.syntax(), SyntaxKind::CONTENT_LINE),
2,
"both prose lines must parse as CONTENT_LINEs inside the one flow"
);
}
#[test]
fn a_tag_containing_a_balanced_alternation_brace_does_not_end_early() {
let src = "flow f() {\n Hello #tag {gold|silver} coins.\n The river bends.\n}\n";
let p = assert_lossless(src);
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
assert_eq!(count_node_kind(&p.syntax(), SyntaxKind::FLOW_DECL), 1);
assert_eq!(count_node_kind(&p.syntax(), SyntaxKind::CONTENT_LINE), 2);
}
#[test]
fn a_tag_immediately_followed_by_the_enclosing_blocks_own_closer_still_stops_there() {
let src = "flow f() { Hello #tag }\n";
let p = assert_lossless(src);
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
assert_eq!(count_node_kind(&p.syntax(), SyntaxKind::FLOW_DECL), 1);
}
#[test]
fn an_unbalanced_open_brace_in_a_tag_eats_the_enclosing_blocks_own_closer() {
let src = "flow f() { Hello #tag { }\n";
let p = assert_lossless(src);
assert!(
!p.errors().is_empty(),
"expected the unbalanced `{{` to consume the flow's own closer and error, got: {:?}",
p.errors()
);
}
#[test]
fn a_tag_with_an_escaped_open_brace_does_not_swallow_the_enclosing_blocks_own_closer() {
let src = "flow f() { Hello #tag \\{ }\n";
let p = assert_lossless(src);
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
assert_eq!(count_node_kind(&p.syntax(), SyntaxKind::FLOW_DECL), 1);
}
#[test]
fn a_tag_with_an_escaped_backslash_before_a_real_brace_counts_the_brace() {
let src = "flow f() { Hello #tag \\\\{ } coins. }\n";
let p = assert_lossless(src);
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
assert_eq!(count_node_kind(&p.syntax(), SyntaxKind::FLOW_DECL), 1);
let tag = find_child::<ast::Tag>(
&p.syntax()
.descendants()
.find(|n| n.kind() == SyntaxKind::CONTENT_LINE)
.expect("CONTENT_LINE"),
)
.expect("Tag");
assert_eq!(
tag.text(),
"tag \\{ } coins.",
"an escaped backslash before a real brace collapses to one literal \
`\\`, matching `markup::escape`'s greedy consumption for ordinary \
content"
);
}
#[test]
fn a_tag_with_an_escaped_hash_does_not_end_the_tag_early() {
let p = assert_lossless("Hello #tag \\#not a new tag\n");
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
assert_eq!(
count_node_kind(&p.syntax(), SyntaxKind::TAG),
1,
"an escaped `#` must not split the tag in two"
);
let tag = p
.syntax()
.descendants()
.find(|n| n.kind() == SyntaxKind::TAG)
.expect("TAG");
assert_eq!(
tag.text(),
"#tag \\#not a new tag",
"the raw CST node's own text is never rewritten — it stays a \
faithful, lossless copy of the source, backslash and all; \
`ast::Tag::text()` is the separate, later materialization point \
that strips a recognized escape's backslash (issue #2045), see \
`a_tags_text_accessor_strips_a_recognized_escapes_backslash` below"
);
}
#[test]
fn a_tags_own_unescaped_closing_brace_remains_the_terminator_even_when_preceded_by_a_backslash() {
let src = "flow f() { Hello #tag \\{a\\} more. }\n";
let p = assert_lossless(src);
assert!(
!p.errors().is_empty(),
"expected the tag's own `\\}}` to swallow the flow's closer early \
and leave a stray top-level `}}`, got: {:?}",
p.errors()
);
let tag = p
.syntax()
.descendants()
.find(|n| n.kind() == SyntaxKind::TAG)
.expect("TAG");
assert_eq!(
tag.text(),
"#tag \\{a\\",
"the tag stops the instant it meets the `}}` from `\\}}`, backslash \
and all, exactly as it would for an unescaped `}}` at depth zero"
);
}
#[test]
fn a_tags_text_accessor_strips_a_recognized_escapes_backslash() {
let p = assert_lossless("Hello #tag \\#not a new tag\n");
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
let tag = find_child::<ast::Tag>(
&p.syntax()
.descendants()
.find(|n| n.kind() == SyntaxKind::CONTENT_LINE)
.expect("CONTENT_LINE"),
)
.expect("Tag");
assert_eq!(
tag.text(),
"tag #not a new tag",
"a recognized `\\#` strips its backslash in the tag's materialized \
text, same as ordinary content"
);
}
#[test]
fn a_tags_text_accessor_strips_a_recognized_open_brace_escapes_backslash() {
let p = assert_lossless("Hello #tag \\{gold\n");
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
let tag = find_child::<ast::Tag>(
&p.syntax()
.descendants()
.find(|n| n.kind() == SyntaxKind::CONTENT_LINE)
.expect("CONTENT_LINE"),
)
.expect("Tag");
assert_eq!(
tag.text(),
"tag {gold",
"a recognized `\\{{` strips its backslash in the tag's materialized \
text, same as `\\#`"
);
}
#[test]
fn a_tags_own_unbalanced_brace_does_not_leak_depth_into_a_sibling_tag() {
let src = "flow f() { Hello #a {x #b}\n";
let p = assert_lossless(src);
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
assert_eq!(count_node_kind(&p.syntax(), SyntaxKind::FLOW_DECL), 1);
let tags: Vec<String> = p
.syntax()
.descendants()
.filter(|n| n.kind() == SyntaxKind::TAG)
.map(|n| n.text().to_string())
.collect();
assert_eq!(tags.len(), 2, "expected two sibling TAG nodes: {tags:?}");
assert_eq!(
tags[0], "#a {x",
"tag `a` keeps its own unmatched `{{` — HASH cut its scan short \
before depth was ever consulted"
);
assert_eq!(
tags[1], " #b",
"tag `b` starts at depth zero and stops at the very first `}}` — \
it never inherits `a`'s leftover unmatched depth"
);
}
#[test]
fn a_top_level_tag_with_an_embedded_brace_reproduces_with_no_flow_or_tag_guard_involved() {
let src = "Hello #tag {gold} coins.\n";
let p = assert_lossless(src);
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
assert_eq!(count_node_kind(&p.syntax(), SyntaxKind::TAG), 1);
}
#[test]
fn tag_on_a_labeled_content_line() {
let p = assert_lossless("(start) Hello #tag\n");
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
assert!(has_node_kind(&p.syntax(), SyntaxKind::LABEL));
assert!(has_node_kind(&p.syntax(), SyntaxKind::TAG));
}
#[test]
fn comment_only_line_before_content_is_excluded_from_the_content_line() {
let src = "flow f() {\n // just a comment\n Hello\n}\n";
let p = assert_lossless(src);
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
let line = p
.syntax()
.descendants()
.find(|n| n.kind() == SyntaxKind::CONTENT_LINE)
.expect("CONTENT_LINE");
assert_eq!(text_run_concat(&line), "Hello");
}
#[test]
fn line_comment_mid_text_run_is_literal_prose_per_text_run_untils_contract() {
let src = "flow f() {\n Hello // not actually a comment here\n}\n";
let p = assert_lossless(src);
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
let line = p
.syntax()
.descendants()
.find(|n| n.kind() == SyntaxKind::CONTENT_LINE)
.expect("CONTENT_LINE");
assert_eq!(
text_run_concat(&line),
"Hello // not actually a comment here",
"current, contract-documented behavior: the // comment's text is part of TEXT"
);
}
#[test]
fn block_comment_mid_text_run_is_literal_prose_per_text_run_untils_contract() {
let src = "flow f() {\n Hello /* aside */ world\n}\n";
let p = assert_lossless(src);
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
let line = p
.syntax()
.descendants()
.find(|n| n.kind() == SyntaxKind::CONTENT_LINE)
.expect("CONTENT_LINE");
assert_eq!(
text_run_concat(&line),
"Hello /* aside */ world",
"current, contract-documented behavior: the block comment's text is part of TEXT"
);
}
#[test]
fn unterminated_block_comment_mid_text_run_never_panics_and_roundtrips() {
let src = "Hello /* never closed\n";
let p = assert_lossless(src);
let _ = p.errors();
}
#[test]
fn unterminated_interpolation_missing_closing_brace_before_newline() {
let src = "Hello {name\n";
let p = assert_lossless(src);
assert!(
!p.errors().is_empty(),
"expected a parse error for the missing `}}`"
);
assert!(has_node_kind(&p.syntax(), SyntaxKind::INTERPOLATION));
}
#[test]
fn unterminated_interpolation_runs_to_eof_with_no_newline_at_all() {
let src = "Hello {name";
let p = assert_lossless(src);
assert!(
!p.errors().is_empty(),
"expected a parse error for the missing `}}`"
);
assert!(has_node_kind(&p.syntax(), SyntaxKind::INTERPOLATION));
}
#[test]
fn empty_interpolation_body_errors_but_recovers() {
let src = "Hello {}\n";
let p = assert_lossless(src);
assert!(
!p.errors().is_empty(),
"expected a parse error for the missing expression"
);
assert!(has_node_kind(&p.syntax(), SyntaxKind::INTERPOLATION));
}
#[test]
fn stray_closing_brace_in_content_recovers_without_panicking() {
let src = "Hello } world\n";
let p = assert_lossless(src);
assert!(!p.errors().is_empty(), "errors: {:?}", p.errors());
assert!(has_node_kind(&p.syntax(), SyntaxKind::ERROR));
assert_eq!(text_run_concat(&p.syntax()), "Hello world");
}
#[test]
fn leading_stray_closing_brace_recovers_without_panicking() {
let src = "} Hello\n";
let p = assert_lossless(src);
assert!(!p.errors().is_empty(), "errors: {:?}", p.errors());
assert!(has_node_kind(&p.syntax(), SyntaxKind::ERROR));
assert_eq!(text_run_concat(&p.syntax()), "Hello");
}
#[test]
fn adversarial_brace_soup_never_panics_and_roundtrips() {
for src in [
"{{{}}}\n",
"{}{}{}\n",
"{{{{{{{{{{\n",
"}}}}}}}}}}\n",
"{expr}{expr2}{\n",
] {
let p = assert_lossless(src);
let _ = p.errors();
}
}
#[test]
fn backslash_escapes_hash_no_tag_opens() {
let src = "\\# not a tag\n";
let p = assert_lossless(src);
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
assert!(!has_node_kind(&p.syntax(), SyntaxKind::TAG));
assert!(has_node_kind(&p.syntax(), SyntaxKind::ESCAPE));
}
#[test]
fn backslash_escapes_open_brace_no_interpolation_opens() {
let src = "\\{ not logic\n";
let p = assert_lossless(src);
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
assert!(!has_node_kind(&p.syntax(), SyntaxKind::INTERPOLATION));
assert!(has_node_kind(&p.syntax(), SyntaxKind::ESCAPE));
}
#[test]
fn blank_line_produces_no_content_line() {
let p = assert_lossless("\n");
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
assert!(!has_node_kind(&p.syntax(), SyntaxKind::CONTENT_LINE));
}
#[test]
fn bare_divert_line_produces_no_content_line_wrapper() {
let p = assert_lossless("-> knot\n");
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
assert!(has_node_kind(&p.syntax(), SyntaxKind::DIVERT_STMT));
assert!(!has_node_kind(&p.syntax(), SyntaxKind::CONTENT_LINE));
}
#[test]
fn choice_point_line_is_not_a_content_line() {
let src = "flow f() {\n {?\n * Hello\n }\n}\n";
let p = assert_lossless(src);
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
assert!(has_node_kind(&p.syntax(), SyntaxKind::CHOICE_POINT));
assert!(!has_node_kind(&p.syntax(), SyntaxKind::CONTENT_LINE));
}
#[test]
fn return_line_is_not_a_content_line() {
let src = "flow f() {\n return\n}\n";
let p = assert_lossless(src);
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
assert!(!has_node_kind(&p.syntax(), SyntaxKind::CONTENT_LINE));
}
#[test]
fn trailing_brace_expr_on_choice_line_is_interpolation_not_choice_body() {
let src = "flow f() {\n {?\n * hello {x}\n }\n}\n";
let p = assert_lossless(src);
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
assert!(has_node_kind(&p.syntax(), SyntaxKind::INTERPOLATION));
assert!(!has_node_kind(&p.syntax(), SyntaxKind::CHOICE_BODY));
}
#[test]
fn multiline_brace_on_choice_line_is_choice_body_not_interpolation() {
let src = "flow f() {\n {?\n * hello {\n inner\n }\n }\n}\n";
let p = assert_lossless(src);
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
assert!(has_node_kind(&p.syntax(), SyntaxKind::CHOICE_BODY));
assert!(!has_node_kind(&p.syntax(), SyntaxKind::INTERPOLATION));
}
use proptest::prelude::*;
fn arb_word() -> impl Strategy<Value = String> {
"[a-z][a-z0-9]{0,6}"
}
fn arb_glue_chain_line() -> impl Strategy<Value = String> {
prop::collection::vec(arb_word(), 1..=4).prop_map(|words| format!("{}\n", words.join("<>")))
}
fn arb_tagged_content_line() -> impl Strategy<Value = String> {
(arb_word(), prop::collection::vec(arb_word(), 1..=3)).prop_map(|(text, tags)| {
let mut line = text;
for tag in &tags {
line.push_str(" #");
line.push_str(tag);
}
line.push('\n');
line
})
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(256))]
#[test]
fn glue_chain_line_roundtrips(input in arb_glue_chain_line()) {
let p = parse(&input);
prop_assert_eq!(p.syntax().text().to_string(), input);
}
#[test]
fn glue_chain_line_has_no_errors(input in arb_glue_chain_line()) {
let p = parse(&input);
prop_assert!(p.errors().is_empty(), "input: {:?}\nerrors: {:?}", input, p.errors());
}
#[test]
fn glue_chain_line_glue_node_count_matches_separator_count(input in arb_glue_chain_line()) {
let p = parse(&input);
let expected = input.matches("<>").count();
prop_assert_eq!(count_node_kind(&p.syntax(), SyntaxKind::GLUE_NODE), expected);
}
#[test]
fn tagged_content_line_roundtrips(input in arb_tagged_content_line()) {
let p = parse(&input);
prop_assert_eq!(p.syntax().text().to_string(), input);
}
#[test]
fn tagged_content_line_has_no_errors(input in arb_tagged_content_line()) {
let p = parse(&input);
prop_assert!(p.errors().is_empty(), "input: {:?}\nerrors: {:?}", input, p.errors());
}
#[test]
fn tagged_content_line_tag_count_matches_hash_count(input in arb_tagged_content_line()) {
let p = parse(&input);
let expected = input.matches('#').count();
prop_assert_eq!(count_node_kind(&p.syntax(), SyntaxKind::TAG), expected);
}
}