fix(compliance): pull in spec test suite and fix issues (#40)

This commit includes a whole bunch of fixes, some of which are significant changes to the parser and some related functionality. But I consider all changes to be bugfixes because they were compliance failures.
This commit is contained in:
Kat Marchán 2022-04-27 23:21:28 -07:00 committed by GitHub
parent 71df712c0c
commit 58a40fdf48
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
406 changed files with 897 additions and 59 deletions

View File

@ -96,6 +96,32 @@ Error:
╰────
help: Floating point numbers must be base 10, and have numbers after the decimal point.
```
### Quirks
#### Properties
Multiple properties with the same name are allowed, and all duplicated
**will be preserved**, meaning those documents will correctly round-trip.
When using `node.get()`/`node["key"]` & company, the _last_ property with
that name's value will be returned.
#### Numbers
KDL itself does not specify a particular representation for numbers and
accepts just about anything valid, no matter how large and how small. This
means a few things:
* Numbers without a decimal point are interpreted as u64.
* Numbers with a decimal point are interpreted as f64.
* Floating point numbers that evaluate to f64::INFINITY or
f64::NEG_INFINITY or NaN will be represented as such in the values,
instead of the original numbers.
* A similar restriction applies to overflowed u64 values.
* The original _representation_ of these numbers will be preserved, unless
you `doc.fmt()`, in which case the original representation will be
thrown away and the actual value will be used when serializing.
### License
The code in this repository is covered by [the Apache-2.0

View File

@ -188,7 +188,12 @@ impl KdlDocument {
/// Auto-formats this Document, making everything nice while preserving
/// comments.
pub fn fmt(&mut self) {
self.fmt_impl(0);
self.fmt_impl(0, false);
}
/// Formats the document and removes all comments from the document.
pub fn fmt_no_comments(&mut self) {
self.fmt_impl(0, true);
}
}
@ -199,15 +204,20 @@ impl Display for KdlDocument {
}
impl KdlDocument {
pub(crate) fn fmt_impl(&mut self, indent: usize) {
pub(crate) fn fmt_impl(&mut self, indent: usize, no_comments: bool) {
if let Some(s) = self.leading.as_mut() {
crate::fmt::fmt_leading(s, indent);
crate::fmt::fmt_leading(s, indent, no_comments);
}
let mut has_nodes = false;
for node in &mut self.nodes {
has_nodes = true;
node.fmt_impl(indent, no_comments);
}
if let Some(s) = self.trailing.as_mut() {
crate::fmt::fmt_trailing(s);
}
for node in &mut self.nodes {
node.fmt_impl(indent);
crate::fmt::fmt_trailing(s, no_comments);
if !has_nodes {
s.push('\n');
}
}
}

View File

@ -124,12 +124,12 @@ impl Display for KdlEntry {
if let Some(leading) = &self.leading {
write!(f, "{}", leading)?;
}
if let Some(ty) = &self.ty {
write!(f, "({})", ty)?;
}
if let Some(name) = &self.name {
write!(f, "{}=", name)?;
}
if let Some(ty) = &self.ty {
write!(f, "({})", ty)?;
}
if let Some(repr) = &self.value_repr {
write!(f, "{}", repr)?;
} else {
@ -165,7 +165,7 @@ impl FromStr for KdlEntry {
type Err = KdlError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
parser::parse(s, parser::entry_with_node_space)
parser::parse(s, parser::entry_with_trailing)
}
}
@ -217,7 +217,7 @@ mod test {
}
);
let entry: KdlEntry = " \\\n (\"m\\\"eh\")\"foo\"=0xDEADbeef\t\\\n".parse()?;
let entry: KdlEntry = " \\\n \"foo\"=(\"m\\\"eh\")0xDEADbeef\t\\\n".parse()?;
assert_eq!(
entry,
KdlEntry {

View File

@ -1,30 +1,34 @@
pub(crate) fn fmt_leading(leading: &mut String, indent: usize) {
pub(crate) fn fmt_leading(leading: &mut String, indent: usize, no_comments: bool) {
if leading.is_empty() {
return;
}
let comments = crate::parser::parse(leading.trim(), crate::parser::leading_comments)
.expect("invalid leading text");
let mut result = String::new();
for line in comments {
let trimmed = line.trim();
if !trimmed.is_empty() {
result.push_str(&format!("{:indent$}{}\n", "", trimmed, indent = indent));
if !no_comments {
let comments = crate::parser::parse(leading.trim(), crate::parser::leading_comments)
.expect("invalid leading text");
for line in comments {
let trimmed = line.trim();
if !trimmed.is_empty() {
result.push_str(&format!("{:indent$}{}\n", "", trimmed, indent = indent));
}
}
}
result.push_str(&format!("{:indent$}", "", indent = indent));
*leading = result;
}
pub(crate) fn fmt_trailing(decor: &mut String) {
pub(crate) fn fmt_trailing(decor: &mut String, no_comments: bool) {
if decor.is_empty() {
return;
}
*decor = decor.trim().to_string();
let mut result = String::new();
let comments = crate::parser::parse(decor, crate::parser::trailing_comments)
.expect("invalid trailing text");
for comment in comments {
result.push_str(comment);
if !no_comments {
let comments = crate::parser::parse(decor, crate::parser::trailing_comments)
.expect("invalid trailing text");
for comment in comments {
result.push_str(comment);
}
}
*decor = result;
}

View File

@ -4,7 +4,7 @@ use crate::{parser, KdlError};
/// Represents a KDL
/// [Identifier](https://github.com/kdl-org/kdl/blob/main/SPEC.md#identifier).
#[derive(Debug, Clone, PartialEq)]
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct KdlIdentifier {
pub(crate) value: String,
pub(crate) repr: Option<String>,
@ -199,9 +199,6 @@ mod test {
let invalid = "\"x";
assert!(invalid.parse::<KdlIdentifier>().is_err());
let invalid = "r#\"foo\"#";
assert!(invalid.parse::<KdlIdentifier>().is_err());
Ok(())
}

View File

@ -94,6 +94,32 @@
//! ╰────
//! help: Floating point numbers must be base 10, and have numbers after the decimal point.
//! ```
//!
//! ## Quirks
//!
//! ### Properties
//!
//! Multiple properties with the same name are allowed, and all duplicated
//! **will be preserved**, meaning those documents will correctly round-trip.
//! When using `node.get()`/`node["key"]` & company, the _last_ property with
//! that name's value will be returned.
//!
//! ### Numbers
//!
//! KDL itself does not specify a particular representation for numbers and
//! accepts just about anything valid, no matter how large and how small. This
//! means a few things:
//!
//! * Numbers without a decimal point are interpreted as [`u64`].
//! * Numbers with a decimal point are interpreted as [`f64`].
//! * Floating point numbers that evaluate to [`f64::INFINITY`] or
//! [`f64::NEG_INFINITY`] or NaN will be represented as such in the values,
//! instead of the original numbers.
//! * A similar restriction applies to overflowed [`u64`] values.
//! * The original _representation_ of these numbers will be preserved, unless
//! you [`KdlDocument::fmt`] in which case the original representation will be
//! thrown away and the actual value will be used when serializing.
//!
//! ## License
//!
//! The code in this repository is covered by [the Apache-2.0
@ -102,6 +128,7 @@
#![deny(missing_debug_implementations, nonstandard_style)]
#![warn(missing_docs, unreachable_pub, rust_2018_idioms, unreachable_pub)]
#![cfg_attr(test, deny(warnings))]
#![doc(html_favicon_url = "https://kdl.dev/favicon.ico")]
#![doc(html_logo_url = "https://kdl.dev/logo.svg")]
pub use document::*;

View File

@ -134,14 +134,15 @@ impl KdlNode {
fn get_impl(&self, key: NodeKey) -> Option<&KdlEntry> {
match key {
NodeKey::Key(key) => {
let mut current = None;
for entry in &self.entries {
if entry.name.is_some()
&& entry.name.as_ref().map(|i| i.value()) == Some(key.value())
{
return Some(entry);
current = Some(entry);
}
}
None
current
}
NodeKey::Index(idx) => {
let mut current_idx = 0;
@ -170,14 +171,15 @@ impl KdlNode {
fn get_mut_impl(&mut self, key: NodeKey) -> Option<&mut KdlEntry> {
match key {
NodeKey::Key(key) => {
let mut current = None;
for entry in &mut self.entries {
if entry.name.is_some()
&& entry.name.as_ref().map(|i| i.value()) == Some(key.value())
{
return Some(entry);
current = Some(entry);
}
}
None
current
}
NodeKey::Index(idx) => {
let mut current_idx = 0;
@ -340,7 +342,12 @@ impl KdlNode {
/// Auto-formats this node and its contents.
pub fn fmt(&mut self) {
self.fmt_impl(0);
self.fmt_impl(0, false);
}
/// Auto-formats this node and its contents, stripping comments.
pub fn fmt_no_comments(&mut self) {
self.fmt_impl(0, true);
}
}
@ -421,12 +428,12 @@ impl Display for KdlNode {
}
impl KdlNode {
pub(crate) fn fmt_impl(&mut self, indent: usize) {
pub(crate) fn fmt_impl(&mut self, indent: usize, no_comments: bool) {
if let Some(s) = self.leading.as_mut() {
crate::fmt::fmt_leading(s, indent);
crate::fmt::fmt_leading(s, indent, no_comments);
}
if let Some(s) = self.trailing.as_mut() {
crate::fmt::fmt_trailing(s);
crate::fmt::fmt_trailing(s, no_comments);
if s.starts_with(';') {
s.remove(0);
}
@ -446,7 +453,7 @@ impl KdlNode {
entry.fmt();
}
if let Some(children) = self.children.as_mut() {
children.fmt_impl(indent + 4);
children.fmt_impl(indent + 4, no_comments);
if let Some(leading) = children.leading.as_mut() {
leading.push('\n');
}
@ -511,6 +518,11 @@ mod test {
assert_eq!(node.name(), &"\"node\"".parse()?);
assert_eq!(node.get(0), Some(&"0xDEADbeef".parse()?));
r#"
node "test" {
link "blah" anything="self"
}"#
.parse::<KdlNode>()?;
Ok(())
}
@ -528,5 +540,9 @@ mod test {
assert_eq!(node[0], false.into());
assert_eq!(node["foo"], KdlValue::Null);
node.entries_mut().push(KdlEntry::new_prop("x", 1));
node.entries_mut().push(KdlEntry::new_prop("x", 2));
assert_eq!(&node["x"], &2.into())
}
}

View File

@ -2,9 +2,9 @@ use std::ops::RangeTo;
use crate::nom_compat::{many0, many1, many_till};
use nom::branch::alt;
use nom::bytes::complete::{tag, take_until, take_until1, take_while, take_while_m_n};
use nom::bytes::complete::{tag, take_until, take_while, take_while_m_n};
use nom::character::complete::{anychar, char, none_of, one_of};
use nom::combinator::{all_consuming, cut, eof, map, map_opt, map_res, opt, recognize};
use nom::combinator::{all_consuming, cut, eof, map, map_opt, map_res, opt, peek, recognize};
use nom::error::{context, ParseError};
use nom::sequence::{delimited, preceded, terminated, tuple};
use nom::{Finish, IResult, Offset, Parser, Slice};
@ -81,7 +81,7 @@ pub(crate) fn node(input: &str) -> IResult<&str, KdlNode, KdlParseError<&str>> {
cut(recognize(preceded(
many0(node_space),
alt((
terminated(recognize(opt(tag(";"))), alt((linespace, eof))),
terminated(recognize(tag(";")), opt(alt((linespace, eof)))),
alt((newline, single_line_comment, eof)),
)),
))),
@ -109,7 +109,7 @@ pub(crate) fn node(input: &str) -> IResult<&str, KdlNode, KdlParseError<&str>> {
}
pub(crate) fn identifier(input: &str) -> IResult<&str, KdlIdentifier, KdlParseError<&str>> {
alt((plain_identifier, quoted_identifier))(input)
alt((quoted_identifier, plain_identifier))(input)
}
pub(crate) fn leading_comments(input: &str) -> IResult<&str, Vec<&str>, KdlParseError<&str>> {
@ -145,22 +145,38 @@ fn plain_identifier(input: &str) -> IResult<&str, KdlIdentifier, KdlParseError<&
take_while_m_n(1, 1, KdlIdentifier::is_initial_char),
cut(take_while(KdlIdentifier::is_identifier_char)),
))(input).map_err(|e| set_details(e, start, Some("invalid identifier character"), Some("See https://github.com/kdl-org/kdl/blob/main/SPEC.md#identifier for an explanation of valid KDL identifiers.")))?;
match name {
"false" | "true" | "null" => {
return Err(nom::Err::Error(KdlParseError {
input,
context: Some("non-keyword identifier"),
len: name.len(),
label: Some("reserved keyword"),
help: Some("Reserved keywords cannot be used as identifiers."),
kind: None,
touched: false,
}))
}
_ => {}
}
let mut ident = KdlIdentifier::from(name);
ident.set_repr(name);
Ok((input, ident))
}
fn quoted_identifier(input: &str) -> IResult<&str, KdlIdentifier, KdlParseError<&str>> {
let (input, (raw, val)) = string(input)?;
let (input, (raw, val)) = alt((string, raw_string))(input)?;
let mut ident = KdlIdentifier::from(val.as_string().unwrap());
ident.set_repr(raw);
Ok((input, ident))
}
pub(crate) fn entry_with_node_space(input: &str) -> IResult<&str, KdlEntry, KdlParseError<&str>> {
let (input, leading) = recognize(many0(node_space))(input)?;
let leading = if leading.is_empty() { " " } else { leading };
let (input, mut entry) = entry(input)?;
pub(crate) fn entry_with_trailing(input: &str) -> IResult<&str, KdlEntry, KdlParseError<&str>> {
let (input, mut leading) = recognize(many0(node_space))(input)?;
if leading.is_empty() {
leading = " ";
};
let (input, mut entry) = alt((property, argument))(input)?;
let (input, trailing) = recognize(many0(node_space))(input)?;
entry.set_leading(leading);
entry.set_trailing(trailing);
@ -168,25 +184,32 @@ pub(crate) fn entry_with_node_space(input: &str) -> IResult<&str, KdlEntry, KdlP
}
fn entry(input: &str) -> IResult<&str, KdlEntry, KdlParseError<&str>> {
alt((property, argument))(input)
let (input, leading) = recognize(many1(node_space))(input)?;
let (input, mut entry) = alt((property, argument))(input)?;
entry.set_leading(leading);
Ok((input, entry))
}
fn entry_maybe_space(input: &str) -> IResult<&str, KdlEntry, KdlParseError<&str>> {
let (input, leading) = recognize(many0(node_space))(input)?;
let (input, mut entry) = alt((property, argument))(input)?;
entry.set_leading(leading);
Ok((input, entry))
}
fn property(input: &str) -> IResult<&str, KdlEntry, KdlParseError<&str>> {
let (input, leading) = recognize(many0(node_space))(input)?;
let (input, ty) = opt(annotation)(input)?;
let (input, name) = identifier(input)?;
let (input, _) = context("'=' after property name", tag("="))(input)?;
let (input, ty) = opt(annotation)(input)?;
let (input, (raw, value)) = context("property value", cut(value))(input).map_err(|e| set_details(e, input, Some("invalid value"), Some("Please refer to https://github.com/kdl-org/kdl/blob/main/SPEC.md#value for valid KDL value syntaxes.")))?;
let mut entry = KdlEntry::new_prop(name, value);
entry.ty = ty;
entry.set_leading(leading);
entry.set_trailing("");
entry.set_value_repr(raw);
Ok((input, entry))
}
fn argument(input: &str) -> IResult<&str, KdlEntry, KdlParseError<&str>> {
let (input, leading) = recognize(many0(node_space))(input)?;
let (input, ty) = opt(annotation)(input)?;
let (input, (raw, value)) = if ty.is_some() {
context("valid value", cut(value))(input)
@ -195,7 +218,6 @@ fn argument(input: &str) -> IResult<&str, KdlEntry, KdlParseError<&str>> {
}?;
let mut entry = KdlEntry::new(value);
entry.ty = ty;
entry.set_leading(leading);
entry.set_trailing("");
entry.set_value_repr(raw);
Ok((input, entry))
@ -329,7 +351,7 @@ fn multi_line_comment(input: &str) -> IResult<&str, &str, KdlParseError<&str>> {
tag("/*"),
context("comment block body", cut(commented_block)),
))(input)
.map_err(|e| set_details(e, input, Some("comment"), None))
.map_err(|e| set_details(e, input, Some("comment"), Some("multi-line comments must start with /* and be terminated with a matching */. They may be nested, but their */ must match.")))
}
/// `commented-block := '*/' | (multi-line-comment | '*' | '/' | [^*/]+) commented-block`
@ -337,7 +359,12 @@ fn commented_block(input: &str) -> IResult<&str, &str, KdlParseError<&str>> {
alt((
tag("*/"),
terminated(
alt((multi_line_comment, take_until1("*/"), tag("*"), tag("/"))),
alt((
multi_line_comment,
tag("*"),
tag("/"),
recognize(many_till(anychar, peek(alt((tag("*"), tag("/")))))),
)),
commented_block,
),
))(input)
@ -348,7 +375,7 @@ fn node_slashdash(input: &str) -> IResult<&str, &str, KdlParseError<&str>> {
tag("/-"),
context(
"node following a slashdash",
cut(alt((recognize(entry), recognize(children)))),
cut(alt((recognize(entry_maybe_space), recognize(children)))),
),
))(input)
.map_err(|e| set_details(e, input, Some("slashdash"), None))
@ -430,6 +457,7 @@ fn escape(input: &str) -> IResult<&str, char, KdlParseError<&str>> {
}
fn unicode(input: &str) -> IResult<&str, char, KdlParseError<&str>> {
// TODO: This should only accept up to 0x10FFFF.
map_opt(
map_res(
take_while_m_n(1, 6, |c: char| c.is_ascii_hexdigit()),
@ -448,7 +476,7 @@ fn raw_string(input: &str) -> IResult<&str, (String, KdlValue), KdlParseError<&s
raw.push('r');
let (input, hashes) = recognize(many0(char('#')))(input)?;
raw.push_str(hashes);
let (input, _) = cut(char('"'))(input)?;
let (input, _) = char('"')(input)?;
raw.push('"');
let close = format!("\"{}", hashes);
let (input, value) = take_until(&close[..])(input)?;
@ -528,6 +556,7 @@ fn hexadecimal(input: &str) -> IResult<&str, (String, KdlValue), KdlParseError<&
)),
move |(raw_body, hex): (&str, &str)| {
raw.push_str(raw_body);
// TODO: Failure in case of int overflow!
i64::from_str_radix(&str::replace(hex, "_", ""), 16)
.map(|x| x * sign)
.map(|x| (raw.clone(), KdlValue::Base16(x)))
@ -664,6 +693,10 @@ mod comment_tests {
#[test]
fn multi_line() {
assert_eq!(comment("/* Hello world */"), Ok(("", "/* Hello world */")));
assert_eq!(
comment("/* Hello /* world */ blah */"),
Ok(("", "/* Hello /* world */ blah */"))
);
}
#[test]
@ -752,6 +785,8 @@ mod value_tests {
)
))
);
let (_, n) = node("node 0x0123_4567_89ab_cdef").expect("failed to parse node");
assert_eq!(&n[0], &KdlValue::Base16(0x0123456789abcdef));
assert_eq!(
value("0x123_4567"),
Ok(("", ("0x123_4567".into(), KdlValue::Base16(0x1234567))))

View File

@ -151,11 +151,23 @@ impl Display for KdlValue {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::RawString(_) => self.write_raw_string(f),
Self::String(string) => write!(f, "{:?}", string),
Self::String(_) => self.write_string(f),
Self::Base2(value) => write!(f, "0b{:b}", value),
Self::Base8(value) => write!(f, "0o{:o}", value),
Self::Base10(value) => write!(f, "{}", value),
Self::Base10Float(value) => write!(f, "{}", value),
Self::Base10(value) => write!(f, "{:?}", value),
Self::Base10Float(value) => write!(
f,
"{:?}",
if value == &f64::INFINITY {
f64::MAX
} else if value == &f64::NEG_INFINITY {
-f64::MAX
} else if value.is_nan() {
0.0
} else {
*value
}
),
Self::Base16(value) => write!(f, "0x{:x}", value),
Self::Bool(value) => write!(f, "{}", value),
Self::Null => write!(f, "null"),
@ -164,6 +176,23 @@ impl Display for KdlValue {
}
impl KdlValue {
fn write_string(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let string = self.as_string().unwrap();
write!(f, "\"")?;
for char in string.chars() {
match char {
'\\' | '"' => write!(f, "\\{}", char)?,
'\n' => write!(f, "\\n")?,
'\r' => write!(f, "\\r")?,
'\t' => write!(f, "\\t")?,
'\u{08}' => write!(f, "\\b")?,
'\u{0C}' => write!(f, "\\f")?,
_ => write!(f, "{}", char)?,
}
}
write!(f, "\"")?;
Ok(())
}
fn write_raw_string(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let raw = self.as_string().unwrap();
let mut consecutive = 0usize;

61
tests/README.md Normal file
View File

@ -0,0 +1,61 @@
# Full Document Test Cases
The `input` folder contains test cases for KDL parsers. The `expected_kdl`
folder contains files with the same name as those in `input` with the expected
output after being run through the parser and printed out again. If there's no
file in `expected_kdl` with a name corresponding to one in `input` it
indicates that parsing for that case should fail.
## Translation Rules
By necessity, the files in `expected_kdl` are not identical to their
corresponding inputs. They are instead pretty-printed according to the
following rules:
* All comments removed
* Extra empty lines removed except for a newline after the last node
* All nodes should be reformatted without escaped newlines
* Node fields should be `identifier <values> <properties> <children only if non-empty>`
* All values and all children must be in the same order as they were defined.
* Properties must be in _alphabetical order_ and separated by a single space.
* All strings must be represented as regular strings, with appropriate escapes
for invalid bare characters. That means that raw strings must be converted
to plain strings, and escaped.
* Any literal newlines or other ascii escape characters in escaped strings
replaced with their escape sequences.
* All identifiers must be unquoted unless they _must_ be quoted. That means
`"foo"` becomes `foo`, and `"foo bar"` stays that way.
* Any duplicate properties must be removed, with only the rightmost one
remaining. This also means duplicate properties must be allowed.
* 4 space indents
* All numbers must be converted to their simplest decimal representation. That
means that hex, octal, and binary must all be converted to decimals. All
floats must be represented using `E` notation, with a single digit left of
the decimal point if the float is less than 1. While parsers are required to
_consume_ different number syntaxes, they are under no obligation to
represent numbers in any particular way.
Data may be manipulated as you wish in order to output the expected KDL. This
test suite verifies the ability to **parse**, not specific quirks about
internal representations.
## What to do if a test fails for you
This test suite was originally designed for a pre-1.0 version of the KDL
specification. If you encounter a failure, it's likely that the test suite
will need to be updated, rather than your parser itself. This test suite is
NOT AUTHORITATIVE. If this test suite disagrees with the KDL spec in any way,
the most desirable resolution is to send a PR to this repository to fix the
test itself. Likewise, if you think a test succeeded but should not have,
please send a PR.
If you think the disagreement is due to a genuine error or oversight in the
KDL specification, please open an issue explaining the matter and the change
will be considered for the next version of the KDL spec.
## Credit
This test suite was extracted from
[`kdl4j`](https://github.com/hkolbeck/kdl4j), the original Java
implementation of KDL, with huge thanks to
[@hkolbeck](https://github.com/hkolbeck) for authoring them!

144
tests/compliance.rs Normal file
View File

@ -0,0 +1,144 @@
use std::{
collections::HashMap,
fs,
path::{Path, PathBuf},
};
use kdl::{KdlDocument, KdlError, KdlIdentifier, KdlValue};
use miette::IntoDiagnostic;
#[test]
fn spec_compliance() -> miette::Result<()> {
let input = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("tests")
.join("test_cases")
.join("input");
for test_name in fs::read_dir(&input).into_diagnostic()? {
let test_path = test_name.into_diagnostic()?.path();
println!(
"parsing {}:",
PathBuf::from(test_path.file_name().unwrap()).display()
);
let src = normalize_line_endings(fs::read_to_string(&test_path).into_diagnostic()?);
println!("src: {}", src);
let res: Result<KdlDocument, KdlError> = src.parse();
validate_res(res, &test_path)?;
}
Ok(())
}
fn validate_res(res: Result<KdlDocument, KdlError>, path: &Path) -> miette::Result<()> {
let file_name = path.file_name().unwrap();
let expected_dir = path
.parent()
.unwrap()
.parent()
.unwrap()
.join("expected_kdl");
let expected_path = expected_dir.join(file_name);
let underscored = expected_dir.join(&format!("_{}", PathBuf::from(file_name).display()));
if expected_path.exists() {
let doc = res?;
let expected =
normalize_line_endings(fs::read_to_string(&expected_path).into_diagnostic()?);
println!("expected: {}", expected);
let stringified = stringify_to_expected(doc);
println!("stringified: {}", stringified);
assert_eq!(stringified, expected);
} else if underscored.exists() {
println!(
"skipped reserialization for {}",
PathBuf::from(file_name).display()
);
} else {
assert!(res.is_err(), "parse should not have succeeded");
}
Ok(())
}
fn normalize_line_endings(src: String) -> String {
src.replace("\r\n", "\n")
}
fn stringify_to_expected(mut doc: KdlDocument) -> String {
doc.fmt_no_comments();
normalize_numbers(&mut doc);
normalize_strings(&mut doc);
dedupe_props(&mut doc);
remove_empty_children(&mut doc);
doc.to_string()
}
fn normalize_numbers(doc: &mut KdlDocument) {
for node in doc.nodes_mut() {
for entry in node.entries_mut() {
if let Some(value) = entry.value().as_i64() {
*entry.value_mut() = KdlValue::Base10(value);
}
}
if let Some(children) = node.children_mut() {
normalize_numbers(children);
}
}
}
fn normalize_strings(doc: &mut KdlDocument) {
for node in doc.nodes_mut() {
for entry in node.entries_mut() {
if let Some(value) = entry.value().as_string() {
*entry.value_mut() = KdlValue::String(value.to_string());
}
}
if let Some(children) = node.children_mut() {
normalize_strings(children);
}
}
}
fn dedupe_props(doc: &mut KdlDocument) {
for node in doc.nodes_mut() {
let mut props = HashMap::<KdlIdentifier, Vec<usize>>::new();
for (idx, entry) in node.entries_mut().iter_mut().enumerate() {
if let Some(name) = entry.name() {
if !props.contains_key(name) {
props.insert(name.clone(), Vec::new());
}
if let Some(indices) = props.get_mut(name) {
indices.push(idx);
}
}
}
let new_entries = node
.entries()
.iter()
.enumerate()
.filter_map(|(idx, entry)| {
if let Some(name) = entry.name() {
if let Some(indices) = props.get(name) {
if &idx == indices.last().unwrap() {
return Some(entry.clone());
} else {
return None;
}
}
}
Some(entry.clone())
});
*node.entries_mut() = new_entries.collect();
if let Some(children) = node.children_mut() {
dedupe_props(children);
}
}
}
fn remove_empty_children(doc: &mut KdlDocument) {
for node in doc.nodes_mut() {
let maybe_children = node.children_mut();
if maybe_children.is_some() && maybe_children.as_ref().unwrap().nodes().is_empty() {
*maybe_children = None;
}
if let Some(children) = maybe_children {
remove_empty_children(children);
}
}
}

View File

@ -0,0 +1 @@
node 1e-10

View File

@ -0,0 +1 @@
node 1 1.0 10000000000.0 1e-10 1 7 2 "arg" "arg\\\\" true false null

View File

@ -0,0 +1 @@
node 10000000000.0

View File

@ -0,0 +1 @@
node prop=1.23E+1000

View File

@ -0,0 +1 @@
node prop=1.23E-1000

View File

@ -0,0 +1 @@
node 1e-100

View File

@ -0,0 +1 @@
node "\"\\/\b\f\n\r\t"

View File

@ -0,0 +1,3 @@
node "arg" prop="val" {
inner_node
}

View File

@ -0,0 +1 @@
node "arg" arg="val"

View File

@ -0,0 +1 @@
node (type)false

View File

@ -0,0 +1 @@
node (type)2.5

View File

@ -0,0 +1 @@
node (type)16

View File

@ -0,0 +1 @@
node (type)null

View File

@ -0,0 +1 @@
node (type)"str"

View File

@ -0,0 +1 @@
node (type)"str"

View File

@ -0,0 +1 @@
node (type)true

View File

@ -0,0 +1 @@
node (type)"arg"

View File

@ -0,0 +1 @@
node (type)0

View File

@ -0,0 +1 @@
node

View File

@ -0,0 +1 @@
😁 "happy!"

View File

@ -0,0 +1 @@
node 2

View File

@ -0,0 +1 @@
node 2

View File

@ -0,0 +1 @@
node 2

View File

@ -0,0 +1 @@
node ("")10

View File

@ -0,0 +1 @@
("")node

View File

@ -0,0 +1 @@
node key=("")true

View File

@ -0,0 +1 @@
node "arg"

View File

@ -0,0 +1 @@
node "arg"

View File

@ -0,0 +1 @@
node

View File

@ -0,0 +1 @@

View File

@ -0,0 +1 @@
node false true

View File

@ -0,0 +1 @@
node prop1=true prop2=false

View File

@ -0,0 +1 @@
node "arg2"

View File

@ -0,0 +1 @@
node "arg"

View File

@ -0,0 +1 @@
node_2

View File

@ -0,0 +1 @@
node_2

View File

@ -0,0 +1 @@
node "arg"

View File

@ -0,0 +1,2 @@
node1
node2

View File

@ -0,0 +1 @@
node "😀"

View File

@ -0,0 +1 @@

View File

@ -0,0 +1 @@
node

View File

@ -0,0 +1 @@
node

View File

@ -0,0 +1 @@
node

View File

@ -0,0 +1 @@
node

View File

@ -0,0 +1 @@
"" "arg"

View File

@ -0,0 +1 @@
node ""="empty"

View File

@ -0,0 +1 @@
node ""

View File

@ -0,0 +1 @@
node "hello\nworld"

View File

@ -0,0 +1 @@
node "hello\nworld"

View File

@ -0,0 +1 @@
node "arg"

View File

@ -0,0 +1 @@
node "arg" "arg2\n"

View File

@ -0,0 +1,2 @@
node1
node2

View File

@ -0,0 +1 @@
false_id

View File

@ -0,0 +1 @@
node false_id=1

View File

@ -0,0 +1 @@
node 1311768467294899695

View File

@ -0,0 +1 @@
node 1311768467294899695

View File

@ -0,0 +1 @@
node 737894400291

View File

@ -0,0 +1 @@
node 1

View File

@ -0,0 +1 @@
node 1234

View File

@ -0,0 +1 @@

View File

@ -0,0 +1,3 @@
node {
inner_node
}

View File

@ -0,0 +1 @@

View File

@ -0,0 +1 @@
node

View File

@ -0,0 +1 @@

View File

@ -0,0 +1 @@
node

View File

@ -0,0 +1 @@
node 1

View File

@ -0,0 +1 @@
node 11

View File

@ -0,0 +1 @@
node 1

View File

@ -0,0 +1 @@
node "arg"

View File

@ -0,0 +1 @@
node "arg1" "arg2"

View File

@ -0,0 +1 @@
node " hey\neveryone\nhow goes?\n"

View File

@ -0,0 +1 @@
node -1.0 key=-10.0

View File

@ -0,0 +1 @@
node -10 prop=-15

View File

@ -0,0 +1 @@
node "arg"

View File

@ -0,0 +1,5 @@
node1 {
node2 {
node
}
}

View File

@ -0,0 +1 @@
node "arg"

View File

@ -0,0 +1 @@
node "arg"

View File

@ -0,0 +1,2 @@
node1
node2

View File

@ -0,0 +1 @@
node "arg"

View File

@ -0,0 +1 @@
node 10000000000.0

View File

@ -0,0 +1 @@
node false

View File

@ -0,0 +1 @@
node true

View File

@ -0,0 +1 @@
(type)node

View File

@ -0,0 +1 @@
node null

View File

@ -0,0 +1 @@
null_id

View File

@ -0,0 +1 @@
node null_id=1

View File

@ -0,0 +1 @@
node prop=null

View File

@ -0,0 +1 @@
node 15.7

Some files were not shown because too many files have changed in this diff Show More