use super::*;
#[test]
fn minimal_flow_decl() {
let p = assert_lossless("flow greet() {\n}\n");
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
}
#[test]
fn flow_with_prose_body() {
let p = assert_lossless("flow greet(name) {\n Hello, {name}! <>\n}\n");
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
}
#[test]
fn fn_decl_parses() {
let p = assert_lossless("fn heal(hp) {\n let x = hp + 1;\n}\n");
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
}
#[test]
fn use_and_import_and_module() {
let src = "use story::npcs::{guard, merchant as trader};\nimport story::items\nmodule inner {\n var secret = 1\n}\n";
let p = assert_lossless(src);
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
}
#[test]
fn use_decl_semicolon_is_consumed_by_the_decl_not_left_as_prose() {
let src = "use a::b;\n";
let p = assert_lossless(src);
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
let use_decl = p
.syntax()
.children()
.find(|n| n.kind() == SyntaxKind::USE_DECL)
.expect("USE_DECL");
assert!(
use_decl
.children_with_tokens()
.any(|t| t.kind() == SyntaxKind::SEMICOLON),
"expected the `;` inside USE_DECL, tree: {use_decl:#?}"
);
assert_eq!(p.syntax().children().count(), 1);
}
#[test]
fn use_decl_without_semicolon_still_parses() {
let src = "use a::b\n";
let p = assert_lossless(src);
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
}
#[test]
fn var_const_flags_struct_extern() {
let src = "var hp = 10\nconst MAX = 100\nflags Mood = (calm), wary, hostile\nstruct Item {\n name: string,\n weight: int\n}\nextern log(msg)\n";
let p = assert_lossless(src);
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
}
#[test]
fn nested_stitch_flow() {
let src = "flow garden() {\n flow gate() {\n Creak.\n }\n -> gate\n}\n";
let p = assert_lossless(src);
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
}
#[test]
fn leading_doc_comment_attaches_as_flow_decls_first_child() {
let src = "/// Greets the player.\nflow greet() {\n}\n";
let p = assert_lossless(src);
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
let flow: ast::FlowDecl = find_child(&p.syntax()).expect("flow decl");
let first_child = flow
.syntax()
.children()
.next()
.expect("flow decl has a child node");
assert_eq!(first_child.kind(), SyntaxKind::DOC_COMMENT);
assert_eq!(first_child.text(), "/// Greets the player.\n");
}
#[test]
fn multiline_leading_doc_comment_is_one_doc_comment_node() {
let src = "/// Line one.\n/// Line two.\nflow greet() {\n}\n";
let p = assert_lossless(src);
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
let flow: ast::FlowDecl = find_child(&p.syntax()).expect("flow decl");
assert_eq!(count_node_kind(flow.syntax(), SyntaxKind::DOC_COMMENT), 1);
let doc = flow.doc().expect("doc attached");
assert_eq!(doc.lines().len(), 2);
}
#[test]
fn leading_doc_comment_attaches_to_var_const_flags_struct_extern() {
for (src, kind) in [
("/// x\nvar x = 1\n", SyntaxKind::VAR_DECL),
("/// x\nconst x = 1\n", SyntaxKind::CONST_DECL),
("/// x\nflags F = a, b\n", SyntaxKind::FLAGS_DECL),
("/// x\nstruct S { f: int }\n", SyntaxKind::STRUCT_DECL),
("/// x\nextern e(a)\n", SyntaxKind::EXTERN_DECL),
] {
let p = assert_lossless(src);
assert!(p.errors().is_empty(), "{src:?} errors: {:?}", p.errors());
let decl = p.syntax().children().find(|n| n.kind() == kind);
assert!(decl.is_some(), "{kind:?} not found in {src:?}");
let decl = decl.expect("checked above");
let first_child = decl.children().next().expect("decl has a child node");
assert_eq!(
first_child.kind(),
SyntaxKind::DOC_COMMENT,
"{src:?}: doc did not attach as leading child"
);
}
}
#[test]
fn blank_line_breaks_the_leading_doc_run_unattached_falls_back_to_bare_tokens() {
let src = "/// orphaned\n\nflow greet() {\n}\n";
let p = assert_lossless(src);
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
assert!(!has_node_kind(&p.syntax(), SyntaxKind::DOC_COMMENT));
let flow: ast::FlowDecl = find_child(&p.syntax()).expect("flow decl still parses");
assert!(flow.doc().is_none());
}
#[test]
fn doc_comment_with_no_following_declaration_falls_back_to_bare_tokens() {
let src = "/// just some prose after this, no decl\nHello there.\n";
let p = assert_lossless(src);
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
assert!(!has_node_kind(&p.syntax(), SyntaxKind::DOC_COMMENT));
}
#[test]
fn plain_comment_between_doc_lines_breaks_the_run() {
let src = "/// kept\n// not a doc line\nflow greet() {\n}\n";
let p = assert_lossless(src);
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
assert!(!has_node_kind(&p.syntax(), SyntaxKind::DOC_COMMENT));
let flow: ast::FlowDecl = find_child(&p.syntax()).expect("flow decl");
assert!(flow.doc().is_none());
}
#[test]
fn inner_doc_comment_attaches_to_flow_body_block() {
let src = "flow greet() {\n//! Describes this flow from within.\nHi!\n}\n";
let p = assert_lossless(src);
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
let flow: ast::FlowDecl = find_child(&p.syntax()).expect("flow decl");
let body = expect_prose_body(flow.body());
let first_child = body
.syntax()
.children()
.next()
.expect("block has a child node");
assert_eq!(first_child.kind(), SyntaxKind::DOC_COMMENT);
let doc = body.doc().expect("inner doc accessor");
assert!(doc.is_inner());
assert_eq!(doc.lines().len(), 1);
}
#[test]
fn inner_doc_comment_attaches_to_source_file() {
let src = "//! File-level doc.\nflow greet() {\n}\n";
let p = assert_lossless(src);
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
let file = ast::SourceFile::cast(p.syntax()).expect("source file");
let doc = file.doc().expect("file-level inner doc");
assert!(doc.is_inner());
assert_eq!(doc.lines()[0].0, "File-level doc.");
}
#[test]
fn inner_doc_tolerates_leading_blank_lines() {
let src = "flow greet() {\n\n//! still attaches\nHi!\n}\n";
let p = assert_lossless(src);
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
let flow: ast::FlowDecl = find_child(&p.syntax()).expect("flow decl");
let body = expect_prose_body(flow.body());
let doc = body
.doc()
.expect("inner doc still attaches past blank lines");
assert!(doc.is_inner());
}
#[test]
fn inner_doc_after_real_content_does_not_attach_to_block() {
let src = "flow greet() {\nHi!\n//! not attached\n}\n";
let p = assert_lossless(src);
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
let flow: ast::FlowDecl = find_child(&p.syntax()).expect("flow decl");
let body = expect_prose_body(flow.body());
assert!(body.doc().is_none());
assert!(!has_node_kind(body.syntax(), SyntaxKind::DOC_COMMENT));
let leaked = body
.syntax()
.descendants()
.filter(|n| n.kind() == SyntaxKind::TEXT)
.any(|n| n.text().to_string().contains("not attached"));
assert!(!leaked, "doc-comment token folded into visible TEXT");
}
#[test]
fn outer_and_inner_doc_tokens_are_not_trivia_in_the_tree() {
let src = "/// doc\nflow greet() {\n}\n";
let p = assert_lossless(src);
let doc_node = p
.syntax()
.descendants()
.find(|n| n.kind() == SyntaxKind::DOC_COMMENT)
.expect("DOC_COMMENT node exists");
let tok = doc_node
.first_token()
.expect("doc comment node has a token");
assert_eq!(tok.kind(), SyntaxKind::DOC_COMMENT_OUTER);
assert!(!tok.kind().is_trivia());
}
#[test]
fn flow_decl_single_param() {
let p = assert_lossless("flow greet(name) {\n}\n");
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
let flow: ast::FlowDecl = find_child(&p.syntax()).expect("flow decl");
let params: Vec<_> = flow.param_list().expect("param list").params().collect();
assert_eq!(params.len(), 1);
assert_eq!(params[0].name_token().expect("param name").text(), "name");
assert!(!params[0].is_ref());
}
#[test]
fn flow_decl_multiple_params_mixed_ref() {
let p = assert_lossless("flow modify(ref x, y, ref z) {\n}\n");
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
let flow: ast::FlowDecl = find_child(&p.syntax()).expect("flow decl");
let params: Vec<_> = flow.param_list().expect("param list").params().collect();
assert_eq!(params.len(), 3);
let shape: Vec<(String, bool)> = params
.iter()
.map(|p| (p.name_token().expect("name").text().to_string(), p.is_ref()))
.collect();
assert_eq!(
shape,
vec![
("x".to_string(), true),
("y".to_string(), false),
("z".to_string(), true),
]
);
}
#[test]
fn fn_decl_ref_param() {
let p = assert_lossless("fn tweak(ref amount) {\n}\n");
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
let f: ast::FnDecl = find_child(&p.syntax()).expect("fn decl");
let params: Vec<_> = f.param_list().expect("param list").params().collect();
assert_eq!(params.len(), 1);
assert!(params[0].is_ref());
}
#[test]
fn fn_plain_brace_defaults_to_code_ground() {
let p = assert_lossless("fn heal(hp) {\n return hp;\n}\n");
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
let f: ast::FnDecl = find_child(&p.syntax()).expect("fn decl");
assert!(
matches!(f.body(), Some(ast::Body::Code(_))),
"plain `{{ }}` on a fn must default to STMT_BLOCK, got {:?}",
f.body()
);
}
#[test]
fn flow_plain_brace_defaults_to_prose_ground() {
let p = assert_lossless("flow greet() {\n Hi.\n}\n");
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
let f: ast::FlowDecl = find_child(&p.syntax()).expect("flow decl");
assert!(
matches!(f.body(), Some(ast::Body::Prose(_))),
"plain `{{ }}` on a flow must default to BLOCK, got {:?}",
f.body()
);
}
#[test]
fn fn_prose_override_selects_block() {
let p = assert_lossless("fn heal(hp) >{\n Hi.\n}\n");
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
let f: ast::FnDecl = find_child(&p.syntax()).expect("fn decl");
assert!(
matches!(f.body(), Some(ast::Body::Prose(_))),
"`>{{ }}` on a fn must select BLOCK, got {:?}",
f.body()
);
assert!(
f.syntax()
.children_with_tokens()
.any(|t| t.kind() == SyntaxKind::GT)
);
}
#[test]
fn flow_code_override_selects_stmt_block() {
let p = assert_lossless("flow guard() ~{\n let ok = true;\n}\n");
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
let f: ast::FlowDecl = find_child(&p.syntax()).expect("flow decl");
assert!(
matches!(f.body(), Some(ast::Body::Code(_))),
"`~{{ }}` on a flow must select STMT_BLOCK, got {:?}",
f.body()
);
assert!(
f.syntax()
.children_with_tokens()
.any(|t| t.kind() == SyntaxKind::TILDE)
);
}
#[test]
fn body_selector_tolerates_whitespace_before_the_brace() {
let p = assert_lossless("fn heal(hp) > {\n Hi.\n}\n");
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
let f: ast::FnDecl = find_child(&p.syntax()).expect("fn decl");
assert!(matches!(f.body(), Some(ast::Body::Prose(_))));
}
#[test]
fn missing_body_after_selector_prefix_still_errors() {
let p = crate::parse("fn heal(hp)\n");
assert!(
p.errors()
.iter()
.any(|e| e.message.contains("expected a braced body")),
"errors: {:?}",
p.errors()
);
}
#[test]
fn flow_decl_empty_param_list() {
let p = assert_lossless("flow greet() {\n}\n");
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
let flow: ast::FlowDecl = find_child(&p.syntax()).expect("flow decl");
assert_eq!(flow.param_list().expect("param list").params().count(), 0);
}
#[test]
fn flow_decl_no_param_list_at_all() {
let p = assert_lossless("flow greet {\n}\n");
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
let flow: ast::FlowDecl = find_child(&p.syntax()).expect("flow decl");
assert!(flow.param_list().is_none());
}
#[test]
fn multiple_sibling_stitches_under_one_flow() {
let src =
"flow garden() {\n flow gate() {\n Creak.\n }\n flow shed() {\n Dusty.\n }\n}\n";
let p = assert_lossless(src);
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
let flow: ast::FlowDecl = find_child(&p.syntax()).expect("flow decl");
let stitches: Vec<_> = flow.stitches().collect();
assert_eq!(stitches.len(), 2);
assert_eq!(stitches[0].name_token().expect("name").text(), "gate");
assert_eq!(stitches[1].name_token().expect("name").text(), "shed");
}
#[test]
fn doubly_nested_stitch_flow() {
let src = "flow outer() {\n flow middle() {\n flow inner() {\n Deep.\n }\n }\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), 3);
}
#[test]
fn var_decl_value_accessor_returns_the_initializer_node() {
let p = assert_lossless("var hp = 10\n");
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
let v: ast::VarDecl = find_child(&p.syntax()).expect("var decl");
assert_eq!(v.name_token().expect("name").text(), "hp");
let value = v.value().expect("initializer");
assert_eq!(value.kind(), SyntaxKind::INTEGER_LIT);
}
#[test]
fn const_decl_value_accessor_returns_the_initializer_node() {
let p = assert_lossless("const MAX = 100\n");
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
let c: ast::ConstDecl = find_child(&p.syntax()).expect("const decl");
assert_eq!(c.name_token().expect("name").text(), "MAX");
let value = c.value().expect("initializer");
assert_eq!(value.kind(), SyntaxKind::INTEGER_LIT);
}
#[test]
fn var_without_initializer_shape_is_prose_not_a_decl() {
let src = "var hp on the wall.\n";
let p = assert_lossless(src);
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
assert!(!has_node_kind(&p.syntax(), SyntaxKind::VAR_DECL));
}
#[test]
fn const_without_initializer_shape_is_prose_not_a_decl() {
let src = "const answers are hard to find.\n";
let p = assert_lossless(src);
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
assert!(!has_node_kind(&p.syntax(), SyntaxKind::CONST_DECL));
}
#[test]
fn var_decl_square_bracket_after_type_name_fails_loudly_instead_of_dropping_to_content() {
let p = parse("var x: Option[int] = none\n");
assert_eq!(
p.errors().first().map(|e| e.message.as_str()),
Some("expected `<` or end of type name, found L_BRACKET"),
"errors: {:?}",
p.errors()
);
}
#[test]
fn const_decl_square_bracket_after_type_name_fails_loudly_instead_of_dropping_to_content() {
let p = parse("const MAX: Option[int] = none\n");
assert_eq!(
p.errors().first().map(|e| e.message.as_str()),
Some("expected `<` or end of type name, found L_BRACKET"),
"errors: {:?}",
p.errors()
);
}
#[test]
fn fn_param_square_bracket_after_type_name_gets_the_unified_message_first() {
let p = parse("fn f(x: Option[int]) {}\n");
assert_eq!(
p.errors().first().map(|e| e.message.as_str()),
Some("expected `<` or end of type name, found L_BRACKET"),
"errors: {:?}",
p.errors()
);
assert!(
p.errors().len() > 1,
"recovery cascade is unchanged by #2792 (message-only fix): {:?}",
p.errors()
);
}
#[test]
fn flow_param_square_bracket_after_type_name_gets_the_unified_message_first() {
let p = parse("flow f(x: Option[int]) {}\n");
assert_eq!(
p.errors().first().map(|e| e.message.as_str()),
Some("expected `<` or end of type name, found L_BRACKET"),
"errors: {:?}",
p.errors()
);
}
#[test]
fn fn_return_type_square_bracket_after_type_name_gets_the_unified_message_first() {
let p = parse("fn f(): Option[int] { none }\n");
assert_eq!(
p.errors().first().map(|e| e.message.as_str()),
Some("expected `<` or end of type name, found L_BRACKET"),
"errors: {:?}",
p.errors()
);
}
#[test]
fn struct_field_square_bracket_after_type_name_gets_the_unified_message_first() {
let p = parse("struct S { x: Option[int] }\n");
assert_eq!(
p.errors().first().map(|e| e.message.as_str()),
Some("expected `<` or end of type name, found L_BRACKET"),
"errors: {:?}",
p.errors()
);
}
#[test]
fn nested_type_argument_square_bracket_mistake_is_reported_once_not_twice() {
let p = parse("var x: List<Option[int]> = none\n");
let unified: Vec<_> = p
.errors()
.iter()
.filter(|e| e.message == "expected `<` or end of type name, found L_BRACKET")
.collect();
assert_eq!(
unified.len(),
1,
"the unified diagnostic should fire once, from the inner `Option[` \
mistake, not again from the outer `List<…>` that never closed; \
errors: {:?}",
p.errors()
);
}
#[test]
fn flags_member_is_active_distinguishes_parenthesized_members() {
let p = assert_lossless("flags Mood = (calm), wary, (hostile)\n");
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
let decl: ast::FlagsDecl = find_child(&p.syntax()).expect("flags decl");
let members: Vec<_> = decl.member_list().expect("member list").members().collect();
assert_eq!(members.len(), 3);
let shape: Vec<(String, bool)> = members
.iter()
.map(|m| {
(
m.name_token().expect("name").text().to_string(),
m.is_active(),
)
})
.collect();
assert_eq!(
shape,
vec![
("calm".to_string(), true),
("wary".to_string(), false),
("hostile".to_string(), true),
]
);
}
#[test]
fn flags_member_all_bare_no_active_markers() {
let p = assert_lossless("flags Colors = red, green, blue\n");
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
let decl: ast::FlagsDecl = find_child(&p.syntax()).expect("flags decl");
let members: Vec<_> = decl.member_list().expect("member list").members().collect();
assert_eq!(members.len(), 3);
assert!(members.iter().all(|m| !m.is_active()));
}
#[test]
fn flags_member_all_parenthesized() {
let p = assert_lossless("flags Colors = (red), (green), (blue)\n");
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
let decl: ast::FlagsDecl = find_child(&p.syntax()).expect("flags decl");
let members: Vec<_> = decl.member_list().expect("member list").members().collect();
assert!(members.iter().all(ast::FlagsMember::is_active));
}
#[test]
fn flags_decl_single_member() {
let p = assert_lossless("flags Solo = only\n");
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
let decl: ast::FlagsDecl = find_child(&p.syntax()).expect("flags decl");
assert_eq!(
decl.member_list().expect("member list").members().count(),
1
);
}
#[test]
fn flags_decl_dangling_no_members_after_eq_parses_with_empty_member_list() {
let src = "flags F =\n";
let p = assert_lossless(src);
assert!(
p.errors()
.iter()
.any(|e| e.message.contains("flags member")),
"expected a 'flags member' error, got: {:?}",
p.errors()
);
let decl: ast::FlagsDecl = find_child(&p.syntax()).expect("flags decl still parses");
assert_eq!(
decl.member_list()
.expect("member list node still present")
.members()
.count(),
0
);
}
#[test]
fn flags_decl_explicit_empty_parens_is_the_empty_set_no_error() {
let src = "flags F = ()\n";
let p = assert_lossless(src);
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
let decl: ast::FlagsDecl = find_child(&p.syntax()).expect("flags decl");
assert_eq!(
decl.member_list()
.expect("member list node present")
.members()
.count(),
0
);
}
#[test]
fn flags_without_eq_shape_is_prose_not_a_decl() {
let src = "flags are usually red or white.\n";
let p = assert_lossless(src);
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
assert!(!has_node_kind(&p.syntax(), SyntaxKind::FLAGS_DECL));
}
fn expect_type_name(te: &ast::TypeExpr) -> String {
match te.kind() {
Some(ast::TypeExprKind::Name(n)) => n.name(),
_ => None,
}
.expect("expected a bare type name")
}
fn expect_type_generic(te: &ast::TypeExpr) -> ast::TypeGeneric {
match te.kind() {
Some(ast::TypeExprKind::Generic(g)) => Some(g),
_ => None,
}
.expect("expected a generic type")
}
fn expect_type_fn(te: &ast::TypeExpr) -> ast::TypeFn {
match te.kind() {
Some(ast::TypeExprKind::Fn(f)) => Some(f),
_ => None,
}
.expect("expected a fn type")
}
#[test]
fn struct_decl_single_field() {
let p = assert_lossless("struct Wrapper {\n value: int\n}\n");
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
let decl: ast::StructDecl = find_child(&p.syntax()).expect("struct decl");
let fields: Vec<_> = decl.fields().collect();
assert_eq!(fields.len(), 1);
assert_eq!(fields[0].name_token().expect("name").text(), "value");
let ty = fields[0]
.type_annotation()
.expect("type annotation")
.type_expr()
.expect("type expr");
assert_eq!(expect_type_name(&ty), "int");
}
#[test]
fn struct_decl_multiple_fields() {
let p = assert_lossless("struct Item {\n name: string,\n weight: int\n}\n");
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
let decl: ast::StructDecl = find_child(&p.syntax()).expect("struct decl");
let names: Vec<_> = decl
.fields()
.map(|f| f.name_token().expect("name").text().to_string())
.collect();
assert_eq!(names, vec!["name", "weight"]);
}
#[test]
fn struct_decl_empty_body() {
let p = assert_lossless("struct Empty {\n}\n");
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
let decl: ast::StructDecl = find_child(&p.syntax()).expect("struct decl");
assert_eq!(decl.fields().count(), 0);
}
#[test]
fn struct_field_dotted_type_path_is_a_documented_gap_not_a_panic() {
let p = assert_lossless("struct Wrapper {\n loc: geo::Point\n}\n");
assert!(
!p.errors().is_empty(),
"expected the unsupported `::`-qualified type name to surface at least one error"
);
assert!(has_node_kind(&p.syntax(), SyntaxKind::STRUCT_DECL));
let p = assert_lossless("struct W {\n loc: geo.Point\n}\n");
assert!(
!p.errors().is_empty(),
"expected the unsupported `.`-qualified type name to surface at least one error"
);
assert!(has_node_kind(&p.syntax(), SyntaxKind::STRUCT_DECL));
}
#[test]
fn struct_decl_missing_colon_recovers() {
let src = "struct S {\n name int\n}\n";
let p = assert_lossless(src);
assert!(!p.errors().is_empty(), "expected a missing-colon error");
assert!(has_node_kind(&p.syntax(), SyntaxKind::STRUCT_DECL));
assert!(has_node_kind(&p.syntax(), SyntaxKind::STRUCT_FIELD));
}
#[test]
fn struct_decl_unexpected_token_in_body_recovers() {
let src = "struct S {\n 1\n ok: int\n}\n";
let p = assert_lossless(src);
assert!(
p.errors().iter().any(|e| e.message.contains("struct body")),
"expected 'unexpected token in struct body' error, got: {:?}",
p.errors()
);
assert!(has_node_kind(&p.syntax(), SyntaxKind::ERROR));
let decl: ast::StructDecl = find_child(&p.syntax()).expect("struct decl still recovers");
assert_eq!(
decl.fields().count(),
1,
"the well-formed field after the garbage still parses"
);
}
#[test]
fn struct_decl_generic_field_type() {
let src = "struct Bag {\n items: List<int>\n}\n";
let p = assert_lossless(src);
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
let decl: ast::StructDecl = find_child(&p.syntax()).expect("struct decl");
let field = decl.fields().next().expect("field");
let ty = field
.type_annotation()
.expect("type annotation")
.type_expr()
.expect("type expr");
let g = expect_type_generic(&ty);
assert_eq!(g.name().as_deref(), Some("List"));
let arg_names: Vec<_> = g.args().map(|a| expect_type_name(&a)).collect();
assert_eq!(arg_names, vec!["int".to_string()]);
}
#[test]
fn struct_decl_map_field_type() {
let src = "struct Registry {\n items: Map<string, int>\n}\n";
let p = assert_lossless(src);
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
let decl: ast::StructDecl = find_child(&p.syntax()).expect("struct decl");
let field = decl.fields().next().expect("field");
let ty = field
.type_annotation()
.expect("type annotation")
.type_expr()
.expect("type expr");
let g = expect_type_generic(&ty);
assert_eq!(g.name().as_deref(), Some("Map"));
let arg_names: Vec<_> = g.args().map(|a| expect_type_name(&a)).collect();
assert_eq!(arg_names, vec!["string".to_string(), "int".to_string()]);
}
#[test]
fn struct_decl_fn_typed_field() {
let src = "struct Guest {\n greet: fn(string): string\n}\n";
let p = assert_lossless(src);
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
let decl: ast::StructDecl = find_child(&p.syntax()).expect("struct decl");
let field = decl.fields().next().expect("field");
let ty = field
.type_annotation()
.expect("type annotation")
.type_expr()
.expect("type expr");
let f = expect_type_fn(&ty);
let param_names: Vec<_> = f.params().iter().map(expect_type_name).collect();
assert_eq!(param_names, vec!["string".to_string()]);
let ret = f.return_type().expect("return type");
assert_eq!(expect_type_name(&ret), "string");
}
#[test]
fn struct_without_brace_shape_is_prose_not_a_decl() {
let src = "struct is just a word in this sentence.\n";
let p = assert_lossless(src);
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
assert!(!has_node_kind(&p.syntax(), SyntaxKind::STRUCT_DECL));
}
#[test]
fn extern_decl_no_params() {
let p = assert_lossless("extern log()\n");
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
let decl: ast::ExternDecl = find_child(&p.syntax()).expect("extern decl");
assert_eq!(decl.name_token().expect("name").text(), "log");
assert_eq!(decl.param_list().expect("param list").params().count(), 0);
}
#[test]
fn extern_decl_multiple_params() {
let p = assert_lossless("extern setBrightness(x, y, ref z)\n");
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
let decl: ast::ExternDecl = find_child(&p.syntax()).expect("extern decl");
assert_eq!(decl.param_list().expect("param list").params().count(), 3);
}
#[test]
fn extern_decl_bare_name_with_no_parens_is_prose_not_a_decl() {
let src = "extern log\n";
let p = assert_lossless(src);
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
assert!(!has_node_kind(&p.syntax(), SyntaxKind::EXTERN_DECL));
}
#[test]
fn extern_without_paren_shape_is_prose_not_a_decl() {
let src = "extern circumstances prevented it.\n";
let p = assert_lossless(src);
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
assert!(!has_node_kind(&p.syntax(), SyntaxKind::EXTERN_DECL));
}
#[test]
fn missing_param_close_paren_recovers() {
let src = "flow greet(a, b {\n}\n";
let p = assert_lossless(src);
assert!(
p.errors().iter().any(|e| e.message.contains("R_PAREN")),
"expected an R_PAREN error, got: {:?}",
p.errors()
);
let flow: ast::FlowDecl = find_child(&p.syntax()).expect("flow decl still recovers");
assert!(
flow.body().is_some(),
"body still parses after the missing `)`"
);
}
#[test]
fn param_ref_without_following_name_records_error_but_recovers() {
let src = "flow f(ref) {\n}\n";
let p = assert_lossless(src);
assert!(
!p.errors().is_empty(),
"expected a missing-param-name error"
);
let flow: ast::FlowDecl = find_child(&p.syntax()).expect("flow decl still recovers");
assert!(flow.body().is_some());
}
#[test]
fn unexpected_token_in_param_list_recovers() {
let src = "flow f(, a) {\n}\n";
let p = assert_lossless(src);
assert!(
p.errors()
.iter()
.any(|e| e.message.contains("parameter list")),
"expected 'unexpected token in parameter list' error, got: {:?}",
p.errors()
);
let flow: ast::FlowDecl = find_child(&p.syntax()).expect("flow decl still recovers");
let params: Vec<_> = flow.param_list().expect("param list").params().collect();
assert_eq!(
params.len(),
2,
"the zero-width nameless PARAM left by the garbage comma is a real \
sibling of the well-formed param, not absorbed by it: {:?}",
params
.iter()
.map(|p| p.syntax().text().to_string())
.collect::<Vec<_>>()
);
assert!(
params[0].name_token().is_none(),
"the garbage comma produced a nameless PARAM"
);
assert_eq!(
params[1]
.name_token()
.expect("second param has a name")
.text(),
"a"
);
assert_eq!(
params
.iter()
.map(|p| p.syntax().text().to_string())
.collect::<Vec<_>>(),
vec![String::new(), " a".to_string()],
"the nameless PARAM is zero-width; the well-formed one still carries its leading space"
);
}
#[test]
fn import_decl_single_segment_path() {
let p = assert_lossless("import items\n");
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
let decl: ast::ImportDecl = find_child(&p.syntax()).expect("import decl");
let segs: Vec<_> = decl
.path()
.expect("path")
.segments()
.map(|t| t.text().to_string())
.collect();
assert_eq!(segs, vec!["items"]);
}
#[test]
fn import_decl_dotted_path() {
let p = assert_lossless("import story::items.detail\n");
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
let decl: ast::ImportDecl = find_child(&p.syntax()).expect("import decl");
let path = decl.path().expect("path");
assert!(path.crosses_module_wall());
let segs: Vec<_> = path.segments().map(|t| t.text().to_string()).collect();
assert_eq!(segs, vec!["story", "items", "detail"]);
}
#[test]
fn leading_doc_comment_attaches_to_import_decl() {
let src = "/// what this pulls in\nimport story::items\n";
let p = assert_lossless(src);
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
let decl: ast::ImportDecl = find_child(&p.syntax()).expect("import decl");
assert!(decl.doc().is_some());
}
#[test]
fn use_decl_bare_path_no_alias_no_group() {
let p = assert_lossless("use story::npcs::guard;\n");
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
let decl: ast::UseDecl = find_child(&p.syntax()).expect("use decl");
let tree = decl.tree().expect("use tree");
let segs: Vec<_> = tree.path_segments().map(|t| t.text().to_string()).collect();
assert_eq!(segs, vec!["story", "npcs", "guard"]);
assert!(tree.alias_token().is_none());
assert!(tree.nested_list().is_none());
}
#[test]
fn use_decl_aliased_form() {
let p = assert_lossless("use story::npcs::merchant as trader;\n");
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
let decl: ast::UseDecl = find_child(&p.syntax()).expect("use decl");
let tree = decl.tree().expect("use tree");
assert_eq!(tree.alias_token().expect("alias").text(), "trader");
}
#[test]
fn use_decl_nested_group_form() {
let p = assert_lossless("use story::npcs::{guard, merchant as trader};\n");
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
let decl: ast::UseDecl = find_child(&p.syntax()).expect("use decl");
let tree = decl.tree().expect("use tree");
let group = tree.nested_list().expect("nested group");
let members: Vec<_> = group.trees().collect();
assert_eq!(members.len(), 2);
assert_eq!(
members[0]
.path_segments()
.map(|t| t.text().to_string())
.collect::<Vec<_>>(),
vec!["guard"]
);
assert_eq!(members[1].alias_token().expect("alias").text(), "trader");
}
#[test]
fn use_decl_nested_group_of_groups() {
let p = assert_lossless("use a::{b::{c, d}, e};\n");
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
assert_eq!(count_node_kind(&p.syntax(), SyntaxKind::USE_TREE_LIST), 2);
assert_eq!(count_node_kind(&p.syntax(), SyntaxKind::USE_TREE), 5);
}
#[test]
fn use_decl_bare_group_with_no_leading_path_is_a_parse_error() {
let src = "use {a, b};\n";
let p = assert_lossless(src);
assert!(!has_node_kind(&p.syntax(), SyntaxKind::USE_DECL));
assert!(
p.errors()
.iter()
.any(|e| e.message.contains("expected R_BRACE")),
"documents the misparse-as-interpolation fallout, got: {:?}",
p.errors()
);
}
#[test]
fn use_tree_list_nested_bare_group_with_no_leading_path_errors() {
let src = "use a::{ {b, c} };\n";
let p = assert_lossless(src);
assert!(
p.errors()
.iter()
.any(|e| e.message.contains("needs a module path")),
"expected the use-tree error, got: {:?}",
p.errors()
);
}
#[test]
fn use_tree_malformed_missing_path_does_not_commit() {
let src = "use ::foo;\n";
let p = assert_lossless(src);
assert!(
p.errors()
.iter()
.any(|e| e.message.contains("a `use` path cannot start with `::`")),
"expected the leading-`::` diagnostic, got: {:?}",
p.errors()
);
let decl: Option<ast::UseDecl> = find_child(&p.syntax());
assert!(
decl.is_none(),
"no USE_DECL should be created for `use ::foo;`"
);
}
#[test]
fn use_tree_list_unexpected_token_recovers() {
let src = "use a::{1, b};\n";
let p = assert_lossless(src);
assert!(
p.errors().len() >= 2,
"expected at least 2 recovery errors, got: {:?}",
p.errors()
);
assert_eq!(
count_node_kind(&p.syntax(), SyntaxKind::USE_TREE),
4,
"outer a::{{...}} tree + 2 empty garbage-token artifacts + the real `b`"
);
let decl: ast::UseDecl = find_child(&p.syntax()).expect("use decl still recovers");
let group = decl
.tree()
.expect("use tree")
.nested_list()
.expect("nested group");
let members: Vec<_> = group.trees().collect();
assert_eq!(
members.len(),
3,
"2 empty USE_TREE artifacts from the garbage `1` and `,`, plus the real `b`"
);
let names: Vec<_> = members
.iter()
.flat_map(|t| {
t.path_segments()
.map(|tok| tok.text().to_string())
.collect::<Vec<_>>()
})
.collect();
assert_eq!(names, vec!["b"]);
}
#[test]
fn use_decl_optional_semicolon_both_present_and_absent_round_trip() {
for src in ["use a::b;\n", "use a::b\n"] {
let p = assert_lossless(src);
assert!(p.errors().is_empty(), "{src:?} errors: {:?}", p.errors());
assert!(has_node_kind(&p.syntax(), SyntaxKind::USE_DECL));
}
}
#[test]
fn leading_doc_comment_attaches_to_use_decl() {
let src = "/// bring these into scope\nuse a::b;\n";
let p = assert_lossless(src);
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
let decl: ast::UseDecl = find_child(&p.syntax()).expect("use decl");
assert!(decl.doc().is_some());
}
#[test]
fn module_decl_basic() {
let p = assert_lossless("module inner {\n var secret = 1\n}\n");
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
let decl: ast::ModuleDecl = find_child(&p.syntax()).expect("module decl");
assert_eq!(decl.name_token().expect("name").text(), "inner");
let body = decl.body().expect("module body");
assert!(has_node_kind(body.syntax(), SyntaxKind::VAR_DECL));
}
#[test]
fn module_decl_nested_module() {
let p = assert_lossless("module outer {\n module inner {\n var x = 1\n }\n}\n");
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
assert_eq!(count_node_kind(&p.syntax(), SyntaxKind::MODULE_DECL), 2);
}
#[test]
fn leading_doc_comment_attaches_to_module_decl() {
let src = "/// groups the secret stuff\nmodule inner {\n}\n";
let p = assert_lossless(src);
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
let decl: ast::ModuleDecl = find_child(&p.syntax()).expect("module decl");
assert!(decl.doc().is_some());
}
#[test]
fn module_without_brace_shape_is_prose_not_a_decl() {
let src = "module citizens gathered in the square.\n";
let p = assert_lossless(src);
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
assert!(!has_node_kind(&p.syntax(), SyntaxKind::MODULE_DECL));
}
#[test]
fn module_decl_missing_closing_brace_recovers_via_block_eof() {
let src = "module inner {\n var x = 1\n";
let p = assert_lossless(src);
assert_eq!(p.errors().len(), 1, "errors: {:?}", p.errors());
assert!(
p.errors()[0].message.contains("R_BRACE") && p.errors()[0].message.contains("EOF"),
"expected the block's missing-R_BRACE-at-EOF error, got: {:?}",
p.errors()
);
let decl: ast::ModuleDecl = find_child(&p.syntax()).expect("module decl still recovers");
assert!(
decl.body().is_some(),
"the unterminated body still parses as BLOCK"
);
}
fn type_heads(node: &SyntaxNode) -> Vec<String> {
node.descendants()
.filter_map(ast::TypeExpr::cast)
.filter_map(|te| match te.kind()? {
ast::TypeExprKind::Name(n) => n.name(),
ast::TypeExprKind::Generic(g) => g.name(),
ast::TypeExprKind::Fn(_) => Some("fn".to_string()),
})
.collect()
}
#[test]
fn fn_param_takes_a_type_annotation() {
let p = assert_lossless("fn probability(g: Guest) {\n return 1;\n}\n");
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
let decl: ast::FnDecl = find_child(&p.syntax()).expect("fn decl");
let param = decl
.param_list()
.expect("param list")
.params()
.next()
.expect("one param");
assert_eq!(param.name_token().expect("name").text(), "g");
let annotation = param.type_annotation().expect("`: Guest` annotation");
assert_eq!(type_heads(annotation.syntax()), vec!["Guest".to_string()]);
}
#[test]
fn unannotated_param_still_parses_with_no_annotation() {
let p = assert_lossless("fn heal(hp) {\n return hp;\n}\n");
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
let decl: ast::FnDecl = find_child(&p.syntax()).expect("fn decl");
let param = decl
.param_list()
.expect("param list")
.params()
.next()
.expect("one param");
assert!(param.type_annotation().is_none());
}
#[test]
fn ref_param_takes_a_type_annotation_after_the_name() {
let p = assert_lossless("flow spend(ref gold: int) {\n Spent.\n}\n");
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
let decl: ast::FlowDecl = find_child(&p.syntax()).expect("flow decl");
let param = decl
.param_list()
.expect("param list")
.params()
.next()
.expect("one param");
assert!(param.is_ref());
assert_eq!(
type_heads(param.type_annotation().expect("annotation").syntax()),
vec!["int".to_string()]
);
}
#[test]
fn extern_params_take_type_annotations_via_the_shared_param_list() {
let p = assert_lossless("extern log(msg: string, level: int)\n");
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
let decl: ast::ExternDecl = find_child(&p.syntax()).expect("extern decl");
let heads: Vec<String> = decl
.param_list()
.expect("param list")
.params()
.map(|param| type_heads(param.type_annotation().expect("annotation").syntax()).join(""))
.collect();
assert_eq!(heads, vec!["string".to_string(), "int".to_string()]);
}
#[test]
fn generic_and_nested_generic_type_arguments_parse() {
let p = assert_lossless("fn tally(m: Map<string, List<int>>) {\n return 1;\n}\n");
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
let decl: ast::FnDecl = find_child(&p.syntax()).expect("fn decl");
let annotation = decl
.param_list()
.expect("param list")
.params()
.next()
.expect("one param")
.type_annotation()
.expect("annotation");
assert_eq!(
type_heads(annotation.syntax()),
vec![
"Map".to_string(),
"string".to_string(),
"List".to_string(),
"int".to_string()
]
);
}
#[test]
fn fn_type_annotation_parses_with_its_colon_return() {
let p = assert_lossless("fn apply(f: fn(int): bool) {\n return 1;\n}\n");
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
let decl: ast::FnDecl = find_child(&p.syntax()).expect("fn decl");
let annotation = decl
.param_list()
.expect("param list")
.params()
.next()
.expect("one param")
.type_annotation()
.expect("annotation");
let te = annotation.type_expr().expect("type expr");
let Some(ast::TypeExprKind::Fn(f)) = te.kind() else {
unreachable!("expected a fn type, tree: {:#?}", te.syntax())
};
assert_eq!(f.params().len(), 1);
assert_eq!(
type_heads(f.return_type().expect("return type").syntax()),
vec!["bool".to_string()]
);
}
#[test]
fn fn_header_takes_a_colon_return_type_after_the_param_list() {
let p = assert_lossless("fn probability(g: Guest): float {\n return 1;\n}\n");
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
let decl: ast::FnDecl = find_child(&p.syntax()).expect("fn decl");
assert_eq!(
type_heads(decl.return_type().expect("return clause").syntax()),
vec!["float".to_string()]
);
let param = decl
.param_list()
.expect("param list")
.params()
.next()
.expect("one param");
assert_eq!(
type_heads(param.type_annotation().expect("annotation").syntax()),
vec!["Guest".to_string()]
);
assert!(
decl.body().is_some(),
"the body still parses after `: float`"
);
}
#[test]
fn flow_header_takes_a_colon_return_type_on_an_empty_param_list() {
let p = assert_lossless("flow quest(): QuestResult {\n return;\n}\n");
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
let decl: ast::FlowDecl = find_child(&p.syntax()).expect("flow decl");
assert_eq!(
type_heads(decl.return_type().expect("return clause").syntax()),
vec!["QuestResult".to_string()]
);
}
#[test]
fn plain_flow_header_has_no_return_clause() {
let p = assert_lossless("flow greet() {\n Hi.\n}\n");
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
let decl: ast::FlowDecl = find_child(&p.syntax()).expect("flow decl");
assert!(decl.return_type().is_none());
}
#[test]
fn a_prose_line_starting_with_flow_is_still_prose_not_a_return_typed_decl() {
let src = "flow onwards: the river bends.\n";
let p = assert_lossless(src);
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
assert!(
!has_node_kind(&p.syntax(), SyntaxKind::FLOW_DECL),
"tree: {:#?}",
p.syntax()
);
}
#[test]
fn var_and_const_take_type_annotations_before_the_initializer() {
let p = assert_lossless("var hp: int = 10\nconst MAX: int = 100\n");
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
let var: ast::VarDecl = find_child(&p.syntax()).expect("var decl");
assert_eq!(
type_heads(var.type_annotation().expect("annotation").syntax()),
vec!["int".to_string()]
);
assert_eq!(
var.value().expect("initializer").kind(),
SyntaxKind::INTEGER_LIT
);
let konst: ast::ConstDecl = find_child(&p.syntax()).expect("const decl");
assert_eq!(
type_heads(konst.type_annotation().expect("annotation").syntax()),
vec!["int".to_string()]
);
assert_eq!(
konst.value().expect("initializer").kind(),
SyntaxKind::INTEGER_LIT
);
}
#[test]
fn unannotated_var_initializer_is_unchanged() {
let p = assert_lossless("var hp = 10\n");
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
let var: ast::VarDecl = find_child(&p.syntax()).expect("var decl");
assert!(var.type_annotation().is_none());
assert_eq!(
var.value().expect("initializer").kind(),
SyntaxKind::INTEGER_LIT
);
}
#[test]
fn a_type_annotation_with_no_type_after_it_records_an_error() {
let p = assert_lossless("var hp: = 10\n");
assert!(
!p.errors().is_empty(),
"a bare `:` with no type must be diagnosed"
);
}
#[test]
fn pub_prefixes_every_visibility_bearing_declaration() {
for (src, kind) in [
("pub flow greet() {\n}\n", SyntaxKind::FLOW_DECL),
("pub fn heal() {\n}\n", SyntaxKind::FN_DECL),
("pub var hp = 10\n", SyntaxKind::VAR_DECL),
("pub const MAX = 100\n", SyntaxKind::CONST_DECL),
("pub flags Mood = calm, wary\n", SyntaxKind::FLAGS_DECL),
("pub struct Npc {\n hp: int\n}\n", SyntaxKind::STRUCT_DECL),
("pub extern log_msg(msg)\n", SyntaxKind::EXTERN_DECL),
] {
let p = assert_lossless(src);
assert!(p.errors().is_empty(), "{src:?} errors: {:?}", p.errors());
let decl = p
.syntax()
.children()
.find(|n| n.kind() == kind)
.expect("declaration kind not found");
assert!(
decl.children_with_tokens()
.filter_map(rowan::NodeOrToken::into_token)
.any(|t| t.kind() == SyntaxKind::KW_PUB),
"{src:?}: expected a KW_PUB child of {kind:?}, tree: {decl:#?}"
);
}
}
#[test]
fn nested_pub_flow_stitch_also_carries_the_marker() {
let src = "flow garden() {\n pub flow gate() {\n Creak.\n }\n}\n";
let p = assert_lossless(src);
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
let outer: ast::FlowDecl = find_child(&p.syntax()).expect("outer flow decl");
assert!(!outer.is_pub(), "the outer flow was not marked pub");
let inner = outer.stitches().next().expect("nested flow (stitch)");
assert!(inner.is_pub(), "the nested `pub flow` must report is_pub()");
}
#[test]
fn absent_pub_means_not_pub() {
let p = assert_lossless("flow greet() {\n}\n");
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
let flow: ast::FlowDecl = find_child(&p.syntax()).expect("flow decl");
assert!(!flow.is_pub());
}
#[test]
fn pub_not_followed_by_a_declaration_falls_back_to_prose() {
let src = "pub is a nice place to eat.\n";
let p = assert_lossless(src);
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
assert!(
!has_node_kind(&p.syntax(), SyntaxKind::FLOW_DECL)
&& !has_node_kind(&p.syntax(), SyntaxKind::FN_DECL)
&& !has_node_kind(&p.syntax(), SyntaxKind::VAR_DECL),
"must not be mis-parsed as any declaration: {:#?}",
p.syntax()
);
let content = p
.syntax()
.children()
.find(|n| n.kind() == SyntaxKind::CONTENT_LINE);
assert!(content.is_some(), "expected a plain CONTENT_LINE fallback");
}
#[test]
fn pub_is_not_recognized_before_import_use_or_module() {
for src in [
"pub use story::a;\n",
"pub import story::a\n",
"pub module thing entirely.\n",
] {
let p = assert_lossless(src);
assert!(p.errors().is_empty(), "{src:?} errors: {:?}", p.errors());
assert!(
!has_node_kind(&p.syntax(), SyntaxKind::USE_DECL)
&& !has_node_kind(&p.syntax(), SyntaxKind::IMPORT_DECL)
&& !has_node_kind(&p.syntax(), SyntaxKind::MODULE_DECL),
"{src:?}: `pub` must not be consumed ahead of use/import/module: {:#?}",
p.syntax()
);
}
}
#[test]
fn doc_comment_still_attaches_when_pub_sits_between_it_and_the_keyword() {
let src = "/// Heals.\npub fn heal() {\n return 1;\n}\n";
let p = assert_lossless(src);
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
let fn_decl: ast::FnDecl = find_child(&p.syntax()).expect("fn decl");
assert!(fn_decl.is_pub());
let doc = fn_decl.doc().expect("doc attached");
assert_eq!(doc.lines().len(), 1);
assert!(
fn_decl
.syntax()
.children_with_tokens()
.filter_map(rowan::NodeOrToken::into_token)
.any(|t| t.kind() == SyntaxKind::KW_PUB),
"expected KW_PUB as a direct FN_DECL child, tree: {:#?}",
fn_decl.syntax()
);
assert_eq!(
count_node_kind(fn_decl.syntax(), SyntaxKind::DOC_COMMENT),
1,
"exactly one DOC_COMMENT, not a spurious nested wrap: {:#?}",
fn_decl.syntax()
);
}
#[test]
fn an_annotation_line_precedes_a_pub_prefixed_declaration() {
let src = "@[effects(pure)]\npub fn heal() {\n return 1;\n}\n";
let p = assert_lossless(src);
assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
let fn_decl: ast::FnDecl = find_child(&p.syntax()).expect("fn decl");
assert!(fn_decl.is_pub());
let siblings: Vec<_> = p.syntax().children().map(|n| n.kind()).collect();
let fn_pos = siblings
.iter()
.position(|k| *k == SyntaxKind::FN_DECL)
.expect("fn decl among top-level children");
assert_eq!(
siblings.get(fn_pos.wrapping_sub(1)),
Some(&SyntaxKind::ANNOTATION_LINE),
"expected the ANNOTATION_LINE immediately before FN_DECL: {siblings:?}"
);
}
mod prop {
use proptest::prelude::*;
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_type_ident() -> impl Strategy<Value = String> {
"[A-Z][A-Za-z0-9_]{0,7}"
}
fn arb_const_decl() -> impl Strategy<Value = String> {
(arb_type_ident(), 0..10_000u32).prop_map(|(name, n)| format!("const {name} = {n}\n"))
}
fn arb_flags_member() -> impl Strategy<Value = String> {
(arb_ident(), prop::bool::ANY)
.prop_map(|(name, active)| if active { format!("({name})") } else { name })
}
fn arb_flags_decl() -> impl Strategy<Value = String> {
(
arb_type_ident(),
prop::collection::vec(arb_flags_member(), 1..=5),
)
.prop_map(|(name, members)| format!("flags {name} = {}\n", members.join(", ")))
}
fn arb_struct_field() -> impl Strategy<Value = String> {
(arb_ident(), arb_ident()).prop_map(|(name, ty)| format!(" {name}: {ty}"))
}
fn arb_struct_decl() -> impl Strategy<Value = String> {
(
arb_type_ident(),
prop::collection::vec(arb_struct_field(), 0..=4),
)
.prop_map(|(name, fields)| format!("struct {name} {{\n{}\n}}\n", fields.join(",\n")))
}
fn arb_param() -> impl Strategy<Value = String> {
(prop::bool::ANY, arb_ident())
.prop_map(|(is_ref, name)| if is_ref { format!("ref {name}") } else { name })
}
fn arb_flow_decl() -> impl Strategy<Value = String> {
(arb_ident(), prop::collection::vec(arb_param(), 0..=4))
.prop_map(|(name, params)| format!("flow {name}({}) {{\n}}\n", params.join(", ")))
}
fn arb_fn_decl() -> impl Strategy<Value = String> {
(arb_ident(), prop::collection::vec(arb_param(), 0..=4))
.prop_map(|(name, params)| format!("fn {name}({}) {{\n}}\n", params.join(", ")))
}
fn arb_extern_decl() -> impl Strategy<Value = String> {
(arb_ident(), prop::collection::vec(arb_param(), 0..=4))
.prop_map(|(name, params)| format!("extern {name}({})\n", params.join(", ")))
}
fn arb_import_decl() -> impl Strategy<Value = String> {
prop::collection::vec(arb_ident(), 1..=3)
.prop_map(|segs| format!("import {}\n", segs.join("::")))
}
fn arb_use_tree() -> impl Strategy<Value = String> {
(
prop::collection::vec(arb_ident(), 1..=3),
prop::option::of(arb_ident()),
)
.prop_map(|(segs, alias)| {
let path = segs.join("::");
match alias {
Some(a) => format!("{path} as {a}"),
None => path,
}
})
}
fn arb_use_decl() -> impl Strategy<Value = String> {
(arb_ident(), prop::collection::vec(arb_use_tree(), 1..=3)).prop_map(|(prefix, trees)| {
if trees.len() == 1 {
format!("use {prefix}::{};\n", trees[0])
} else {
format!("use {prefix}::{{{}}};\n", trees.join(", "))
}
})
}
fn arb_module_decl() -> impl Strategy<Value = String> {
(arb_ident(), arb_const_decl())
.prop_map(|(name, inner)| format!("module {name} {{\n {inner}}}\n"))
}
fn truncated(s: &str, cut_ratio: u32) -> String {
let target = (s.len() as u64 * u64::from(cut_ratio) / 100) as usize;
let mut end = target.min(s.len());
while end > 0 && !s.is_char_boundary(end) {
end -= 1;
}
s[..end].to_string()
}
fn parse_ok_roundtrip(input: &str) -> bool {
let parsed = crate::parse(input);
parsed.syntax().text() == input
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(NUM_CASES))]
#[test]
fn const_decl_roundtrip(input in arb_const_decl()) {
prop_assert!(parse_ok_roundtrip(&input));
}
#[test]
fn const_decl_no_errors(input in arb_const_decl()) {
let parsed = crate::parse(&input);
prop_assert!(parsed.errors().is_empty(), "input: {input:?} errors: {:?}", parsed.errors());
}
#[test]
fn flags_decl_roundtrip(input in arb_flags_decl()) {
prop_assert!(parse_ok_roundtrip(&input));
}
#[test]
fn flags_decl_no_errors(input in arb_flags_decl()) {
let parsed = crate::parse(&input);
prop_assert!(parsed.errors().is_empty(), "input: {input:?} errors: {:?}", parsed.errors());
}
#[test]
fn struct_decl_roundtrip(input in arb_struct_decl()) {
prop_assert!(parse_ok_roundtrip(&input));
}
#[test]
fn struct_decl_no_errors(input in arb_struct_decl()) {
let parsed = crate::parse(&input);
prop_assert!(parsed.errors().is_empty(), "input: {input:?} errors: {:?}", parsed.errors());
}
#[test]
fn flow_decl_roundtrip(input in arb_flow_decl()) {
prop_assert!(parse_ok_roundtrip(&input));
}
#[test]
fn flow_decl_no_errors(input in arb_flow_decl()) {
let parsed = crate::parse(&input);
prop_assert!(parsed.errors().is_empty(), "input: {input:?} errors: {:?}", parsed.errors());
}
#[test]
fn fn_decl_roundtrip(input in arb_fn_decl()) {
prop_assert!(parse_ok_roundtrip(&input));
}
#[test]
fn fn_decl_no_errors(input in arb_fn_decl()) {
let parsed = crate::parse(&input);
prop_assert!(parsed.errors().is_empty(), "input: {input:?} errors: {:?}", parsed.errors());
}
#[test]
fn extern_decl_roundtrip(input in arb_extern_decl()) {
prop_assert!(parse_ok_roundtrip(&input));
}
#[test]
fn extern_decl_no_errors(input in arb_extern_decl()) {
let parsed = crate::parse(&input);
prop_assert!(parsed.errors().is_empty(), "input: {input:?} errors: {:?}", parsed.errors());
}
#[test]
fn import_decl_roundtrip(input in arb_import_decl()) {
prop_assert!(parse_ok_roundtrip(&input));
}
#[test]
fn use_decl_roundtrip(input in arb_use_decl()) {
prop_assert!(parse_ok_roundtrip(&input));
}
#[test]
fn use_decl_no_errors(input in arb_use_decl()) {
let parsed = crate::parse(&input);
prop_assert!(parsed.errors().is_empty(), "input: {input:?} errors: {:?}", parsed.errors());
}
#[test]
fn module_decl_roundtrip(input in arb_module_decl()) {
prop_assert!(parse_ok_roundtrip(&input));
}
#[test]
fn module_decl_no_errors(input in arb_module_decl()) {
let parsed = crate::parse(&input);
prop_assert!(parsed.errors().is_empty(), "input: {input:?} errors: {:?}", parsed.errors());
}
#[test]
fn truncated_flow_decl_never_panics(input in arb_flow_decl(), cut in 0u32..100) {
let mutated = truncated(&input, cut);
let parsed = crate::parse(&mutated);
prop_assert_eq!(parsed.syntax().text().to_string(), mutated);
}
#[test]
fn truncated_struct_decl_never_panics(input in arb_struct_decl(), cut in 0u32..100) {
let mutated = truncated(&input, cut);
let parsed = crate::parse(&mutated);
prop_assert_eq!(parsed.syntax().text().to_string(), mutated);
}
#[test]
fn truncated_module_decl_never_panics(input in arb_module_decl(), cut in 0u32..100) {
let mutated = truncated(&input, cut);
let parsed = crate::parse(&mutated);
prop_assert_eq!(parsed.syntax().text().to_string(), mutated);
}
#[test]
fn truncated_use_decl_never_panics(input in arb_use_decl(), cut in 0u32..100) {
let mutated = truncated(&input, cut);
let parsed = crate::parse(&mutated);
prop_assert_eq!(parsed.syntax().text().to_string(), mutated);
}
#[test]
fn truncated_extern_decl_never_panics(input in arb_extern_decl(), cut in 0u32..100) {
let mutated = truncated(&input, cut);
let parsed = crate::parse(&mutated);
prop_assert_eq!(parsed.syntax().text().to_string(), mutated);
}
}
}