kimun 0.20.0

Code metrics tool — health score, complexity, duplication, hotspots, ownership
//! Maintainability Index computation (Visual Studio variant).
//!
//! Computes MI per file using the Visual Studio formula: no comment weight,
//! normalized to 0–100 scale, clamped at 0. Invoked via `km mi`.
//!
//! This module directly calls `hal::analyze_file` and `cycom::analyze_file`
//! (pub(crate) functions). This creates tight coupling but avoids duplicating
//! file I/O and parsing logic. Changes to hal/cycom `analyze_file` signatures
//! must be coordinated with this module.
//!
//! Each file is read once via `read_and_classify`, then the resulting lines
//! and line kinds are passed to `hal::analyze_content` and
//! `cycom::analyze_content` for Halstead and cyclomatic analysis respectively.

pub(crate) mod analyzer;
pub(crate) mod report;

use std::error::Error;
use std::path::Path;

use crate::loc::counter::LineKind;
use crate::loc::language::LanguageSpec;
use crate::report_helpers;
use crate::util::read_and_classify;
use crate::walk::WalkConfig;
use analyzer::compute_mi;
use report::{FileMIMetrics, print_json, print_report};

fn analyze_file(path: &Path, spec: &LanguageSpec) -> Result<Option<FileMIMetrics>, Box<dyn Error>> {
    let (lines, kinds) = match read_and_classify(path, spec)? {
        Some(v) => v,
        None => return Ok(None),
    };
    let code_lines = kinds.iter().filter(|k| **k == LineKind::Code).count();

    let volume = match crate::hal::analyze_content(&lines, &kinds, spec) {
        Some(h) => h.volume,
        None => return Ok(None),
    };

    let complexity = match crate::cycom::analyze_content(&lines, &kinds, spec) {
        Some(c) => c.total_complexity,
        None => return Ok(None),
    };

    // compute_mi returns None only if code_lines==0, volume<=0, or complexity==0.
    // These should not occur when hal/cycom returned valid results, but guard anyway.
    let metrics = match compute_mi(volume, complexity, code_lines) {
        Some(m) => m,
        None => return Ok(None),
    };

    Ok(Some(FileMIMetrics {
        path: path.to_path_buf(),
        language: spec.name.to_string(),
        metrics,
    }))
}

pub fn run(
    cfg: &WalkConfig<'_>,
    json: bool,
    top: usize,
    sort_by: &str,
) -> Result<(), Box<dyn Error>> {
    let mut results = cfg.collect_analysis(analyze_file);

    // Sort: mi ascending (worst first), volume/complexity/loc descending
    match sort_by {
        "volume" => results.sort_by(|a, b| {
            b.metrics
                .halstead_volume
                .total_cmp(&a.metrics.halstead_volume)
        }),
        "complexity" => {
            results.sort_by(|a, b| {
                b.metrics
                    .cyclomatic_complexity
                    .cmp(&a.metrics.cyclomatic_complexity)
            });
        }
        "loc" => results.sort_by(|a, b| b.metrics.loc.cmp(&a.metrics.loc)),
        _ => results.sort_by(|a, b| a.metrics.mi_score.total_cmp(&b.metrics.mi_score)),
    }

    report_helpers::output_results(&mut results, top, json, print_json, print_report)
}

#[cfg(test)]
#[path = "mod_test.rs"]
mod tests;