Skip to main content

panache_parser/parser/
yaml.rs

1//! YAML parser groundwork for long-term Panache integration.
2//!
3//! This module is intentionally minimal and currently acts as a placeholder for a
4//! future in-tree YAML parser that can produce Panache-compatible CST structures.
5//! Initial goals:
6//! - support plain YAML and hashpipe-prefixed YAML from shared parsing primitives,
7//! - preserve lossless syntax/trivia needed for exact host document ranges,
8//! - enable shadow-mode comparison against the existing YAML engine before rollout.
9//! - prepare for first-class YAML formatting support once parser parity is proven.
10
11#[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        // `this` at the top of a block-map context is a stray scalar with no
233        // following colon — flagged at the leading scalar rather than at the
234        // later indent that surfaced as a side-effect.
235        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        // Tag-shape: tag dispatch terminates the scalar before `%TAG`
335        // hits column 0, so the directive lands in its real position
336        // after content.
337        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        // The scanner emits the leading `!` as a dedicated YAML_TAG
493        // token; the projection layer reads the tag from that token.
494        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        // An unterminated flow sequence is rejected by the v2-aware
598        // structural validator, which is the rejection signal exercised
599        // by the shadow parse plumbing.
600        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}