#![allow(dead_code)]
use rowan::GreenNodeBuilder;
use crate::syntax::{SyntaxKind, SyntaxNode};
use super::scanner::{Scanner, TokenKind, TriviaKind};
pub fn parse_v2(input: &str) -> SyntaxNode {
let mut builder = GreenNodeBuilder::new();
builder.start_node(SyntaxKind::YAML_STREAM.into());
let mut scanner = Scanner::new(input);
let mut doc_open = false;
let mut doc_only_has_directives = false;
let mut block_stack: Vec<BlockFrame> = Vec::new();
while let Some(tok) = scanner.next_token() {
match tok.kind {
TokenKind::StreamStart | TokenKind::StreamEnd => continue,
TokenKind::BlockMappingStart => {
ensure_doc_open(&mut builder, &mut doc_open);
doc_only_has_directives = false;
ensure_flow_seq_item_open(&mut builder, &mut block_stack);
builder.start_node(SyntaxKind::YAML_BLOCK_MAP.into());
block_stack.push(BlockFrame::BlockMap {
entry_open: false,
in_value: false,
});
continue;
}
TokenKind::BlockSequenceStart => {
ensure_doc_open(&mut builder, &mut doc_open);
doc_only_has_directives = false;
ensure_flow_seq_item_open(&mut builder, &mut block_stack);
builder.start_node(SyntaxKind::YAML_BLOCK_SEQUENCE.into());
block_stack.push(BlockFrame::BlockSequence { item_open: false });
continue;
}
TokenKind::BlockEnd => {
close_open_sub_wrapper(&mut builder, &mut block_stack);
if block_stack.pop().is_some() {
builder.finish_node();
}
continue;
}
TokenKind::FlowSequenceStart => {
ensure_doc_open(&mut builder, &mut doc_open);
doc_only_has_directives = false;
ensure_flow_seq_item_open(&mut builder, &mut block_stack);
builder.start_node(SyntaxKind::YAML_FLOW_SEQUENCE.into());
block_stack.push(BlockFrame::FlowSequence { item_open: false });
let text = &input[tok.start.index..tok.end.index];
builder.token(SyntaxKind::YAML_SCALAR.into(), text);
continue;
}
TokenKind::FlowSequenceEnd => {
close_open_sub_wrapper(&mut builder, &mut block_stack);
let text = &input[tok.start.index..tok.end.index];
builder.token(SyntaxKind::YAML_SCALAR.into(), text);
if matches!(
block_stack.last(),
Some(BlockFrame::FlowSequence { .. } | BlockFrame::FlowMap { .. })
) {
block_stack.pop();
builder.finish_node();
}
continue;
}
TokenKind::FlowMappingStart => {
ensure_doc_open(&mut builder, &mut doc_open);
doc_only_has_directives = false;
ensure_flow_seq_item_open(&mut builder, &mut block_stack);
builder.start_node(SyntaxKind::YAML_FLOW_MAP.into());
block_stack.push(BlockFrame::FlowMap {
entry_open: false,
in_value: false,
});
let text = &input[tok.start.index..tok.end.index];
builder.token(SyntaxKind::YAML_SCALAR.into(), text);
continue;
}
TokenKind::FlowMappingEnd => {
close_open_sub_wrapper(&mut builder, &mut block_stack);
let text = &input[tok.start.index..tok.end.index];
builder.token(SyntaxKind::YAML_SCALAR.into(), text);
if matches!(
block_stack.last(),
Some(BlockFrame::FlowMap { .. } | BlockFrame::FlowSequence { .. })
) {
block_stack.pop();
builder.finish_node();
}
continue;
}
TokenKind::FlowEntry => {
close_open_sub_wrapper(&mut builder, &mut block_stack);
let text = &input[tok.start.index..tok.end.index];
builder.token(SyntaxKind::YAML_SCALAR.into(), text);
continue;
}
TokenKind::Key => {
if matches!(
block_stack.last(),
Some(BlockFrame::BlockMap { .. } | BlockFrame::FlowMap { .. })
) {
open_map_entry_with_key(&mut builder, &mut block_stack);
}
if tok.start.index == tok.end.index {
continue;
}
ensure_flow_seq_item_open(&mut builder, &mut block_stack);
}
TokenKind::Value => {
let map_state = match block_stack.last().copied() {
Some(BlockFrame::BlockMap {
entry_open,
in_value,
}) => Some((false, entry_open, in_value)),
Some(BlockFrame::FlowMap {
entry_open,
in_value,
}) => Some((true, entry_open, in_value)),
_ => None,
};
if let Some((is_flow, entry_open, in_value)) = map_state {
if !entry_open {
open_map_entry_with_key(&mut builder, &mut block_stack);
}
if !in_value {
let text = &input[tok.start.index..tok.end.index];
if !text.is_empty() {
builder.token(SyntaxKind::YAML_COLON.into(), text);
}
builder.finish_node(); let value_kind = if is_flow {
SyntaxKind::YAML_FLOW_MAP_VALUE
} else {
SyntaxKind::YAML_BLOCK_MAP_VALUE
};
builder.start_node(value_kind.into());
if let Some(
BlockFrame::BlockMap { in_value, .. }
| BlockFrame::FlowMap { in_value, .. },
) = block_stack.last_mut()
{
*in_value = true;
}
continue;
}
}
ensure_flow_seq_item_open(&mut builder, &mut block_stack);
}
TokenKind::BlockEntry => {
if matches!(block_stack.last(), Some(BlockFrame::BlockSequence { .. })) {
close_open_sub_wrapper(&mut builder, &mut block_stack);
builder.start_node(SyntaxKind::YAML_BLOCK_SEQUENCE_ITEM.into());
if let Some(BlockFrame::BlockSequence { item_open }) = block_stack.last_mut() {
*item_open = true;
}
}
}
TokenKind::Trivia(_) => {
}
_ => {
if !matches!(tok.kind, TokenKind::DocumentStart | TokenKind::DocumentEnd) {
ensure_flow_seq_item_open(&mut builder, &mut block_stack);
}
}
}
let text = &input[tok.start.index..tok.end.index];
if text.is_empty() {
continue;
}
let kind = map_token_to_syntax_kind(tok.kind);
match tok.kind {
TokenKind::DocumentStart => {
if doc_open && doc_only_has_directives {
builder.token(kind.into(), text);
doc_only_has_directives = false;
} else {
close_block_containers(&mut builder, &mut block_stack);
if doc_open {
builder.finish_node();
}
builder.start_node(SyntaxKind::YAML_DOCUMENT.into());
doc_open = true;
doc_only_has_directives = false;
builder.token(kind.into(), text);
}
}
TokenKind::DocumentEnd => {
close_block_containers(&mut builder, &mut block_stack);
if !doc_open {
builder.start_node(SyntaxKind::YAML_DOCUMENT.into());
}
builder.token(kind.into(), text);
builder.finish_node();
doc_open = false;
doc_only_has_directives = false;
}
TokenKind::Trivia(_) => {
builder.token(kind.into(), text);
}
TokenKind::Directive => {
let was_open = doc_open;
ensure_doc_open(&mut builder, &mut doc_open);
if !was_open {
doc_only_has_directives = true;
}
builder.token(kind.into(), text);
}
_ => {
ensure_doc_open(&mut builder, &mut doc_open);
doc_only_has_directives = false;
builder.token(kind.into(), text);
}
}
}
close_block_containers(&mut builder, &mut block_stack);
if doc_open {
builder.finish_node();
}
builder.finish_node();
SyntaxNode::new_root(builder.finish())
}
#[derive(Debug, Clone, Copy)]
enum BlockFrame {
BlockMap { entry_open: bool, in_value: bool },
BlockSequence { item_open: bool },
FlowMap { entry_open: bool, in_value: bool },
FlowSequence { item_open: bool },
}
fn ensure_doc_open(builder: &mut GreenNodeBuilder<'_>, doc_open: &mut bool) {
if !*doc_open {
builder.start_node(SyntaxKind::YAML_DOCUMENT.into());
*doc_open = true;
}
}
fn ensure_flow_seq_item_open(builder: &mut GreenNodeBuilder<'_>, stack: &mut [BlockFrame]) {
if let Some(BlockFrame::FlowSequence { item_open }) = stack.last_mut()
&& !*item_open
{
builder.start_node(SyntaxKind::YAML_FLOW_SEQUENCE_ITEM.into());
*item_open = true;
}
}
fn open_map_entry_with_key(builder: &mut GreenNodeBuilder<'_>, stack: &mut [BlockFrame]) {
close_open_sub_wrapper(builder, stack);
let (entry_kind, key_kind) = match stack.last() {
Some(BlockFrame::BlockMap { .. }) => (
SyntaxKind::YAML_BLOCK_MAP_ENTRY,
SyntaxKind::YAML_BLOCK_MAP_KEY,
),
Some(BlockFrame::FlowMap { .. }) => (
SyntaxKind::YAML_FLOW_MAP_ENTRY,
SyntaxKind::YAML_FLOW_MAP_KEY,
),
_ => return,
};
builder.start_node(entry_kind.into());
builder.start_node(key_kind.into());
if let Some(
BlockFrame::BlockMap {
entry_open,
in_value,
}
| BlockFrame::FlowMap {
entry_open,
in_value,
},
) = stack.last_mut()
{
*entry_open = true;
*in_value = false;
}
}
fn close_open_sub_wrapper(builder: &mut GreenNodeBuilder<'_>, stack: &mut [BlockFrame]) {
let Some(frame) = stack.last_mut() else {
return;
};
match frame {
BlockFrame::BlockMap {
entry_open: true,
in_value,
} => {
if *in_value {
builder.finish_node(); } else {
builder.finish_node(); builder.start_node(SyntaxKind::YAML_BLOCK_MAP_VALUE.into());
builder.finish_node(); }
builder.finish_node(); *frame = BlockFrame::BlockMap {
entry_open: false,
in_value: false,
};
}
BlockFrame::FlowMap {
entry_open: true,
in_value,
} => {
if *in_value {
builder.finish_node();
} else {
builder.finish_node();
builder.start_node(SyntaxKind::YAML_FLOW_MAP_VALUE.into());
builder.finish_node();
}
builder.finish_node();
*frame = BlockFrame::FlowMap {
entry_open: false,
in_value: false,
};
}
BlockFrame::BlockSequence { item_open: true } => {
builder.finish_node();
*frame = BlockFrame::BlockSequence { item_open: false };
}
BlockFrame::FlowSequence { item_open: true } => {
builder.finish_node();
*frame = BlockFrame::FlowSequence { item_open: false };
}
_ => {}
}
}
fn close_block_containers(builder: &mut GreenNodeBuilder<'_>, stack: &mut Vec<BlockFrame>) {
while let Some(frame) = stack.pop() {
match frame {
BlockFrame::BlockMap {
entry_open: true,
in_value,
} => {
if in_value {
builder.finish_node(); } else {
builder.finish_node(); builder.start_node(SyntaxKind::YAML_BLOCK_MAP_VALUE.into());
builder.finish_node();
}
builder.finish_node(); }
BlockFrame::FlowMap {
entry_open: true,
in_value,
} => {
if in_value {
builder.finish_node();
} else {
builder.finish_node();
builder.start_node(SyntaxKind::YAML_FLOW_MAP_VALUE.into());
builder.finish_node();
}
builder.finish_node();
}
BlockFrame::BlockSequence { item_open: true }
| BlockFrame::FlowSequence { item_open: true } => {
builder.finish_node();
}
_ => {}
}
builder.finish_node();
}
}
fn map_token_to_syntax_kind(kind: TokenKind) -> SyntaxKind {
match kind {
TokenKind::Trivia(TriviaKind::Whitespace) => SyntaxKind::WHITESPACE,
TokenKind::Trivia(TriviaKind::Newline) => SyntaxKind::NEWLINE,
TokenKind::Trivia(TriviaKind::Comment) => SyntaxKind::YAML_COMMENT,
TokenKind::DocumentStart => SyntaxKind::YAML_DOCUMENT_START,
TokenKind::DocumentEnd => SyntaxKind::YAML_DOCUMENT_END,
TokenKind::Directive => SyntaxKind::YAML_SCALAR,
TokenKind::BlockEntry => SyntaxKind::YAML_BLOCK_SEQ_ENTRY,
TokenKind::FlowEntry => SyntaxKind::YAML_SCALAR,
TokenKind::FlowSequenceStart | TokenKind::FlowSequenceEnd => SyntaxKind::YAML_SCALAR,
TokenKind::FlowMappingStart | TokenKind::FlowMappingEnd => SyntaxKind::YAML_SCALAR,
TokenKind::Value => SyntaxKind::YAML_COLON,
TokenKind::Anchor | TokenKind::Alias | TokenKind::Tag => SyntaxKind::YAML_TAG,
TokenKind::Scalar(_) => SyntaxKind::YAML_SCALAR,
TokenKind::Key => SyntaxKind::YAML_KEY,
TokenKind::StreamStart
| TokenKind::StreamEnd
| TokenKind::BlockSequenceStart
| TokenKind::BlockMappingStart
| TokenKind::BlockEnd => SyntaxKind::YAML_SCALAR,
}
}
#[derive(Debug, Clone)]
pub struct ShadowParserV2Report {
pub text_lossless: bool,
pub stream_child_count: usize,
}
pub fn shadow_parser_v2_check(input: &str) -> ShadowParserV2Report {
let tree = parse_v2(input);
let text = tree.text().to_string();
ShadowParserV2Report {
text_lossless: text == input,
stream_child_count: tree.children().count(),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn v2_returns_byte_lossless_cst_for_empty_input() {
let report = shadow_parser_v2_check("");
assert!(report.text_lossless);
}
#[test]
fn v2_returns_byte_lossless_cst_for_simple_mapping() {
let report = shadow_parser_v2_check("key: value\n");
assert!(report.text_lossless);
}
#[test]
fn v2_returns_byte_lossless_cst_for_block_sequence() {
let report = shadow_parser_v2_check("- a\n- b\n");
assert!(report.text_lossless);
}
#[test]
fn v2_returns_byte_lossless_cst_for_flow_mapping() {
let report = shadow_parser_v2_check("{a: b, c: d}\n");
assert!(report.text_lossless);
}
#[test]
fn v2_returns_byte_lossless_cst_for_block_scalar() {
let report = shadow_parser_v2_check("key: |\n hello\n world\n");
assert!(report.text_lossless);
}
#[test]
fn v2_returns_byte_lossless_cst_for_quoted_scalar() {
let report = shadow_parser_v2_check("\"key\": \"value\"\n");
assert!(report.text_lossless);
}
#[test]
fn v2_returns_byte_lossless_cst_for_multi_line_plain_scalar() {
let report = shadow_parser_v2_check("key: hello\n world\n");
assert!(report.text_lossless);
}
#[test]
fn v2_preserves_explicit_key_indicator_byte_in_flow_context() {
let input = "{ ?foo: bar }\n";
let report = shadow_parser_v2_check(input);
assert!(report.text_lossless, "input {input:?} not preserved");
}
#[test]
fn v2_does_not_absorb_terminator_line_break_into_flow_scalar() {
let input = "{a: 42\n}\n";
let report = shadow_parser_v2_check(input);
assert!(report.text_lossless, "input {input:?} not preserved");
}
fn document_count(tree: &SyntaxNode) -> usize {
tree.children()
.filter(|n| n.kind() == SyntaxKind::YAML_DOCUMENT)
.count()
}
#[test]
fn implicit_document_wraps_body_with_no_markers() {
let input = "key: value\n";
let tree = parse_v2(input);
assert_eq!(document_count(&tree), 1);
assert_eq!(tree.text().to_string(), input);
}
#[test]
fn explicit_doc_start_opens_document_marker_lives_inside() {
let input = "---\nkey: value\n";
let tree = parse_v2(input);
assert_eq!(document_count(&tree), 1);
let doc = tree
.children()
.find(|n| n.kind() == SyntaxKind::YAML_DOCUMENT)
.expect("document node");
assert!(
doc.children_with_tokens().any(|el| el
.as_token()
.is_some_and(|t| t.kind() == SyntaxKind::YAML_DOCUMENT_START)),
"`---` token should live inside YAML_DOCUMENT"
);
assert_eq!(tree.text().to_string(), input);
}
#[test]
fn explicit_doc_end_closes_document_marker_lives_inside() {
let input = "key: value\n...\n";
let tree = parse_v2(input);
assert_eq!(document_count(&tree), 1);
let doc = tree
.children()
.find(|n| n.kind() == SyntaxKind::YAML_DOCUMENT)
.expect("document node");
assert!(
doc.children_with_tokens().any(|el| el
.as_token()
.is_some_and(|t| t.kind() == SyntaxKind::YAML_DOCUMENT_END)),
"`...` token should live inside YAML_DOCUMENT"
);
assert_eq!(tree.text().to_string(), input);
}
#[test]
fn consecutive_doc_starts_emit_two_documents() {
let input = "---\na\n---\nb\n";
let tree = parse_v2(input);
assert_eq!(document_count(&tree), 2);
assert_eq!(tree.text().to_string(), input);
}
#[test]
fn pre_document_trivia_stays_at_stream_level() {
let input = "\n---\nkey: value\n";
let tree = parse_v2(input);
let stream_token_kinds: Vec<SyntaxKind> = tree
.children_with_tokens()
.filter_map(|el| el.into_token())
.map(|t| t.kind())
.collect();
assert!(
stream_token_kinds.contains(&SyntaxKind::NEWLINE),
"leading newline should be a direct child of YAML_STREAM, got {stream_token_kinds:?}"
);
assert_eq!(tree.text().to_string(), input);
}
#[test]
fn bare_doc_end_at_stream_start_opens_synthetic_empty_document() {
let input = "...\n";
let tree = parse_v2(input);
assert_eq!(document_count(&tree), 1);
assert_eq!(tree.text().to_string(), input);
}
fn first_document(tree: &SyntaxNode) -> SyntaxNode {
tree.children()
.find(|n| n.kind() == SyntaxKind::YAML_DOCUMENT)
.expect("at least one document")
}
fn block_map_under(parent: &SyntaxNode) -> Option<SyntaxNode> {
parent
.children()
.find(|n| n.kind() == SyntaxKind::YAML_BLOCK_MAP)
}
fn block_seq_under(parent: &SyntaxNode) -> Option<SyntaxNode> {
parent
.children()
.find(|n| n.kind() == SyntaxKind::YAML_BLOCK_SEQUENCE)
}
fn block_map_entries(map: &SyntaxNode) -> Vec<SyntaxNode> {
map.children()
.filter(|n| n.kind() == SyntaxKind::YAML_BLOCK_MAP_ENTRY)
.collect()
}
fn block_seq_items(seq: &SyntaxNode) -> Vec<SyntaxNode> {
seq.children()
.filter(|n| n.kind() == SyntaxKind::YAML_BLOCK_SEQUENCE_ITEM)
.collect()
}
fn entry_key(entry: &SyntaxNode) -> SyntaxNode {
entry
.children()
.find(|n| n.kind() == SyntaxKind::YAML_BLOCK_MAP_KEY)
.expect("entry should have a YAML_BLOCK_MAP_KEY child")
}
fn entry_value(entry: &SyntaxNode) -> SyntaxNode {
entry
.children()
.find(|n| n.kind() == SyntaxKind::YAML_BLOCK_MAP_VALUE)
.expect("entry should have a YAML_BLOCK_MAP_VALUE child")
}
#[test]
fn block_mapping_wraps_key_value_with_key_and_value_sub_wrappers() {
let input = "key: value\n";
let tree = parse_v2(input);
let doc = first_document(&tree);
let map = block_map_under(&doc).expect("YAML_BLOCK_MAP child");
let entries = block_map_entries(&map);
assert_eq!(entries.len(), 1, "expected one ENTRY for `key: value`");
let key = entry_key(&entries[0]);
let value = entry_value(&entries[0]);
assert!(
key.children_with_tokens().any(|el| el
.as_token()
.is_some_and(|t| t.kind() == SyntaxKind::YAML_COLON)),
"colon should be the trailing token of YAML_BLOCK_MAP_KEY",
);
assert!(
value.children_with_tokens().any(|el| el
.as_token()
.is_some_and(|t| t.kind() == SyntaxKind::YAML_SCALAR)),
"scalar `value` should live inside YAML_BLOCK_MAP_VALUE",
);
assert_eq!(tree.text().to_string(), input);
}
#[test]
fn block_sequence_wraps_entries_in_yaml_block_sequence() {
let input = "- a\n- b\n";
let tree = parse_v2(input);
let doc = first_document(&tree);
let seq = block_seq_under(&doc).expect("YAML_BLOCK_SEQUENCE child");
let items = block_seq_items(&seq);
assert_eq!(items.len(), 2, "expected 2 YAML_BLOCK_SEQUENCE_ITEM");
for item in &items {
let dash_count = item
.children_with_tokens()
.filter(|el| {
el.as_token()
.is_some_and(|t| t.kind() == SyntaxKind::YAML_BLOCK_SEQ_ENTRY)
})
.count();
assert_eq!(dash_count, 1, "each item owns exactly one `-` token");
}
assert_eq!(tree.text().to_string(), input);
}
#[test]
fn nested_block_mapping_nests_inner_block_map_inside_outer_value() {
let input = "outer:\n inner: x\n";
let tree = parse_v2(input);
let doc = first_document(&tree);
let outer = block_map_under(&doc).expect("outer YAML_BLOCK_MAP");
let outer_entries = block_map_entries(&outer);
assert_eq!(outer_entries.len(), 1);
let outer_value = entry_value(&outer_entries[0]);
let inner = outer_value
.children()
.find(|n| n.kind() == SyntaxKind::YAML_BLOCK_MAP)
.expect("inner YAML_BLOCK_MAP nested under outer VALUE");
let inner_entries = block_map_entries(&inner);
assert_eq!(inner_entries.len(), 1);
let inner_key = entry_key(&inner_entries[0]);
assert!(
inner_key.children_with_tokens().any(|el| el
.as_token()
.is_some_and(|t| t.kind() == SyntaxKind::YAML_COLON)),
"inner key should own its colon",
);
assert_eq!(tree.text().to_string(), input);
}
#[test]
fn block_sequence_inside_mapping_nests_under_outer_map_value() {
let input = "items:\n - a\n - b\n";
let tree = parse_v2(input);
let doc = first_document(&tree);
let map = block_map_under(&doc).expect("YAML_BLOCK_MAP child");
let entries = block_map_entries(&map);
assert_eq!(entries.len(), 1, "one entry: `items: <seq>`");
let value = entry_value(&entries[0]);
let seq = value
.children()
.find(|n| n.kind() == SyntaxKind::YAML_BLOCK_SEQUENCE)
.expect("YAML_BLOCK_SEQUENCE nested under map VALUE");
let items = block_seq_items(&seq);
assert_eq!(items.len(), 2);
assert_eq!(tree.text().to_string(), input);
}
#[test]
fn dedent_closes_inner_block_map_before_next_outer_key() {
let input = "outer:\n inner: x\nsibling: y\n";
let tree = parse_v2(input);
let doc = first_document(&tree);
let outer = block_map_under(&doc).expect("outer YAML_BLOCK_MAP");
let entries = block_map_entries(&outer);
assert_eq!(
entries.len(),
2,
"outer map should have two entries (`outer:` and `sibling:`)",
);
let first_value = entry_value(&entries[0]);
let nested_in_first = first_value
.children()
.filter(|n| n.kind() == SyntaxKind::YAML_BLOCK_MAP)
.count();
assert_eq!(nested_in_first, 1);
let second_value = entry_value(&entries[1]);
let nested_in_second = second_value
.children()
.filter(|n| n.kind() == SyntaxKind::YAML_BLOCK_MAP)
.count();
assert_eq!(nested_in_second, 0);
assert_eq!(tree.text().to_string(), input);
}
#[test]
fn block_map_with_two_top_level_entries_emits_two_entry_wrappers() {
let input = "a: 1\nb: 2\n";
let tree = parse_v2(input);
let doc = first_document(&tree);
let map = block_map_under(&doc).expect("YAML_BLOCK_MAP child");
assert_eq!(block_map_entries(&map).len(), 2);
assert_eq!(tree.text().to_string(), input);
}
#[test]
fn explicit_key_indicator_question_mark_lives_inside_key() {
let input = "? a\n: b\n";
let tree = parse_v2(input);
let doc = first_document(&tree);
let map = block_map_under(&doc).expect("YAML_BLOCK_MAP child");
let entries = block_map_entries(&map);
assert_eq!(entries.len(), 1);
let key = entry_key(&entries[0]);
let has_question = key.children_with_tokens().any(|el| {
el.as_token()
.is_some_and(|t| t.kind() == SyntaxKind::YAML_KEY)
});
assert!(has_question, "`?` should live inside YAML_BLOCK_MAP_KEY");
assert_eq!(tree.text().to_string(), input);
}
#[test]
fn empty_key_shorthand_opens_entry_with_empty_key() {
let input = ": value\n";
let tree = parse_v2(input);
let doc = first_document(&tree);
let map = block_map_under(&doc).expect("YAML_BLOCK_MAP child");
let entries = block_map_entries(&map);
assert_eq!(entries.len(), 1);
let key = entry_key(&entries[0]);
assert!(
!key.children_with_tokens().any(|el| el
.as_token()
.is_some_and(|t| t.kind() == SyntaxKind::YAML_SCALAR)),
"empty-key shorthand has no scalar in KEY",
);
assert!(
key.children_with_tokens().any(|el| el
.as_token()
.is_some_and(|t| t.kind() == SyntaxKind::YAML_COLON)),
"empty-key KEY still owns the `:` token",
);
let value = entry_value(&entries[0]);
assert!(
value.children_with_tokens().any(|el| el
.as_token()
.is_some_and(|t| t.kind() == SyntaxKind::YAML_SCALAR)),
"VALUE owns the `value` scalar",
);
assert_eq!(tree.text().to_string(), input);
}
#[test]
fn document_end_marker_lives_at_document_level_not_inside_block_map() {
let input = "key: value\n...\n";
let tree = parse_v2(input);
let doc = first_document(&tree);
let has_doc_end = doc.children_with_tokens().any(|el| {
el.as_token()
.is_some_and(|t| t.kind() == SyntaxKind::YAML_DOCUMENT_END)
});
assert!(
has_doc_end,
"DOCUMENT_END should be a direct child of YAML_DOCUMENT"
);
assert_eq!(tree.text().to_string(), input);
}
fn flow_map_under(parent: &SyntaxNode) -> Option<SyntaxNode> {
parent
.children()
.find(|n| n.kind() == SyntaxKind::YAML_FLOW_MAP)
}
fn flow_seq_under(parent: &SyntaxNode) -> Option<SyntaxNode> {
parent
.children()
.find(|n| n.kind() == SyntaxKind::YAML_FLOW_SEQUENCE)
}
fn flow_map_entries(map: &SyntaxNode) -> Vec<SyntaxNode> {
map.children()
.filter(|n| n.kind() == SyntaxKind::YAML_FLOW_MAP_ENTRY)
.collect()
}
fn flow_seq_items(seq: &SyntaxNode) -> Vec<SyntaxNode> {
seq.children()
.filter(|n| n.kind() == SyntaxKind::YAML_FLOW_SEQUENCE_ITEM)
.collect()
}
#[test]
fn flow_sequence_wraps_each_item_in_flow_sequence_item() {
let input = "[a, b, c]\n";
let tree = parse_v2(input);
let doc = first_document(&tree);
let seq = flow_seq_under(&doc).expect("YAML_FLOW_SEQUENCE child");
let items = flow_seq_items(&seq);
assert_eq!(items.len(), 3);
let bracket_count = seq
.children_with_tokens()
.filter(|el| {
el.as_token().map(|t| t.text()) == Some("[")
|| el.as_token().map(|t| t.text()) == Some("]")
})
.count();
assert_eq!(bracket_count, 2, "`[` and `]` at SEQUENCE level");
assert_eq!(tree.text().to_string(), input);
}
#[test]
fn flow_mapping_wraps_each_entry_with_key_and_value() {
let input = "{a: 1, b: 2}\n";
let tree = parse_v2(input);
let doc = first_document(&tree);
let map = flow_map_under(&doc).expect("YAML_FLOW_MAP child");
let entries = flow_map_entries(&map);
assert_eq!(entries.len(), 2);
for entry in &entries {
let key = entry
.children()
.find(|n| n.kind() == SyntaxKind::YAML_FLOW_MAP_KEY)
.expect("entry has YAML_FLOW_MAP_KEY");
assert!(
key.children_with_tokens().any(|el| el
.as_token()
.is_some_and(|t| t.kind() == SyntaxKind::YAML_COLON)),
"flow KEY owns trailing `:`",
);
let value = entry
.children()
.find(|n| n.kind() == SyntaxKind::YAML_FLOW_MAP_VALUE)
.expect("entry has YAML_FLOW_MAP_VALUE");
assert!(
value.children_with_tokens().any(|el| el
.as_token()
.is_some_and(|t| t.kind() == SyntaxKind::YAML_SCALAR)),
"flow VALUE owns its scalar",
);
}
assert_eq!(tree.text().to_string(), input);
}
#[test]
fn flow_sequence_inside_flow_sequence_nests_under_outer_item() {
let input = "[[1, 2], [3, 4]]\n";
let tree = parse_v2(input);
let doc = first_document(&tree);
let outer = flow_seq_under(&doc).expect("outer YAML_FLOW_SEQUENCE");
let outer_items = flow_seq_items(&outer);
assert_eq!(outer_items.len(), 2);
for item in &outer_items {
assert!(
item.children()
.any(|n| n.kind() == SyntaxKind::YAML_FLOW_SEQUENCE),
"outer item should contain a nested YAML_FLOW_SEQUENCE",
);
}
assert_eq!(tree.text().to_string(), input);
}
#[test]
fn flow_mapping_inside_flow_sequence_nests_under_item() {
let input = "[{a: 1}, {b: 2}]\n";
let tree = parse_v2(input);
let doc = first_document(&tree);
let seq = flow_seq_under(&doc).expect("YAML_FLOW_SEQUENCE child");
let items = flow_seq_items(&seq);
assert_eq!(items.len(), 2);
for item in &items {
assert!(
item.children()
.any(|n| n.kind() == SyntaxKind::YAML_FLOW_MAP),
"each item should contain a nested YAML_FLOW_MAP",
);
}
assert_eq!(tree.text().to_string(), input);
}
#[test]
fn flow_mapping_at_block_map_value_nests_under_block_map_value() {
let input = "key: {a: 1, b: 2}\n";
let tree = parse_v2(input);
let doc = first_document(&tree);
let block_map = block_map_under(&doc).expect("YAML_BLOCK_MAP child");
let entries = block_map_entries(&block_map);
assert_eq!(entries.len(), 1);
let value = entry_value(&entries[0]);
assert!(
value
.children()
.any(|n| n.kind() == SyntaxKind::YAML_FLOW_MAP),
"flow map should be nested under outer block map's VALUE",
);
assert_eq!(tree.text().to_string(), input);
}
#[test]
fn directive_prelude_stays_inside_document_opened_by_marker() {
let input = "%TAG !e! tag:example.com,2000:app/\n---\n!e!foo \"bar\"\n";
let tree = parse_v2(input);
assert_eq!(document_count(&tree), 1);
let doc = first_document(&tree);
let has_doc_start = doc.children_with_tokens().any(|el| {
el.as_token()
.is_some_and(|t| t.kind() == SyntaxKind::YAML_DOCUMENT_START)
});
assert!(has_doc_start, "the `---` should live inside the same doc");
assert_eq!(tree.text().to_string(), input);
}
#[test]
fn explicit_key_without_value_emits_empty_value_for_shape_parity() {
let input = "? a\n? b\n";
let tree = parse_v2(input);
let doc = first_document(&tree);
let map = block_map_under(&doc).expect("YAML_BLOCK_MAP");
let entries = block_map_entries(&map);
assert_eq!(entries.len(), 2);
for entry in &entries {
assert!(
entry
.children()
.any(|n| n.kind() == SyntaxKind::YAML_BLOCK_MAP_KEY),
"ENTRY missing KEY child",
);
assert!(
entry
.children()
.any(|n| n.kind() == SyntaxKind::YAML_BLOCK_MAP_VALUE),
"ENTRY missing VALUE child",
);
}
assert_eq!(tree.text().to_string(), input);
}
}