1mod directives;
26mod documents;
27mod flow;
28mod keys;
29mod properties;
30mod scalars;
31mod structure;
32
33use crate::as_yaml::SharedIdentities;
34use crate::column::ColumnIndex;
35use crate::nodes::{in_flow_collection, starts_a_flow_collection};
36use crate::yaml::{Document, SyntaxNode};
37use rowan::ast::AstNode;
38use std::fmt;
39
40use directives::{
41 check_directive_after_document, check_directive_in_content, check_directives,
42 check_tag_handles_declared, check_tag_shorthand_scope,
43};
44use documents::{
45 check_doc_start_token_content, check_document_level_anchors, check_document_marker_placement,
46 check_single_content_node,
47};
48use flow::{
49 check_block_scalar_in_flow, check_flow_collection_commas, check_flow_continuation_indent,
50 check_flow_pair_placement, check_tag_followed_by_comma,
51};
52use keys::{
53 check_duplicate_keys, check_merge_operands, check_omap_keys, check_pairs_items,
54 check_set_values, DocumentRegistry,
55};
56use properties::{
57 check_anchor_and_alias, check_collection_tag_content, check_multiple_anchors,
58 check_multiple_tags, check_tag_characters, check_tag_on_alias, check_undefined_aliases,
59};
60use scalars::{
61 check_block_scalar_content_indent, check_block_scalar_header_indent,
62 check_block_scalar_indicator, check_colon_in_plain_scalar, check_escape_sequences,
63 check_flow_indicator_in_plain_scalar, check_legacy_yaml_1_1, check_plain_scalar_start,
64 check_quoted_string_indentation, check_trailing_content_after_quoted,
65};
66use structure::{
67 check_block_mapping_after_properties, check_block_mapping_entries_on_same_line,
68 check_collection_on_same_line_as_key, check_comment_token_whitespace,
69 check_explicit_key_separation, check_explicit_value_column, check_implicit_key_multiline,
70 check_mapping_entry_indentation, check_sequence_indentation, check_tab_usage,
71};
72
73#[derive(Debug, Clone, PartialEq, Eq)]
75pub struct Violation {
76 pub message: String,
78 pub location: Option<String>,
80 pub text_range: Option<crate::TextPosition>,
82 pub severity: Severity,
84 pub rule: Rule,
86}
87
88#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
90#[non_exhaustive]
91pub enum Severity {
92 Error,
94 Warning,
96}
97
98#[derive(Debug, Clone, Copy, PartialEq, Eq)]
100#[non_exhaustive]
101pub enum Rule {
102 InvalidIndentation,
104 InvalidDocumentMarker,
106 InvalidTabUsage,
108 MissingSyntax,
110 InvalidEscape,
112 DuplicateKeys,
114 InvalidAnchor,
116 InvalidTag,
118 InvalidCharacter,
121 LegacyYaml11,
123 Other,
125}
126
127impl fmt::Display for Violation {
128 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
129 match &self.location {
130 Some(loc) => write!(
131 f,
132 "[{}] {}: {} ({:?})",
133 match self.severity {
134 Severity::Error => "ERROR",
135 Severity::Warning => "WARN",
136 },
137 loc,
138 self.message,
139 self.rule
140 ),
141 None => write!(
142 f,
143 "[{}] {} ({:?})",
144 match self.severity {
145 Severity::Error => "ERROR",
146 Severity::Warning => "WARN",
147 },
148 self.message,
149 self.rule
150 ),
151 }
152 }
153}
154
155#[derive(Debug, Clone)]
161pub struct Validator {
162 config: ValidatorConfig,
164}
165
166#[derive(Debug, Clone)]
168pub struct ValidatorConfig {
169 pub check_duplicate_keys: bool,
171 pub check_indentation: bool,
173 pub check_tabs: bool,
175 pub check_document_markers: bool,
177 pub check_anchors: bool,
179}
180
181impl Default for ValidatorConfig {
182 fn default() -> Self {
183 Self {
184 check_duplicate_keys: true,
185 check_indentation: true,
186 check_tabs: true,
187 check_document_markers: true,
188 check_anchors: true,
189 }
190 }
191}
192
193fn check_printable(node: &SyntaxNode, violations: &mut Vec<Violation>) {
196 for token in node
197 .descendants_with_tokens()
198 .filter_map(|element| element.into_token())
199 .filter(|token| token.kind() != crate::SyntaxKind::BOM)
200 {
201 let text = token.text();
202 if let Some((offset, c)) = text
203 .char_indices()
204 .find(|&(_, c)| !crate::lex::is_printable(c))
205 {
206 let start = token.text_range().start() + rowan::TextSize::new(offset as u32);
207 violations.push(Violation::error_at(
208 Rule::InvalidCharacter,
209 rowan::TextRange::at(start, rowan::TextSize::of(c)),
210 format!("Non-printable character U+{:04X}", c as u32),
211 ));
212 }
213 }
214}
215
216fn find_root(node: &SyntaxNode) -> SyntaxNode {
218 if node.kind() == crate::SyntaxKind::ROOT {
219 return node.clone();
220 }
221 node.ancestors()
222 .find(|n| n.kind() == crate::SyntaxKind::ROOT)
223 .unwrap_or_else(|| node.clone())
224}
225
226fn directive_groups(root: &SyntaxNode) -> Vec<(Vec<SyntaxNode>, Option<SyntaxNode>)> {
230 let mut groups = Vec::new();
231 let mut directives = Vec::new();
232 for child in root.children() {
233 match child.kind() {
234 crate::SyntaxKind::DIRECTIVE => directives.push(child),
235 crate::SyntaxKind::DOCUMENT => {
236 groups.push((std::mem::take(&mut directives), Some(child)));
237 }
238 _ => {}
239 }
240 }
241 if !directives.is_empty() {
242 groups.push((directives, None));
243 }
244 groups
245}
246
247fn continuation_indent(node: &SyntaxNode, columns: &ColumnIndex) -> Option<usize> {
251 let entry = node.ancestors().skip(1).find(is_block_entry)?;
252 Some(columns.column(&entry.first_token()?) + 1)
253}
254
255fn is_block_entry(node: &SyntaxNode) -> bool {
257 matches!(
258 node.kind(),
259 crate::SyntaxKind::MAPPING_ENTRY | crate::SyntaxKind::SEQUENCE_ENTRY
260 ) && node.parent().is_some_and(|c| !starts_a_flow_collection(&c))
261}
262
263#[derive(Clone, Copy)]
265struct Scope {
266 in_flow: bool,
268 continuation_indent: Option<usize>,
271}
272
273impl Scope {
274 fn enter(self, node: &SyntaxNode, columns: &ColumnIndex) -> Self {
276 let opens_flow = matches!(
277 node.kind(),
278 crate::SyntaxKind::MAPPING | crate::SyntaxKind::SEQUENCE
279 ) && starts_a_flow_collection(node);
280 Scope {
281 in_flow: self.in_flow || opens_flow,
282 continuation_indent: if is_block_entry(node) {
283 columns.column_at(node.text_range().start()).map(|c| c + 1)
284 } else {
285 self.continuation_indent
286 },
287 }
288 }
289}
290
291fn fill_locations(node: &SyntaxNode, violations: &mut [Violation]) {
294 if violations.is_empty() {
297 return;
298 }
299 let tree = node.ancestors().last().unwrap_or_else(|| node.clone());
300 let index = crate::line_index::LineIndex::new(tree.text().to_string());
301 for v in violations.iter_mut() {
302 let Some(range) = v.text_range else {
303 continue;
304 };
305 v.location = Some(index.line_column(range.start as usize).to_string());
306 }
307}
308
309impl Violation {
310 fn error_at(rule: Rule, range: rowan::TextRange, message: impl Into<String>) -> Self {
314 Violation {
315 message: message.into(),
316 location: None,
317 text_range: Some(range.into()),
318 severity: Severity::Error,
319 rule,
320 }
321 }
322
323 fn warning_at(rule: Rule, range: rowan::TextRange, message: impl Into<String>) -> Self {
328 Violation {
329 severity: Severity::Warning,
330 ..Violation::error_at(rule, range, message)
331 }
332 }
333}
334
335impl Validator {
336 pub fn new() -> Self {
338 Self {
339 config: ValidatorConfig::default(),
340 }
341 }
342
343 pub fn with_config(config: ValidatorConfig) -> Self {
345 Self { config }
346 }
347
348 pub fn validate(&self, doc: &Document) -> Vec<Violation> {
352 let mut violations = Vec::new();
353 let doc = crate::nodes::immutable_copy(doc.syntax());
354
355 let mut directives: Vec<SyntaxNode> = doc
358 .siblings(rowan::Direction::Prev)
359 .skip(1)
360 .take_while(|n| n.kind() != crate::SyntaxKind::DOCUMENT)
361 .filter(|n| n.kind() == crate::SyntaxKind::DIRECTIVE)
362 .collect();
363 directives.reverse();
364 check_directives(&directives, Some(&doc), &mut violations);
365 check_tag_handles_declared(&directives, &doc, &mut violations);
366
367 self.validate_node(&doc, &mut violations);
370
371 fill_locations(&doc, &mut violations);
372 violations
373 }
374
375 pub fn validate_syntax(&self, node: &SyntaxNode) -> Vec<Violation> {
380 let mut violations = Vec::new();
381 let node = &crate::nodes::immutable_copy(node);
382
383 for (directives, document) in directive_groups(&find_root(node)) {
384 check_directives(&directives, document.as_ref(), &mut violations);
385 }
386
387 check_directive_after_document(node, &mut violations);
389
390 check_tag_shorthand_scope(node, &mut violations);
392
393 check_explicit_key_separation(node, &mut violations);
394
395 self.validate_node(node, &mut violations);
397
398 fill_locations(node, &mut violations);
399 violations
400 }
401
402 fn validate_node(&self, node: &SyntaxNode, violations: &mut Vec<Violation>) {
406 check_printable(node, violations);
407 let columns = ColumnIndex::new(node);
408 let mut identities = SharedIdentities::default();
409 let mut registry = DocumentRegistry::default();
410 let mut scopes = vec![Scope {
413 in_flow: in_flow_collection(node),
414 continuation_indent: continuation_indent(node, &columns),
415 }];
416 for event in node.preorder() {
417 match event {
418 rowan::WalkEvent::Enter(n) => {
419 let scope = *scopes.last().expect("the walk's own scope stays");
420 self.check_node(
421 &n,
422 &columns,
423 &mut identities,
424 &mut registry,
425 scope,
426 violations,
427 );
428 scopes.push(scope.enter(&n, &columns));
429 }
430 rowan::WalkEvent::Leave(_) => {
431 scopes.pop();
432 }
433 }
434 }
435 }
436
437 fn check_node(
439 &self,
440 node: &SyntaxNode,
441 columns: &ColumnIndex,
442 identities: &mut SharedIdentities,
443 registry: &mut DocumentRegistry,
444 scope: Scope,
445 violations: &mut Vec<Violation>,
446 ) {
447 use crate::SyntaxKind;
448 let in_flow = scope.in_flow;
449
450 if self.config.check_tabs {
452 check_tab_usage(node, violations);
453 }
454
455 if self.config.check_anchors {
458 check_multiple_anchors(node, violations);
459 }
460
461 match node.kind() {
462 SyntaxKind::ERROR => {
463 let content = node.text().to_string();
466 let preview = match content.char_indices().nth(50) {
467 Some((end, _)) => format!("{}...", &content[..end]),
468 None => content,
469 };
470 violations.push(Violation::error_at(
471 Rule::Other,
472 node.text_range(),
473 format!("Invalid content in document: {preview:?}"),
474 ));
475 }
476 SyntaxKind::MAPPING_ENTRY => {
477 check_implicit_key_multiline(node, columns, violations);
479 check_collection_on_same_line_as_key(node, in_flow, violations);
481 check_explicit_value_column(node, columns, in_flow, violations);
482 }
483 SyntaxKind::SCALAR => {
484 check_escape_sequences(node, violations);
486 check_block_scalar_indicator(node, violations);
488 check_block_scalar_header_indent(node, columns, scope, violations);
489 check_block_scalar_content_indent(node, violations);
491 check_trailing_content_after_quoted(node, violations);
493 check_colon_in_plain_scalar(node, violations);
495 check_plain_scalar_start(node, in_flow, violations);
496 check_flow_indicator_in_plain_scalar(node, in_flow, violations);
497 check_block_scalar_in_flow(node, in_flow, violations);
498 check_directive_in_content(node, violations);
500 check_legacy_yaml_1_1(node, violations);
502 }
503 SyntaxKind::DOC_START | SyntaxKind::DOC_END if self.config.check_document_markers => {
504 check_document_marker_placement(node, violations);
505 }
506 SyntaxKind::MAPPING => {
507 check_flow_collection_commas(node, violations);
508 check_flow_pair_placement(node, in_flow, violations);
509 check_flow_continuation_indent(node, scope, violations);
510 check_block_mapping_entries_on_same_line(node, violations);
511 check_block_mapping_after_properties(node, violations);
512 check_mapping_entry_indentation(node, columns, violations);
513 if self.config.check_duplicate_keys {
514 check_duplicate_keys(node, identities, violations);
515 }
516 check_merge_operands(node, registry, violations);
517 }
518 SyntaxKind::SEQUENCE => {
519 check_flow_collection_commas(node, violations);
520 check_flow_continuation_indent(node, scope, violations);
521 }
522 SyntaxKind::KEY | SyntaxKind::SEQUENCE_ENTRY | SyntaxKind::VALUE => {
523 check_anchor_and_alias(node, violations);
524 check_single_content_node(node, violations);
525 }
526 SyntaxKind::TAGGED_NODE => {
527 check_anchor_and_alias(node, violations);
528 check_single_content_node(node, violations);
529 check_tag_on_alias(node, violations);
530 check_multiple_tags(node, violations);
531 check_collection_tag_content(node, violations);
532 check_pairs_items(node, violations);
533 check_omap_keys(node, identities, violations);
534 check_set_values(node, violations);
535 }
536 SyntaxKind::DOCUMENT => {
537 check_anchor_and_alias(node, violations);
538 check_single_content_node(node, violations);
539 check_document_level_anchors(node, violations);
540 if self.config.check_anchors {
541 check_undefined_aliases(node, violations);
542 }
543 }
544 _ => {}
545 }
546
547 for element in node.children_with_tokens() {
549 if let Some(token) = element.as_token() {
550 if token.kind() == crate::SyntaxKind::COMMENT {
552 check_comment_token_whitespace(token, violations);
553 }
554 if token.kind() == crate::SyntaxKind::DOC_START {
556 check_doc_start_token_content(token, violations);
557 }
558 match token.kind() {
561 crate::SyntaxKind::AMPERSAND => violations.push(Violation::error_at(
562 Rule::InvalidAnchor,
563 token.text_range(),
564 "Anchor without a name",
565 )),
566 crate::SyntaxKind::ASTERISK => violations.push(Violation::error_at(
567 Rule::InvalidAnchor,
568 token.text_range(),
569 "Alias without a name",
570 )),
571 _ => {}
572 }
573 if token.kind() == crate::SyntaxKind::TAG {
575 check_tag_characters(token, violations);
576 check_tag_followed_by_comma(token, violations);
577 }
578 }
579 }
580
581 check_sequence_indentation(node, columns, violations);
583 check_quoted_string_indentation(node, scope, violations);
584 }
585}
586
587impl Default for Validator {
588 fn default() -> Self {
589 Self::new()
590 }
591}
592
593#[cfg(test)]
594mod tests {
595 use super::*;
596 use rowan::ast::AstNode;
597 use std::str::FromStr;
598
599 #[test]
600 fn test_deep_nesting_fits_a_small_stack() {
601 let text = format!("{}{}", "[{".repeat(127), "}]".repeat(127));
604 let green = crate::YamlFile::from_str(&text)
605 .unwrap()
606 .syntax()
607 .green()
608 .into_owned();
609 let tree = green.clone();
612 let violations = std::thread::Builder::new()
613 .stack_size(SMALL_STACK)
614 .spawn(move || {
615 Validator::new()
616 .validate_syntax(&SyntaxNode::new_root_mut(tree))
617 .len()
618 })
619 .unwrap()
620 .join()
621 .unwrap();
622 assert_eq!(violations, 0);
623 drop(green);
624 }
625
626 const SMALL_STACK: usize = 64 * 1024;
629}