use super::*;
#[test]
fn divert_and_tunnel_and_return() {
let src = "flow a() {\n -> b\n}\nflow b() {\n -> c ->\n return\n}\nflow c() {\n return -> a\n -> END\n}\n";
let p = assert_lossless(src);
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
}
#[test]
fn splice_inside_choice_point() {
let src = "flow hub() {\n {?\n <- options()\n }\n}\n";
let p = assert_lossless(src);
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
}
#[test]
fn divert_after_choice_bracket_text_is_a_divert_node_not_text() {
let src = "flow f() {\n {?\n * [The wager.] -> know_about_wager\n }\n}\n";
let p = assert_lossless(src);
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
let choice_inner = p
.syntax()
.descendants()
.find(|n| n.kind() == SyntaxKind::CHOICE_INNER_CONTENT)
.expect("CHOICE_INNER_CONTENT");
assert!(
has_node_kind(&choice_inner, SyntaxKind::DIVERT_STMT),
"expected a DIVERT_STMT inside CHOICE_INNER_CONTENT, tree: {choice_inner:#?}"
);
let divert = choice_inner
.descendants()
.find(|n| n.kind() == SyntaxKind::DIVERT_STMT)
.expect("DIVERT_STMT");
let target = find_child::<crate::ast::DivertTarget>(&divert).expect("DIVERT_TARGET");
let path = target.path().expect("path");
let segs: Vec<_> = path.segments().map(|t| t.text().to_string()).collect();
assert_eq!(segs, vec!["know_about_wager".to_string()]);
}
#[test]
fn divert_after_dotted_path_target_in_choice_text_parses() {
let src = "flow f() {\n {?\n * [go] -> f.g\n }\n}\n";
let p = assert_lossless(src);
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
let divert = p
.syntax()
.descendants()
.find(|n| n.kind() == SyntaxKind::DIVERT_STMT)
.expect("DIVERT_STMT");
let target = find_child::<crate::ast::DivertTarget>(&divert).expect("DIVERT_TARGET");
let path = target.path().expect("path");
assert!(!path.crosses_module_wall()); let segs: Vec<_> = path.segments().map(|t| t.text().to_string()).collect();
assert_eq!(segs, vec!["f".to_string(), "g".to_string()]);
}
#[test]
fn divert_inside_multiline_choice_body_after_prose_is_a_divert_node() {
let src = "flow f() {\n {?\n + [Eat] {\n You eat another donut. -> f\n }\n }\n}\n";
let p = assert_lossless(src);
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
let body = p
.syntax()
.descendants()
.find(|n| n.kind() == SyntaxKind::CHOICE_BODY)
.expect("CHOICE_BODY");
let content_line = body
.descendants()
.find(|n| n.kind() == SyntaxKind::CONTENT_LINE)
.expect("CONTENT_LINE");
assert!(
has_node_kind(&content_line, SyntaxKind::DIVERT_STMT),
"expected DIVERT_STMT nested inside the CONTENT_LINE, tree: {content_line:#?}"
);
assert!(has_node_kind(&content_line, SyntaxKind::TEXT));
}
#[test]
fn tunnel_call_in_content_position_parses() {
let src = "flow f() {\n {?\n * [go] visit -> place ->\n }\n}\n";
let p = assert_lossless(src);
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
assert!(has_node_kind(&p.syntax(), SyntaxKind::TUNNEL_CALL));
}
#[test]
fn divert_to_end_in_content_position_parses() {
let src = "flow f() {\n {?\n * [go] The end. -> END\n }\n}\n";
let p = assert_lossless(src);
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
let divert = p
.syntax()
.descendants()
.find(|n| n.kind() == SyntaxKind::DIVERT_STMT)
.expect("DIVERT_STMT");
let target = find_child::<crate::ast::DivertTarget>(&divert).expect("DIVERT_TARGET");
assert!(target.is_end());
}
#[test]
fn divert_target_end() {
let src = "flow f() {\n -> END\n}\n";
let p = assert_lossless(src);
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
let target = p
.syntax()
.descendants()
.find_map(crate::ast::DivertTarget::cast)
.expect("DIVERT_TARGET");
assert!(target.is_end());
assert!(!target.is_done());
assert!(target.path().is_none());
}
#[test]
fn divert_target_done() {
let src = "flow f() {\n -> DONE\n}\n";
let p = assert_lossless(src);
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
let target = p
.syntax()
.descendants()
.find_map(crate::ast::DivertTarget::cast)
.expect("DIVERT_TARGET");
assert!(target.is_done());
assert!(!target.is_end());
assert!(target.path().is_none());
}
#[test]
fn divert_target_path() {
let src = "flow f() {\n -> knot\n}\n";
let p = assert_lossless(src);
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
let target = p
.syntax()
.descendants()
.find_map(crate::ast::DivertTarget::cast)
.expect("DIVERT_TARGET");
assert!(!target.is_end());
assert!(!target.is_done());
let path = target.path().expect("PATH");
let segs: Vec<_> = path.segments().map(|t| t.text().to_string()).collect();
assert_eq!(segs, vec!["knot".to_string()]);
}
#[test]
fn divert_target_path_dotted() {
let src = "flow f() {\n -> knot.stitch\n}\n";
let p = assert_lossless(src);
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
let target = p
.syntax()
.descendants()
.find_map(crate::ast::DivertTarget::cast)
.expect("DIVERT_TARGET");
let path = target.path().expect("PATH");
assert!(!path.crosses_module_wall());
let segs: Vec<_> = path.segments().map(|t| t.text().to_string()).collect();
assert_eq!(segs, vec!["knot".to_string(), "stitch".to_string()]);
}
#[test]
fn divert_target_path_module_wall() {
let src = "flow f() {\n -> a::b\n}\n";
let p = assert_lossless(src);
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
let target = p
.syntax()
.descendants()
.find_map(crate::ast::DivertTarget::cast)
.expect("DIVERT_TARGET");
let path = target.path().expect("PATH");
assert!(path.crosses_module_wall());
let segs: Vec<_> = path.segments().map(|t| t.text().to_string()).collect();
assert_eq!(segs, vec!["a".to_string(), "b".to_string()]);
}
#[test]
fn simple_divert_is_divert_stmt_not_tunnel_call() {
let src = "flow f() {\n -> knot\n}\n";
let p = assert_lossless(src);
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
assert!(has_node_kind(&p.syntax(), SyntaxKind::DIVERT_STMT));
assert!(!has_node_kind(&p.syntax(), SyntaxKind::TUNNEL_CALL));
}
#[test]
fn content_then_divert_at_top_level() {
let src = "flow f() {\n Hello -> knot\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::TEXT));
assert!(has_node_kind(&content_line, SyntaxKind::DIVERT_STMT));
let divert = find_child::<crate::ast::DivertStmt>(&content_line)
.or_else(|| {
content_line
.descendants()
.find_map(crate::ast::DivertStmt::cast)
})
.expect("DIVERT_STMT");
let target = divert.target().expect("DIVERT_TARGET");
let segs: Vec<_> = target
.path()
.expect("PATH")
.segments()
.map(|t| t.text().to_string())
.collect();
assert_eq!(segs, vec!["knot".to_string()]);
}
#[test]
fn tunnel_call_simple() {
let src = "flow f() {\n -> place ->\n}\n";
let p = assert_lossless(src);
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
let call = p
.syntax()
.descendants()
.find_map(crate::ast::TunnelCall::cast)
.expect("TUNNEL_CALL");
let target = call.target().expect("DIVERT_TARGET");
let segs: Vec<_> = target
.path()
.expect("PATH")
.segments()
.map(|t| t.text().to_string())
.collect();
assert_eq!(segs, vec!["place".to_string()]);
}
#[test]
fn tunnel_call_dotted_target() {
let src = "flow f() {\n -> knot.stitch ->\n}\n";
let p = assert_lossless(src);
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
let call = p
.syntax()
.descendants()
.find_map(crate::ast::TunnelCall::cast)
.expect("TUNNEL_CALL");
let target = call.target().expect("DIVERT_TARGET");
let segs: Vec<_> = target
.path()
.expect("PATH")
.segments()
.map(|t| t.text().to_string())
.collect();
assert_eq!(segs, vec!["knot".to_string(), "stitch".to_string()]);
}
#[test]
fn tunnel_call_to_end_target_parses_but_is_a_semantic_question() {
let src = "flow f() {\n -> END ->\n}\n";
let p = assert_lossless(src);
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
let call = p
.syntax()
.descendants()
.find_map(crate::ast::TunnelCall::cast)
.expect("TUNNEL_CALL");
assert!(call.target().expect("DIVERT_TARGET").is_end());
}
#[test]
fn regular_divert_not_tunnel_call() {
let src = "flow f() {\n -> target\n}\n";
let p = assert_lossless(src);
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
assert!(!has_node_kind(&p.syntax(), SyntaxKind::TUNNEL_CALL));
assert!(has_node_kind(&p.syntax(), SyntaxKind::DIVERT_STMT));
}
#[test]
fn divert_with_unrelated_trailing_content_is_not_a_tunnel_call() {
let src = "flow f() {\n -> knot extra text\n}\n";
let p = assert_lossless(src);
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
assert!(!has_node_kind(&p.syntax(), SyntaxKind::TUNNEL_CALL));
let divert = p
.syntax()
.descendants()
.find_map(crate::ast::DivertStmt::cast)
.expect("DIVERT_STMT");
let segs: Vec<_> = divert
.target()
.expect("DIVERT_TARGET")
.path()
.expect("PATH")
.segments()
.map(|t| t.text().to_string())
.collect();
assert_eq!(
segs,
vec!["knot".to_string()],
"the divert target must not have swallowed `extra`/`text`"
);
assert!(
p.syntax()
.descendants()
.any(|n| n.kind() == SyntaxKind::CONTENT_LINE),
"`extra text` must land in its own CONTENT_LINE"
);
}
#[test]
fn second_arrow_not_immediately_after_target_is_still_a_tunnel_call() {
let src = "flow f() {\n -> place ->\n}\n";
let p = assert_lossless(src);
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
assert!(has_node_kind(&p.syntax(), SyntaxKind::TUNNEL_CALL));
}
#[test]
fn no_third_arrow_native_grammar_has_no_divert_chaining() {
let src = "flow f() {\n -> a -> b\n}\n";
let p = assert_lossless(src);
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
assert!(has_node_kind(&p.syntax(), SyntaxKind::TUNNEL_CALL));
assert_eq!(count_node_kind(&p.syntax(), SyntaxKind::TUNNEL_CALL), 1);
assert_eq!(count_node_kind(&p.syntax(), SyntaxKind::DIVERT_STMT), 0);
let call = p
.syntax()
.descendants()
.find_map(crate::ast::TunnelCall::cast)
.expect("TUNNEL_CALL");
let segs: Vec<_> = call
.target()
.expect("DIVERT_TARGET")
.path()
.expect("PATH")
.segments()
.map(|t| t.text().to_string())
.collect();
assert_eq!(segs, vec!["a".to_string()]);
assert!(
p.syntax()
.descendants()
.any(|n| n.kind() == SyntaxKind::CONTENT_LINE),
"`b` must land in a trailing CONTENT_LINE, not a chained divert"
);
}
#[test]
fn bare_return_stmt() {
let src = "flow f() {\n -> place ->\n return\n}\n";
let p = assert_lossless(src);
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
assert!(has_node_kind(&p.syntax(), SyntaxKind::RETURN_STMT));
assert!(!has_node_kind(&p.syntax(), SyntaxKind::RETURN_REDIRECT));
}
#[test]
fn return_redirect_simple() {
let src = "flow f() {\n return -> x\n}\n";
let p = assert_lossless(src);
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
let redirect = p
.syntax()
.descendants()
.find_map(crate::ast::ReturnRedirect::cast)
.expect("RETURN_REDIRECT");
let segs: Vec<_> = redirect
.target()
.expect("DIVERT_TARGET")
.path()
.expect("PATH")
.segments()
.map(|t| t.text().to_string())
.collect();
assert_eq!(segs, vec!["x".to_string()]);
assert!(!has_node_kind(&p.syntax(), SyntaxKind::RETURN_STMT));
}
#[test]
fn return_redirect_to_end() {
let src = "flow f() {\n return -> END\n}\n";
let p = assert_lossless(src);
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
let redirect = p
.syntax()
.descendants()
.find_map(crate::ast::ReturnRedirect::cast)
.expect("RETURN_REDIRECT");
assert!(redirect.target().expect("DIVERT_TARGET").is_end());
}
#[test]
fn return_redirect_dotted_target() {
let src = "flow f() {\n return -> knot.stitch\n}\n";
let p = assert_lossless(src);
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
let redirect = p
.syntax()
.descendants()
.find_map(crate::ast::ReturnRedirect::cast)
.expect("RETURN_REDIRECT");
let segs: Vec<_> = redirect
.target()
.expect("DIVERT_TARGET")
.path()
.expect("PATH")
.segments()
.map(|t| t.text().to_string())
.collect();
assert_eq!(segs, vec!["knot".to_string(), "stitch".to_string()]);
}
#[test]
fn bare_return_on_its_own_line_has_no_value() {
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::RETURN_STMT));
assert!(!has_node_kind(&p.syntax(), SyntaxKind::RETURN_REDIRECT));
let ret = p
.syntax()
.descendants()
.find_map(ast::ReturnStmt::cast)
.expect("RETURN_STMT");
assert!(ret.value().is_none(), "no value token follows this return");
}
#[test]
fn return_with_value_expression_on_same_line() {
let src = "flow f() {\n return home\n}\n";
let p = assert_lossless(src);
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
assert!(has_node_kind(&p.syntax(), SyntaxKind::RETURN_STMT));
assert!(!has_node_kind(&p.syntax(), SyntaxKind::RETURN_REDIRECT));
assert!(
!p.syntax()
.descendants()
.any(|n| n.kind() == SyntaxKind::CONTENT_LINE),
"`home` must be the RETURN_STMT's value, not folded into a separate CONTENT_LINE"
);
let ret = p
.syntax()
.descendants()
.find_map(ast::ReturnStmt::cast)
.expect("RETURN_STMT");
let value = ret.value().expect("value expression");
assert_eq!(value.kind(), SyntaxKind::PATH_EXPR);
}
#[test]
fn return_with_value_then_next_line_content_is_unaffected() {
let src = "flow f() {\n return\n home\n}\n";
let p = assert_lossless(src);
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
let ret = p
.syntax()
.descendants()
.find_map(ast::ReturnStmt::cast)
.expect("RETURN_STMT");
assert!(
ret.value().is_none(),
"no value — `home` is on the next line"
);
assert!(
p.syntax()
.descendants()
.any(|n| n.kind() == SyntaxKind::CONTENT_LINE),
"`home` on the next line must land in its own CONTENT_LINE"
);
}
#[test]
fn return_with_numeric_value_expression() {
let src = "flow f() {\n return 5\n}\n";
let p = assert_lossless(src);
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
let ret = p
.syntax()
.descendants()
.find_map(ast::ReturnStmt::cast)
.expect("RETURN_STMT");
let value = ret.value().expect("value expression");
assert_eq!(value.kind(), SyntaxKind::INTEGER_LIT);
}
#[test]
fn return_with_infix_value_expression() {
let src = "flow f() {\n return hp > 0\n}\n";
let p = assert_lossless(src);
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
let ret = p
.syntax()
.descendants()
.find_map(ast::ReturnStmt::cast)
.expect("RETURN_STMT");
let value = ret.value().expect("value expression");
assert_eq!(value.kind(), SyntaxKind::INFIX_EXPR);
}
#[test]
fn return_redirect_still_wins_over_value_grammar() {
let src = "flow f() {\n return -> x\n}\n";
let p = assert_lossless(src);
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
assert!(has_node_kind(&p.syntax(), SyntaxKind::RETURN_REDIRECT));
assert!(!has_node_kind(&p.syntax(), SyntaxKind::RETURN_STMT));
}
#[test]
fn return_value_in_colon_body_form() {
let src = "var score = 1\nflow f() {\n {if score == 1: return 5}\n -> END\n}\n";
let p = assert_lossless(src);
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
let ret = p
.syntax()
.descendants()
.find_map(ast::ReturnStmt::cast)
.expect("RETURN_STMT");
let value = ret.value().expect("value expression");
assert_eq!(value.kind(), SyntaxKind::INTEGER_LIT);
}
#[test]
fn bare_return_in_colon_body_before_else_arm() {
let src = "var score = 1\nflow f() {\n {if score == 1: return else: Other.}\n -> END\n}\n";
let p = assert_lossless(src);
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
let ret = p
.syntax()
.descendants()
.find_map(ast::ReturnStmt::cast)
.expect("RETURN_STMT");
assert!(ret.value().is_none(), "no value before the `else` arm");
assert!(has_node_kind(&p.syntax(), SyntaxKind::ELSE_BRANCH));
}
#[test]
fn splice_basic() {
let src = "flow f() {\n {?\n <- side_thread\n }\n}\n";
let p = assert_lossless(src);
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
let splice = p
.syntax()
.descendants()
.find_map(crate::ast::Splice::cast)
.expect("SPLICE");
let segs: Vec<_> = splice
.path()
.expect("PATH")
.segments()
.map(|t| t.text().to_string())
.collect();
assert_eq!(segs, vec!["side_thread".to_string()]);
assert!(splice.arg_list().is_none());
}
#[test]
fn splice_with_args() {
let src = "flow f() {\n {?\n <- options(gold, 2)\n }\n}\n";
let p = assert_lossless(src);
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
let splice = p
.syntax()
.descendants()
.find_map(crate::ast::Splice::cast)
.expect("SPLICE");
let arg_list = splice.arg_list().expect("ARG_LIST");
assert!(arg_list.is_open());
}
#[test]
fn splice_with_dotted_path() {
let src = "flow f() {\n {?\n <- hub.options\n }\n}\n";
let p = assert_lossless(src);
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
let splice = p
.syntax()
.descendants()
.find_map(crate::ast::Splice::cast)
.expect("SPLICE");
let segs: Vec<_> = splice
.path()
.expect("PATH")
.segments()
.map(|t| t.text().to_string())
.collect();
assert_eq!(segs, vec!["hub".to_string(), "options".to_string()]);
}
#[test]
fn splice_coexists_with_choice_lines() {
let src = "flow f() {\n {?\n * [Look] You look around.\n <- extra_choices\n }\n}\n";
let p = assert_lossless(src);
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
assert!(has_node_kind(&p.syntax(), SyntaxKind::CHOICE));
assert!(has_node_kind(&p.syntax(), SyntaxKind::SPLICE));
}
#[test]
fn splice_outside_choice_point_is_not_a_splice_node() {
let src = "flow f() {\n <- side_thread\n}\n";
let p = assert_lossless(src);
assert_eq!(
p.errors().len(),
1,
"expected exactly one warning-severity diagnostic for `<-` outside \
a choice point; errors: {:?}",
p.errors()
);
let diag = &p.errors()[0];
assert_eq!(
diag.severity,
ParseSeverity::Warning,
"a splice outside a choice point must warn, never hard-error — \
`<-` can be literal dialogue (issue #1263); diagnostic: {diag:?}"
);
assert!(
diag.message.contains("knot/flow reference"),
"`<- side_thread` is shaped exactly like a real flow reference (a \
bare identifier, nothing else on the line) — the diagnostic \
should be the higher-confidence variant; message: {}",
diag.message
);
assert_eq!(diag.range, rowan::TextRange::at(13.into(), 2.into()));
assert!(!has_node_kind(&p.syntax(), SyntaxKind::SPLICE));
assert!(has_node_kind(&p.syntax(), SyntaxKind::CONTENT_LINE));
assert!(has_node_kind(&p.syntax(), SyntaxKind::TEXT));
}
#[test]
fn splice_outside_choice_point_low_confidence_when_not_reference_shaped() {
let src = "flow f() {\n <- \"not a reference\"\n}\n";
let p = assert_lossless(src);
assert_eq!(p.errors().len(), 1, "errors: {:?}", p.errors());
let diag = &p.errors()[0];
assert_eq!(diag.severity, ParseSeverity::Warning);
assert!(
!diag.message.contains("knot/flow reference"),
"message should be the generic (low-confidence) variant: {}",
diag.message
);
}
#[test]
fn splice_outside_choice_point_high_confidence_with_call_args() {
let src = "flow f() {\n <- options(true)\n}\n";
let p = assert_lossless(src);
assert_eq!(p.errors().len(), 1, "errors: {:?}", p.errors());
let diag = &p.errors()[0];
assert_eq!(diag.severity, ParseSeverity::Warning);
assert!(
diag.message.contains("knot/flow reference"),
"message: {}",
diag.message
);
}
#[test]
fn divert_target_with_call_args_captures_the_args_bug_1196() {
let src = "flow f() {\n -> greet(\"hello\")\n}\n";
let p = assert_lossless(src);
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
let divert = p
.syntax()
.descendants()
.find_map(crate::ast::DivertStmt::cast)
.expect("DIVERT_STMT");
let target = divert.target().expect("DIVERT_TARGET");
let segs: Vec<_> = target
.path()
.expect("PATH")
.segments()
.map(|t| t.text().to_string())
.collect();
assert_eq!(segs, vec!["greet".to_string()]);
let arg_list = target
.call_args()
.expect("ARG_LIST captured under DIVERT_TARGET");
assert_eq!(
arg_list.syntax().parent().expect("DIVERT_TARGET").kind(),
SyntaxKind::DIVERT_TARGET,
"the ARG_LIST must be a direct child of DIVERT_TARGET, not wrapped \
in a CALL_EXPR — a divert target is not itself an expression"
);
let args_text = arg_list.syntax().text().to_string();
assert!(
args_text.contains("hello"),
"expected the string literal argument inside ARG_LIST, got: {args_text:?}"
);
assert!(
!has_node_kind(&p.syntax(), SyntaxKind::CONTENT_LINE),
"the args must not leak into a sibling CONTENT_LINE anymore"
);
}
#[test]
fn divert_with_no_target_recovers() {
let src = "flow f() {\n -> \n}\n";
let p = assert_lossless(src);
assert!(
!p.errors().is_empty(),
"a target-less divert should record a diagnostic"
);
assert!(has_node_kind(&p.syntax(), SyntaxKind::DIVERT_STMT));
assert!(has_node_kind(&p.syntax(), SyntaxKind::DIVERT_TARGET));
}
#[test]
fn tunnel_call_with_no_target_recovers() {
let src = "flow f() {\n ->->\n}\n";
let p = assert_lossless(src);
assert!(!p.errors().is_empty(), "errors: {:?}", p.errors());
assert!(has_node_kind(&p.syntax(), SyntaxKind::TUNNEL_CALL));
}
#[test]
fn return_redirect_with_no_target_recovers() {
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::RETURN_REDIRECT));
}
#[test]
fn divert_at_eof_with_no_newline_recovers() {
let src = "-> knot";
let p = assert_lossless(src);
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
assert!(has_node_kind(&p.syntax(), SyntaxKind::DIVERT_STMT));
}
#[test]
fn tunnel_call_at_eof_with_no_newline_recovers() {
let src = "-> place ->";
let p = assert_lossless(src);
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
assert!(has_node_kind(&p.syntax(), SyntaxKind::TUNNEL_CALL));
}
#[test]
fn splice_with_no_target_recovers() {
let src = "flow f() {\n {?\n <- \n }\n}\n";
let p = assert_lossless(src);
assert!(!p.errors().is_empty(), "errors: {:?}", p.errors());
assert!(has_node_kind(&p.syntax(), SyntaxKind::SPLICE));
}
#[test]
fn splice_with_unterminated_arg_list_recovers() {
let src = "flow f() {\n {?\n <- options(gold\n }\n}\n";
let p = assert_lossless(src);
assert!(!p.errors().is_empty(), "errors: {:?}", p.errors());
assert!(has_node_kind(&p.syntax(), SyntaxKind::SPLICE));
}
#[test]
fn bare_arrow_soup_never_panics() {
let src = "flow f() {\n -> -> -> -> -> ->\n}\n";
let p = assert_lossless(src);
let _ = p.errors();
}
#[test]
fn return_redirect_chained_arrows_never_panics() {
let src = "flow f() {\n return -> -> ->\n}\n";
let p = assert_lossless(src);
let _ = p.errors();
}
#[test]
fn mismatched_tunnel_arrows_across_lines_recovers() {
let src = "flow f() {\n -> place\n -> next\n}\n";
let p = assert_lossless(src);
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
assert!(!has_node_kind(&p.syntax(), SyntaxKind::TUNNEL_CALL));
assert_eq!(count_node_kind(&p.syntax(), SyntaxKind::DIVERT_STMT), 2);
}
#[test]
fn splice_immediately_followed_by_r_brace_recovers() {
let src = "flow f() {\n {?\n <-\n }\n}\n";
let p = assert_lossless(src);
assert!(!p.errors().is_empty(), "errors: {:?}", p.errors());
assert!(has_node_kind(&p.syntax(), SyntaxKind::SPLICE));
}
#[test]
fn deeply_nested_divert_targets_do_not_overflow() {
let mut src = String::from("flow f() {\n -> ");
for i in 0..500 {
if i > 0 {
src.push('.');
}
src.push('a');
}
src.push('\n');
src.push_str("}\n");
let p = assert_lossless(&src);
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
}
#[test]
fn insta_tunnel_call() {
let p = parse("flow f() {\n -> place ->\n}\n");
insta::assert_snapshot!(format!("{:#?}", p.syntax()));
}
#[test]
fn insta_return_redirect() {
let p = parse("flow f() {\n return -> x\n}\n");
insta::assert_snapshot!(format!("{:#?}", p.syntax()));
}
#[test]
fn insta_splice_with_args() {
let p = parse("flow f() {\n {?\n <- options(gold, 2)\n }\n}\n");
insta::assert_snapshot!(format!("{:#?}", p.syntax()));
}
mod proptest_divert {
use proptest::prelude::*;
use crate::parse;
const NUM_CASES: u32 = 256;
const KEYWORDS: &[&str] = &[
"pub", "flow", "fn", "var", "const", "let", "flags", "struct", "extern", "import", "use",
"module", "return", "ref", "if", "match", "else", "while", "for", "in", "until", "break",
"continue", "as", "or", "true", "false", "END", "DONE",
];
fn arb_ident() -> impl Strategy<Value = String> {
"[a-z][a-z0-9_]{0,7}"
.prop_filter("must not be a keyword", |s| !KEYWORDS.contains(&s.as_str()))
}
fn arb_dotted_path() -> impl Strategy<Value = String> {
prop::collection::vec(arb_ident(), 1..=4).prop_map(|segs| segs.join("."))
}
fn arb_divert_target() -> impl Strategy<Value = String> {
prop_oneof![
Just("END".to_string()),
Just("DONE".to_string()),
arb_dotted_path(),
]
}
fn arb_flow_wrapped_divert() -> impl Strategy<Value = String> {
arb_divert_target().prop_map(|target| format!("flow f() {{\n -> {target}\n}}\n"))
}
fn arb_flow_wrapped_tunnel_call() -> impl Strategy<Value = String> {
arb_divert_target().prop_map(|target| format!("flow f() {{\n -> {target} ->\n}}\n"))
}
fn arb_flow_wrapped_return_redirect() -> impl Strategy<Value = String> {
arb_divert_target().prop_map(|target| format!("flow f() {{\n return -> {target}\n}}\n"))
}
fn arb_flow_wrapped_splice() -> impl Strategy<Value = String> {
(arb_dotted_path(), prop::collection::vec(arb_ident(), 0..=3)).prop_map(|(path, args)| {
let call = if args.is_empty() {
path
} else {
format!("{path}({})", args.join(", "))
};
format!("flow f() {{\n {{?\n <- {call}\n }}\n}}\n")
})
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(NUM_CASES))]
#[test]
fn divert_roundtrips(input in arb_flow_wrapped_divert()) {
let parsed = parse(&input);
prop_assert_eq!(parsed.syntax().text().to_string(), input.clone());
prop_assert!(parsed.errors().is_empty(), "input: {:?}\nerrors: {:?}", input, parsed.errors());
}
#[test]
fn tunnel_call_roundtrips(input in arb_flow_wrapped_tunnel_call()) {
let parsed = parse(&input);
prop_assert_eq!(parsed.syntax().text().to_string(), input.clone());
prop_assert!(parsed.errors().is_empty(), "input: {:?}\nerrors: {:?}", input, parsed.errors());
prop_assert!(
parsed.syntax().descendants().any(|n| n.kind() == crate::SyntaxKind::TUNNEL_CALL)
);
}
#[test]
fn return_redirect_roundtrips(input in arb_flow_wrapped_return_redirect()) {
let parsed = parse(&input);
prop_assert_eq!(parsed.syntax().text().to_string(), input.clone());
prop_assert!(parsed.errors().is_empty(), "input: {:?}\nerrors: {:?}", input, parsed.errors());
prop_assert!(
parsed.syntax().descendants().any(|n| n.kind() == crate::SyntaxKind::RETURN_REDIRECT)
);
}
#[test]
fn splice_roundtrips(input in arb_flow_wrapped_splice()) {
let parsed = parse(&input);
prop_assert_eq!(parsed.syntax().text().to_string(), input.clone());
prop_assert!(parsed.errors().is_empty(), "input: {:?}\nerrors: {:?}", input, parsed.errors());
prop_assert!(
parsed.syntax().descendants().any(|n| n.kind() == crate::SyntaxKind::SPLICE)
);
}
#[test]
fn arrow_soup_never_panics(n in 1usize..12) {
let arrows = "-> ".repeat(n);
let src = format!("flow f() {{\n {arrows}\n}}\n");
let parsed = parse(&src);
prop_assert_eq!(parsed.syntax().text().to_string(), src);
}
#[test]
fn truncated_divert_never_panics(target in arb_dotted_path(), cut in 0u32..100) {
let full = format!("flow f() {{\n -> {target} ->\n}}\n");
let target_len = (full.len() as u64 * u64::from(cut) / 100) as usize;
let mut end = target_len.min(full.len());
while end > 0 && !full.is_char_boundary(end) {
end -= 1;
}
let truncated = &full[..end];
let parsed = parse(truncated);
prop_assert_eq!(parsed.syntax().text().to_string(), truncated);
}
#[test]
fn splice_outside_choice_point_never_panics(path in arb_dotted_path()) {
let src = format!("flow f() {{\n <- {path}\n}}\n");
let parsed = parse(&src);
prop_assert_eq!(parsed.syntax().text().to_string(), src);
}
}
}