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
//! Per-file analysis pipeline: scope resolution feeding the backend
//! dispatch (`backends`), cache round-trip, and changeset narrowing
//! (`narrow`).

use crate::abc::Limits;
use crate::cache;
use crate::git_changes;
use crate::paths::{Lang, lang_for, parse_file_lang};
use crate::srcbuf::{SrcBuf, load_src};
mod backends;
mod narrow;
mod non_clike;

#[cfg(test)]
mod tests;
#[cfg(test)]
mod tests_fs;

#[derive(Debug)]
pub(crate) struct Checks {
    pub want_abc: bool,
    pub want_used: bool,
    pub want_never: bool,
}

impl Checks {
    pub fn new(only: Option<&str>) -> Self {
        Self {
            want_abc: only.is_none_or(|o| o == "abc"),
            want_used: only.is_none_or(|o| o == "used-once"),
            want_never: only.is_none_or(|o| o == "never-used"),
        }
    }
}

fn blank_with(path: &std::path::Path) -> crate::output::FileResult {
    crate::output::FileResult {
        path: path.display().to_string(),
        abc: Vec::new(),
        used_once: Vec::new(),
        never_used: Vec::new(),
        module_abc: None,
    }
}

fn reparsed<'a>(src_bytes: &'a [u8], lang: Lang) -> Option<crate::model::FileModel<'a>> {
    let tree = parse_file_lang(src_bytes, lang)?;
    Some(crate::model::build(src_bytes, tree))
}

pub(crate) fn analyze_one(
    path: &std::path::Path,
    only: Option<&str>,
    limits: Limits,
    changeset: Option<&git_changes::Changeset>,
    cache: Option<&cache::Cache>,
) -> crate::output::FileResult {
    let Some((mut r, src_buf)) = loaded_result(path) else {
        return blank_with(path);
    };
    // Cache stores raw per-file results; scope narrowing is per-run and
    // must also apply when the analysis itself comes from the cache.
    let hit = cache.and_then(|c| cache_hit(c, path, &src_buf, only, limits));
    if let Some(mut hit) = hit {
        narrow::apply(changeset, &mut hit, limits.module, &src_buf);
        return hit;
    }

    if !analyze_src_into(&mut r, path, &src_buf, only, limits) {
        return r;
    }
    store_result(cache, path, &src_buf, only, limits, &r);
    narrow::apply(changeset, &mut r, limits.module, &src_buf);
    r
}

/// Analyse in-memory source (MCP inline `source_code`); `path` selects language.
pub(crate) fn analyze_src(
    path: &std::path::Path,
    src: &[u8],
    only: Option<&str>,
    limits: Limits,
) -> crate::output::FileResult {
    let mut r = blank_with(path);
    let _ = analyze_src_into(&mut r, path, src, only, limits);
    r
}

/// Run backends into `r`. False when the tree is unusable (e.g. Ruby reparse).
fn analyze_src_into(
    r: &mut crate::output::FileResult,
    path: &std::path::Path,
    src: &[u8],
    only: Option<&str>,
    limits: Limits,
) -> bool {
    let file_lang = lang_for(path);
    let Some(tree) = parse_file_lang(src, file_lang) else {
        return true;
    };
    let checks = Checks::new(only);
    if file_lang.is_clike() {
        backends::clike_arm(r, file_lang, src, &tree, &checks, limits);
        true
    } else {
        backends::non_clike_arm(r, file_lang, src, tree, &checks, limits)
    }
}

/// Blank result plus source bytes; None when the file cannot be read.
fn loaded_result(path: &std::path::Path) -> Option<(crate::output::FileResult, SrcBuf)> {
    let r = blank_with(path);
    let src_buf = load_src(path).ok()?;
    Some((r, src_buf))
}

/// Cached FileResult rebuilt from the store; None on a miss.
fn cache_hit(
    cache: &cache::Cache,
    path: &std::path::Path,
    src: &[u8],
    only: Option<&str>,
    limits: Limits,
) -> Option<crate::output::FileResult> {
    let (abc, used_once, never_used, module_abc) =
        cache.get(&cache.file_key(path, src, only, limits))?;
    Some(crate::output::FileResult {
        path: path.display().to_string(),
        abc,
        used_once,
        never_used,
        module_abc,
    })
}

fn store_result(
    cache: Option<&cache::Cache>,
    path: &std::path::Path,
    src: &[u8],
    only: Option<&str>,
    limits: Limits,
    r: &crate::output::FileResult,
) {
    if let Some(cache) = cache {
        cache.store(
            &cache.file_key(path, src, only, limits),
            &r.abc,
            &r.used_once,
            &r.never_used,
            r.module_abc.clone(),
        );
    }
}