use open_cypher::ast::{
ClauseKind, Expr, ExprKind, IsPredicate, ProjectionItemKind, QueryKind, RegularQuery,
StatementKind, UnionOperator,
};
use open_cypher::{DiagnosticCode, ParsedProgram, parse, parse_recovering};
fn parse_ok(source: &str) -> ParsedProgram {
match parse(source) {
Ok(parsed) => parsed,
Err(errors) => panic!(
"expected {source:?} to parse, got:\n{}",
errors.render(source)
),
}
}
fn regular_query(parsed: &ParsedProgram) -> &RegularQuery {
let StatementKind::Query(statement) = &parsed.program.statements[0].kind else {
panic!("expected a query statement")
};
let QueryKind::Regular(query) = &statement.query.kind else {
panic!("expected a regular query")
};
query
}
fn match_where_clause(parsed: &ParsedProgram) -> &Expr {
let query = regular_query(parsed);
let ClauseKind::Match(match_clause) = &query.head.kind.clauses[0].kind else {
panic!("expected a MATCH clause")
};
match_clause.where_clause.as_ref().expect("WHERE clause")
}
fn return_item_expression(parsed: &ParsedProgram, item: usize) -> &Expr {
let query = regular_query(parsed);
let ClauseKind::Return(return_clause) = &query.head.kind.clauses.last().unwrap().kind else {
panic!("expected a final RETURN clause")
};
let ProjectionItemKind::Expression { expression, .. } =
&return_clause.projection.items[item].kind
else {
panic!("expected an expression item")
};
expression
}
fn assert_is_label_predicate(expression: &Expr, source: &str, text: &str) {
let ExprKind::Is {
negated, predicate, ..
} = &expression.kind
else {
panic!("expected an IS/label predicate expression, got {expression:#?}")
};
assert!(!negated);
assert!(
matches!(predicate, IsPredicate::Label(_)),
"expected a label predicate, got {predicate:#?}"
);
assert_eq!(expression.span.text(source), Some(text));
}
#[test]
fn where_label_predicate_is_an_is_label_expression() {
let source = "MATCH (n) WHERE n:Person RETURN n";
let parsed = parse_ok(source);
assert_is_label_predicate(match_where_clause(&parsed), source, "n:Person");
}
#[test]
fn where_is_keyword_label_predicate_parses() {
let source = "MATCH (n) WHERE n IS Person RETURN n";
let parsed = parse_ok(source);
assert_is_label_predicate(match_where_clause(&parsed), source, "n IS Person");
}
#[test]
fn label_predicate_inside_exists_subquery_where() {
let source = "MATCH (m) WHERE EXISTS { MATCH (n) WHERE n:Person } RETURN m";
let parsed = parse_ok(source);
let rendered = format!("{:?}", parsed.program);
assert!(
rendered.contains("Label("),
"expected an IsPredicate::Label in {rendered}"
);
}
#[test]
fn label_predicate_after_comprehension_pipe_in_set_context() {
let source = "MATCH (a) SET a.p = [x IN [1] | x:Person] RETURN a";
let parsed = parse_ok(source);
let rendered = format!("{:?}", parsed.program);
assert!(
rendered.contains("Label("),
"expected an IsPredicate::Label in {rendered}"
);
}
#[test]
fn label_predicate_after_completed_list_literal() {
let source = "MATCH (n) WHERE n.p IN [1, 2] AND n:Person RETURN n";
let parsed = parse_ok(source);
let where_clause = match_where_clause(&parsed);
let ExprKind::Binary { right, .. } = &where_clause.kind else {
panic!("expected AND expression, got {where_clause:#?}")
};
assert_is_label_predicate(right, source, "n:Person");
}
#[test]
fn label_predicate_after_completed_map_literal() {
let source = "MATCH (n) WHERE {a: 1} = n.p AND n:Person RETURN n";
let parsed = parse_ok(source);
let where_clause = match_where_clause(&parsed);
let ExprKind::Binary { right, .. } = &where_clause.kind else {
panic!("expected AND expression, got {where_clause:#?}")
};
assert_is_label_predicate(right, source, "n:Person");
}
#[test]
fn case_inside_property_map_does_not_leak_expression_context() {
let source = "MATCH (n {p: CASE WHEN true THEN 1 ELSE 2 END}), (m:Person) RETURN m";
let parsed = parse_ok(source);
let rendered = format!("{:?}", parsed.program);
assert!(
!rendered.contains("Label("),
"node label must not become an IsPredicate::Label in {rendered}"
);
}
#[test]
fn later_match_pattern_colon_stays_a_node_label() {
let source = "MATCH (a) WITH a MATCH (b:Person) RETURN b";
let parsed = parse_ok(source);
let rendered = format!("{:?}", parsed.program);
assert!(
!rendered.contains("Label("),
"node label must not become an IsPredicate::Label in {rendered}"
);
}
#[test]
fn keyword_variable_before_equals_is_skipped_by_the_scan() {
let source = "WITH 1 AS match WHERE match = 1 AND n:Person RETURN n";
let parsed = parse_ok(source);
let rendered = format!("{:?}", parsed.program);
assert!(
rendered.contains("Label("),
"expected an IsPredicate::Label in {rendered}"
);
}
#[test]
fn comma_projection_label_predicate_outside_braces() {
let source = "MATCH (n), (m) RETURN n, m:Person";
let parsed = parse_ok(source);
assert_is_label_predicate(return_item_expression(&parsed, 1), source, "m:Person");
}
#[test]
fn completed_map_before_comma_label_predicate() {
let source = "MATCH (m) RETURN {a: 1}, m:Person";
let parsed = parse_ok(source);
assert_is_label_predicate(return_item_expression(&parsed, 1), source, "m:Person");
}
#[test]
fn nested_map_second_key_stays_a_map_key() {
let source = "MATCH (d) RETURN {a: {b: 1}, c: d}";
let parsed = parse_ok(source);
let expression = return_item_expression(&parsed, 0);
assert_eq!(expression.span.text(source), Some("{a: {b: 1}, c: d}"));
let ExprKind::Map(entries) = &expression.kind else {
panic!("expected a map literal, got {expression:#?}")
};
assert_eq!(entries.len(), 2);
assert_eq!(entries[1].key.kind.text, "c");
assert_eq!(entries[1].value.span.text(source), Some("d"));
}
#[test]
fn closed_property_map_does_not_capture_later_node_label() {
let source = "MATCH (n {a: 1}) MATCH (m:Person) RETURN m";
let parsed = parse_ok(source);
let rendered = format!("{:?}", parsed.program);
assert!(
!rendered.contains("Label("),
"node label must not become an IsPredicate::Label in {rendered}"
);
}
#[test]
fn bracketed_comma_inside_map_value_keeps_entry_value_context() {
let source = "MATCH (m), (n {flag: [1, 2] = $x AND m:Person}) RETURN n";
let parsed = parse_ok(source);
let rendered = format!("{:?}", parsed.program);
assert!(
rendered.contains("Label("),
"expected an IsPredicate::Label in {rendered}"
);
}
#[test]
fn map_key_after_completed_list_value_stays_a_map_key() {
let source = "MATCH (c) RETURN {a: [1], b: c}";
let parsed = parse_ok(source);
let expression = return_item_expression(&parsed, 0);
let ExprKind::Map(entries) = &expression.kind else {
panic!("expected a map literal, got {expression:#?}")
};
assert_eq!(entries.len(), 2);
assert_eq!(entries[1].key.kind.text, "b");
assert_eq!(entries[1].value.span.text(source), Some("c"));
}
#[test]
fn map_key_after_top_level_comma_stays_a_map_key() {
let source = "MATCH (c) RETURN {a: 1, b: c}";
let parsed = parse_ok(source);
let expression = return_item_expression(&parsed, 0);
let ExprKind::Map(entries) = &expression.kind else {
panic!("expected a map literal, got {expression:#?}")
};
assert_eq!(entries.len(), 2);
assert_eq!(entries[1].key.kind.text, "b");
assert_eq!(entries[1].value.span.text(source), Some("c"));
}
#[test]
fn nested_property_map_colon_in_exists_body_is_not_a_separator() {
let source = "MATCH (m) WHERE EXISTS { MATCH (a {p: 1}) MATCH (b:Person) } RETURN m";
let parsed = parse_ok(source);
let rendered = format!("{:?}", parsed.program);
assert!(
!rendered.contains("Label("),
"node label must not become an IsPredicate::Label in {rendered}"
);
}
#[test]
fn nested_list_comprehension_pipe_does_not_split_the_comprehension() {
let source = "MATCH (a) RETURN [(a)--(b) WHERE a.x IN [z IN [1, 2] | z] | a.name]";
let parsed = parse_ok(source);
let expression = return_item_expression(&parsed, 0);
let ExprKind::PatternComprehension(comprehension) = &expression.kind else {
panic!("expected a pattern comprehension, got {expression:#?}")
};
assert_eq!(comprehension.pattern.span.text(source), Some("(a)--(b)"));
let predicate = comprehension.predicate.as_ref().expect("predicate");
assert_eq!(
predicate.span.text(source),
Some("a.x IN [z IN [1, 2] | z]")
);
assert_eq!(comprehension.projection.span.text(source), Some("a.name"));
}
fn union_parts(source: &str) -> (UnionOperator, String) {
let parsed = parse_ok(source);
let query = regular_query(&parsed);
assert_eq!(query.unions.len(), 1, "expected one union branch");
let operator = &query.unions[0].operator;
(
operator.kind,
operator
.span
.text(source)
.expect("operator span")
.to_owned(),
)
}
#[test]
fn union_all_operator_kind_and_exact_span() {
let (kind, text) = union_parts("RETURN 1 UNION ALL RETURN 2");
assert_eq!(kind, UnionOperator::All);
assert_eq!(text, "UNION ALL");
}
#[test]
fn union_distinct_operator_kind_and_exact_span() {
let (kind, text) = union_parts("RETURN 1 UNION DISTINCT RETURN 2");
assert_eq!(kind, UnionOperator::Distinct);
assert_eq!(text, "UNION DISTINCT");
}
#[test]
fn bare_union_operator_kind_and_exact_span() {
let (kind, text) = union_parts("RETURN 1 UNION RETURN 2");
assert_eq!(kind, UnionOperator::Default);
assert_eq!(text, "UNION");
}
#[test]
fn distinct_delimiter_diagnostics_are_not_deduplicated() {
let source = "RETURN ] RETURN ]";
let outcome = parse_recovering(source);
let observed: Vec<(DiagnosticCode, usize, usize)> = outcome
.diagnostics
.iter()
.map(|diagnostic| {
(
diagnostic.code,
diagnostic.primary_span.start,
diagnostic.primary_span.end,
)
})
.collect();
assert_eq!(
observed,
vec![
(DiagnosticCode::UnexpectedToken, 7, 8),
(DiagnosticCode::MismatchedDelimiter, 7, 8),
(DiagnosticCode::MismatchedDelimiter, 16, 17),
]
);
}
#[test]
fn exactly_max_diagnostics_are_kept_without_truncation() {
let source = format!("RETURN 1 {}", "~ ".repeat(31));
let outcome = parse_recovering(&source);
assert_eq!(outcome.diagnostics.len(), 32);
assert!(
outcome
.diagnostics
.iter()
.all(|diagnostic| diagnostic.code != DiagnosticCode::TooManyErrors),
"no diagnostic may be suppressed at exactly the cap"
);
let source = format!("RETURN 1 {}", "~ ".repeat(32));
let outcome = parse_recovering(&source);
assert_eq!(outcome.diagnostics.len(), 32);
assert_eq!(
outcome.diagnostics.last().map(|diagnostic| diagnostic.code),
Some(DiagnosticCode::TooManyErrors)
);
}
#[test]
fn contextual_retry_reports_the_farthest_error() {
let source = "RETURN count +";
let outcome = parse_recovering(source);
let observed: Vec<(DiagnosticCode, usize, usize)> = outcome
.diagnostics
.iter()
.map(|diagnostic| {
(
diagnostic.code,
diagnostic.primary_span.start,
diagnostic.primary_span.end,
)
})
.collect();
assert_eq!(observed, vec![(DiagnosticCode::UnexpectedEof, 14, 14)]);
}
#[test]
fn non_improving_retry_keeps_the_original_expectation() {
let source = "RETURN 1 + count filter";
let outcome = parse_recovering(source);
assert_eq!(outcome.diagnostics.len(), 1);
let diagnostic = &outcome.diagnostics[0];
assert_eq!(diagnostic.code, DiagnosticCode::UnexpectedToken);
assert_eq!(
(diagnostic.primary_span.start, diagnostic.primary_span.end),
(17, 23)
);
assert_eq!(
diagnostic.message,
"unexpected token `identifier`; expected `(`, or `{`"
);
}
#[test]
fn quantifier_without_in_reports_error_at_the_closing_paren() {
let source = "RETURN any (x) count";
let outcome = parse_recovering(source);
assert_eq!(outcome.diagnostics.len(), 1);
let diagnostic = &outcome.diagnostics[0];
assert_eq!(diagnostic.code, DiagnosticCode::UnexpectedToken);
assert_eq!(
(diagnostic.primary_span.start, diagnostic.primary_span.end),
(13, 14)
);
assert_eq!(diagnostic.message, "unexpected token `)`; expected `IN`");
}
#[test]
fn two_round_contextual_resolution_before_and_after_the_error() {
let source = "RETURN any(x) + count";
let parsed = parse_ok(source);
let expression = return_item_expression(&parsed, 0);
assert_eq!(expression.span.text(source), Some("any(x) + count"));
}
#[test]
fn exact_location_candidate_outranks_aliased_candidate() {
let source = "RETURN 1 + detach, 2 + count AS y";
let parsed = parse_ok(source);
assert_eq!(
return_item_expression(&parsed, 0).span.text(source),
Some("1 + detach")
);
assert_eq!(
return_item_expression(&parsed, 1).span.text(source),
Some("2 + count")
);
}
#[test]
fn two_keyword_signatures_resolve_in_successive_rounds() {
let source = "RETURN 1 + count, 2 + single";
let parsed = parse_ok(source);
assert_eq!(
return_item_expression(&parsed, 0).span.text(source),
Some("1 + count")
);
assert_eq!(
return_item_expression(&parsed, 1).span.text(source),
Some("2 + single")
);
}
#[test]
fn bare_as_projection_resolves_the_as_token_itself() {
let source = "RETURN as";
let parsed = parse_ok(source);
let expression = return_item_expression(&parsed, 0);
assert_eq!(expression.span.text(source), Some("as"));
let ExprKind::Variable(name) = &expression.kind else {
panic!("expected a variable, got {expression:#?}")
};
assert_eq!(name.kind.text, "as");
}
#[test]
fn count_aliased_as_the_word_as_keeps_counts_expectation() {
let source = "RETURN + count AS as";
let outcome = parse_recovering(source);
assert_eq!(outcome.diagnostics.len(), 1);
let diagnostic = &outcome.diagnostics[0];
assert_eq!(diagnostic.code, DiagnosticCode::UnexpectedToken);
assert_eq!(
(diagnostic.primary_span.start, diagnostic.primary_span.end),
(15, 17)
);
assert_eq!(
diagnostic.message,
"unexpected token `AS`; expected `(`, or `{`"
);
}
#[test]
fn pattern_expression_after_completed_list_literal() {
let source = "MATCH (a) WHERE a.p IN [1, 2] AND (a)-->(b) RETURN a";
let parsed = parse_ok(source);
let where_clause = match_where_clause(&parsed);
let ExprKind::Binary { right, .. } = &where_clause.kind else {
panic!("expected AND expression, got {where_clause:#?}")
};
assert!(
matches!(right.kind, ExprKind::Pattern(_)),
"expected a pattern expression, got {right:#?}"
);
assert_eq!(right.span.text(source), Some("(a)-->(b)"));
}
#[test]
fn pattern_expression_after_completed_map_literal() {
let source = "MATCH (a) WHERE {x: 1} = a.p AND (a)-->(b) RETURN a";
let parsed = parse_ok(source);
let where_clause = match_where_clause(&parsed);
let ExprKind::Binary { right, .. } = &where_clause.kind else {
panic!("expected AND expression, got {where_clause:#?}")
};
assert!(
matches!(right.kind, ExprKind::Pattern(_)),
"expected a pattern expression, got {right:#?}"
);
assert_eq!(right.span.text(source), Some("(a)-->(b)"));
}
#[test]
fn case_inside_property_map_does_not_leak_pattern_expression_context() {
let source = "MATCH (n {p: CASE WHEN true THEN 1 ELSE 2 END}), (m)-->(o) RETURN n";
let parsed = parse_ok(source);
let rendered = format!("{:?}", parsed.program);
assert!(
!rendered.contains("Pattern(Pattern"),
"MATCH path must not become a pattern expression in {rendered}"
);
}
#[test]
fn later_match_relationship_pattern_is_not_a_pattern_expression() {
let source = "MATCH (a) WITH a MATCH (b)-->(c) RETURN b";
let parsed = parse_ok(source);
let rendered = format!("{:?}", parsed.program);
assert!(
!rendered.contains("Pattern(Pattern"),
"MATCH path must not become a pattern expression in {rendered}"
);
}
#[test]
fn shortest_path_variable_two_back_does_not_veto_pattern_expression() {
let source = "WITH 1 AS shortestPath RETURN shortestPath, (a)-->(b)";
let parsed = parse_ok(source);
let expression = return_item_expression(&parsed, 1);
assert!(
matches!(expression.kind, ExprKind::Pattern(_)),
"expected a pattern expression, got {expression:#?}"
);
assert_eq!(expression.span.text(source), Some("(a)-->(b)"));
}
#[test]
fn split_left_arrow_where_operand_is_a_pattern_expression() {
let source = "MATCH (a) WHERE (a) < -[:R]- (b) RETURN a";
let parsed = parse_ok(source);
let where_clause = match_where_clause(&parsed);
assert!(
matches!(where_clause.kind, ExprKind::Pattern(_)),
"expected a pattern expression, got {where_clause:#?}"
);
assert_eq!(where_clause.span.text(source), Some("(a) < -[:R]- (b)"));
}
#[test]
fn brace_quantified_pattern_expression_reports_error_at_the_quantifier() {
let source = "MATCH (a) WHERE (a)-[:R]->{1,2}(b) RETURN a";
let outcome = parse_recovering(source);
assert_eq!(outcome.diagnostics.len(), 1, "{:#?}", outcome.diagnostics);
let diagnostic = &outcome.diagnostics[0];
assert_eq!(diagnostic.code, DiagnosticCode::UnexpectedToken);
assert_eq!(
(diagnostic.primary_span.start, diagnostic.primary_span.end),
(26, 27)
);
assert_eq!(diagnostic.message, "unexpected token `{`; expected `(`");
}
#[test]
fn star_quantified_pattern_expression_reports_error_at_the_quantifier() {
let source = "MATCH (a) WHERE (a)-[:R]->*(b) RETURN a";
let outcome = parse_recovering(source);
assert_eq!(outcome.diagnostics.len(), 1, "{:#?}", outcome.diagnostics);
let diagnostic = &outcome.diagnostics[0];
assert_eq!(diagnostic.code, DiagnosticCode::UnexpectedToken);
assert_eq!(
(diagnostic.primary_span.start, diagnostic.primary_span.end),
(26, 27)
);
assert_eq!(diagnostic.message, "unexpected token `*`; expected `(`");
}
#[test]
fn integer_before_colon_inside_brackets_is_not_a_relationship_label_start() {
let source = "WITH [1,2,3] AS a, 2 AS b RETURN a[1:b]";
let parsed = parse_ok(source);
let expression = return_item_expression(&parsed, 0);
let ExprKind::Index { index, .. } = &expression.kind else {
panic!("expected an index expression, got {expression:#?}")
};
assert_is_label_predicate(index, source, "1:b");
}
#[test]
fn percent_after_legacy_pipe_colon_is_not_a_relationship_label_expression() {
let source = "MATCH (a)-[:A|:%]->(b) RETURN a";
let outcome = parse_recovering(source);
assert_eq!(outcome.diagnostics.len(), 1, "{:#?}", outcome.diagnostics);
let diagnostic = &outcome.diagnostics[0];
assert_eq!(diagnostic.code, DiagnosticCode::UnexpectedToken);
assert_eq!(
(diagnostic.primary_span.start, diagnostic.primary_span.end),
(11, 12)
);
assert_eq!(
diagnostic.message,
"unexpected token `:`; expected `*`, `IS`, `MATCH`, `PATH`, `RETURN`, `WHERE`, `]`, `escaped identifier`, `identifier`, `parameter`, `relationship label expression`, or `{`"
);
}
#[test]
fn legacy_relationship_type_list_with_keyword_first_name_parses() {
let source = "MATCH (a)-[:count|:B]->(b) RETURN a";
let parsed = parse_ok(source);
let rendered = format!("{:?}", parsed.program);
assert!(
rendered.contains("text: \"count\"") && rendered.contains("text: \"B\""),
"expected both relationship type names in {rendered}"
);
}
#[test]
fn deep_call_procedure_name_is_not_a_qualified_function() {
let source = "CALL a.b.c.d()";
let parsed = parse_ok(source);
let query = regular_query(&parsed);
assert!(
matches!(query.head.kind.clauses[0].kind, ClauseKind::Call(_)),
"expected a CALL clause"
);
}
#[test]
fn return_all_with_parenthesis_is_a_quantifier_not_set_all() {
let source = "RETURN all(x IN [1] WHERE x > 0)";
let parsed = parse_ok(source);
let expression = return_item_expression(&parsed, 0);
assert!(
matches!(expression.kind, ExprKind::QuantifiedPredicate(_)),
"expected a quantified predicate, got {expression:#?}"
);
assert_eq!(
expression.span.text(source),
Some("all(x IN [1] WHERE x > 0)")
);
}
#[test]
fn same_keyword_with_different_next_token_keeps_distinct_signatures() {
let source = "RETURN 1 + exists, 1 + exists { MATCH (n) }";
let parsed = parse_ok(source);
assert_eq!(
return_item_expression(&parsed, 0).span.text(source),
Some("1 + exists")
);
assert_eq!(
return_item_expression(&parsed, 1).span.text(source),
Some("1 + exists { MATCH (n) }")
);
}
#[test]
fn empty_quantifier_parens_report_error_at_the_close_paren() {
let source = "RETURN any ( ) as";
let outcome = parse_recovering(source);
assert_eq!(outcome.diagnostics.len(), 1);
let diagnostic = &outcome.diagnostics[0];
assert_eq!(diagnostic.code, DiagnosticCode::UnexpectedToken);
assert_eq!(
(diagnostic.primary_span.start, diagnostic.primary_span.end),
(13, 14)
);
assert_eq!(
diagnostic.message,
"unexpected token `)`; expected `MATCH`, `PATH`, `RETURN`, `escaped identifier`, or `identifier`"
);
}