use std::fmt;
use std::fs;
use std::io;
use std::path::PathBuf;
use super::FormatStyle;
use super::source::{Formatted, cache_key, format_file, merge};
use crate::file_discovery::{
DiscoveredFiles, ExcludeFilter, FileDiscoveryError, collect_r_files, collect_source_files,
};
use crate::formatter::cache::FormatCache;
use crate::project::description::MarkdownDefaultResolver;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CheckResult {
pub checked_files: usize,
pub changed_files: Vec<ChangedFile>,
pub failed_files: Vec<FailedFile>,
pub skipped: Vec<PathBuf>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FailedFile {
pub path: PathBuf,
pub reason: String,
}
impl fmt::Display for FailedFile {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}: {}", self.path.display(), self.reason)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ChangedFile {
pub path: PathBuf,
pub original: String,
pub formatted: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum CheckError {
MissingPaths,
NoFiles,
UnsupportedFilePath { path: PathBuf },
WalkError { path: PathBuf, message: String },
}
impl fmt::Display for CheckError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::MissingPaths => {
write!(
f,
"--check requires at least one input path (file or directory)"
)
}
Self::NoFiles => {
write!(
f,
"no .R files or DESCRIPTION files found under the provided input paths"
)
}
Self::UnsupportedFilePath { path } => {
write!(
f,
"input file {} is not formattable; --check supports .R files and DESCRIPTION",
path.display()
)
}
Self::WalkError { path, message } => {
write!(f, "failed while scanning {}: {message}", path.display())
}
}
}
}
impl std::error::Error for CheckError {}
impl From<FileDiscoveryError> for CheckError {
fn from(value: FileDiscoveryError) -> Self {
match value {
FileDiscoveryError::UnsupportedFilePath { path } => Self::UnsupportedFilePath { path },
FileDiscoveryError::WalkError { path, message } => Self::WalkError { path, message },
}
}
}
pub fn check_paths(paths: &[PathBuf]) -> Result<CheckResult, CheckError> {
check_paths_with_style(paths, FormatStyle::default(), &ExcludeFilter::none())
}
pub fn check_paths_with_style(
paths: &[PathBuf],
style: FormatStyle,
exclude: &ExcludeFilter,
) -> Result<CheckResult, CheckError> {
check_paths_with_style_cached(paths, style, exclude, true, None)
}
pub fn check_paths_with_style_cached(
paths: &[PathBuf],
style: FormatStyle,
exclude: &ExcludeFilter,
descriptions: bool,
mut cache: Option<&mut FormatCache>,
) -> Result<CheckResult, CheckError> {
if paths.is_empty() {
return Err(CheckError::MissingPaths);
}
let discovered = if descriptions {
collect_source_files(paths, exclude).map_err(CheckError::from)?
} else {
DiscoveredFiles {
r: collect_r_files(paths, exclude).map_err(CheckError::from)?,
description: Vec::new(),
}
};
let files = merge(discovered);
if files.is_empty() {
if exclude.force() {
return Ok(CheckResult {
checked_files: 0,
changed_files: Vec::new(),
failed_files: Vec::new(),
skipped: Vec::new(),
});
}
return Err(CheckError::NoFiles);
}
let total = files.len();
let mut changed_files = Vec::new();
let mut failed_files = Vec::new();
let mut skipped = Vec::new();
let mut markdown = MarkdownDefaultResolver::new();
for path in files {
let content = match fs::read_to_string(&path) {
Ok(content) => content,
Err(err) if err.kind() == io::ErrorKind::InvalidData => {
skipped.push(path);
continue;
}
Err(err) => {
failed_files.push(FailedFile {
path,
reason: format!("failed to read: {err}"),
});
continue;
}
};
if cache
.as_deref()
.is_some_and(|c| c.is_fixed_point(cache_key(&path, &content, &mut markdown)))
{
continue;
}
let formatted = match format_file(&path, &content, style, &mut markdown) {
Ok(Formatted::Text(formatted)) => formatted,
Ok(Formatted::Declined(_)) => continue,
Err(err) => {
failed_files.push(FailedFile {
path,
reason: format!("failed to format: {err}"),
});
continue;
}
};
if formatted != content {
changed_files.push(ChangedFile {
path,
original: content,
formatted,
});
} else if let Some(c) = cache.as_deref_mut() {
c.record_fixed_point(cache_key(&path, &content, &mut markdown));
}
}
if let Some(c) = cache.as_deref()
&& let Err(err) = c.store()
{
log::warn!("failed to write format cache: {err}");
}
Ok(CheckResult {
checked_files: total - failed_files.len() - skipped.len(),
changed_files,
failed_files,
skipped,
})
}