abcop 0.18.2

Must-have ABC complexity gate for AI development. Ruby, Rust, Python, Go, JS/TS, C/C++, PHP, Java, C#, Swift, Zig, Dart, Solidity, Haskell, ObjC
//! Inspection logic shared by MCP tools.

use std::path::{Path, PathBuf};

use serde_json::{json, Value};

use crate::abc::Limits;
use crate::modulesize;
use crate::output::FileResult;
use crate::pipeline;
use crate::walker;

use super::offense;

pub(crate) struct State {
    pub(crate) limits: Limits,
}

impl Default for State {
    fn default() -> Self {
        Self {
            limits: Limits {
                method: 17.0,
                module: modulesize::MAX_ABC,
            },
        }
    }
}

pub(crate) fn inspect(
    state: &State,
    path: Option<String>,
    source: Option<String>,
) -> Result<String, String> {
    match source {
        Some(code) => Ok(inspect_inline(state, path, &code)),
        None => inspect_paths(state, path),
    }
}

fn inspect_inline(state: &State, path: Option<String>, code: &str) -> String {
    offense::offenses_json(&pipeline::analyze_src(
        Path::new(path.as_deref().unwrap_or("example.rb")),
        code.as_bytes(),
        None,
        state.limits,
    ))
}

fn inspect_paths(state: &State, path: Option<String>) -> Result<String, String> {
    let files = target_files(path)?;
    Ok(pack_offenses(
        &files,
        &files
            .iter()
            .map(|file| {
                (
                    file.display().to_string(),
                    pipeline::analyze_one(file, None, state.limits, None, None),
                )
            })
            .collect::<Vec<_>>(),
    ))
}

fn pack_offenses(targets: &[PathBuf], all: &[(String, FileResult)]) -> String {
    let offense_count: usize = all.iter().map(|(_, r)| finding_count(r)).sum();
    let files: Vec<Value> = all
        .iter()
        .filter(|(_, r)| !r.is_clean())
        .map(|(path, r)| {
            json!({
                "path": path,
                "offenses": offense::to_lsp_offenses(r)
            })
        })
        .collect();
    json!({
        "files": files,
        "summary": {
            "target_file_count": targets.len(),
            "offense_count": offense_count
        }
    })
    .to_string()
}

fn finding_count(r: &FileResult) -> usize {
    r.abc.len()
        + r.used_once.len()
        + r.never_used.len()
        + usize::from(r.module_abc.is_some())
}

fn target_files(path: Option<String>) -> Result<Vec<PathBuf>, String> {
    let root = path.unwrap_or_else(|| ".".into());
    if !Path::new(&root).exists() {
        return Err(format!("No such file or directory: {root}"));
    }
    Ok(walker::collect_files(&[root], false))
}