use rlsp_yaml_parser::Span;
use rlsp_yaml_parser::loader::LoaderBuilder;
use rlsp_yaml_parser::node::Document;
use tower_lsp::lsp_types::{Diagnostic, DiagnosticSeverity, NumberOrString, Position, Range};
const MAX_NESTING_DEPTH: usize = 256;
pub struct ParseResult {
pub documents: Vec<Document<Span>>,
pub diagnostics: Vec<Diagnostic>,
}
#[must_use]
pub fn parse_yaml(text: &str) -> ParseResult {
match LoaderBuilder::new()
.lossless()
.max_nesting_depth(MAX_NESTING_DEPTH)
.build()
.load(text)
{
Ok(documents) => ParseResult {
documents,
diagnostics: Vec::new(),
},
Err(err) => {
let (pos, message) = match &err {
rlsp_yaml_parser::loader::LoadError::Parse { pos, message } => {
(*pos, message.clone())
}
rlsp_yaml_parser::loader::LoadError::NestingDepthLimitExceeded { .. }
| rlsp_yaml_parser::loader::LoadError::AnchorCountLimitExceeded { .. }
| rlsp_yaml_parser::loader::LoadError::AliasExpansionLimitExceeded { .. }
| rlsp_yaml_parser::loader::LoadError::CircularAlias { .. }
| rlsp_yaml_parser::loader::LoadError::UndefinedAlias { .. }
| rlsp_yaml_parser::loader::LoadError::UnexpectedEndOfStream => {
(rlsp_yaml_parser::Pos::ORIGIN, err.to_string())
}
};
#[allow(clippy::cast_possible_truncation)]
let line = pos.line.saturating_sub(1) as u32;
#[allow(clippy::cast_possible_truncation)]
let col = pos.column as u32;
let start = Position::new(line, col);
let end = Position::new(line, u32::MAX);
let diagnostic = Diagnostic {
range: Range::new(start, end),
severity: Some(DiagnosticSeverity::ERROR),
code: Some(NumberOrString::String("yamlSyntax".to_string())),
message,
source: Some("rlsp-yaml".to_string()),
..Diagnostic::default()
};
ParseResult {
documents: Vec::new(),
diagnostics: vec![diagnostic],
}
}
}
}
#[cfg(test)]
#[allow(clippy::indexing_slicing, clippy::expect_used, clippy::unwrap_used)]
mod tests {
use std::fmt::Write as _;
use super::*;
#[test]
fn should_return_no_diagnostics_for_valid_yaml() {
let result = parse_yaml("key: value\n");
assert!(result.diagnostics.is_empty());
assert_eq!(result.documents.len(), 1);
}
#[test]
fn should_return_diagnostic_for_invalid_yaml() {
let result = parse_yaml("key: [invalid\n");
assert!(!result.diagnostics.is_empty());
let diag = &result.diagnostics[0];
assert_eq!(diag.severity, Some(DiagnosticSeverity::ERROR));
assert!(!diag.message.is_empty());
}
#[test]
fn should_return_correct_line_position_in_diagnostic() {
let input = "key1: value1\nkey2: value2\nkey3: [bad\n";
let result = parse_yaml(input);
assert!(!result.diagnostics.is_empty());
let diag = &result.diagnostics[0];
assert!(
diag.range.start.line >= 2,
"error should be reported on or after the unclosed bracket line, got line {}",
diag.range.start.line
);
}
#[test]
fn should_return_correct_column_position_in_diagnostic() {
let result = parse_yaml("a: [bad\n");
assert!(!result.diagnostics.is_empty());
let diag = &result.diagnostics[0];
assert!(diag.range.start.line <= 1);
}
#[test]
fn should_parse_multi_document_yaml() {
let input = "key1: value1\n---\nkey2: value2\n";
let result = parse_yaml(input);
assert!(result.diagnostics.is_empty());
assert_eq!(result.documents.len(), 2);
}
#[test]
fn should_return_no_diagnostics_for_empty_document() {
let result = parse_yaml("");
assert!(result.diagnostics.is_empty());
}
#[test]
fn should_return_no_diagnostics_for_comment_only_document() {
let result = parse_yaml("# this is a comment\n");
assert!(result.diagnostics.is_empty());
}
#[test]
fn should_return_diagnostic_with_error_severity() {
let result = parse_yaml(":\n bad: [");
assert!(!result.diagnostics.is_empty());
assert_eq!(
result.diagnostics[0].severity,
Some(DiagnosticSeverity::ERROR)
);
}
#[test]
fn should_include_error_message_in_diagnostic() {
let result = parse_yaml("key: [bad\n");
assert!(!result.diagnostics.is_empty());
assert!(!result.diagnostics[0].message.is_empty());
}
#[test]
fn should_handle_yaml_with_only_document_separator() {
let result = parse_yaml("---\n");
assert!(result.diagnostics.is_empty());
assert!(!result.documents.is_empty());
}
#[test]
fn should_not_panic_on_deeply_nested_yaml() {
let mut text = String::new();
for i in 0..64usize {
let indent = " ".repeat(i);
writeln!(text, "{indent}level{i}:").unwrap();
}
let leaf_indent = " ".repeat(64);
writeln!(text, "{leaf_indent}leaf: value").unwrap();
let result = parse_yaml(&text);
assert!(
result.documents.len() + result.diagnostics.len() > 0,
"should return a result (documents or diagnostics), not both empty"
);
}
#[test]
fn should_not_panic_on_large_document() {
let mut text = String::new();
for i in 0..10_000usize {
writeln!(text, "key{i}: value{i}").unwrap();
}
let result = parse_yaml(&text);
assert!(result.diagnostics.is_empty(), "should parse without errors");
assert!(
!result.documents.is_empty(),
"should produce at least 1 document"
);
}
#[test]
fn should_handle_very_large_yaml_document() {
let text = format!("key: {}", "a".repeat(1_000_000));
let result = parse_yaml(&text);
let _ = result;
}
#[test]
fn should_parse_valid_yaml_with_complex_types() {
let input = "root:\n list:\n - item1\n - item2\n nested:\n key: value\n";
let result = parse_yaml(input);
assert!(result.diagnostics.is_empty());
assert_eq!(result.documents.len(), 1);
}
#[test]
fn should_parse_yaml_with_anchors_and_aliases() {
let input = "defaults: &defaults\n adapter: postgres\n host: localhost\nproduction:\n <<: *defaults\n host: production-server\n";
let result = parse_yaml(input);
assert!(result.diagnostics.is_empty());
assert!(!result.documents.is_empty());
}
#[test]
fn should_return_diagnostic_for_multi_document_with_invalid_section() {
let input = "key1: value1\n---\nkey2: [invalid\n";
let result = parse_yaml(input);
assert!(!result.diagnostics.is_empty());
assert_eq!(
result.diagnostics[0].severity,
Some(DiagnosticSeverity::ERROR)
);
assert!(result.documents.is_empty());
}
#[test]
fn should_return_documents_as_document_span_vec() {
let result = parse_yaml("key: value\n");
assert!(!result.documents.is_empty());
let _ = &result.documents[0].root;
}
#[test]
fn should_include_span_in_parsed_scalar() {
use rlsp_yaml_parser::node::Node;
let result = parse_yaml("hello\n");
assert!(!result.documents.is_empty());
match &result.documents[0].root {
Node::Scalar { loc, .. } => {
assert_eq!(loc.start.byte_offset, 0);
}
Node::Mapping { .. } | Node::Sequence { .. } | Node::Alias { .. } => {
panic!("expected Scalar")
}
}
}
#[test]
fn should_produce_error_diagnostic_with_pos_offset() {
let result = parse_yaml("key: [bad\n");
assert!(!result.diagnostics.is_empty());
let diag = &result.diagnostics[0];
assert!(diag.range.start.line <= 1);
}
#[test]
fn should_return_no_documents_on_parse_error() {
let result = parse_yaml("key: [bad\n");
assert!(result.documents.is_empty());
}
#[test]
fn parse_yaml_returns_documents_with_string_value_type() {
use rlsp_yaml_parser::node::Node;
let result = parse_yaml("key: value\n");
assert!(!result.documents.is_empty());
match &result.documents[0].root {
Node::Mapping { entries, .. } => {
assert!(!entries.is_empty());
let (k, v) = &entries[0];
match k {
Node::Scalar { value, .. } => {
let s: &str = value.as_str();
assert_eq!(s, "key");
}
Node::Mapping { .. } | Node::Sequence { .. } | Node::Alias { .. } => {
panic!("expected Scalar key")
}
}
match v {
Node::Scalar { value, .. } => {
assert_eq!(value.as_str(), "value");
}
Node::Mapping { .. } | Node::Sequence { .. } | Node::Alias { .. } => {
panic!("expected Scalar value")
}
}
}
Node::Scalar { .. } | Node::Sequence { .. } | Node::Alias { .. } => {
panic!("expected Mapping root")
}
}
}
#[test]
fn parse_yaml_nesting_depth_limit_produces_diagnostic() {
let result = std::thread::Builder::new()
.stack_size(64 * 1024 * 1024)
.spawn(|| {
let mut text = String::new();
for i in 0..260usize {
let indent = " ".repeat(i);
writeln!(text, "{indent}level{i}:").unwrap();
}
let leaf_indent = " ".repeat(260);
writeln!(text, "{leaf_indent}leaf: value").unwrap();
parse_yaml(&text)
})
.expect("thread spawn")
.join()
.expect("thread join");
assert!(
!result.diagnostics.is_empty(),
"expected diagnostic for deep nesting"
);
assert!(
result.documents.is_empty(),
"expected no documents on error"
);
}
#[test]
fn parse_yaml_undefined_alias_in_lossless_mode_produces_alias_node() {
use rlsp_yaml_parser::node::Node;
let result = parse_yaml("key: *undefined\n");
if result.documents.is_empty() {
assert!(
!result.diagnostics.is_empty(),
"either documents or diagnostics must be non-empty"
);
} else {
match &result.documents[0].root {
Node::Mapping { entries, .. } => {
let (_, v) = &entries[0];
assert!(
matches!(v, Node::Alias { .. }),
"expected Alias node for *undefined in lossless mode, got: {v:?}"
);
}
Node::Scalar { .. } | Node::Sequence { .. } | Node::Alias { .. } => {
panic!("expected Mapping root")
}
}
}
}
}