efmt 0.21.0

Erlang code formatter
Documentation
use efmt_core::items::{Config, Expr};
use efmt_core::parse::{TokenStream, Tokenizer};
use ignore::Walk;
use std::path::{Path, PathBuf};

pub fn find_rebar_config_dir() -> Option<PathBuf> {
    let mut dir = std::env::current_dir().ok()?;
    while !dir.join("rebar.config").exists() {
        dir = dir.parent()?.to_path_buf();
    }
    Some(dir)
}

pub fn load_rebar_config<P: AsRef<Path>>(path: P) -> crate::Result<Vec<RebarConfigValue>> {
    let text = std::fs::read_to_string(&path)?;
    let mut tokenizer = Tokenizer::new(text);
    tokenizer.set_filepath(path);
    let mut ts = TokenStream::new(tokenizer);
    let config: Config = ts.parse()?;

    let mut values = Vec::new();
    for expr in config.exprs() {
        if let Some(x) = RebarConfigValue::from_expr(expr, &ts.text()) {
            values.push(x);
        }
    }
    Ok(values)
}

#[derive(Debug, Clone)]
pub enum RebarConfigValue {
    Atom(String),
    String(String),
    Integer(u32),
    List(Vec<Self>),
    Tuple(Vec<Self>),
}

impl RebarConfigValue {
    pub fn as_kv_tuple(&self) -> Option<(&str, &Self)> {
        if let Self::Tuple(kv) = self
            && kv.len() == 2
            && let Self::Atom(k) = &kv[0]
        {
            return Some((k.as_str(), &kv[1]));
        }
        None
    }
}

impl RebarConfigValue {
    fn from_expr(expr: &Expr, text: &str) -> Option<Self> {
        if let Some(x) = expr.as_atom() {
            Some(Self::Atom(x.to_owned()))
        } else if let Some(x) = expr.as_string() {
            Some(Self::String(x.to_owned()))
        } else if let Some(x) = expr.as_u32(text) {
            Some(Self::Integer(x))
        } else if let Some(xs) = expr.as_list() {
            Some(Self::List(
                xs.iter().filter_map(|x| Self::from_expr(x, text)).collect(),
            ))
        } else if let Some((tag, xs)) = expr.as_tuple() {
            Some(Self::Tuple(
                tag.iter()
                    .map(|tag| Self::Atom(tag.value().to_owned()))
                    .chain(xs.iter().filter_map(|x| Self::from_expr(x, text)))
                    .collect(),
            ))
        } else {
            None
        }
    }
}

pub fn collect_default_target_files() -> crate::Result<Vec<PathBuf>> {
    let mut files = Vec::new();
    for result in Walk::new("./") {
        let entry = result?;
        if entry.file_type().is_some_and(|t| t.is_file()) {
            let path = entry.path();
            if is_format_target(path) {
                let path = path.strip_prefix("./").unwrap_or(path).to_path_buf();
                files.push(path);
            }
        }
    }
    Ok(files)
}

fn is_format_target(path: &Path) -> bool {
    path.file_name().and_then(|n| n.to_str()).is_some_and(|n| {
        n == "rebar.config"
            || n.ends_with(".erl")
            || n.ends_with(".hrl")
            || n.ends_with(".app.src")
            || n.ends_with(".escript")
    })
}