#![allow(clippy::uninlined_format_args)]
use ox_mf2_parser::{
parse_source, CstChild, CstNodeView, CstView, DiagnosticCode, ParseOptions, SourceFileInput,
SourceStore, SyntaxKind,
};
fn parse(source: &str) -> (SourceStore, ox_mf2_parser::ParseResult) {
let mut sources = SourceStore::new();
let id = sources.add(SourceFileInput {
source,
..Default::default()
});
let result = parse_source(&sources, id, ParseOptions::default());
(sources, result)
}
fn descendant_count(root: CstNodeView<'_>) -> usize {
fn visit(node: CstNodeView<'_>, count: &mut usize) {
*count += 1;
for child in node.children() {
if let CstChild::Node(n) = child {
visit(n, count);
}
}
}
let mut count = 0;
visit(root, &mut count);
count
}
fn collect_kinds_recovery(root: CstNodeView<'_>) -> Vec<SyntaxKind> {
fn visit(node: CstNodeView<'_>, out: &mut Vec<SyntaxKind>) {
out.push(node.kind());
for child in node.children() {
if let CstChild::Node(n) = child {
visit(n, out);
}
}
}
let mut out = Vec::new();
visit(root, &mut out);
out
}
#[test]
fn unclosed_placeholder_returns_root_and_one_diagnostic() {
let (sources, result) = parse("Hello, {$name");
assert_eq!(result.diagnostics.len(), 1, "{:?}", result.diagnostics);
let diag = &result.diagnostics[0];
assert_eq!(diag.code, DiagnosticCode::UnclosedExpression);
assert_eq!(diag.span.start, 7);
assert_eq!(diag.span.end, 13);
let view = CstView::new(&sources, result.source, &result.cst);
let root = view.root().expect("root exists");
assert_eq!(root.kind(), SyntaxKind::Root);
assert!(descendant_count(root) >= 5);
}
#[test]
fn unclosed_quoted_literal_returns_root_and_one_diagnostic() {
let (sources, result) = parse("{|incomplete}");
let codes: Vec<_> = result.diagnostics.iter().map(|d| d.code).collect();
assert!(
codes.contains(&DiagnosticCode::UnclosedQuotedLiteral),
"{:?}",
codes
);
let view = CstView::new(&sources, result.source, &result.cst);
let root = view.root().expect("root exists");
assert_eq!(root.kind(), SyntaxKind::Root);
}
#[test]
fn unclosed_quoted_pattern_returns_root_and_diagnostic() {
let (sources, result) = parse("{{unterminated");
let codes: Vec<_> = result.diagnostics.iter().map(|d| d.code).collect();
assert!(
codes.contains(&DiagnosticCode::UnclosedQuotedPattern),
"{:?}",
codes
);
let view = CstView::new(&sources, result.source, &result.cst);
assert_eq!(view.root().unwrap().kind(), SyntaxKind::Root);
}
#[test]
fn declarations_without_complex_body_emit_missing_body_diagnostic() {
let (sources, result) = parse(".input {$x}\n");
let codes: Vec<_> = result.diagnostics.iter().map(|d| d.code).collect();
assert!(
codes.contains(&DiagnosticCode::MissingComplexBody),
"{:?}",
codes
);
let view = CstView::new(&sources, result.source, &result.cst);
let root = view.root().expect("root exists");
assert_eq!(root.kind(), SyntaxKind::Root);
}
#[test]
fn invalid_escape_emits_one_diagnostic_and_keeps_progress() {
let (sources, result) = parse("Hello \\X world");
let invalid: Vec<_> = result
.diagnostics
.iter()
.filter(|d| d.code == DiagnosticCode::InvalidEscape)
.collect();
assert_eq!(invalid.len(), 1);
let view = CstView::new(&sources, result.source, &result.cst);
let root = view.root().expect("root exists");
assert_eq!(root.kind(), SyntaxKind::Root);
}
#[test]
fn malformed_variant_emits_only_one_diagnostic() {
let (sources, result) = parse(".match $x\n*");
let invalid: Vec<_> = result
.diagnostics
.iter()
.filter(|d| d.code == DiagnosticCode::InvalidVariantBoundary)
.collect();
assert_eq!(invalid.len(), 1, "{:?}", result.diagnostics);
let view = CstView::new(&sources, result.source, &result.cst);
assert_eq!(view.root().unwrap().kind(), SyntaxKind::Root);
}
#[test]
fn missing_s_before_function_emits_diagnostic() {
let (_, result) = parse("{$x:number}");
let codes: Vec<_> = result.diagnostics.iter().map(|d| d.code).collect();
assert!(
codes.contains(&DiagnosticCode::MissingRequiredWhitespace),
"{:?}",
codes
);
}
#[test]
fn missing_s_before_local_variable_emits_diagnostic() {
let (_, result) = parse(".local$x = {$y}\n{{Hi}}");
let codes: Vec<_> = result.diagnostics.iter().map(|d| d.code).collect();
assert!(
codes.contains(&DiagnosticCode::MissingRequiredWhitespace),
"{:?}",
codes
);
}
#[test]
fn missing_s_before_selector_emits_diagnostic() {
let (_, result) = parse(".match$x\n* {{fallback}}");
let codes: Vec<_> = result.diagnostics.iter().map(|d| d.code).collect();
assert!(
codes.contains(&DiagnosticCode::MissingRequiredWhitespace),
"{:?}",
codes
);
}
#[test]
fn missing_s_before_attribute_emits_diagnostic() {
let (_, result) = parse("{#tag@attr}");
let codes: Vec<_> = result.diagnostics.iter().map(|d| d.code).collect();
assert!(
codes.contains(&DiagnosticCode::MissingRequiredWhitespace),
"{:?}",
codes
);
}
#[test]
fn missing_s_between_adjacent_variant_keys_emits_diagnostic() {
let (_, result) = parse(".match $x\none|two| {{combo}}\n* {{fallback}}");
let codes: Vec<_> = result.diagnostics.iter().map(|d| d.code).collect();
assert!(
codes.contains(&DiagnosticCode::MissingRequiredWhitespace),
"{:?}",
codes
);
}
#[test]
fn trailing_colon_in_function_identifier_emits_diagnostic() {
let (sources, result) = parse("{:foo:}");
let codes: Vec<_> = result.diagnostics.iter().map(|d| d.code).collect();
assert!(
codes.contains(&DiagnosticCode::MissingIdentifierName),
"{:?}",
codes
);
let view = CstView::new(&sources, result.source, &result.cst);
let root = view.root().unwrap();
let kinds = collect_kinds_recovery(root);
assert!(kinds.contains(&SyntaxKind::Missing));
}
#[test]
fn trailing_colon_in_markup_identifier_emits_diagnostic() {
let (_, result) = parse("{#ns:}");
let codes: Vec<_> = result.diagnostics.iter().map(|d| d.code).collect();
assert!(
codes.contains(&DiagnosticCode::MissingIdentifierName),
"{:?}",
codes
);
}
#[test]
fn trailing_colon_in_option_identifier_emits_diagnostic() {
let (_, result) = parse("{:fn opt:=value}");
let codes: Vec<_> = result.diagnostics.iter().map(|d| d.code).collect();
assert!(
codes.contains(&DiagnosticCode::MissingIdentifierName),
"{:?}",
codes
);
}
#[test]
fn trailing_colon_in_attribute_identifier_emits_diagnostic() {
let (_, result) = parse("{|x| @ns:=|v|}");
let codes: Vec<_> = result.diagnostics.iter().map(|d| d.code).collect();
assert!(
codes.contains(&DiagnosticCode::MissingIdentifierName),
"{:?}",
codes
);
}
#[test]
fn speculative_branch_does_not_duplicate_trivia() {
let (_, result) = parse("{$x }");
assert_eq!(result.cst.trivia_count(), 1, "expected one trivia record");
}