arborist-cli 0.1.1

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

use arborist::{AnalysisConfig, FileReport, Language};

use crate::cli::AnalyzeArgs;
use crate::error::ArboristError;
use crate::traversal;

pub fn build_config(args: &AnalyzeArgs) -> AnalysisConfig {
    AnalysisConfig {
        cognitive_threshold: args.threshold,
        include_methods: !args.no_methods,
    }
}

pub fn analyze_file(path: &Path, config: &AnalysisConfig) -> Result<FileReport, ArboristError> {
    arborist::analyze_file_with_config(path, config)
        .map_err(|e| ArboristError::Analysis(e.to_string()))
}

pub fn analyze_stdin(language: &str, config: &AnalysisConfig) -> Result<FileReport, ArboristError> {
    let mut source = String::new();
    std::io::stdin()
        .read_to_string(&mut source)
        .map_err(ArboristError::Io)?;

    let lang: Language = language
        .parse()
        .map_err(|e: String| ArboristError::Analysis(e))?;

    arborist::analyze_source_with_config(&source, lang, config)
        .map_err(|e| ArboristError::Analysis(e.to_string()))
}

pub fn analyze_paths(
    paths: &[PathBuf],
    config: &AnalysisConfig,
    args: &AnalyzeArgs,
) -> Result<(Vec<FileReport>, Vec<String>), ArboristError> {
    let files = traversal::collect_files(paths, args.languages.as_deref(), args.gitignore)?;

    let mut reports = Vec::new();
    let mut errors = Vec::new();

    for file in &files {
        match analyze_file(file, config) {
            Ok(report) => reports.push(report),
            Err(e) => errors.push(format!("{}: {e}", file.display())),
        }
    }

    Ok((reports, errors))
}

pub fn apply_filters(reports: &[FileReport], args: &AnalyzeArgs) -> (Vec<FileReport>, bool) {
    let mut threshold_exceeded = false;
    let mut filtered: Vec<FileReport> = Vec::new();

    for report in reports {
        let mut report = report.clone();

        if let Some(threshold) = args.threshold {
            let has_exceeding = report.functions.iter().any(|f| f.cognitive > threshold);

            if has_exceeding {
                threshold_exceeded = true;
            }

            if args.exceeds_only {
                report.functions.retain(|f| f.cognitive > threshold);
                if report.functions.is_empty() {
                    continue;
                }
            }
        }

        filtered.push(report);
    }

    (filtered, threshold_exceeded)
}

#[derive(Debug, Clone)]
pub struct FlatFunction {
    pub name: String,
    pub file_path: String,
    pub language: String,
    pub line_start: usize,
    pub line_end: usize,
    pub cognitive: u64,
    pub cyclomatic: u64,
    pub sloc: u64,
}

pub fn flatten_reports(reports: &[FileReport]) -> Vec<FlatFunction> {
    let mut flat = Vec::new();
    for report in reports {
        for func in &report.functions {
            flat.push(FlatFunction {
                name: func.name.clone(),
                file_path: report.path.clone(),
                language: report.language.to_string(),
                line_start: func.start_line,
                line_end: func.end_line,
                cognitive: func.cognitive,
                cyclomatic: func.cyclomatic,
                sloc: func.sloc,
            });
        }
    }
    flat
}

pub fn sort_and_top(
    flat: &mut Vec<FlatFunction>,
    sort: &crate::cli::SortMetric,
    top: Option<usize>,
) {
    use crate::cli::SortMetric;

    match sort {
        SortMetric::Cognitive => flat.sort_by(|a, b| b.cognitive.cmp(&a.cognitive)),
        SortMetric::Cyclomatic => flat.sort_by(|a, b| b.cyclomatic.cmp(&a.cyclomatic)),
        SortMetric::Sloc => flat.sort_by(|a, b| b.sloc.cmp(&a.sloc)),
        SortMetric::Name => flat.sort_by(|a, b| a.name.cmp(&b.name)),
    }

    if let Some(n) = top {
        flat.truncate(n);
    }
}