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_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 LegacyYaml11,
120 Other,
122}
123
124impl fmt::Display for Violation {
125 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
126 match &self.location {
127 Some(loc) => write!(
128 f,
129 "[{}] {}: {} ({:?})",
130 match self.severity {
131 Severity::Error => "ERROR",
132 Severity::Warning => "WARN",
133 },
134 loc,
135 self.message,
136 self.rule
137 ),
138 None => write!(
139 f,
140 "[{}] {} ({:?})",
141 match self.severity {
142 Severity::Error => "ERROR",
143 Severity::Warning => "WARN",
144 },
145 self.message,
146 self.rule
147 ),
148 }
149 }
150}
151
152#[derive(Debug, Clone)]
158pub struct Validator {
159 config: ValidatorConfig,
161}
162
163#[derive(Debug, Clone)]
165pub struct ValidatorConfig {
166 pub check_duplicate_keys: bool,
168 pub check_indentation: bool,
170 pub check_tabs: bool,
172 pub check_document_markers: bool,
174 pub check_anchors: bool,
176}
177
178impl Default for ValidatorConfig {
179 fn default() -> Self {
180 Self {
181 check_duplicate_keys: true,
182 check_indentation: true,
183 check_tabs: true,
184 check_document_markers: true,
185 check_anchors: true,
186 }
187 }
188}
189
190fn find_root(node: &SyntaxNode) -> SyntaxNode {
192 if node.kind() == crate::SyntaxKind::ROOT {
193 return node.clone();
194 }
195 node.ancestors()
196 .find(|n| n.kind() == crate::SyntaxKind::ROOT)
197 .unwrap_or_else(|| node.clone())
198}
199
200fn directive_groups(root: &SyntaxNode) -> Vec<(Vec<SyntaxNode>, Option<SyntaxNode>)> {
204 let mut groups = Vec::new();
205 let mut directives = Vec::new();
206 for child in root.children() {
207 match child.kind() {
208 crate::SyntaxKind::DIRECTIVE => directives.push(child),
209 crate::SyntaxKind::DOCUMENT => {
210 groups.push((std::mem::take(&mut directives), Some(child)));
211 }
212 _ => {}
213 }
214 }
215 if !directives.is_empty() {
216 groups.push((directives, None));
217 }
218 groups
219}
220
221fn continuation_indent(node: &SyntaxNode, columns: &ColumnIndex) -> Option<usize> {
225 let entry = node.ancestors().skip(1).find(is_block_entry)?;
226 Some(columns.column(&entry.first_token()?) + 1)
227}
228
229fn is_block_entry(node: &SyntaxNode) -> bool {
231 matches!(
232 node.kind(),
233 crate::SyntaxKind::MAPPING_ENTRY | crate::SyntaxKind::SEQUENCE_ENTRY
234 ) && node.parent().is_some_and(|c| !starts_a_flow_collection(&c))
235}
236
237#[derive(Clone, Copy)]
239struct Scope {
240 in_flow: bool,
242 continuation_indent: Option<usize>,
245}
246
247impl Scope {
248 fn enter(self, node: &SyntaxNode, columns: &ColumnIndex) -> Self {
250 let opens_flow = matches!(
251 node.kind(),
252 crate::SyntaxKind::MAPPING | crate::SyntaxKind::SEQUENCE
253 ) && starts_a_flow_collection(node);
254 Scope {
255 in_flow: self.in_flow || opens_flow,
256 continuation_indent: if is_block_entry(node) {
257 columns.column_at(node.text_range().start()).map(|c| c + 1)
258 } else {
259 self.continuation_indent
260 },
261 }
262 }
263}
264
265fn fill_locations(node: &SyntaxNode, violations: &mut [Violation]) {
268 if violations.is_empty() {
271 return;
272 }
273 let tree = node.ancestors().last().unwrap_or_else(|| node.clone());
274 let index = crate::line_index::LineIndex::new(tree.text().to_string());
275 for v in violations.iter_mut() {
276 let Some(range) = v.text_range else {
277 continue;
278 };
279 v.location = Some(index.line_column(range.start as usize).to_string());
280 }
281}
282
283impl Violation {
284 fn error_at(rule: Rule, range: rowan::TextRange, message: impl Into<String>) -> Self {
288 Violation {
289 message: message.into(),
290 location: None,
291 text_range: Some(range.into()),
292 severity: Severity::Error,
293 rule,
294 }
295 }
296
297 fn warning_at(rule: Rule, range: rowan::TextRange, message: impl Into<String>) -> Self {
302 Violation {
303 severity: Severity::Warning,
304 ..Violation::error_at(rule, range, message)
305 }
306 }
307}
308
309impl Validator {
310 pub fn new() -> Self {
312 Self {
313 config: ValidatorConfig::default(),
314 }
315 }
316
317 pub fn with_config(config: ValidatorConfig) -> Self {
319 Self { config }
320 }
321
322 pub fn validate(&self, doc: &Document) -> Vec<Violation> {
326 let mut violations = Vec::new();
327 let doc = crate::nodes::immutable_copy(doc.syntax());
328
329 let mut directives: Vec<SyntaxNode> = doc
332 .siblings(rowan::Direction::Prev)
333 .skip(1)
334 .take_while(|n| n.kind() != crate::SyntaxKind::DOCUMENT)
335 .filter(|n| n.kind() == crate::SyntaxKind::DIRECTIVE)
336 .collect();
337 directives.reverse();
338 check_directives(&directives, Some(&doc), &mut violations);
339
340 self.validate_node(&doc, &mut violations);
343
344 fill_locations(&doc, &mut violations);
345 violations
346 }
347
348 pub fn validate_syntax(&self, node: &SyntaxNode) -> Vec<Violation> {
353 let mut violations = Vec::new();
354 let node = &crate::nodes::immutable_copy(node);
355
356 for (directives, document) in directive_groups(&find_root(node)) {
357 check_directives(&directives, document.as_ref(), &mut violations);
358 }
359
360 check_directive_after_document(node, &mut violations);
362
363 check_tag_shorthand_scope(node, &mut violations);
365
366 check_explicit_key_separation(node, &mut violations);
367
368 self.validate_node(node, &mut violations);
370
371 fill_locations(node, &mut violations);
372 violations
373 }
374
375 fn validate_node(&self, node: &SyntaxNode, violations: &mut Vec<Violation>) {
379 let columns = ColumnIndex::new(node);
380 let mut identities = SharedIdentities::default();
381 let mut registry = DocumentRegistry::default();
382 let mut scopes = vec![Scope {
385 in_flow: in_flow_collection(node),
386 continuation_indent: continuation_indent(node, &columns),
387 }];
388 for event in node.preorder() {
389 match event {
390 rowan::WalkEvent::Enter(n) => {
391 let scope = *scopes.last().expect("the walk's own scope stays");
392 self.check_node(
393 &n,
394 &columns,
395 &mut identities,
396 &mut registry,
397 scope,
398 violations,
399 );
400 scopes.push(scope.enter(&n, &columns));
401 }
402 rowan::WalkEvent::Leave(_) => {
403 scopes.pop();
404 }
405 }
406 }
407 }
408
409 fn check_node(
411 &self,
412 node: &SyntaxNode,
413 columns: &ColumnIndex,
414 identities: &mut SharedIdentities,
415 registry: &mut DocumentRegistry,
416 scope: Scope,
417 violations: &mut Vec<Violation>,
418 ) {
419 use crate::SyntaxKind;
420 let in_flow = scope.in_flow;
421
422 if self.config.check_tabs {
424 check_tab_usage(node, violations);
425 }
426
427 if self.config.check_anchors {
430 check_multiple_anchors(node, violations);
431 }
432
433 match node.kind() {
434 SyntaxKind::ERROR => {
435 let content = node.text().to_string();
438 let preview = match content.char_indices().nth(50) {
439 Some((end, _)) => format!("{}...", &content[..end]),
440 None => content,
441 };
442 violations.push(Violation::error_at(
443 Rule::Other,
444 node.text_range(),
445 format!("Invalid content in document: {preview:?}"),
446 ));
447 }
448 SyntaxKind::MAPPING_ENTRY => {
449 check_implicit_key_multiline(node, columns, violations);
451 check_collection_on_same_line_as_key(node, in_flow, violations);
453 check_explicit_value_column(node, columns, in_flow, violations);
454 }
455 SyntaxKind::SCALAR => {
456 check_escape_sequences(node, violations);
458 check_block_scalar_indicator(node, violations);
460 check_block_scalar_header_indent(node, columns, scope, violations);
461 check_block_scalar_content_indent(node, violations);
463 check_trailing_content_after_quoted(node, violations);
465 check_colon_in_plain_scalar(node, violations);
467 check_plain_scalar_start(node, in_flow, violations);
468 check_flow_indicator_in_plain_scalar(node, in_flow, violations);
469 check_block_scalar_in_flow(node, in_flow, violations);
470 check_directive_in_content(node, violations);
472 check_legacy_yaml_1_1(node, violations);
474 }
475 SyntaxKind::DOC_START | SyntaxKind::DOC_END if self.config.check_document_markers => {
476 check_document_marker_placement(node, violations);
477 }
478 SyntaxKind::MAPPING => {
479 check_flow_collection_commas(node, violations);
480 check_flow_pair_placement(node, in_flow, violations);
481 check_flow_continuation_indent(node, scope, violations);
482 check_block_mapping_entries_on_same_line(node, violations);
483 check_block_mapping_after_properties(node, violations);
484 check_mapping_entry_indentation(node, columns, violations);
485 if self.config.check_duplicate_keys {
486 check_duplicate_keys(node, identities, violations);
487 }
488 check_merge_operands(node, registry, violations);
489 }
490 SyntaxKind::SEQUENCE => {
491 check_flow_collection_commas(node, violations);
492 check_flow_continuation_indent(node, scope, violations);
493 }
494 SyntaxKind::KEY | SyntaxKind::SEQUENCE_ENTRY | SyntaxKind::VALUE => {
495 check_anchor_and_alias(node, violations);
496 check_single_content_node(node, violations);
497 }
498 SyntaxKind::TAGGED_NODE => {
499 check_anchor_and_alias(node, violations);
500 check_single_content_node(node, violations);
501 check_tag_on_alias(node, violations);
502 check_multiple_tags(node, violations);
503 check_collection_tag_content(node, violations);
504 check_pairs_items(node, violations);
505 check_omap_keys(node, identities, violations);
506 check_set_values(node, violations);
507 }
508 SyntaxKind::DOCUMENT => {
509 check_anchor_and_alias(node, violations);
510 check_single_content_node(node, violations);
511 check_document_level_anchors(node, violations);
512 if self.config.check_anchors {
513 check_undefined_aliases(node, violations);
514 }
515 }
516 _ => {}
517 }
518
519 for element in node.children_with_tokens() {
521 if let Some(token) = element.as_token() {
522 if token.kind() == crate::SyntaxKind::COMMENT {
524 check_comment_token_whitespace(token, violations);
525 }
526 if token.kind() == crate::SyntaxKind::DOC_START {
528 check_doc_start_token_content(token, violations);
529 }
530 match token.kind() {
533 crate::SyntaxKind::AMPERSAND => violations.push(Violation::error_at(
534 Rule::InvalidAnchor,
535 token.text_range(),
536 "Anchor without a name",
537 )),
538 crate::SyntaxKind::ASTERISK => violations.push(Violation::error_at(
539 Rule::InvalidAnchor,
540 token.text_range(),
541 "Alias without a name",
542 )),
543 _ => {}
544 }
545 if token.kind() == crate::SyntaxKind::TAG {
547 check_tag_characters(token, violations);
548 check_tag_followed_by_comma(token, violations);
549 }
550 }
551 }
552
553 check_sequence_indentation(node, columns, violations);
555 check_quoted_string_indentation(node, scope, violations);
556 }
557}
558
559impl Default for Validator {
560 fn default() -> Self {
561 Self::new()
562 }
563}
564
565#[cfg(test)]
566mod tests {
567 use super::*;
568 use rowan::ast::AstNode;
569 use std::str::FromStr;
570
571 #[test]
572 fn test_deep_nesting_fits_a_small_stack() {
573 let text = format!("{}{}", "[{".repeat(127), "}]".repeat(127));
576 let green = crate::YamlFile::from_str(&text)
577 .unwrap()
578 .syntax()
579 .green()
580 .into_owned();
581 let tree = green.clone();
584 let violations = std::thread::Builder::new()
585 .stack_size(SMALL_STACK)
586 .spawn(move || {
587 Validator::new()
588 .validate_syntax(&SyntaxNode::new_root_mut(tree))
589 .len()
590 })
591 .unwrap()
592 .join()
593 .unwrap();
594 assert_eq!(violations, 0);
595 drop(green);
596 }
597
598 const SMALL_STACK: usize = 64 * 1024;
601}