use cyrs_syntax::{SyntaxKind, SyntaxNode, parse};
fn assert_ok(src: &str) -> SyntaxNode {
let p = parse(src);
assert_eq!(
p.syntax().to_string(),
src,
"lossless round-trip failed for {src:?}"
);
assert!(
p.errors().is_empty(),
"unexpected errors parsing {src:?}: {:?}",
p.errors()
);
p.syntax()
}
fn assert_err_recovery(src: &str) -> SyntaxNode {
let p = parse(src);
assert_eq!(
p.syntax().to_string(),
src,
"lossless round-trip failed for {src:?}"
);
assert!(
!p.errors().is_empty(),
"expected at least one error for {src:?}"
);
p.syntax()
}
fn find_first(node: &SyntaxNode, kind: SyntaxKind) -> Option<SyntaxNode> {
if node.kind() == kind {
return Some(node.clone());
}
node.descendants().find(|d| d.kind() == kind)
}
#[test]
fn parse_match_return() {
assert_ok("MATCH (n) RETURN n");
}
#[test]
fn parse_optional_match_with_label_and_property_map() {
assert_ok("OPTIONAL MATCH (a:Person {name: 'Alice'}) RETURN a.name AS name");
}
#[test]
fn parse_directed_rel_with_type_and_predicate() {
assert_ok("MATCH (a)-[:KNOWS]->(b) WHERE a.age > 18 RETURN a, b");
}
#[test]
fn parse_arithmetic_pratt_precedence() {
assert_ok("RETURN 1 + 2 * 3");
}
#[test]
fn parse_logical_precedence() {
assert_ok("RETURN NOT a OR b AND c");
}
#[test]
fn parse_chained_postfix() {
assert_ok("RETURN a.b[0].c");
}
#[test]
fn parse_function_call() {
assert_ok("RETURN f(x, y)");
}
#[test]
fn parse_function_call_sum() {
assert_ok("MATCH (n:Order) RETURN sum(n.amount)");
assert_ok("MATCH (n:Person) RETURN count(n)");
}
#[test]
fn parse_string_concat() {
assert_ok("RETURN \"hello\" + ' world'");
}
#[test]
fn parse_parameter() {
assert_ok("RETURN $param");
}
#[test]
fn parse_is_null_postfix_forms() {
assert_ok("RETURN a IS NULL, b IS NOT NULL");
}
#[test]
fn parse_starts_with() {
assert_ok("RETURN a STARTS WITH 'foo'");
}
#[test]
fn parse_ends_with_contains_regex() {
assert_ok("RETURN a ENDS WITH 'x', b CONTAINS 'y', c =~ 'r.*'");
}
#[test]
fn parse_unary_minus() {
assert_ok("RETURN -1 + -2");
}
#[test]
fn parse_power_is_right_assoc() {
assert_ok("RETURN 2 ^ 3 ^ 2");
}
#[test]
fn parse_comparison_chain() {
assert_ok("RETURN a < b AND b < c");
}
#[test]
fn parse_paren_expr() {
assert_ok("RETURN (a + b) * c");
}
#[test]
fn parse_multiple_patterns() {
assert_ok("MATCH (a), (b) RETURN a, b");
}
#[test]
fn parse_multi_label() {
assert_ok("MATCH (a:Person:Employee) RETURN a");
}
#[test]
fn parse_return_distinct_order_limit() {
assert_ok("MATCH (n) RETURN DISTINCT n.name ORDER BY n.age DESC SKIP 1 LIMIT 5");
}
fn count_statements(tree: &SyntaxNode) -> usize {
tree.children()
.filter(|c| c.kind() == SyntaxKind::STATEMENT)
.count()
}
#[test]
fn empty_input() {
let p = parse("");
assert!(p.errors().is_empty(), "empty input must have no errors");
assert_eq!(p.syntax().kind(), SyntaxKind::SOURCE_FILE);
assert_eq!(p.syntax().to_string(), "");
assert_eq!(
count_statements(&p.syntax()),
0,
"empty file must have 0 Statement children"
);
}
#[test]
fn whitespace_only_input() {
let p = parse(" \n ");
assert!(p.errors().is_empty());
assert_eq!(p.syntax().to_string(), " \n ");
assert_eq!(count_statements(&p.syntax()), 0);
}
#[test]
fn single_statement_no_semicolon() {
let tree = assert_ok("MATCH (n) RETURN n");
assert_eq!(count_statements(&tree), 1, "one Statement expected");
}
#[test]
fn single_statement_trailing_semicolon() {
let tree = assert_ok("MATCH (n) RETURN n;");
assert_eq!(count_statements(&tree), 1, "one Statement expected");
}
#[test]
fn trailing_semicolon_ok() {
assert_ok("MATCH (n) RETURN n;");
}
#[test]
fn two_statements_semi_separated() {
let tree = assert_ok("MATCH (n) RETURN n; MATCH (m) RETURN m");
assert_eq!(count_statements(&tree), 2, "two Statements expected");
}
#[test]
fn two_statements_with_trailing_semi() {
let tree = assert_ok("MATCH (n) RETURN n; MATCH (m) RETURN m;");
assert_eq!(count_statements(&tree), 2, "two Statements expected");
}
#[test]
fn multi_clause_single_statement() {
let tree = assert_ok("MATCH (n) RETURN n");
assert_eq!(count_statements(&tree), 1, "MATCH+RETURN is 1 Statement");
}
#[test]
fn two_statements_missing_separator_error_recovery() {
let tree = assert_err_recovery("RETURN 1; junk RETURN 2");
assert!(
find_first(&tree, SyntaxKind::RETURN_CLAUSE).is_some(),
"expected at least one RETURN_CLAUSE after recovery"
);
let stmts = count_statements(&tree);
assert!(
stmts >= 1,
"expected at least 1 Statement after recovery, got {stmts}"
);
}
#[test]
fn union_simple() {
let tree = assert_ok("RETURN 1 UNION RETURN 2");
assert!(
find_first(&tree, SyntaxKind::UNION_TAIL).is_some(),
"expected a UNION_TAIL node"
);
}
#[test]
fn union_all() {
let tree = assert_ok("RETURN 1 UNION ALL RETURN 2");
assert!(
find_first(&tree, SyntaxKind::UNION_TAIL).is_some(),
"expected a UNION_TAIL node for UNION ALL"
);
}
#[test]
fn union_three_way() {
let tree = assert_ok("RETURN 1 UNION RETURN 2 UNION ALL RETURN 3");
let tails = tree
.descendants()
.filter(|d| d.kind() == SyntaxKind::UNION_TAIL)
.count();
assert_eq!(tails, 2, "expected 2 UNION_TAIL nodes, got {tails}");
}
#[test]
fn union_semicolon_separated_statements() {
let tree = assert_ok("RETURN 1 UNION RETURN 2; RETURN 3 UNION RETURN 4");
assert_eq!(count_statements(&tree), 2, "expected 2 statements");
}
#[test]
fn recover_unclosed_paren_in_node_pattern() {
let tree = assert_err_recovery("MATCH (n RETURN n");
assert!(
find_first(&tree, SyntaxKind::RETURN_CLAUSE).is_some(),
"RETURN clause missing after recovery"
);
}
#[test]
fn recover_missing_expression_after_where() {
let tree = assert_err_recovery("MATCH (n) WHERE RETURN n");
assert!(find_first(&tree, SyntaxKind::RETURN_CLAUSE).is_some());
}
#[test]
fn recover_leading_junk_before_match() {
let tree = assert_err_recovery("garbage MATCH (n) RETURN n");
assert!(find_first(&tree, SyntaxKind::MATCH_CLAUSE).is_some());
assert!(find_first(&tree, SyntaxKind::RETURN_CLAUSE).is_some());
}
#[test]
fn recover_unclosed_property_map() {
let tree = assert_err_recovery("MATCH (n {x: 1 RETURN n");
assert!(find_first(&tree, SyntaxKind::RETURN_CLAUSE).is_some());
}
fn has_direct_token(node: &SyntaxNode, kind: SyntaxKind) -> bool {
node.children_with_tokens()
.filter_map(rowan::NodeOrToken::into_token)
.any(|t| t.kind() == kind)
}
#[test]
fn pratt_additive_times_multiplicative_structure() {
let tree = assert_ok("RETURN a + b * c");
let top_bin =
find_first(&tree, SyntaxKind::BINARY_EXPR).expect("top-level BINARY_EXPR missing");
assert!(
has_direct_token(&top_bin, SyntaxKind::PLUS),
"top-level binop is not `+`"
);
let rhs = top_bin
.children()
.find(|c| c.kind() == SyntaxKind::BINARY_EXPR)
.expect("no nested BINARY_EXPR for RHS of `+`");
assert!(
has_direct_token(&rhs, SyntaxKind::STAR),
"RHS binop is not `*` (precedence wrong)"
);
}
#[test]
fn pratt_unary_not_binds_tighter_than_and() {
let tree = assert_ok("RETURN NOT a AND b");
let top_bin =
find_first(&tree, SyntaxKind::BINARY_EXPR).expect("top-level BINARY_EXPR missing");
assert!(
has_direct_token(&top_bin, SyntaxKind::AND_KW),
"top-level binop is not AND"
);
assert!(
top_bin
.children()
.any(|c| c.kind() == SyntaxKind::UNARY_EXPR),
"LHS of AND is not a UNARY_EXPR (NOT binding wrong)"
);
}
#[test]
fn pratt_power_is_right_associative() {
let tree = assert_ok("RETURN 2 ^ 3 ^ 2");
let top_bin =
find_first(&tree, SyntaxKind::BINARY_EXPR).expect("top-level BINARY_EXPR missing");
assert!(
has_direct_token(&top_bin, SyntaxKind::CARET),
"top-level binop is not `^`"
);
let rhs_bin = top_bin
.children()
.find(|c| c.kind() == SyntaxKind::BINARY_EXPR)
.expect("RHS of outer ^ is not a binop");
assert!(
has_direct_token(&rhs_bin, SyntaxKind::CARET),
"RHS binop is not `^` — associativity wrong"
);
}