1#[path = "yaml/cooking.rs"]
12mod cooking;
13#[path = "yaml/events.rs"]
14mod events;
15#[path = "yaml/model.rs"]
16mod model;
17#[path = "yaml/parser.rs"]
18mod parser;
19#[path = "yaml/scanner.rs"]
20mod scanner;
21#[path = "yaml/validator.rs"]
22mod validator;
23
24pub use events::{project_events, project_events_from_tree};
25pub use model::{
26 ShadowYamlOptions, ShadowYamlOutcome, ShadowYamlReport, YamlDiagnostic, YamlInputKind,
27 YamlParseReport, diagnostic_codes,
28};
29pub use parser::{
30 ShadowParserReport, parse_shadow, parse_stream, parse_yaml_report, parse_yaml_tree,
31 shadow_parser_check,
32};
33pub use scanner::{ShadowScannerReport, shadow_scanner_check};
34
35#[doc(hidden)]
36pub fn validate_yaml_for_test(input: &str) -> Option<YamlDiagnostic> {
37 validator::validate_yaml(input)
38}
39
40#[cfg(test)]
41mod tests {
42 use super::*;
43 use crate::syntax::SyntaxKind;
44
45 #[test]
46 fn builds_basic_rowan_tree_for_multiline_mapping() {
47 let tree = parse_yaml_tree("title: My Title\nauthor: Me\n").expect("tree");
48 assert_eq!(tree.kind(), SyntaxKind::DOCUMENT);
49 assert_eq!(tree.text().to_string(), "title: My Title\nauthor: Me\n");
50
51 let mapping = tree
52 .descendants()
53 .find(|n| n.kind() == SyntaxKind::YAML_BLOCK_MAP)
54 .expect("yaml block map");
55 let entries: Vec<_> = mapping
56 .children()
57 .filter(|n| n.kind() == SyntaxKind::YAML_BLOCK_MAP_ENTRY)
58 .collect();
59 assert_eq!(entries.len(), 2);
60
61 let token_kinds: Vec<_> = mapping
62 .descendants_with_tokens()
63 .filter_map(|el| el.into_token())
64 .map(|tok| tok.kind())
65 .collect();
66 assert_eq!(
67 token_kinds,
68 vec![
69 SyntaxKind::YAML_SCALAR,
70 SyntaxKind::YAML_COLON,
71 SyntaxKind::WHITESPACE,
72 SyntaxKind::YAML_SCALAR,
73 SyntaxKind::NEWLINE,
74 SyntaxKind::YAML_SCALAR,
75 SyntaxKind::YAML_COLON,
76 SyntaxKind::WHITESPACE,
77 SyntaxKind::YAML_SCALAR,
78 SyntaxKind::NEWLINE,
79 ]
80 );
81 }
82
83 fn block_map_key_texts(tree: &crate::syntax::SyntaxNode) -> Vec<String> {
84 tree.descendants()
85 .filter(|n| n.kind() == SyntaxKind::YAML_BLOCK_MAP_KEY)
86 .map(|key| {
87 key.children_with_tokens()
88 .filter_map(|el| el.into_token())
89 .filter(|tok| tok.kind() == SyntaxKind::YAML_SCALAR)
90 .map(|tok| tok.text().to_string())
91 .collect::<Vec<_>>()
92 .join("")
93 })
94 .filter(|s| !s.is_empty())
95 .collect()
96 }
97
98 #[test]
99 fn mapping_nodes_preserve_entry_text_boundaries() {
100 let tree = parse_yaml_tree("title: A\nauthor: B\n").expect("tree");
101 let mapping = tree
102 .descendants()
103 .find(|n| n.kind() == SyntaxKind::YAML_BLOCK_MAP)
104 .expect("yaml block map");
105
106 let entry_texts: Vec<_> = mapping
107 .children()
108 .filter(|n| n.kind() == SyntaxKind::YAML_BLOCK_MAP_ENTRY)
109 .map(|n| n.text().to_string())
110 .collect();
111 assert_eq!(
112 entry_texts,
113 vec!["title: A\n".to_string(), "author: B\n".to_string(),]
114 );
115 }
116
117 #[test]
118 fn splits_mapping_on_colon_outside_quoted_key() {
119 let input = "\"foo:bar\": 23\n'x:y': 24\n";
120 let tree = parse_yaml_tree(input).expect("tree");
121 assert_eq!(tree.text().to_string(), input);
122 assert_eq!(
123 block_map_key_texts(&tree),
124 vec!["\"foo:bar\"".to_string(), "'x:y'".to_string()]
125 );
126 }
127
128 #[test]
129 fn keeps_colon_inside_escaped_double_quoted_key() {
130 let input = "\"foo\\\":bar\": 23\n";
131 let tree = parse_yaml_tree(input).expect("tree");
132 assert_eq!(tree.text().to_string(), input);
133 assert_eq!(
134 block_map_key_texts(&tree),
135 vec!["\"foo\\\":bar\"".to_string()]
136 );
137 }
138
139 #[test]
140 fn keeps_hash_in_double_quoted_scalar_value() {
141 let input = "foo: \"a#b\"\n";
142 let tree = parse_yaml_tree(input).expect("tree");
143
144 let comment_count = tree
145 .descendants_with_tokens()
146 .filter_map(|el| el.into_token())
147 .filter(|tok| tok.kind() == SyntaxKind::YAML_COMMENT)
148 .count();
149 assert_eq!(comment_count, 0);
150
151 let value_scalars: Vec<String> = tree
152 .descendants()
153 .filter(|n| n.kind() == SyntaxKind::YAML_BLOCK_MAP_VALUE)
154 .flat_map(|value| {
155 value
156 .children_with_tokens()
157 .filter_map(|el| el.into_token())
158 .filter(|tok| tok.kind() == SyntaxKind::YAML_SCALAR)
159 .map(|tok| tok.text().to_string())
160 .collect::<Vec<_>>()
161 })
162 .collect();
163 assert_eq!(value_scalars, vec!["\"a#b\"".to_string()]);
164 }
165
166 #[test]
167 fn keeps_colon_inside_single_quoted_key_with_escaped_quote() {
168 let input = "'foo'':bar': 23\n";
169 let tree = parse_yaml_tree(input).expect("tree");
170 assert_eq!(tree.text().to_string(), input);
171 assert_eq!(block_map_key_texts(&tree), vec!["'foo'':bar'".to_string()]);
172 }
173
174 #[test]
175 fn parser_preserves_document_markers_and_directives() {
176 let input = "%YAML 1.2\n---\nfoo: bar\n...\n";
177 let tree = parse_yaml_tree(input).expect("tree");
178 assert_eq!(tree.text().to_string(), input);
179
180 let scalar_tokens: Vec<String> = tree
181 .descendants_with_tokens()
182 .filter_map(|el| el.into_token())
183 .filter(|tok| tok.kind() == SyntaxKind::YAML_SCALAR)
184 .map(|tok| tok.text().to_string())
185 .collect();
186
187 assert!(scalar_tokens.contains(&"%YAML 1.2".to_string()));
188 assert!(scalar_tokens.contains(&"bar".to_string()));
189
190 let has_doc_start = tree
191 .descendants_with_tokens()
192 .filter_map(|el| el.into_token())
193 .any(|tok| tok.kind() == SyntaxKind::YAML_DOCUMENT_START && tok.text() == "---");
194 assert!(has_doc_start, "--- should be a YAML_DOCUMENT_START token");
195
196 let has_doc_end = tree
197 .descendants_with_tokens()
198 .filter_map(|el| el.into_token())
199 .any(|tok| tok.kind() == SyntaxKind::YAML_DOCUMENT_END && tok.text() == "...");
200 assert!(has_doc_end, "... should be a YAML_DOCUMENT_END token");
201 }
202
203 #[test]
204 fn parser_preserves_standalone_flow_mapping_lines() {
205 let input = "{foo: bar}\n";
206 let tree = parse_yaml_tree(input).expect("tree");
207 assert_eq!(tree.text().to_string(), input);
208
209 let flow_entry_count = tree
210 .descendants()
211 .filter(|n| n.kind() == SyntaxKind::YAML_FLOW_MAP_ENTRY)
212 .count();
213 assert_eq!(flow_entry_count, 1);
214
215 let flow_values: Vec<String> = tree
216 .descendants()
217 .filter(|n| n.kind() == SyntaxKind::YAML_FLOW_MAP_VALUE)
218 .map(|n| n.text().to_string())
219 .collect();
220 assert_eq!(flow_values, vec![" bar".to_string()]);
221 }
222
223 #[test]
224 fn parser_preserves_top_level_quoted_scalar_document() {
225 let input = "\"foo: bar\\\": baz\"\n";
226 let tree = parse_yaml_tree(input).expect("tree");
227 assert_eq!(tree.text().to_string(), input);
228 }
229
230 #[test]
231 fn parse_yaml_report_emits_error_code_for_invalid_yaml() {
232 let report = parse_yaml_report("this\n is\n invalid: x\n");
236 assert!(report.tree.is_none());
237 assert_eq!(report.diagnostics.len(), 1);
238 assert_eq!(
239 report.diagnostics[0].code,
240 diagnostic_codes::PARSE_INVALID_KEY_TOKEN
241 );
242 }
243
244 #[test]
245 fn parse_yaml_report_detects_trailing_content_after_document_end() {
246 let report = parse_yaml_report("---\nkey: value\n... invalid\n");
247 assert!(report.tree.is_none());
248 assert_eq!(report.diagnostics.len(), 1);
249 assert_eq!(
250 report.diagnostics[0].code,
251 diagnostic_codes::LEX_TRAILING_CONTENT_AFTER_DOCUMENT_END
252 );
253 }
254
255 #[test]
256 fn parse_yaml_report_detects_unexpected_flow_closer() {
257 let report = parse_yaml_report("---\n[ a, b, c ] ]\n");
258 assert!(report.tree.is_none());
259 assert_eq!(report.diagnostics.len(), 1);
260 assert_eq!(
261 report.diagnostics[0].code,
262 diagnostic_codes::PARSE_TRAILING_CONTENT_AFTER_FLOW_END
263 );
264 }
265
266 #[test]
267 fn parse_yaml_report_detects_unterminated_nested_flow_sequence() {
268 let report = parse_yaml_report("---\n[ [ a, b, c ]\n");
269 assert!(report.tree.is_none());
270 assert_eq!(report.diagnostics.len(), 1);
271 assert_eq!(
272 report.diagnostics[0].code,
273 diagnostic_codes::PARSE_UNTERMINATED_FLOW_SEQUENCE
274 );
275 }
276
277 #[test]
278 fn parse_yaml_report_detects_invalid_leading_flow_sequence_comma() {
279 let report = parse_yaml_report("---\n[ , a, b, c ]\n");
280 assert!(report.tree.is_none());
281 assert_eq!(report.diagnostics.len(), 1);
282 assert_eq!(
283 report.diagnostics[0].code,
284 diagnostic_codes::PARSE_INVALID_FLOW_SEQUENCE_COMMA
285 );
286 }
287
288 #[test]
289 fn parse_yaml_report_detects_trailing_content_after_flow_end() {
290 let report = parse_yaml_report("---\n[ a, b, c, ]#invalid\n");
291 assert!(report.tree.is_none());
292 assert_eq!(report.diagnostics.len(), 1);
293 assert_eq!(
294 report.diagnostics[0].code,
295 diagnostic_codes::PARSE_TRAILING_CONTENT_AFTER_FLOW_END
296 );
297 }
298
299 #[test]
300 fn parse_yaml_report_detects_invalid_double_quoted_escape() {
301 let report = parse_yaml_report("---\n\"\\.\"\n");
302 assert!(report.tree.is_none());
303 assert_eq!(report.diagnostics.len(), 1);
304 assert_eq!(
305 report.diagnostics[0].code,
306 diagnostic_codes::LEX_INVALID_DOUBLE_QUOTED_ESCAPE
307 );
308 }
309
310 #[test]
311 fn parse_yaml_report_detects_trailing_content_after_document_start() {
312 let report = parse_yaml_report("--- key1: value1\n key2: value2\n");
313 assert!(report.tree.is_none());
314 assert_eq!(report.diagnostics.len(), 1);
315 assert_eq!(
316 report.diagnostics[0].code,
317 diagnostic_codes::LEX_TRAILING_CONTENT_AFTER_DOCUMENT_START
318 );
319 }
320
321 #[test]
322 fn parse_yaml_report_detects_directive_without_document_start() {
323 let report = parse_yaml_report("%YAML 1.2\n");
324 assert!(report.tree.is_none());
325 assert_eq!(report.diagnostics.len(), 1);
326 assert_eq!(
327 report.diagnostics[0].code,
328 diagnostic_codes::PARSE_DIRECTIVE_WITHOUT_DOCUMENT_START
329 );
330 }
331
332 #[test]
333 fn parse_yaml_report_detects_directive_after_content() {
334 let report = parse_yaml_report("!foo \"bar\"\n%TAG !x! tag:example.com,2014:\n---\n");
338 assert!(report.tree.is_none());
339 assert_eq!(report.diagnostics.len(), 1);
340 assert_eq!(
341 report.diagnostics[0].code,
342 diagnostic_codes::PARSE_DIRECTIVE_AFTER_CONTENT
343 );
344 }
345
346 #[test]
347 fn parse_yaml_report_detects_wrong_indented_flow_continuation() {
348 let report = parse_yaml_report("---\nflow: [a,\nb,\nc]\n");
349 assert!(report.tree.is_none());
350 assert_eq!(report.diagnostics.len(), 1);
351 assert_eq!(
352 report.diagnostics[0].code,
353 diagnostic_codes::LEX_WRONG_INDENTED_FLOW
354 );
355 }
356
357 #[test]
358 fn parser_builds_flow_sequence_nodes_in_mapping_value() {
359 let input = "a: [b, c]\n";
360 let tree = parse_yaml_tree(input).expect("tree");
361 assert_eq!(tree.text().to_string(), input);
362
363 let seq = tree
364 .descendants()
365 .find(|n| n.kind() == SyntaxKind::YAML_FLOW_SEQUENCE)
366 .expect("flow sequence node");
367 let item_count = seq
368 .children()
369 .filter(|n| n.kind() == SyntaxKind::YAML_FLOW_SEQUENCE_ITEM)
370 .count();
371 assert_eq!(item_count, 2);
372 }
373
374 #[test]
375 fn parser_absorbs_literal_block_scalar_into_map_value() {
376 let input = "a: |\n line1\n line2\n";
377 let tree = parse_yaml_tree(input).expect("tree");
378 assert_eq!(tree.text().to_string(), input);
379
380 let map = tree
381 .descendants()
382 .find(|n| n.kind() == SyntaxKind::YAML_BLOCK_MAP)
383 .expect("block map");
384 let entry = map
385 .children()
386 .find(|n| n.kind() == SyntaxKind::YAML_BLOCK_MAP_ENTRY)
387 .expect("entry");
388 let value = entry
389 .children()
390 .find(|n| n.kind() == SyntaxKind::YAML_BLOCK_MAP_VALUE)
391 .expect("value");
392 let value_text = value.text().to_string();
393 assert!(
394 value_text.starts_with('|') || value_text.starts_with(" |"),
395 "value should contain the `|` header, got {value_text:?}"
396 );
397 assert!(
398 value_text.contains("line1") && value_text.contains("line2"),
399 "value should absorb block scalar content, got {value_text:?}"
400 );
401 }
402
403 #[test]
404 fn parser_builds_nested_block_sequence_on_same_line() {
405 let input = "- - a\n - b\n- c\n";
406 let tree = parse_yaml_tree(input).expect("tree");
407 assert_eq!(tree.text().to_string(), input);
408
409 let outer = tree
410 .descendants()
411 .find(|n| n.kind() == SyntaxKind::YAML_BLOCK_SEQUENCE)
412 .expect("outer block sequence");
413 let outer_items: Vec<_> = outer
414 .children()
415 .filter(|n| n.kind() == SyntaxKind::YAML_BLOCK_SEQUENCE_ITEM)
416 .collect();
417 assert_eq!(outer_items.len(), 2);
418
419 let nested = outer_items[0]
420 .children()
421 .find(|n| n.kind() == SyntaxKind::YAML_BLOCK_SEQUENCE)
422 .expect("nested block sequence inside first item");
423 let nested_items = nested
424 .children()
425 .filter(|n| n.kind() == SyntaxKind::YAML_BLOCK_SEQUENCE_ITEM)
426 .count();
427 assert_eq!(nested_items, 2);
428 }
429
430 #[test]
431 fn parser_builds_multiline_flow_map_inside_block_sequence_item() {
432 let input = "- { multi\n line, a: b}\n";
433 let tree = parse_yaml_tree(input).expect("tree");
434 assert_eq!(tree.text().to_string(), input);
435
436 let seq = tree
437 .descendants()
438 .find(|n| n.kind() == SyntaxKind::YAML_BLOCK_SEQUENCE)
439 .expect("block sequence");
440 let item = seq
441 .children()
442 .find(|n| n.kind() == SyntaxKind::YAML_BLOCK_SEQUENCE_ITEM)
443 .expect("sequence item");
444 item.children()
445 .find(|n| n.kind() == SyntaxKind::YAML_FLOW_MAP)
446 .expect("flow map inside sequence item");
447 }
448
449 #[test]
450 fn parser_builds_flow_sequence_inside_block_sequence_item() {
451 let input = "- [a, b]\n- [c, d]\n";
452 let tree = parse_yaml_tree(input).expect("tree");
453 assert_eq!(tree.text().to_string(), input);
454
455 let seq = tree
456 .descendants()
457 .find(|n| n.kind() == SyntaxKind::YAML_BLOCK_SEQUENCE)
458 .expect("block sequence");
459 let items: Vec<_> = seq
460 .children()
461 .filter(|n| n.kind() == SyntaxKind::YAML_BLOCK_SEQUENCE_ITEM)
462 .collect();
463 assert_eq!(items.len(), 2);
464
465 for item in &items {
466 let flow = item
467 .children()
468 .find(|n| n.kind() == SyntaxKind::YAML_FLOW_SEQUENCE)
469 .expect("flow sequence inside item");
470 let flow_items = flow
471 .children()
472 .filter(|n| n.kind() == SyntaxKind::YAML_FLOW_SEQUENCE_ITEM)
473 .count();
474 assert_eq!(flow_items, 2);
475 }
476 }
477
478 #[test]
479 fn parser_emits_scalar_document_for_tag_without_colon() {
480 let input = "! a\n";
481 let tree = parse_yaml_tree(input).expect("tree");
482 assert_eq!(tree.text().to_string(), input);
483
484 let has_block_map = tree
485 .descendants()
486 .any(|n| n.kind() == SyntaxKind::YAML_BLOCK_MAP);
487 assert!(
488 !has_block_map,
489 "scalar document should not be wrapped in YAML_BLOCK_MAP"
490 );
491
492 let has_tag_token = tree
495 .descendants_with_tokens()
496 .filter_map(|el| el.into_token())
497 .any(|tok| tok.kind() == SyntaxKind::YAML_TAG && tok.text() == "!");
498 assert!(
499 has_tag_token,
500 "tree should contain a YAML_TAG token for the leading `!`"
501 );
502 }
503
504 #[test]
505 fn parser_builds_nested_block_map_inside_block_sequence() {
506 let input = "-\n name: Mark\n hr: 65\n";
507 let tree = parse_yaml_tree(input).expect("tree");
508 assert_eq!(tree.text().to_string(), input);
509
510 let seq = tree
511 .descendants()
512 .find(|n| n.kind() == SyntaxKind::YAML_BLOCK_SEQUENCE)
513 .expect("block sequence");
514 let items: Vec<_> = seq
515 .children()
516 .filter(|n| n.kind() == SyntaxKind::YAML_BLOCK_SEQUENCE_ITEM)
517 .collect();
518 assert_eq!(items.len(), 1);
519
520 let nested_map = items[0]
521 .children()
522 .find(|n| n.kind() == SyntaxKind::YAML_BLOCK_MAP)
523 .expect("nested block map inside sequence item");
524 let entry_count = nested_map
525 .children()
526 .filter(|n| n.kind() == SyntaxKind::YAML_BLOCK_MAP_ENTRY)
527 .count();
528 assert_eq!(entry_count, 2);
529 }
530
531 #[test]
532 fn parser_builds_nested_block_map_from_indent_tokens() {
533 let input = "root:\n child: 2\n";
534 let tree = parse_yaml_tree(input).expect("tree");
535
536 let outer_map = tree
537 .descendants()
538 .find(|n| n.kind() == SyntaxKind::YAML_BLOCK_MAP)
539 .expect("outer map");
540 let outer_entry = outer_map
541 .children()
542 .find(|n| n.kind() == SyntaxKind::YAML_BLOCK_MAP_ENTRY)
543 .expect("outer entry");
544 let outer_value = outer_entry
545 .children()
546 .find(|n| n.kind() == SyntaxKind::YAML_BLOCK_MAP_VALUE)
547 .expect("outer value");
548
549 let nested_map = outer_value
550 .children()
551 .find(|n| n.kind() == SyntaxKind::YAML_BLOCK_MAP)
552 .expect("nested map");
553 let nested_entry_count = nested_map
554 .children()
555 .filter(|n| n.kind() == SyntaxKind::YAML_BLOCK_MAP_ENTRY)
556 .count();
557 assert_eq!(nested_entry_count, 1);
558 }
559
560 #[test]
561 fn shadow_parse_is_disabled_by_default() {
562 let report = parse_shadow("title: My Title", ShadowYamlOptions::default());
563 assert_eq!(report.outcome, ShadowYamlOutcome::SkippedDisabled);
564 assert_eq!(report.shadow_reason, "shadow-disabled");
565 assert_eq!(report.normalized_input, None);
566 }
567
568 #[test]
569 fn shadow_parse_skips_when_disabled_even_for_valid_input() {
570 let report = parse_shadow(
571 "title: My Title",
572 ShadowYamlOptions {
573 enabled: false,
574 input_kind: YamlInputKind::Plain,
575 },
576 );
577 assert_eq!(report.outcome, ShadowYamlOutcome::SkippedDisabled);
578 assert_eq!(report.shadow_reason, "shadow-disabled");
579 }
580
581 #[test]
582 fn shadow_parse_reports_prototype_parsed_when_enabled() {
583 let report = parse_shadow(
584 "title: My Title",
585 ShadowYamlOptions {
586 enabled: true,
587 input_kind: YamlInputKind::Plain,
588 },
589 );
590 assert_eq!(report.outcome, ShadowYamlOutcome::PrototypeParsed);
591 assert_eq!(report.shadow_reason, "prototype-basic-mapping-parsed");
592 assert_eq!(report.normalized_input.as_deref(), Some("title: My Title"));
593 }
594
595 #[test]
596 fn shadow_parse_reports_prototype_rejected_when_enabled() {
597 let report = parse_shadow(
601 "[ a, b",
602 ShadowYamlOptions {
603 enabled: true,
604 input_kind: YamlInputKind::Plain,
605 },
606 );
607 assert_eq!(report.outcome, ShadowYamlOutcome::PrototypeRejected);
608 assert_eq!(report.shadow_reason, "prototype-basic-mapping-rejected");
609 }
610
611 #[test]
612 fn shadow_parse_accepts_hashpipe_mode_but_remains_prototype_scoped() {
613 let report = parse_shadow(
614 "#| title: My Title",
615 ShadowYamlOptions {
616 enabled: true,
617 input_kind: YamlInputKind::Hashpipe,
618 },
619 );
620 assert_eq!(report.outcome, ShadowYamlOutcome::PrototypeParsed);
621 assert_eq!(report.shadow_reason, "prototype-basic-mapping-parsed");
622 assert_eq!(report.normalized_input.as_deref(), Some("title: My Title"));
623 }
624}