1#[path = "yaml/lexer.rs"]
12mod lexer;
13#[path = "yaml/model.rs"]
14mod model;
15#[path = "yaml/parser.rs"]
16mod parser;
17
18pub use lexer::lex_mapping_tokens;
19pub use model::{
20 ShadowYamlOptions, ShadowYamlOutcome, ShadowYamlReport, YamlDiagnostic, YamlInputKind,
21 YamlParseReport, YamlToken, YamlTokenSpan, diagnostic_codes,
22};
23pub use parser::{parse_shadow, parse_yaml_report, parse_yaml_tree};
24
25#[cfg(test)]
26mod tests {
27 use super::*;
28 use crate::syntax::SyntaxKind;
29
30 #[test]
31 fn builds_basic_rowan_tree_for_multiline_mapping() {
32 let tree = parse_yaml_tree("title: My Title\nauthor: Me\n").expect("tree");
33 assert_eq!(tree.kind(), SyntaxKind::DOCUMENT);
34 assert_eq!(tree.text().to_string(), "title: My Title\nauthor: Me\n");
35
36 let content = tree
37 .children()
38 .find(|n| n.kind() == SyntaxKind::YAML_METADATA_CONTENT)
39 .expect("yaml metadata content");
40 let mapping = content
41 .children()
42 .find(|n| n.kind() == SyntaxKind::YAML_BLOCK_MAP)
43 .expect("yaml block map");
44 let entries: Vec<_> = mapping
45 .children()
46 .filter(|n| n.kind() == SyntaxKind::YAML_BLOCK_MAP_ENTRY)
47 .collect();
48 assert_eq!(entries.len(), 2);
49
50 let token_kinds: Vec<_> = mapping
51 .descendants_with_tokens()
52 .filter_map(|el| el.into_token())
53 .map(|tok| tok.kind())
54 .collect();
55 assert_eq!(
56 token_kinds,
57 vec![
58 SyntaxKind::YAML_KEY,
59 SyntaxKind::YAML_COLON,
60 SyntaxKind::WHITESPACE,
61 SyntaxKind::YAML_SCALAR,
62 SyntaxKind::NEWLINE,
63 SyntaxKind::YAML_KEY,
64 SyntaxKind::YAML_COLON,
65 SyntaxKind::WHITESPACE,
66 SyntaxKind::YAML_SCALAR,
67 SyntaxKind::NEWLINE,
68 ]
69 );
70 }
71
72 #[test]
73 fn mapping_nodes_preserve_entry_text_boundaries() {
74 let tree = parse_yaml_tree("title: A\nauthor: B\n").expect("tree");
75 let content = tree
76 .children()
77 .find(|n| n.kind() == SyntaxKind::YAML_METADATA_CONTENT)
78 .expect("yaml metadata content");
79 let mapping = content
80 .children()
81 .find(|n| n.kind() == SyntaxKind::YAML_BLOCK_MAP)
82 .expect("yaml block map");
83
84 let entry_texts: Vec<_> = mapping
85 .children()
86 .filter(|n| n.kind() == SyntaxKind::YAML_BLOCK_MAP_ENTRY)
87 .map(|n| n.text().to_string())
88 .collect();
89 assert_eq!(
90 entry_texts,
91 vec!["title: A\n".to_string(), "author: B\n".to_string(),]
92 );
93 }
94
95 #[test]
96 fn splits_mapping_on_colon_outside_quoted_key() {
97 let input = "\"foo:bar\": 23\n'x:y': 24\n";
98 let tree = parse_yaml_tree(input).expect("tree");
99 assert_eq!(tree.text().to_string(), input);
100
101 let keys: Vec<String> = tree
102 .descendants_with_tokens()
103 .filter_map(|el| el.into_token())
104 .filter(|tok| tok.kind() == SyntaxKind::YAML_KEY)
105 .map(|tok| tok.text().to_string())
106 .collect();
107 assert_eq!(keys, vec!["\"foo:bar\"".to_string(), "'x:y'".to_string()]);
108 }
109
110 #[test]
111 fn splits_mapping_on_colon_outside_flow_key() {
112 let input = "{a: b}: 23\n";
113 let tree = parse_yaml_tree(input).expect("tree");
114 assert_eq!(tree.text().to_string(), input);
115
116 let keys: Vec<String> = tree
117 .descendants_with_tokens()
118 .filter_map(|el| el.into_token())
119 .filter(|tok| tok.kind() == SyntaxKind::YAML_KEY)
120 .map(|tok| tok.text().to_string())
121 .collect();
122 assert_eq!(keys, vec!["{a: b}".to_string()]);
123 }
124
125 #[test]
126 fn keeps_colon_inside_escaped_double_quoted_key() {
127 let input = "\"foo\\\":bar\": 23\n";
128 let tree = parse_yaml_tree(input).expect("tree");
129 assert_eq!(tree.text().to_string(), input);
130
131 let keys: Vec<String> = tree
132 .descendants_with_tokens()
133 .filter_map(|el| el.into_token())
134 .filter(|tok| tok.kind() == SyntaxKind::YAML_KEY)
135 .map(|tok| tok.text().to_string())
136 .collect();
137 assert_eq!(keys, vec!["\"foo\\\":bar\"".to_string()]);
138 }
139
140 #[test]
141 fn keeps_hash_in_double_quoted_scalar_value() {
142 let input = "foo: \"a#b\"\n";
143 let tree = parse_yaml_tree(input).expect("tree");
144
145 let comment_count = tree
146 .descendants_with_tokens()
147 .filter_map(|el| el.into_token())
148 .filter(|tok| tok.kind() == SyntaxKind::YAML_COMMENT)
149 .count();
150 assert_eq!(comment_count, 0);
151
152 let scalar_values: Vec<String> = tree
153 .descendants_with_tokens()
154 .filter_map(|el| el.into_token())
155 .filter(|tok| tok.kind() == SyntaxKind::YAML_SCALAR)
156 .map(|tok| tok.text().to_string())
157 .collect();
158 assert_eq!(scalar_values, vec!["\"a#b\"".to_string()]);
159 }
160
161 #[test]
162 fn keeps_colon_inside_single_quoted_key_with_escaped_quote() {
163 let input = "'foo'':bar': 23\n";
164 let tree = parse_yaml_tree(input).expect("tree");
165 assert_eq!(tree.text().to_string(), input);
166
167 let keys: Vec<String> = tree
168 .descendants_with_tokens()
169 .filter_map(|el| el.into_token())
170 .filter(|tok| tok.kind() == SyntaxKind::YAML_KEY)
171 .map(|tok| tok.text().to_string())
172 .collect();
173 assert_eq!(keys, vec!["'foo'':bar'".to_string()]);
174 }
175
176 #[test]
177 fn preserves_explicit_tag_tokens_in_key_and_value() {
178 let input = "!!str a: !!int 42\n";
179 let tree = parse_yaml_tree(input).expect("tree");
180 assert_eq!(tree.text().to_string(), input);
181
182 let tag_tokens: Vec<_> = tree
183 .descendants_with_tokens()
184 .filter_map(|el| el.into_token())
185 .filter(|tok| tok.kind() == SyntaxKind::YAML_TAG)
186 .map(|tok| tok.text().to_string())
187 .collect();
188 assert_eq!(tag_tokens, vec!["!!str".to_string(), "!!int".to_string()]);
189 }
190
191 #[test]
192 fn lexer_emits_tokens_for_quoted_keys_and_inline_comments() {
193 let input = "\"foo:bar\": 23 # note\n'x:y': 'z' # ok\n";
194 let tokens = lex_mapping_tokens(input).expect("tokens");
195 let kinds: Vec<_> = tokens.iter().map(|t| t.kind).collect();
196 assert_eq!(
197 kinds,
198 vec![
199 YamlToken::Key,
200 YamlToken::Colon,
201 YamlToken::Whitespace,
202 YamlToken::Scalar,
203 YamlToken::Whitespace,
204 YamlToken::Comment,
205 YamlToken::Newline,
206 YamlToken::Key,
207 YamlToken::Colon,
208 YamlToken::Whitespace,
209 YamlToken::Scalar,
210 YamlToken::Whitespace,
211 YamlToken::Comment,
212 YamlToken::Newline,
213 ]
214 );
215 let comments: Vec<_> = tokens
216 .iter()
217 .filter(|t| t.kind == YamlToken::Comment)
218 .map(|t| t.text)
219 .collect();
220 assert_eq!(comments, vec!["# note", "# ok"]);
221 }
222
223 #[test]
224 fn lexer_emits_indent_and_dedent_for_indented_entries() {
225 let input = "root: 1\n child: 2\n";
226 let tokens = lex_mapping_tokens(input).expect("tokens");
227 let kinds: Vec<_> = tokens.iter().map(|t| t.kind).collect();
228 assert!(kinds.contains(&YamlToken::Indent));
229 assert!(kinds.contains(&YamlToken::Dedent));
230 }
231
232 #[test]
233 fn lexer_emits_document_start_marker_token() {
234 let input = "---\n";
235 let tokens = lex_mapping_tokens(input).expect("tokens");
236 let kinds: Vec<_> = tokens.iter().map(|t| t.kind).collect();
237 assert_eq!(kinds, vec![YamlToken::DocumentStart, YamlToken::Newline,]);
238 }
239
240 #[test]
241 fn lexer_emits_flow_tokens_for_standalone_flow_mapping() {
242 let input = "{foo: bar}\n";
243 let tokens = lex_mapping_tokens(input).expect("tokens");
244 let kinds: Vec<_> = tokens.iter().map(|t| t.kind).collect();
245 assert_eq!(
246 kinds,
247 vec![
248 YamlToken::FlowMapStart,
249 YamlToken::Scalar,
250 YamlToken::FlowMapEnd,
251 YamlToken::Newline,
252 ]
253 );
254 }
255
256 #[test]
257 fn lexer_emits_flow_sequence_tokens_in_mapping_value() {
258 let input = "a: [b, c]\n";
259 let tokens = lex_mapping_tokens(input).expect("tokens");
260 let kinds: Vec<_> = tokens.iter().map(|t| t.kind).collect();
261 assert_eq!(
262 kinds,
263 vec![
264 YamlToken::Key,
265 YamlToken::Colon,
266 YamlToken::Whitespace,
267 YamlToken::FlowSeqStart,
268 YamlToken::Scalar,
269 YamlToken::Comma,
270 YamlToken::Scalar,
271 YamlToken::FlowSeqEnd,
272 YamlToken::Newline,
273 ]
274 );
275 }
276
277 #[test]
278 fn lexer_tokens_round_trip_input_bytes_for_supported_cases() {
279 let cases = [
280 "foo: bar\n",
281 "a: [b, c]\n",
282 "---\nfoo: bar\n...\n",
283 "%YAML 1.2\nfoo: \"a#b\"\n",
284 ];
285
286 for input in cases {
287 let tokens = lex_mapping_tokens(input).expect("tokens");
288 let rebuilt = tokens.iter().map(|t| t.text).collect::<String>();
289 assert_eq!(rebuilt, input);
290 }
291 }
292
293 #[test]
294 fn lexer_emits_monotonic_byte_ranges() {
295 let input = "root: 1\n child: 2\n";
296 let tokens = lex_mapping_tokens(input).expect("tokens");
297
298 let mut offset = 0usize;
299 for token in tokens {
300 if token.text.is_empty() {
301 assert_eq!(token.byte_start, offset);
302 assert_eq!(token.byte_end, offset);
303 continue;
304 }
305
306 assert_eq!(token.byte_start, offset);
307 assert_eq!(&input[token.byte_start..token.byte_end], token.text);
308 offset = token.byte_end;
309 }
310
311 assert_eq!(offset, input.len());
312 }
313
314 #[test]
315 fn parser_preserves_document_markers_and_directives() {
316 let input = "%YAML 1.2\n---\nfoo: bar\n...\n";
317 let tree = parse_yaml_tree(input).expect("tree");
318 assert_eq!(tree.text().to_string(), input);
319
320 let scalar_tokens: Vec<String> = tree
321 .descendants_with_tokens()
322 .filter_map(|el| el.into_token())
323 .filter(|tok| tok.kind() == SyntaxKind::YAML_SCALAR)
324 .map(|tok| tok.text().to_string())
325 .collect();
326
327 assert!(scalar_tokens.contains(&"%YAML 1.2".to_string()));
328 assert!(scalar_tokens.contains(&"bar".to_string()));
329
330 let has_doc_start = tree
331 .descendants_with_tokens()
332 .filter_map(|el| el.into_token())
333 .any(|tok| tok.kind() == SyntaxKind::YAML_DOCUMENT_START && tok.text() == "---");
334 assert!(has_doc_start, "--- should be a YAML_DOCUMENT_START token");
335
336 let has_doc_end = tree
337 .descendants_with_tokens()
338 .filter_map(|el| el.into_token())
339 .any(|tok| tok.kind() == SyntaxKind::YAML_DOCUMENT_END && tok.text() == "...");
340 assert!(has_doc_end, "... should be a YAML_DOCUMENT_END token");
341 }
342
343 #[test]
344 fn parser_preserves_standalone_flow_mapping_lines() {
345 let input = "{foo: bar}\n";
346 let tree = parse_yaml_tree(input).expect("tree");
347 assert_eq!(tree.text().to_string(), input);
348
349 let flow_entry_count = tree
350 .descendants()
351 .filter(|n| n.kind() == SyntaxKind::YAML_FLOW_MAP_ENTRY)
352 .count();
353 assert_eq!(flow_entry_count, 1);
354
355 let flow_values: Vec<String> = tree
356 .descendants()
357 .filter(|n| n.kind() == SyntaxKind::YAML_FLOW_MAP_VALUE)
358 .map(|n| n.text().to_string())
359 .collect();
360 assert_eq!(flow_values, vec![" bar".to_string()]);
361 }
362
363 #[test]
364 fn parser_preserves_top_level_quoted_scalar_document() {
365 let input = "\"foo: bar\\\": baz\"\n";
366 let tree = parse_yaml_tree(input).expect("tree");
367 assert_eq!(tree.text().to_string(), input);
368 }
369
370 #[test]
371 fn parse_yaml_report_emits_error_code_for_invalid_yaml() {
372 let report = parse_yaml_report("this\n is\n invalid: x\n");
373 assert!(report.tree.is_none());
374 assert_eq!(report.diagnostics.len(), 1);
375 assert_eq!(
376 report.diagnostics[0].code,
377 diagnostic_codes::PARSE_UNEXPECTED_INDENT
378 );
379 }
380
381 #[test]
382 fn parse_yaml_report_detects_trailing_content_after_document_end() {
383 let report = parse_yaml_report("---\nkey: value\n... invalid\n");
384 assert!(report.tree.is_none());
385 assert_eq!(report.diagnostics.len(), 1);
386 assert_eq!(
387 report.diagnostics[0].code,
388 diagnostic_codes::LEX_TRAILING_CONTENT_AFTER_DOCUMENT_END
389 );
390 }
391
392 #[test]
393 fn parse_yaml_report_detects_unexpected_flow_closer() {
394 let report = parse_yaml_report("---\n[ a, b, c ] ]\n");
395 assert!(report.tree.is_none());
396 assert_eq!(report.diagnostics.len(), 1);
397 assert_eq!(
398 report.diagnostics[0].code,
399 diagnostic_codes::PARSE_TRAILING_CONTENT_AFTER_FLOW_END
400 );
401 }
402
403 #[test]
404 fn parse_yaml_report_detects_unterminated_nested_flow_sequence() {
405 let report = parse_yaml_report("---\n[ [ a, b, c ]\n");
406 assert!(report.tree.is_none());
407 assert_eq!(report.diagnostics.len(), 1);
408 assert_eq!(
409 report.diagnostics[0].code,
410 diagnostic_codes::PARSE_UNTERMINATED_FLOW_SEQUENCE
411 );
412 }
413
414 #[test]
415 fn parse_yaml_report_detects_invalid_leading_flow_sequence_comma() {
416 let report = parse_yaml_report("---\n[ , a, b, c ]\n");
417 assert!(report.tree.is_none());
418 assert_eq!(report.diagnostics.len(), 1);
419 assert_eq!(
420 report.diagnostics[0].code,
421 diagnostic_codes::PARSE_INVALID_FLOW_SEQUENCE_COMMA
422 );
423 }
424
425 #[test]
426 fn parse_yaml_report_detects_trailing_content_after_flow_end() {
427 let report = parse_yaml_report("---\n[ a, b, c, ]#invalid\n");
428 assert!(report.tree.is_none());
429 assert_eq!(report.diagnostics.len(), 1);
430 assert_eq!(
431 report.diagnostics[0].code,
432 diagnostic_codes::PARSE_TRAILING_CONTENT_AFTER_FLOW_END
433 );
434 }
435
436 #[test]
437 fn parse_yaml_report_detects_invalid_double_quoted_escape() {
438 let report = parse_yaml_report("---\n\"\\.\"\n");
439 assert!(report.tree.is_none());
440 assert_eq!(report.diagnostics.len(), 1);
441 assert_eq!(
442 report.diagnostics[0].code,
443 diagnostic_codes::LEX_INVALID_DOUBLE_QUOTED_ESCAPE
444 );
445 }
446
447 #[test]
448 fn parse_yaml_report_detects_trailing_content_after_document_start() {
449 let report = parse_yaml_report("--- key1: value1\n key2: value2\n");
450 assert!(report.tree.is_none());
451 assert_eq!(report.diagnostics.len(), 1);
452 assert_eq!(
453 report.diagnostics[0].code,
454 diagnostic_codes::LEX_TRAILING_CONTENT_AFTER_DOCUMENT_START
455 );
456 }
457
458 #[test]
459 fn parse_yaml_report_detects_directive_without_document_start() {
460 let report = parse_yaml_report("%YAML 1.2\n");
461 assert!(report.tree.is_none());
462 assert_eq!(report.diagnostics.len(), 1);
463 assert_eq!(
464 report.diagnostics[0].code,
465 diagnostic_codes::PARSE_DIRECTIVE_WITHOUT_DOCUMENT_START
466 );
467 }
468
469 #[test]
470 fn parse_yaml_report_detects_directive_after_content() {
471 let report = parse_yaml_report("!foo \"bar\"\n%TAG ! tag:example.com,2000:app/\n---\n");
472 assert!(report.tree.is_none());
473 assert_eq!(report.diagnostics.len(), 1);
474 assert_eq!(
475 report.diagnostics[0].code,
476 diagnostic_codes::PARSE_DIRECTIVE_AFTER_CONTENT
477 );
478 }
479
480 #[test]
481 fn parse_yaml_report_detects_wrong_indented_flow_continuation() {
482 let report = parse_yaml_report("---\nflow: [a,\nb,\nc]\n");
483 assert!(report.tree.is_none());
484 assert_eq!(report.diagnostics.len(), 1);
485 assert_eq!(
486 report.diagnostics[0].code,
487 diagnostic_codes::LEX_WRONG_INDENTED_FLOW
488 );
489 }
490
491 #[test]
492 fn parser_builds_flow_sequence_nodes_in_mapping_value() {
493 let input = "a: [b, c]\n";
494 let tree = parse_yaml_tree(input).expect("tree");
495 assert_eq!(tree.text().to_string(), input);
496
497 let seq = tree
498 .descendants()
499 .find(|n| n.kind() == SyntaxKind::YAML_FLOW_SEQUENCE)
500 .expect("flow sequence node");
501 let item_count = seq
502 .children()
503 .filter(|n| n.kind() == SyntaxKind::YAML_FLOW_SEQUENCE_ITEM)
504 .count();
505 assert_eq!(item_count, 2);
506 }
507
508 #[test]
509 fn parser_builds_multiline_flow_map_inside_block_sequence_item() {
510 let input = "- { multi\n line, a: b}\n";
511 let tree = parse_yaml_tree(input).expect("tree");
512 assert_eq!(tree.text().to_string(), input);
513
514 let seq = tree
515 .descendants()
516 .find(|n| n.kind() == SyntaxKind::YAML_BLOCK_SEQUENCE)
517 .expect("block sequence");
518 let item = seq
519 .children()
520 .find(|n| n.kind() == SyntaxKind::YAML_BLOCK_SEQUENCE_ITEM)
521 .expect("sequence item");
522 let flow_map = item
523 .children()
524 .find(|n| n.kind() == SyntaxKind::YAML_FLOW_MAP)
525 .expect("flow map inside sequence item");
526 let entry_count = flow_map
527 .children()
528 .filter(|n| n.kind() == SyntaxKind::YAML_FLOW_MAP_ENTRY)
529 .count();
530 assert_eq!(entry_count, 2);
531 }
532
533 #[test]
534 fn parser_builds_flow_sequence_inside_block_sequence_item() {
535 let input = "- [a, b]\n- [c, d]\n";
536 let tree = parse_yaml_tree(input).expect("tree");
537 assert_eq!(tree.text().to_string(), input);
538
539 let seq = tree
540 .descendants()
541 .find(|n| n.kind() == SyntaxKind::YAML_BLOCK_SEQUENCE)
542 .expect("block sequence");
543 let items: Vec<_> = seq
544 .children()
545 .filter(|n| n.kind() == SyntaxKind::YAML_BLOCK_SEQUENCE_ITEM)
546 .collect();
547 assert_eq!(items.len(), 2);
548
549 for item in &items {
550 let flow = item
551 .children()
552 .find(|n| n.kind() == SyntaxKind::YAML_FLOW_SEQUENCE)
553 .expect("flow sequence inside item");
554 let flow_items = flow
555 .children()
556 .filter(|n| n.kind() == SyntaxKind::YAML_FLOW_SEQUENCE_ITEM)
557 .count();
558 assert_eq!(flow_items, 2);
559 }
560 }
561
562 #[test]
563 fn lexer_recognizes_single_bang_tag_in_top_level_scalar() {
564 let tokens = lex_mapping_tokens("! a\n").expect("tokens");
565 let kinds: Vec<_> = tokens.iter().map(|t| t.kind).collect();
566 assert_eq!(
567 kinds,
568 vec![
569 YamlToken::Tag,
570 YamlToken::Whitespace,
571 YamlToken::Scalar,
572 YamlToken::Newline,
573 ]
574 );
575 let texts: Vec<_> = tokens.iter().map(|t| t.text).collect();
576 assert_eq!(texts, vec!["!", " ", "a", "\n"]);
577 }
578
579 #[test]
580 fn parser_emits_scalar_document_for_tag_without_colon() {
581 let input = "! a\n";
582 let tree = parse_yaml_tree(input).expect("tree");
583 assert_eq!(tree.text().to_string(), input);
584
585 let has_block_map = tree
586 .descendants()
587 .any(|n| n.kind() == SyntaxKind::YAML_BLOCK_MAP);
588 assert!(
589 !has_block_map,
590 "scalar document should not be wrapped in YAML_BLOCK_MAP"
591 );
592
593 let has_tag = tree
594 .descendants_with_tokens()
595 .filter_map(|el| el.into_token())
596 .any(|tok| tok.kind() == SyntaxKind::YAML_TAG && tok.text() == "!");
597 assert!(has_tag, "tree should contain YAML_TAG '!'");
598 }
599
600 #[test]
601 fn lexer_extracts_explicit_tag_before_block_sequence_scalar() {
602 let tokens = lex_mapping_tokens("- !!int 1\n").expect("tokens");
603 let kinds: Vec<_> = tokens.iter().map(|t| t.kind).collect();
604 assert_eq!(
605 kinds,
606 vec![
607 YamlToken::BlockSeqEntry,
608 YamlToken::Whitespace,
609 YamlToken::Tag,
610 YamlToken::Whitespace,
611 YamlToken::Scalar,
612 YamlToken::Newline,
613 ]
614 );
615 let texts: Vec<_> = tokens.iter().map(|t| t.text).collect();
616 assert_eq!(texts, vec!["-", " ", "!!int", " ", "1", "\n"]);
617 }
618
619 #[test]
620 fn parser_builds_nested_block_map_inside_block_sequence() {
621 let input = "-\n name: Mark\n hr: 65\n";
622 let tree = parse_yaml_tree(input).expect("tree");
623 assert_eq!(tree.text().to_string(), input);
624
625 let seq = tree
626 .descendants()
627 .find(|n| n.kind() == SyntaxKind::YAML_BLOCK_SEQUENCE)
628 .expect("block sequence");
629 let items: Vec<_> = seq
630 .children()
631 .filter(|n| n.kind() == SyntaxKind::YAML_BLOCK_SEQUENCE_ITEM)
632 .collect();
633 assert_eq!(items.len(), 1);
634
635 let nested_map = items[0]
636 .children()
637 .find(|n| n.kind() == SyntaxKind::YAML_BLOCK_MAP)
638 .expect("nested block map inside sequence item");
639 let entry_count = nested_map
640 .children()
641 .filter(|n| n.kind() == SyntaxKind::YAML_BLOCK_MAP_ENTRY)
642 .count();
643 assert_eq!(entry_count, 2);
644 }
645
646 #[test]
647 fn parser_builds_nested_block_map_from_indent_tokens() {
648 let input = "root: 1\n child: 2\n";
649 let tree = parse_yaml_tree(input).expect("tree");
650
651 let outer_map = tree
652 .descendants()
653 .find(|n| n.kind() == SyntaxKind::YAML_BLOCK_MAP)
654 .expect("outer map");
655 let outer_entry = outer_map
656 .children()
657 .find(|n| n.kind() == SyntaxKind::YAML_BLOCK_MAP_ENTRY)
658 .expect("outer entry");
659 let outer_value = outer_entry
660 .children()
661 .find(|n| n.kind() == SyntaxKind::YAML_BLOCK_MAP_VALUE)
662 .expect("outer value");
663
664 let nested_map = outer_value
665 .children()
666 .find(|n| n.kind() == SyntaxKind::YAML_BLOCK_MAP)
667 .expect("nested map");
668 let nested_entry_count = nested_map
669 .children()
670 .filter(|n| n.kind() == SyntaxKind::YAML_BLOCK_MAP_ENTRY)
671 .count();
672 assert_eq!(nested_entry_count, 1);
673 }
674
675 #[test]
676 fn shadow_parse_is_disabled_by_default() {
677 let report = parse_shadow("title: My Title", ShadowYamlOptions::default());
678 assert_eq!(report.outcome, ShadowYamlOutcome::SkippedDisabled);
679 assert_eq!(report.shadow_reason, "shadow-disabled");
680 assert_eq!(report.normalized_input, None);
681 }
682
683 #[test]
684 fn shadow_parse_skips_when_disabled_even_for_valid_input() {
685 let report = parse_shadow(
686 "title: My Title",
687 ShadowYamlOptions {
688 enabled: false,
689 input_kind: YamlInputKind::Plain,
690 },
691 );
692 assert_eq!(report.outcome, ShadowYamlOutcome::SkippedDisabled);
693 assert_eq!(report.shadow_reason, "shadow-disabled");
694 }
695
696 #[test]
697 fn shadow_parse_reports_prototype_parsed_when_enabled() {
698 let report = parse_shadow(
699 "title: My Title",
700 ShadowYamlOptions {
701 enabled: true,
702 input_kind: YamlInputKind::Plain,
703 },
704 );
705 assert_eq!(report.outcome, ShadowYamlOutcome::PrototypeParsed);
706 assert_eq!(report.shadow_reason, "prototype-basic-mapping-parsed");
707 assert_eq!(report.normalized_input.as_deref(), Some("title: My Title"));
708 }
709
710 #[test]
711 fn shadow_parse_reports_prototype_rejected_when_enabled() {
712 let report = parse_shadow(
714 "\ttitle: value",
715 ShadowYamlOptions {
716 enabled: true,
717 input_kind: YamlInputKind::Plain,
718 },
719 );
720 assert_eq!(report.outcome, ShadowYamlOutcome::PrototypeRejected);
721 assert_eq!(report.shadow_reason, "prototype-basic-mapping-rejected");
722 }
723
724 #[test]
725 fn shadow_parse_accepts_hashpipe_mode_but_remains_prototype_scoped() {
726 let report = parse_shadow(
727 "#| title: My Title",
728 ShadowYamlOptions {
729 enabled: true,
730 input_kind: YamlInputKind::Hashpipe,
731 },
732 );
733 assert_eq!(report.outcome, ShadowYamlOutcome::PrototypeParsed);
734 assert_eq!(report.shadow_reason, "prototype-basic-mapping-parsed");
735 assert_eq!(report.normalized_input.as_deref(), Some("title: My Title"));
736 }
737}