use std::{fmt, path::Path, path::PathBuf, process::Command};
use anyhow::{anyhow, Context, Result};
use console::style;
use globset::{GlobBuilder, GlobSet, GlobSetBuilder};
use log::{debug, warn};
use serde::de::{self, Visitor};
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use crate::config::FmtConfig;
use crate::{expand_exe, expand_if_path, expand_path};
#[derive(Debug, Default, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct FormatterName(String);
#[cfg(test)]
impl FormatterName {
pub(crate) fn new<S>(name: S) -> Self
where
S: Into<String>,
{
Self(name.into())
}
}
impl Serialize for FormatterName {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(&self.0)
}
}
struct FormatterNameVisitor;
impl<'de> Visitor<'de> for FormatterNameVisitor {
type Value = FormatterName;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("a string")
}
fn visit_string<E>(self, value: String) -> Result<Self::Value, E>
where
E: de::Error,
{
Ok(FormatterName(value))
}
fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
where
E: de::Error,
{
Ok(FormatterName(value.to_string()))
}
}
impl<'de> Deserialize<'de> for FormatterName {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
deserializer.deserialize_string(FormatterNameVisitor)
}
}
impl fmt::Display for FormatterName {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "#{}", self.0)
}
}
#[derive(Debug, Clone)]
pub struct Formatter {
pub name: FormatterName,
pub command: PathBuf,
pub options: Vec<String>,
pub work_dir: PathBuf,
pub includes: GlobSet,
pub excludes: GlobSet,
}
impl Formatter {
pub fn fmt(&self, paths: &[PathBuf]) -> Result<()> {
if paths.is_empty() {
return Ok(());
}
let mut cmd = Command::new(&self.command);
cmd.current_dir(&self.work_dir);
cmd.args(&self.options);
cmd.args(paths);
debug!("running {:?}", cmd);
match cmd.output() {
Ok(out) => {
if !out.status.success() {
warn!(
"Error using formatter {}:\n{stdout}:\n{}\n{stderr}:\n{}",
self.name,
String::from_utf8_lossy(&out.stdout),
String::from_utf8_lossy(&out.stderr),
stdout = style("• [STDOUT]").bold().dim(),
stderr = style("• [STDERR]").bold().dim(),
);
match out.status.code() {
Some(scode) => {
return Err(anyhow!(
"{}'s formatter failed: exit status {}",
&self,
scode
));
}
None => {
return Err(anyhow!(
"{}'s formatter failed: unknown formatter error",
&self
));
}
}
}
Ok(())
}
Err(err) => {
Err(anyhow!("{} failed: {}", &self, err))
}
}
}
pub fn is_match<T: AsRef<Path>>(&self, path: T) -> bool {
let path = path.as_ref();
assert!(path.is_absolute());
if !path.starts_with(&self.work_dir) {
return false;
}
if self.excludes.is_match(path) {
return false;
}
if !self.includes.is_match(path) {
return false;
}
true
}
pub fn from_config(tree_root: &Path, name: &str, cfg: &FmtConfig) -> Result<Self> {
let name = FormatterName(name.to_string());
let work_dir = expand_path(&cfg.work_dir, tree_root);
let command = expand_exe(&cfg.command, tree_root)
.with_context(|| format!("could not find '{}' on PATH", &cfg.command))?;
debug!("Found {} at {}", cfg.command, command.display());
assert!(command.is_absolute());
if cfg.includes.is_empty() {
return Err(anyhow!("{} doesn't have any includes", name));
}
let includes = patterns_to_glob_set(tree_root, &cfg.includes)?;
let excludes = patterns_to_glob_set(tree_root, &cfg.excludes)?;
Ok(Self {
name,
command,
options: cfg.options.clone(),
work_dir,
includes,
excludes,
})
}
}
impl fmt::Display for Formatter {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "#{}", self.name.0)
}
}
fn patterns_to_glob_set(tree_root: &Path, patterns: &[String]) -> Result<GlobSet> {
let mut sum = GlobSetBuilder::new();
for pattern in patterns {
let pattern = expand_if_path(pattern.to_string(), tree_root);
let glob = GlobBuilder::new(&pattern).build()?;
sum.add(glob);
}
Ok(sum.build()?)
}