use super::{fields, plan};
use crate::dcf;
use crate::formatter::style::{FormatStyle, apply_line_ending};
const INDENT: &str = " ";
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum DescriptionFormatError {
ParseErrors { count: usize },
Declined(DeclineReason),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum DeclineReason {
MultipleRecords { count: usize },
DuplicateField { name: String },
NameWhitespace { name: String },
Encoding { declared: String },
MalformedLine,
ByteOrderMark,
UnsupportedStructure,
}
impl DescriptionFormatError {
pub fn is_decline(&self) -> bool {
matches!(self, Self::Declined(_))
}
}
impl std::fmt::Display for DescriptionFormatError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::ParseErrors { count } => write!(
f,
"input contains {count} DCF diagnostic(s); formatter only supports parseable input"
),
Self::Declined(reason) => write!(f, "left unformatted: {reason}"),
}
}
}
impl std::fmt::Display for DeclineReason {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::MultipleRecords { count } => {
write!(f, "the file holds {count} records, not one")
}
Self::DuplicateField { name } => write!(f, "duplicate field {name:?}"),
Self::NameWhitespace { name } => {
write!(f, "whitespace before the colon of field {name:?}")
}
Self::Encoding { declared } => write!(f, "declared encoding {declared:?} is not UTF-8"),
Self::MalformedLine => write!(f, "the file has a malformed line"),
Self::ByteOrderMark => write!(f, "the file starts with a byte order mark"),
Self::UnsupportedStructure => write!(f, "unrecognized document structure"),
}
}
}
impl std::error::Error for DescriptionFormatError {}
pub fn format_description(input: &str) -> Result<String, DescriptionFormatError> {
format_description_with_style(input, FormatStyle::default())
}
pub fn format_description_with_style(
input: &str,
style: FormatStyle,
) -> Result<String, DescriptionFormatError> {
if input.starts_with('\u{feff}') {
return Err(DescriptionFormatError::Declined(
DeclineReason::ByteOrderMark,
));
}
let parsed = dcf::parse(input);
if !parsed.diagnostics.is_empty() {
return Err(DescriptionFormatError::ParseErrors {
count: parsed.diagnostics.len(),
});
}
if super::super::directive::dcf_file_is_skipped(&parsed.cst) {
return Ok(input.to_string());
}
let plan = plan::build(&parsed.document()).map_err(DescriptionFormatError::Declined)?;
let lines = render(&plan, style);
if lines.is_empty() {
return Ok(String::new());
}
let mut out = lines.join("\n");
out.push('\n');
Ok(apply_line_ending(&out, style.line_ending.resolve(input)))
}
fn render(plan: &plan::Plan, style: FormatStyle) -> Vec<String> {
let mut lines = plan.orphan_comments.clone();
if let Some(record) = &plan.record {
for field in &record.fields {
lines.extend(field.leading_comments.iter().cloned());
lines.extend(fields::render(field, style, INDENT));
}
lines.extend(record.trailing_comments.iter().cloned());
}
lines
}
#[cfg(test)]
mod tests {
use super::*;
const ADVERSARIAL: &[&str] = &[
"",
"\n",
"Package:",
"Package: p",
"Package: p\n",
": value\n",
"# only a comment\n",
"#\n",
"Package: p\n# a\n# b\n",
"Collate:\n",
"Collate:\n 'a.R'\n",
"Imports:\n",
"Imports: ,\n",
"Imports: a (>= 1.0, < 2.0)\n",
"Authors@R: person(\n",
"Description: a\n b\n c\n",
"Package: p\r\n",
"\u{feff}Package: p\n",
"Package: p\n\n\n",
"Config/x: a\n b\n",
];
#[test]
fn adversarial_inputs_never_panic_and_stay_idempotent() {
for input in ADVERSARIAL {
let Ok(formatted) = format_description(input) else {
continue;
};
let again = format_description(&formatted)
.unwrap_or_else(|err| panic!("reformatting {input:?} failed: {err}"));
assert_eq!(again, formatted, "not idempotent for {input:?}");
assert_eq!(
dcf::reconstruct(&formatted),
formatted,
"not lossless for {input:?}"
);
}
}
#[test]
fn a_bom_is_declined_before_parsing() {
assert_eq!(
format_description("\u{feff}Package: p\n"),
Err(DescriptionFormatError::Declined(
DeclineReason::ByteOrderMark
))
);
}
#[test]
fn crlf_input_round_trips_as_crlf() {
assert_eq!(
format_description("Package: p\r\nImports: b, a\r\n").expect("formats"),
"Package: p\r\nImports:\r\n a,\r\n b\r\n"
);
}
#[test]
fn the_line_ending_style_overrides_the_source() {
let style = FormatStyle {
line_ending: crate::formatter::style::LineEnding::Crlf,
..FormatStyle::default()
};
assert_eq!(
format_description_with_style("Package: p\n", style).expect("formats"),
"Package: p\r\n"
);
}
}