arborist-cli 0.2.0

CLI for arborist-metrics: cognitive/cyclomatic complexity and SLOC metrics
Documentation
use std::path::{Path, PathBuf};

use ignore::WalkBuilder;

use crate::error::ArboristError;

/// Known file extensions mapped to language names for filtering.
fn extension_to_language(ext: &str) -> Option<&'static str> {
    match ext {
        "rs" => Some("rust"),
        "py" => Some("python"),
        "js" => Some("javascript"),
        "ts" => Some("typescript"),
        "tsx" => Some("tsx"),
        "jsx" => Some("jsx"),
        "java" => Some("java"),
        "go" => Some("go"),
        "c" | "h" => Some("c"),
        "cpp" | "cc" | "cxx" | "hpp" => Some("cpp"),
        "cs" => Some("c_sharp"),
        "rb" => Some("ruby"),
        "swift" => Some("swift"),
        "kt" | "kts" => Some("kotlin"),
        "php" => Some("php"),
        _ => None,
    }
}

pub fn collect_files(
    paths: &[PathBuf],
    languages_filter: Option<&[String]>,
    gitignore: bool,
) -> Result<Vec<PathBuf>, ArboristError> {
    let mut files = Vec::new();

    for path in paths {
        if path.is_file() {
            files.push(path.clone());
        } else if path.is_dir() {
            collect_directory(path, languages_filter, gitignore, &mut files)?;
        } else {
            return Err(ArboristError::Io(std::io::Error::new(
                std::io::ErrorKind::NotFound,
                format!("file not found: {}", path.display()),
            )));
        }
    }

    Ok(files)
}

fn collect_directory(
    dir: &Path,
    languages_filter: Option<&[String]>,
    gitignore: bool,
    files: &mut Vec<PathBuf>,
) -> Result<(), ArboristError> {
    let walker = WalkBuilder::new(dir)
        .git_ignore(gitignore)
        .git_global(false)
        .git_exclude(false)
        .hidden(false)
        .build();

    let languages_lower: Option<Vec<String>> =
        languages_filter.map(|langs| langs.iter().map(|l| l.to_lowercase()).collect());

    for entry in walker {
        let entry = entry.map_err(|e| ArboristError::Io(std::io::Error::other(e.to_string())))?;

        let path = entry.path();
        if !path.is_file() {
            continue;
        }

        let ext = match path.extension().and_then(|e| e.to_str()) {
            Some(ext) => ext,
            None => continue,
        };

        let lang = match extension_to_language(ext) {
            Some(lang) => lang,
            None => continue,
        };

        if let Some(ref filter) = languages_lower
            && !filter.iter().any(|f| f == lang)
        {
            continue;
        }

        files.push(path.to_path_buf());
    }

    files.sort();
    Ok(())
}