mod common;
use common::{assert_strings, keys, parse, parse_with_errors, parse_with_violations};
use rowan::ast::AstNode;
use std::str::FromStr;
use yaml_edit::validator::{Rule, Severity, Validator};
use yaml_edit::{
debug, Document, LineColumn, Parse, ParseErrorKind, ScalarType, ScalarValue, Sequence,
SyntaxKind, YamlFile,
};
#[test]
fn error_messages() {
for (yaml, expected) in [
(
r#"name: "unclosed
age: 30"#,
&["1:7: Unterminated quoted string"][..],
),
("name: 'unclosed\nage: 30", &["1:7: Unterminated quoted string"]),
(
r#"name: "unterminated string
age: 30"#,
&["1:7: Unterminated quoted string"],
),
(
r#"valid: "good string"
also_valid: 42
broken: "unclosed
still_broken: value"#,
&["3:9: Unterminated quoted string"],
),
(
"line1: value\nline2: value\nbad: [",
&["3:7: Unclosed flow sequence. Expected: ']' to close sequence. Found: end of input. Context: in flow sequence. Suggestion: Add ']' to close the array, or check for missing commas between elements"],
),
(
r#"valid:
nested:
deep:
error: [unclosed"#,
&["4:23: Unclosed flow sequence. Expected: ']' to close sequence. Found: end of input. Context: in flow sequence. Suggestion: Add ']' to close the array, or check for missing commas between elements"],
),
(
"config: {host: localhost",
&["1:25: Unclosed flow mapping. Expected: '}' to close mapping. Found: end of input. Context: in flow mapping. Suggestion: Add '}' to close the object, or check for missing commas between key-value pairs"],
),
(
"items: [a, b, c",
&["1:16: Unclosed flow sequence. Expected: ']' to close sequence. Found: end of input. Context: in flow sequence. Suggestion: Add ']' to close the array, or check for missing commas between elements"],
),
(
"items: [a, b",
&["1:13: Unclosed flow sequence. Expected: ']' to close sequence. Found: end of input. Context: in flow sequence. Suggestion: Add ']' to close the array, or check for missing commas between elements"],
),
(
r#"config: {
host: localhost,
port: 8080
"#,
&["4:1: Unclosed flow mapping. Expected: '}' to close mapping. Found: end of input. Context: in flow mapping. Suggestion: Add '}' to close the object, or check for missing commas between key-value pairs"],
),
(
r#"items: [
first,
second,
third
"#,
&["5:1: Unclosed flow sequence. Expected: ']' to close sequence. Found: end of input. Context: in flow sequence. Suggestion: Add ']' to close the array, or check for missing commas between elements"],
),
(
r#"outer:
inner: {key: value"#,
&["2:21: Unclosed flow mapping. Expected: '}' to close mapping. Found: end of input. Context: in flow mapping. Suggestion: Add '}' to close the object, or check for missing commas between key-value pairs"],
),
(
r#"outer:
inner: [a, b, c"#,
&["2:18: Unclosed flow sequence. Expected: ']' to close sequence. Found: end of input. Context: in flow sequence. Suggestion: Add ']' to close the array, or check for missing commas between elements"],
),
(
r#"config: {host: localhost, port: 8080
server: running"#,
&["2:16: Unclosed flow mapping. Expected: '}' to close mapping. Found: end of input. Context: in flow mapping. Suggestion: Add '}' to close the object, or check for missing commas between key-value pairs"],
),
(
r#"items: [a, b, c
config: {host localhost}"#,
&["2:25: Unclosed flow sequence. Expected: ']' to close sequence. Found: end of input. Context: in flow sequence. Suggestion: Add ']' to close the array, or check for missing commas between elements"],
),
] {
parse_with_errors(yaml, expected);
}
}
#[test]
fn unclosed_flow_collection_swallows_later_lines() {
for (yaml, expected, keys_left) in [
(
r#"valid1: value1
broken: [unclosed
valid2: value2
valid3: value3"#,
&["4:15: Unclosed flow sequence. Expected: ']' to close sequence. Found: end of input. Context: in flow sequence. Suggestion: Add ']' to close the array, or check for missing commas between elements"][..],
&["valid1", "broken"][..],
),
(
r#"start: value
broken: [
fixed: works
end: value"#,
&["4:11: Unclosed flow sequence. Expected: ']' to close sequence. Found: end of input. Context: in flow sequence. Suggestion: Add ']' to close the array, or check for missing commas between elements"],
&["start", "broken"],
),
(
"items: [a, b, c\nnext: value",
&["2:12: Unclosed flow sequence. Expected: ']' to close sequence. Found: end of input. Context: in flow sequence. Suggestion: Add ']' to close the array, or check for missing commas between elements"],
&["items"],
),
(
"config: {host localhost\nother: value",
&["2:13: Unclosed flow mapping. Expected: '}' to close mapping. Found: end of input. Context: in flow mapping. Suggestion: Add '}' to close the object, or check for missing commas between key-value pairs"],
&["config"],
),
(
"flow: [a, b\nblock:\n key: value",
&["3:13: Unclosed flow sequence. Expected: ']' to close sequence. Found: end of input. Context: in flow sequence. Suggestion: Add ']' to close the array, or check for missing commas between elements"],
&["flow"],
),
(
"outer: [inner: {broken\nrescue: works",
&[
"2:14: Unclosed flow mapping. Expected: '}' to close mapping. Found: end of input. Context: in flow mapping. Suggestion: Add '}' to close the object, or check for missing commas between key-value pairs",
"2:14: Unclosed flow sequence. Expected: ']' to close sequence. Found: end of input. Context: in flow sequence. Suggestion: Add ']' to close the array, or check for missing commas between elements",
],
&["outer"],
),
(
"broken: [a, b\nmissing colon here\nvalid: works",
&["3:13: Unclosed flow sequence. Expected: ']' to close sequence. Found: end of input. Context: in flow sequence. Suggestion: Add ']' to close the array, or check for missing commas between elements"],
&["broken"],
),
(
r#"bad1: [unclosed
bad2: {also_unclosed"#,
&[
"2:21: Unclosed flow mapping. Expected: '}' to close mapping. Found: end of input. Context: in flow mapping. Suggestion: Add '}' to close the object, or check for missing commas between key-value pairs",
"2:21: Unclosed flow sequence. Expected: ']' to close sequence. Found: end of input. Context: in flow sequence. Suggestion: Add ']' to close the array, or check for missing commas between elements",
],
&["bad1"],
),
(
r#"
level1: [
level2: {
level3: [broken structure here
level3b: another broken
level2b: {missing colon
level1b: value
"#,
&[
"8:1: Unclosed flow mapping. Expected: '}' to close mapping. Found: end of input. Context: in flow mapping. Suggestion: Add '}' to close the object, or check for missing commas between key-value pairs",
"8:1: Unclosed flow sequence. Expected: ']' to close sequence. Found: end of input. Context: in flow sequence. Suggestion: Add ']' to close the array, or check for missing commas between elements",
"8:1: Unclosed flow mapping. Expected: '}' to close mapping. Found: end of input. Context: in flow mapping. Suggestion: Add '}' to close the object, or check for missing commas between key-value pairs",
"8:1: Unclosed flow sequence. Expected: ']' to close sequence. Found: end of input. Context: in flow sequence. Suggestion: Add ']' to close the array, or check for missing commas between elements",
],
&["level1"],
),
] {
let file = parse_with_errors(yaml, expected);
assert_eq!(keys(&file), keys_left, "{yaml:?}");
}
}
#[test]
fn error_positions() {
let yaml = r#"
key1: value1
key2: "unclosed quote
key3: value3
"#;
let parse = Parse::parse_yaml(yaml);
assert_eq!(parse.errors(), vec!["3:7: Unterminated quoted string"]);
let positioned = parse.positioned_errors();
assert_eq!(positioned.len(), 1);
assert_eq!(positioned[0].message, "Unterminated quoted string");
assert_eq!(positioned[0].start_position(yaml), LineColumn::new(3, 7));
assert_eq!(
parse.to_result().unwrap_err().to_string(),
"Parse error at line 3, column 7: Unterminated quoted string"
);
}
#[test]
fn recovered_trees() {
for (yaml, errors, tree) in [
(
"[c:",
&["1:4: Unclosed flow sequence. Expected: ']' to close sequence. Found: end of input. Context: in flow sequence. Suggestion: Add ']' to close the array, or check for missing commas between elements"][..],
r#"DOCUMENT
SEQUENCE
LEFT_BRACKET: "["
SEQUENCE_ENTRY
MAPPING
MAPPING_ENTRY
KEY
SCALAR
STRING: "c"
COLON: ":"
VALUE
SCALAR
NULL: ""
RIGHT_BRACKET: ""
"#,
),
(
"{c:",
&["1:4: Unclosed flow mapping. Expected: '}' to close mapping. Found: end of input. Context: in flow mapping. Suggestion: Add '}' to close the object, or check for missing commas between key-value pairs"],
r#"DOCUMENT
MAPPING
LEFT_BRACE: "{"
MAPPING_ENTRY
KEY
SCALAR
STRING: "c"
COLON: ":"
VALUE
SCALAR
NULL: ""
RIGHT_BRACE: ""
"#,
),
(
"{?",
&["1:3: Unclosed flow mapping. Expected: '}' to close mapping. Found: end of input. Context: in flow mapping. Suggestion: Add '}' to close the object, or check for missing commas between key-value pairs"],
r#"DOCUMENT
MAPPING
LEFT_BRACE: "{"
MAPPING_ENTRY
KEY
QUESTION: "?"
SCALAR
NULL: ""
VALUE
SCALAR
NULL: ""
RIGHT_BRACE: ""
"#,
),
(
"[]:\n]:",
&["2:1: Unexpected token in mapping: \"]\""],
r#"DOCUMENT
MAPPING
MAPPING_ENTRY
KEY
SEQUENCE
LEFT_BRACKET: "["
RIGHT_BRACKET: "]"
COLON: ":"
VALUE
SCALAR
NULL: ""
NEWLINE: "\\n"
ERROR
RIGHT_BRACKET: "]"
MAPPING_ENTRY
KEY
SCALAR
NULL: ""
COLON: ":"
VALUE
SCALAR
NULL: ""
"#,
),
(
"?\n] ?:",
&["2:1: Unexpected token in explicit-key mapping: \"]\""],
r#"DOCUMENT
MAPPING
MAPPING_ENTRY
QUESTION: "?"
KEY
SCALAR
NULL: ""
VALUE
SCALAR
NULL: ""
NEWLINE: "\\n"
ERROR
RIGHT_BRACKET: "]"
WHITESPACE: " "
MAPPING_ENTRY
KEY
SCALAR
STRING: "?"
COLON: ":"
VALUE
SCALAR
NULL: ""
"#,
),
(
"[a, }]",
&["1:5: Unexpected token in flow sequence: \"}\""],
r#"DOCUMENT
SEQUENCE
LEFT_BRACKET: "["
SEQUENCE_ENTRY
SCALAR
STRING: "a"
COMMA: ","
WHITESPACE: " "
SEQUENCE_ENTRY
SCALAR
NULL: ""
ERROR
RIGHT_BRACE: "}"
RIGHT_BRACKET: "]"
"#,
),
] {
let file = parse_with_errors(yaml, errors);
assert_eq!(debug::tree_to_string(file.syntax()), tree, "{yaml:?}");
}
}
#[test]
fn invalid_input_reported_by_validator() {
for (yaml, expected) in [
(
"key1 value\nkey2: value",
&[("Document holds more than one node", "2:1")][..],
),
(
r#"---
{}
key1: value1
key2: value2"#,
&[("Document holds more than one node", "3:1")],
),
("!a !b\n- x\n", &[("Multiple tags on the same node", "1:1")]),
("k: !a !b x\n", &[("Multiple tags on the same node", "1:4")]),
(
"- !a\n !b\n - x\n",
&[("Multiple tags on the same node", "1:3")],
),
(
"%INVALID directive\nkey: value",
&[("Directive without document start marker (---)", "1:1")],
),
("ref: *undefined", &[("Undefined alias *undefined", "1:6")]),
] {
parse_with_violations(yaml, expected);
}
}
#[test]
fn colon_space_in_plain_value() {
for (yaml, expected, violations) in [
(
r#"not_url_2: key: value on same line"#,
&[("not_url_2", "key: value on same line")][..],
&[(
"Plain scalar value cannot contain mapping syntax (colon)",
"1:15",
)][..],
),
(
"bad: key: value",
&[("bad", "key: value")],
&[(
"Plain scalar value cannot contain mapping syntax (colon)",
"1:9",
)],
),
(
r#"
good_key: good_value
bad: key: with: many: colons
another_good: value
"#,
&[
("good_key", "good_value"),
("bad", "key: with: many: colons"),
("another_good", "value"),
],
&[(
"Plain scalar value cannot contain mapping syntax (colon)",
"3:9",
)],
),
("key: value", &[("key", "value")], &[]),
(
"url: http://example.com",
&[("url", "http://example.com")],
&[],
),
("url: example.com:8080", &[("url", "example.com:8080")], &[]),
("good: key:value", &[("good", "key:value")], &[]),
("quoted: \"key: value\"", &[("quoted", "key: value")], &[]),
] {
let file = parse_with_violations(yaml, violations);
let doc = file.document().unwrap();
let expected_keys: Vec<&str> = expected.iter().map(|(k, _)| *k).collect();
assert_eq!(keys(&file), expected_keys, "{yaml:?}");
for (key, value) in expected {
assert_eq!(doc.get_string(key).unwrap(), *value, "{yaml:?}");
}
}
}
#[test]
fn text_after_flow_sequence_value() {
let yaml = r#"ipv6: [::1]:8080"#;
let file = parse_with_violations(yaml, &[("Value holds more than one node", "1:12")]);
assert_eq!(keys(&file), ["ipv6"]);
let doc = file.document().unwrap();
let seq = doc.get_sequence("ipv6").unwrap();
assert_eq!(seq.len(), 1);
assert_eq!(seq.get(0).unwrap().as_scalar().unwrap().as_string(), "::1");
}
#[test]
fn tab_indentation() {
let yaml = "key: value\n\tindented: invalid";
let file = parse_with_violations(
yaml,
&[
("Tabs are not allowed for indentation in YAML", "2:1"),
(
"Sibling block mapping entries have inconsistent indentation (expected \"\", found \"\\t\")",
"2:1",
),
],
);
let doc = file.document().unwrap();
assert_eq!(doc.get_string("key").unwrap(), "value");
}
#[test]
fn mixed_indentation_levels() {
let yaml_mixed_indent = r#"root:
level1:
level2:
level2_off: value
level3:
level3_off: value"#;
let file = parse_with_violations(
yaml_mixed_indent,
&[(
"Sibling block mapping entries have inconsistent indentation (expected \" \", found \" \")",
"5:1",
)],
);
let doc = file.document().unwrap();
let level2 = doc
.get_mapping("root")
.unwrap()
.get_mapping("level1")
.unwrap()
.get_mapping("level2")
.unwrap();
assert_eq!(
level2
.get("level2_off")
.unwrap()
.as_scalar()
.unwrap()
.as_string(),
"value"
);
let level3 = level2.get_mapping("level3").unwrap();
assert_eq!(
level3
.get("level3_off")
.unwrap()
.as_scalar()
.unwrap()
.as_string(),
"value"
);
}
#[test]
fn accepted_without_errors() {
let file = parse_with_violations("&123invalid value", &[]);
assert_eq!(
file.document().unwrap().as_scalar().unwrap().as_string(),
"value"
);
let file = parse_with_violations(
r#"first: &x value1
second: &x value2
ref: *x"#,
&[],
);
assert_eq!(keys(&file), ["first", "second", "ref"]);
}
#[test]
fn reparse_keeps_content_after_flow_mapping() {
let invalid_yaml = r#"---
{}
key1: value1
key2: value2"#;
let yaml = YamlFile::parse(invalid_yaml).tree();
let result = yaml.to_string();
assert_eq!(result, invalid_yaml);
let reparsed = YamlFile::parse(&result).tree();
assert_eq!(reparsed.to_string(), invalid_yaml);
assert_eq!(keys(&reparsed), Vec::<String>::new());
}
#[test]
fn set_in_flow_mapping_followed_by_block_entries() {
for (invalid_yaml, key, value, expected) in [
(
r#"---
{}
key1: value1"#,
"key2",
"value2",
"---\n{key2: value2}\nkey1: value1",
),
(
r#"---
{}
name: test
version: 1.0"#,
"author",
"John",
"---\n{author: John}\nname: test\nversion: 1.0",
),
] {
let yaml = YamlFile::parse(invalid_yaml).tree();
assert_eq!(yaml.to_string(), invalid_yaml);
yaml.document()
.unwrap()
.as_mapping()
.unwrap()
.set(key, value);
debug::validate_tree(yaml.syntax()).unwrap();
assert_eq!(yaml.to_string(), expected);
}
}
#[test]
fn set_value_of_key_missing_colon() {
fn set_last(src: &str, value: impl yaml_edit::AsYaml) -> String {
let doc = YamlFile::parse(src).tree().document().unwrap();
let entry = doc.as_mapping().unwrap().entries().last().unwrap();
entry.set_value(value, false);
debug::validate_tree(doc.syntax()).unwrap();
doc.to_string()
}
let block = YamlFile::from_str("k:\n a: 1\n").unwrap();
let block = block.document().unwrap().get("k").unwrap();
for (src, inline, nested) in [
("[{]:", "[{]:: x", "[{]::\n a: 1"),
("?\n[:\n", "?\n[:\n: x", "?\n[:\n:\n a: 1\n"),
(":\n{:", ":\n{:: x", ":\n{::\n a: 1"),
] {
assert_eq!(set_last(src, "x"), inline, "{src:?}");
assert_eq!(set_last(src, block.clone()), nested, "{src:?}");
}
}
fn nth_sequence(file: &YamlFile, n: usize) -> Sequence {
file.syntax()
.descendants()
.filter(|c| c.kind() == SyntaxKind::SEQUENCE)
.nth(n)
.and_then(Sequence::cast)
.unwrap()
}
#[test]
fn remove_last_item_of_sequence_in_recovered_tree() {
for (yaml, n, expected) in [
("!a !b\n- x\n", 0, "!a !b\n[]\n"),
("k: !a !b\n - x\n", 0, "k: !a !b []\n"),
(
"- [a, b ,!str\n-\n !!null : a\n b: !!str\n- !!s ]\n",
2,
"- [a, b ,!str\n[]: a\n b: !!str\n- !!s ]\n",
),
] {
let file = YamlFile::from_str(yaml).unwrap();
nth_sequence(&file, n).remove(0);
assert_eq!(file.to_string(), expected, "{yaml:?}");
}
}
#[test]
fn refill_value_with_two_tags() {
let file = YamlFile::from_str("k: !a !b\n - x\n").unwrap();
let seq = nth_sequence(&file, 0);
seq.remove(0);
seq.push("y");
assert_eq!(file.to_string(), "k: !a !b\n - y\n");
}
#[test]
fn refill_root_sequence_followed_by_mapping() {
let file = YamlFile::from_str(" - x\nb: 1").unwrap();
let seq = nth_sequence(&file, 0);
seq.remove(0);
assert_eq!(file.to_string(), " []\nb: 1");
seq.insert(0, "y");
common::assert_file_edited(&file, " - y\nb: 1");
}
#[test]
fn clear_sequence_with_anchor_text_in_scalar() {
let file = YamlFile::from_str("- f\n - &SS Sammy\nrb: *SS\n").unwrap();
nth_sequence(&file, 0).clear();
assert_eq!(file.to_string(), "[]\nrb: *SS\n");
}
#[test]
fn validator_findings() {
for (yaml, expected) in [
("--- a\n", &[][..]),
("--- >\n ab\n", &[]),
("--- {a: 1}\n", &[]),
("--- [1]\n", &[]),
(
"--- key1: value1\n key2: value2\n",
&[("Content on same line as document start marker", "1:1")],
),
(
"--- |0\n",
&[(
"Block scalar indentation indicator must be a digit 1-9",
"1:6",
)],
),
(
"--- |10\n",
&[(
"Block scalar indentation indicator must be a digit 1-9",
"1:6",
)],
),
("k: |-\n x\n", &[]),
("k: |2-\n x\n", &[]),
("k: |2\n x\n", &[]),
("k: |-2\n x\n", &[]),
("k: |2-\n x\n", &[]),
(
"---\nflow: [a,\nb,\nc]\n",
&[("Flow collection continuation line must be indented", "2:7")],
),
("{\na: 1,\nb: 2,\n}\n", &[]),
("k: [a,\n b]\n", &[]),
("[a, b]\n", &[]),
(
"[\n--- ,\n...\n]\n",
&[("Document marker inside a flow collection", "2:1")],
),
("-\n a: 1\n b: 2\n", &[]),
("- a: 1\n b: 2\n", &[]),
(" - key: value\n key2: value2\n", &[]),
("k:\n a: 1\n b: 2\n", &[]),
(
"a: 1\n b: 2\n",
&[(
"Sibling block mapping entries have inconsistent indentation \
(expected \"\", found \" \")",
"2:1",
)],
),
("&sequence\n- a\n", &[]),
("&a a: b\n", &[]),
("&flowseq [ a ]\n", &[]),
(
"&anchor - sequence entry\n",
&[("Plain scalar cannot start with '-'", "1:9")],
),
("? a\n? b\n", &[]),
("a: 1\nb: 2\n", &[]),
("? a\n: 1\n? b\n: 2\n", &[]),
(
"a:\n\tb: 1\n",
&[("Tabs are not allowed for indentation in YAML", "2:1")],
),
(
"-\t-\n",
&[("Tabs are not allowed for indentation in YAML", "1:2")],
),
(
"foo: \"bar\n\tbaz\"\n",
&[("Tabs are not allowed for indentation in YAML", "1:6")],
),
("1st\n\t2nd\n", &[]),
("x:\n - x\n \tx\n", &[]),
("a: |\n x\n \ty\n", &[]),
("\t[\n\t]\n", &[]),
("a: b\t\nseq:\t\n - a\t\n", &[]),
("-\t-1\n", &[]),
("{\nunquoted : \"separate\",\n}\n", &[]),
("- { multi\n line: value}\n", &[]),
("---\n{ \"foo\"\n :bar }\n", &[]),
("{\n? explicit: entry,\n?\n}\n", &[]),
(
"[23\n]: 42\n",
&[
("Implicit key cannot span multiple lines", "1:1"),
("Flow collection continuation line must be indented", "1:1"),
],
),
(
"[ \"key\"\n :value ]\n",
&[("Implicit key cannot span multiple lines", "1:3")],
),
(
"\"a\nb\": 1\n",
&[
("Implicit key cannot span multiple lines", "1:1"),
("Wrong indented multiline quoted scalar", "1:1"),
],
),
("hr: # c\n", &[]),
("key: # c\n", &[]),
("- # Empty\n", &[]),
("a: 1\n# c\nb: 2\n", &[]),
(
"k: \"v\"# c\n",
&[("Comment without whitespace separation", "1:7")],
),
("!<tag:e.com,2002:x> a\n", &[]),
("k: \"a\\\\$b\"\n", &[]),
("k: \"a\\\n b\"\n", &[]),
("k: \"a\\qb\"\n", &[("Invalid escape sequence: \\q", "1:4")]),
(
"!!str, x\n",
&[("Plain scalar cannot start with ','", "1:6")],
),
("top1: &alias1 k\ntop3: &node3\n *alias1 : v\n", &[]),
("- aaa: |2\n xxx\n bbb: |\n xxx\n", &[]),
(
"a: &b *c\n",
&[
("Undefined alias *c", "1:7"),
("Node cannot have both an anchor and be an alias", "1:4"),
],
),
(
"foo: |\n\t\nbar: 1\n",
&[("Tabs are not allowed for indentation in YAML", "2:1")],
),
(
"k: a: b\n",
&[(
"Plain scalar value cannot contain mapping syntax (colon)",
"1:5",
)],
),
("a\n...\n%YAML 1.2\n---\nb\n", &[]),
("a: 1\n...\n%YAML 1.2\n---\nb\n", &[]),
] {
parse_with_violations(yaml, expected);
}
}
#[test]
fn validator_accepts_document() {
for yaml in [
"- >\n \t\n detected\n",
"text: |\n a\n \n b\n",
"foo: 1\nbar: 2\n \ntext: 3\n",
"- sun: yellow\n- ? earth: blue\n : moon: white\n",
] {
let doc = Document::from_str(yaml).unwrap();
let violations = Validator::new().validate(&doc);
assert_eq!(violations, vec![], "{yaml:?}");
}
}
#[test]
fn carriage_return_ends_a_line_for_error_positions() {
let cr = YamlFile::parse("a: 1\rb: 2\r... x\r");
let lf = YamlFile::parse("a: 1\nb: 2\n... x\n");
let position = |p: &Parse<YamlFile>| -> Option<String> {
let text: String = p.errors().first()?.to_string();
Some(text.split(':').take(2).collect::<Vec<_>>().join(":"))
};
assert_eq!(position(&cr), position(&lf));
assert_eq!(position(&cr).as_deref(), Some("3:5"));
}
#[test]
fn legacy_yaml_1_1_warnings() {
let warn = |src: &str| -> Vec<(Rule, Severity, String)> {
let doc = Document::from_str(src).unwrap();
Validator::new()
.validate(&doc)
.into_iter()
.map(|v| (v.rule, v.severity, v.message))
.collect()
};
for (src, message) in [
(
"mode: 0755\n",
"`0755` is YAML 1.1 octal, read as 493; YAML 1.2 reads it as 755. \
Write `493` to keep the octal meaning",
),
(
"mode: 0777777777777777777777777\n",
"`0777777777777777777777777` is YAML 1.1 octal, read as \
4722366482869645213695; YAML 1.2 reads it as 777777777777777777777777. \
Write `4722366482869645213695` to keep the octal meaning",
),
(
"mode: -00777\n",
"`-00777` is YAML 1.1 octal, read as -511; YAML 1.2 reads it as -777. \
Write `-511` to keep the octal meaning",
),
(
"mode: 000\n",
"`000` is YAML 1.1 octal, read as 0; YAML 1.2 reads it as 0. \
Write `0` to keep the octal meaning",
),
(
"n: 089\n",
"`089` is a string in YAML 1.1; YAML 1.2 reads it as the integer 89. \
Write `'089'` to keep the string meaning",
),
(
"n: -0789\n",
"`-0789` is a string in YAML 1.1; YAML 1.2 reads it as the integer -789. \
Write `'-0789'` to keep the string meaning",
),
(
"n: +09\n",
"`+09` is a string in YAML 1.1; YAML 1.2 reads it as the integer 9. \
Write `'+09'` to keep the string meaning",
),
(
"a: yes\n",
"`yes` is the string \"yes\" in YAML 1.2, but a boolean to a YAML 1.1 reader; \
quote it or write true/false to be unambiguous",
),
(
"a: OFF\n",
"`OFF` is the string \"OFF\" in YAML 1.2, but a boolean to a YAML 1.1 reader; \
quote it or write true/false to be unambiguous",
),
(
"a: 0b1010\n",
"`0b1010` is the integer 10 in YAML 1.1; YAML 1.2 reads it as a string. \
Write `10` to keep the integer meaning",
),
(
"a: -0x1F\n",
"`-0x1F` is the integer -31 in YAML 1.1; YAML 1.2 reads it as a string. \
Write `-31` to keep the integer meaning",
),
(
"a: 0x_1F\n",
"`0x_1F` is the integer 31 in YAML 1.1; YAML 1.2 reads it as a string. \
Write `0x1F` to keep the integer meaning",
),
(
"a: 0_755\n",
"`0_755` is the integer 493 in YAML 1.1; YAML 1.2 reads it as a string. \
Write `493` to keep the integer meaning",
),
(
"a: 1_000\n",
"`1_000` is the integer 1000 in YAML 1.1; YAML 1.2 reads it as a string. \
Write `1000` to keep the integer meaning",
),
(
"a: 1_000.5\n",
"`1_000.5` is the float 1000.5 in YAML 1.1; YAML 1.2 reads it as a string. \
Write `1000.5` to keep the float meaning",
),
(
"t: 1:30\n",
"`1:30` is the integer 90 in YAML 1.1; YAML 1.2 reads it as a string. \
Write `90` to keep the integer meaning",
),
(
"t: 1:30.5\n",
"`1:30.5` is the float 90.5 in YAML 1.1; YAML 1.2 reads it as a string. \
Write `90.5` to keep the float meaning",
),
(
"mode: 0o17\n",
"`0o17` is a string in YAML 1.1; YAML 1.2 reads it as the integer 15. \
Write `'0o17'` to keep the string meaning",
),
(
"a: 1e3\n",
"`1e3` is a string in YAML 1.1; YAML 1.2 reads it as a float. \
Write `'1e3'` to keep the string meaning",
),
(
"a: -1.0E3\n",
"`-1.0E3` is a string in YAML 1.1; YAML 1.2 reads it as a float. \
Write `'-1.0E3'` to keep the string meaning",
),
] {
assert_eq!(
warn(src),
[(Rule::LegacyYaml11, Severity::Warning, message.to_string())],
"{src:?}"
);
}
assert_eq!(
ScalarValue::parse("-0o777").scalar_type(),
ScalarType::String
);
for src in [
"a: 0755\n",
"a: -00777\n",
"a: 000\n",
"a: 0b1010\n",
"a: -0x1F\n",
"a: 0x_1F\n",
"a: 0_755\n",
"a: 1_000\n",
"a: 1_000.5_0\n",
"a: 1_0.5e+5\n",
"a: 0_5.5\n",
"t: 1:30\n",
"t: 0:30.\n",
"t: 1:30.5\n",
"a: 089\n",
"a: 0o17\n",
"a: 1e3\n",
] {
let message = &warn(src)[0].2;
let suggestion = message
.split("Write `")
.nth(1)
.and_then(|rest| rest.split('`').next())
.unwrap();
assert_eq!(warn(&format!("a: {suggestion}\n")), vec![], "{src:?}");
}
for clean in [
"a: 'yes'\n",
"a: \"no\"\n",
"mode: 755\n",
"a: true\n",
"z: 0\n",
"f: 0.5\n",
"n: 89\n",
"n: 0x89\n",
"n: '089'\n",
"a: 1.0e+3\n",
"a: 0x1F\n",
"a: 1000\n",
"a: 1.5\n",
"a: 0b102\n",
"a: 1:60\n",
"a: '1e3'\n",
"a: \"0b1010\"\n",
] {
assert_eq!(warn(clean), vec![], "{clean:?}");
}
}
#[test]
fn terminates_on_stray_tokens() {
let unclosed_sequence = "Unclosed flow sequence. Expected: ']' to close sequence. \
Found: end of input. Context: in flow sequence. Suggestion: Add ']' to close the \
array, or check for missing commas between elements";
let unclosed_mapping = "Unclosed flow mapping. Expected: '}' to close mapping. \
Found: end of input. Context: in flow mapping. Suggestion: Add '}' to close the \
object, or check for missing commas between key-value pairs";
for (yaml, expected) in [
(
"[}9[Z",
vec![
"1:2: Unexpected token in flow sequence: \"}\"".to_string(),
format!("1:6: {unclosed_sequence}"),
],
),
(
"{]",
vec![
"1:2: Unexpected token in flow mapping: \"]\"".to_string(),
format!("1:3: {unclosed_mapping}"),
],
),
("?\u{18}\0\0\0\0\0\0\n\t#\t\t?\t", vec![]),
(
"<<?: ?: \n<<\n ?: : 0\n:@",
vec!["2:1: Content after the document could not be parsed: \"<<\"".to_string()],
),
(
"[%:%:]: \n]: \n\n",
vec!["2:1: Unexpected token in mapping: \"]\"".to_string()],
),
(
"\0:\n\n}:\n\n{",
vec![
"3:1: Unexpected token in mapping: \"}\"".to_string(),
"5:1: Content after the document could not be parsed: \"{\"".to_string(),
],
),
] {
let expected: Vec<&str> = expected.iter().map(String::as_str).collect();
parse_with_errors(yaml, &expected);
}
}
#[test]
fn accepted_by_the_parser() {
for yaml in [
"key: *undefined",
"key: value\n invalid indentation for key",
] {
parse_with_errors(yaml, &[]);
}
assert_strings(
&parse("key: value\n invalid indentation for key"),
&[("key", "value invalid indentation for key")],
);
}
#[test]
fn block_scalar_error_recovery() {
let yaml = r#"good_key: value
bad_block: |
incomplete_key
another_good: works
"#;
parse_with_errors(
yaml,
&["3:1: Content after the document could not be parsed: \"incomplete_key\""],
);
assert_eq!(
YamlFile::from_str(yaml).unwrap_err().to_string(),
"Parse error at line 3, column 1: Content after the document could not be parsed: \
\"incomplete_key\""
);
}
#[test]
fn deep_nesting_is_cut_off() {
let mut block = String::new();
for i in 0..2000 {
block.push_str(&" ".repeat(i));
block.push_str("k:\n");
}
block.push_str(&" ".repeat(2000));
block.push_str("v\n");
for input in [
"{".repeat(2000),
"[".repeat(2000),
block,
format!("{}v", "&x ".repeat(2000)),
] {
let parse = Parse::parse_yaml(&input);
let _ = parse.tree();
assert!(parse.has_errors());
}
}
#[test]
fn deeply_nested_slots_hold_a_node() {
for input in ["{".repeat(300), "{a: ".repeat(300), "[".repeat(300)] {
let parse = yaml_edit::Parse::parse_yaml(&input);
let empty: Vec<_> = parse
.tree()
.syntax()
.descendants()
.filter(|n| {
matches!(
n.kind(),
SyntaxKind::KEY | SyntaxKind::VALUE | SyntaxKind::SEQUENCE_ENTRY
) && n.first_child().is_none()
})
.map(|n| n.kind())
.collect();
assert_eq!(empty, Vec::<SyntaxKind>::new(), "{input:?}");
}
}
fn top_level_keys(src: &str) -> Vec<String> {
YamlFile::from_str(src)
.expect("should parse")
.document()
.expect("should have a document")
.as_mapping()
.expect("root should be a mapping")
.keys()
.map(|k| k.to_string())
.collect()
}
fn checked_value(src: &str) -> yaml_edit::YamlValue {
let file = YamlFile::from_str(src).expect("should parse");
debug::validate_tree(file.syntax()).expect("tree should be valid");
let doc = file.document().expect("should have a document");
assert_eq!(
yaml_edit::validator::Validator::new().validate(&doc),
vec![]
);
yaml_edit::YamlValue::from_document(&doc).expect("should convert")
}
#[test]
fn rest_of_entry_line_stays_in_slot() {
let unclosed = |kind: &str, position: &str, found: &str| {
let (close, what, fix) = match kind {
"mapping" => (
'}',
"mapping",
"object, or check for missing commas between key-value pairs",
),
_ => (
']',
"sequence",
"array, or check for missing commas between elements",
),
};
format!(
"{position}: Unclosed flow {what}. Expected: '{close}' to close {what}. Found: {found}. \
Context: in flow {what}. Suggestion: Add '{close}' to close the {fix}"
)
};
let block_mapping = "Block mapping cannot start on same line as mapping key";
let value_nodes = "Value holds more than one node";
for (src, errors, violations) in [
(": :", vec![], vec![block_mapping, value_nodes]),
("a: :\nb: 1\n", vec![], vec![block_mapping, value_nodes]),
(
"a: ]: x\n",
vec![],
vec![
block_mapping,
value_nodes,
"Plain scalar cannot start with ']'",
],
),
(
"a: * c: x\n",
vec![],
vec![block_mapping, value_nodes, "Alias without a name"],
),
(
"a: {}{: x\n",
vec![unclosed("mapping", "2:1", "end of input")],
vec![
value_nodes,
"Flow collection continuation line must be indented",
],
),
(
"- {: -\n- b\n",
vec![unclosed("mapping", "1:6", "-")],
vec!["Sequence entry holds more than one node"],
),
(
"- [: -\n",
vec![unclosed("sequence", "1:6", "-")],
vec!["Sequence entry holds more than one node"],
),
(
":\n * c: x\n",
vec![],
vec![value_nodes, "Alias without a name"],
),
(
":\n|: x\n",
vec![],
vec![
value_nodes,
"Block scalar header must be indented past its parent node",
],
),
("?\n: >: x\n", vec![], vec![value_nodes]),
("? |b: x\n", vec![], vec!["Key holds more than one node"]),
] {
let parse = YamlFile::parse(src);
let file = parse.tree();
assert_eq!(debug::validate_tree(file.syntax()), Ok(()), "{src:?}");
assert_eq!(file.to_string(), src);
assert_eq!(parse.errors(), errors, "{src:?}");
let found: Vec<String> = file
.documents()
.flat_map(|d| Validator::new().validate(&d))
.map(|v| v.message)
.collect();
assert_eq!(found, violations, "{src:?}");
}
assert_eq!(
checked_value("<< <<\n"),
yaml_edit::YamlValue::Scalar(yaml_edit::ScalarValue::plain("<< <<"))
);
assert_eq!(top_level_keys("a: :\nb: 1\n"), ["a", "b"]);
assert_eq!(top_level_keys("x:\n a: :\n b: 1\nc: 2\n"), ["x", "c"]);
let parsed = YamlFile::from_str("a: :\nb: 1\n").unwrap();
assert_eq!(
debug::tree_to_string(parsed.syntax()),
"\
DOCUMENT
MAPPING
MAPPING_ENTRY
KEY
SCALAR
STRING: \"a\"
COLON: \":\"
WHITESPACE: \" \"
VALUE
SCALAR
NULL: \"\"
MAPPING
MAPPING_ENTRY
KEY
SCALAR
NULL: \"\"
COLON: \":\"
VALUE
SCALAR
NULL: \"\"
NEWLINE: \"\\\\n\"
MAPPING_ENTRY
KEY
SCALAR
STRING: \"b\"
COLON: \":\"
WHITESPACE: \" \"
VALUE
SCALAR
INT: \"1\"
NEWLINE: \"\\\\n\"
"
);
}
#[test]
fn missing_colon_reads_as_omitted_value() {
let tree = |src: &str, errors: usize| {
let parsed = YamlFile::parse(src);
assert_eq!(
parsed.errors().len(),
errors,
"{src:?}: {:?}",
parsed.errors()
);
debug::tree_to_string(parsed.tree().syntax())
};
let explicit_entry = " MAPPING_ENTRY
QUESTION: \"?\"
KEY
SCALAR
NULL: \"\"
VALUE
SCALAR
NULL: \"\"
NEWLINE: \"\\\\n\"
";
assert_eq!(
tree("<<\n", 0),
"DOCUMENT\n SCALAR\n MERGE_KEY: \"<<\"\n NEWLINE: \"\\\\n\"\n"
);
assert_eq!(
tree("?\n<<\n", 1),
format!(
"DOCUMENT\n MAPPING\n{explicit_entry}ERROR\n MERGE_KEY: \"<<\"\n NEWLINE: \"\\\\n\"\n"
)
);
assert_eq!(
tree("?\n[:\n", 2),
format!(
"\
DOCUMENT
MAPPING
{explicit_entry} MAPPING_ENTRY
KEY
SEQUENCE
LEFT_BRACKET: \"[\"
SEQUENCE_ENTRY
MAPPING
MAPPING_ENTRY
KEY
SCALAR
NULL: \"\"
COLON: \":\"
VALUE
SCALAR
NULL: \"\"
NEWLINE: \"\\\\n\"
RIGHT_BRACKET: \"\"
COLON: \"\"
VALUE
SCALAR
NULL: \"\"
"
)
);
}
#[test]
fn missing_colon_keeps_following_document() {
let parsed = YamlFile::parse("?\n<<\n---\na: 1\n");
let docs: Vec<String> = parsed.tree().documents().map(|d| d.to_string()).collect();
assert_eq!(docs, ["?\n", "---\na: 1\n"]);
}
#[test]
fn parse_error_position_with_cr_line_endings() {
let err = YamlFile::from_str("a: 1\rb: [a, }\r").unwrap_err();
assert_eq!(
err.to_string(),
"Parse error at line 2, column 8: Unexpected token in flow sequence: \"}\""
);
}
#[test]
fn parse_error_position_after_bom() {
let err = YamlFile::from_str("\u{FEFF}b: [a, }\n").unwrap_err();
assert_eq!(
err.to_string(),
"Parse error at line 1, column 8: Unexpected token in flow sequence: \"}\""
);
}
#[test]
fn positioned_parse_error() {
let text = "invalid:\n - [unclosed";
let parse = Parse::parse_yaml(text);
let errors = parse.positioned_errors();
assert_eq!(errors.len(), 1);
let err = &errors[0];
let start = err.start_position(text);
assert_eq!(start.line, 2);
assert_eq!(err.kind, ParseErrorKind::UnclosedFlowSequence);
assert_eq!(start, LineColumn::new(2, 14));
assert_eq!(err.end_position(text), LineColumn::new(2, 14));
assert_eq!(err.to_string(), err.message);
}
#[test]
fn unterminated_quote_as_string() {
for text in ["\"", "'"] {
let (doc, errors) = Document::from_str_relaxed(text);
assert_eq!(errors, ["1:1: Unterminated quoted string"]);
let scalar = doc.as_scalar().expect("relaxed parse keeps a scalar");
assert_eq!(scalar.as_string(), text);
assert_eq!(scalar.unquoted_value(), text);
}
}