use std::path::{Path, PathBuf};
use arity_formatter::formatter::{DeclineReason, DescriptionFormatError, FormatVerificationError};
use super::{
FormatError, FormatStyle, format_description_with_style, format_verified_with_options,
format_with_options,
};
use crate::file_discovery::{DiscoveredFiles, is_description_file};
use crate::formatter::cache::CacheKey;
use crate::parser::ParseOptions;
use crate::project::description::MarkdownDefaultResolver;
pub fn merge(files: DiscoveredFiles) -> Vec<PathBuf> {
let mut all = files.r;
all.extend(files.description);
all.sort();
all
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Formatted {
Text(String),
Declined(DeclineReason),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum FormatSourceError {
R(FormatError),
RVerification(FormatVerificationError),
Description(DescriptionFormatError),
DescriptionVerification(String),
}
impl std::fmt::Display for FormatSourceError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::R(err) => write!(f, "{err}"),
Self::RVerification(err) => write!(f, "{err}"),
Self::Description(err) => write!(f, "{err}"),
Self::DescriptionVerification(message) => f.write_str(message),
}
}
}
pub fn format_file_verified(
path: &Path,
content: &str,
style: FormatStyle,
markdown: &mut MarkdownDefaultResolver,
) -> Result<Formatted, FormatSourceError> {
if is_description_file(path) {
let formatted = match format_description_with_style(content, style) {
Ok(text) => text,
Err(DescriptionFormatError::Declined(reason)) => {
return Ok(Formatted::Declined(reason));
}
Err(err) => return Err(FormatSourceError::Description(err)),
};
match format_description_with_style(&formatted, style) {
Ok(reformatted) if reformatted == formatted => Ok(Formatted::Text(formatted)),
Ok(_) => Err(FormatSourceError::DescriptionVerification(
"formatter verification failed (non-idempotent output)".to_string(),
)),
Err(err) => Err(FormatSourceError::DescriptionVerification(format!(
"formatted output failed verification: {err}"
))),
}
} else {
let options = ParseOptions::default().with_roxygen_markdown_default(markdown.resolve(path));
format_verified_with_options(content, style, &options)
.map(Formatted::Text)
.map_err(FormatSourceError::RVerification)
}
}
impl std::error::Error for FormatSourceError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::R(err) => Some(err),
Self::RVerification(err) => Some(err),
Self::Description(err) => Some(err),
Self::DescriptionVerification(_) => None,
}
}
}
pub fn format_file(
path: &Path,
content: &str,
style: FormatStyle,
markdown: &mut MarkdownDefaultResolver,
) -> Result<Formatted, FormatSourceError> {
if is_description_file(path) {
return match format_description_with_style(content, style) {
Ok(text) => Ok(Formatted::Text(text)),
Err(DescriptionFormatError::Declined(reason)) => Ok(Formatted::Declined(reason)),
Err(err) => Err(FormatSourceError::Description(err)),
};
}
let options = ParseOptions::default().with_roxygen_markdown_default(markdown.resolve(path));
format_with_options(content, style, &options)
.map(Formatted::Text)
.map_err(FormatSourceError::R)
}
pub fn cache_key<'a>(
path: &Path,
content: &'a str,
markdown: &mut MarkdownDefaultResolver,
) -> CacheKey<'a> {
if is_description_file(path) {
return CacheKey::dcf(content);
}
CacheKey::r(content, markdown.resolve(path))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_description_path_selects_the_dcf_grammar() {
let mut markdown = MarkdownDefaultResolver::new();
let formatted = format_file(
Path::new("pkg/DESCRIPTION"),
"Package: p\nImports: b, a\n",
FormatStyle::default(),
&mut markdown,
)
.expect("formats");
assert_eq!(
formatted,
Formatted::Text("Package: p\nImports:\n a,\n b\n".to_string())
);
}
#[test]
fn a_declined_description_is_not_a_failure() {
let mut markdown = MarkdownDefaultResolver::new();
let formatted = format_file(
Path::new("pkg/DESCRIPTION"),
"Package: p\n\nPackage: q\n",
FormatStyle::default(),
&mut markdown,
)
.expect("declines without erroring");
assert!(matches!(
formatted,
Formatted::Declined(DeclineReason::MultipleRecords { .. })
));
}
#[test]
fn a_malformed_description_is_a_failure() {
let mut markdown = MarkdownDefaultResolver::new();
let err = format_file(
Path::new("pkg/DESCRIPTION"),
"Package: p\ngarbage\n",
FormatStyle::default(),
&mut markdown,
)
.expect_err("errors");
assert!(matches!(err, FormatSourceError::Description(_)));
}
#[test]
fn verified_r_format_accepts_body_brace_normalization() {
let mut markdown = MarkdownDefaultResolver::new();
let formatted = format_file_verified(
Path::new("pkg/R/code.R"),
"if (x) y else z\n",
FormatStyle::default(),
&mut markdown,
)
.expect("formats and verifies");
assert_eq!(
formatted,
Formatted::Text("if (x) {\n y\n} else {\n z\n}\n".to_string())
);
}
#[test]
fn merge_interleaves_both_grammars_by_path() {
let merged = merge(DiscoveredFiles {
r: vec![PathBuf::from("pkg/R/z.R"), PathBuf::from("pkg/R/a.R")],
description: vec![PathBuf::from("pkg/DESCRIPTION")],
});
assert_eq!(
merged,
vec![
PathBuf::from("pkg/DESCRIPTION"),
PathBuf::from("pkg/R/a.R"),
PathBuf::from("pkg/R/z.R"),
]
);
}
}