abcop 0.16.5

Must-have ABC complexity gate for AI development. Ruby, Rust, Python, Go, JS/TS, C/C++, PHP, Java, C#, Swift, Zig, Dart, Solidity, ObjC
//! Classification and ModuleAbcSize scoring (including scoped rescope).

use super::classify::{is_fixture_tree, is_route_table, is_third_party};
use super::{ModuleAbc, from_scores, rescope};
use crate::abc::AbcOffense;

#[test]
fn rails_route_tables_are_route_files() {
    for p in [
        "config/routes.rb",
        "config/routes/api.rb",
        "engines/billing/config/routes.rb",
        "engines/billing/config/routes/admin.rb",
    ] {
        assert!(is_route_table(std::path::Path::new(p)), "{p}");
    }
}

#[test]
fn ordinary_sources_are_not_route_files() {
    for p in [
        "app/models/route.rb",
        "config/application.rb",
        "config/routes_helper_spec.rb.rb",
        "routes.md",
        "app/routes_loader.rb",
        "main.rb",
    ] {
        assert!(!is_route_table(std::path::Path::new(p)), "{p}");
    }
}

#[test]
fn third_party_trees_are_dropped_from_scope() {
    for p in [
        "vendor/tree-sitter-swift/src/scanner.c",
        "app/assets/node_modules/left-pad/index.js",
        "target/debug/foo.rs",
        "db/migrate/20260101120000_add_users.rb",
        "app/assets/builds/app.min.js",
        "lib/user_pb.rb",
        "/repo/vendor/x.go",
    ] {
        assert!(is_third_party(std::path::Path::new(p)), "{p}");
    }
}

#[test]
fn owned_sources_stay_in_scope() {
    for p in [
        "src/main.rs",
        "spec/models/user_spec.rb",
        "test/lib/format_test.rb",
        "app/models/user.rb",
        "vendor_all/owned.rb",
        "lib/pb.rb",
    ] {
        assert!(!is_third_party(std::path::Path::new(p)), "{p}");
        assert!(!is_fixture_tree(std::path::Path::new(p)), "{p}");
    }
}

#[test]
fn fixture_trees_are_dropped_from_scope() {
    for p in [
        "tests/fixtures/cops/style/global_vars/no_offense.rb",
        "fixtures/probe.rb",
        "spec/fixtures/users.yml.rb",
        "/repo/tests/fixtures/x.go",
    ] {
        assert!(is_fixture_tree(std::path::Path::new(p)), "{p}");
    }
}

fn offense(a: u32, b: u32, c: u32) -> AbcOffense {
    offense_at(1, 1, a, b, c)
}

fn offense_at(line: usize, end_line: usize, a: u32, b: u32, c: u32) -> AbcOffense {
    let raw = ((a * a + b * b + c * c) as f64).sqrt();
    AbcOffense {
        line,
        end_line,
        column: 0,
        name: "m".into(),
        score: (raw * 100.0).round() / 100.0,
        vector: format!("<{a}, {b}, {c}>"),
    }
}

#[test]
fn module_abc_sums_method_vectors() {
    let scores = vec![offense(30, 40, 0), offense(0, 0, 120)];
    let hit = from_scores(&scores, "app/models/user.rb", "", super::MAX_ABC).unwrap();
    assert_eq!(
        hit,
        ModuleAbc {
            score: 130.0,
            vector: "<30, 40, 120>".into(),
            methods: scores,
        }
    );
}

#[test]
fn module_abc_ignores_scores_at_the_threshold() {
    // magnitude 120 exactly must not fire (AbcSize-style `>`).
    let scores = vec![offense(72, 96, 0)]; // 120
    assert!(from_scores(&scores, "app/models/user.rb", "", super::MAX_ABC).is_none());
}

#[test]
fn module_abc_respects_custom_max() {
    let scores = vec![offense(30, 40, 0)]; // 50
    assert!(from_scores(&scores, "app/models/user.rb", "", 50.0).is_none());
    assert!(from_scores(&scores, "app/models/user.rb", "", 49.0).is_some());
}

#[test]
fn rust_cfg_test_tail_is_excluded_from_module_abc() {
    let src = "fn prod() {}\n#[cfg(test)]\nmod tests {\n  fn t() {}\n}\n";
    let scores = vec![
        AbcOffense {
            line: 1,
            end_line: 1,
            column: 0,
            name: "prod".into(),
            score: 0.0,
            vector: "<0, 0, 0>".into(),
        },
        AbcOffense {
            line: 4,
            end_line: 4,
            column: 2,
            name: "t".into(),
            score: 100.0,
            vector: "<60, 80, 0>".into(),
        },
    ];
    assert!(from_scores(&scores, "src/lib.rs", src, super::MAX_ABC).is_none());
}

#[test]
fn rescope_keeps_only_intersecting_methods() {
    let mut module_abc = from_scores(
        &[
            offense_at(1, 10, 40, 0, 0),
            offense_at(20, 30, 0, 80, 0),
            offense_at(40, 50, 0, 0, 90),
        ],
        "app/models/user.rb",
        "",
        50.0,
    );
    assert!(module_abc.is_some());
    rescope(&mut module_abc, 50.0, |o| o.line >= 20 && o.end_line <= 30);
    let hit = module_abc.expect("touched method still over max");
    assert_eq!(hit.vector, "<0, 80, 0>");
    assert_eq!(hit.methods.len(), 1);
}

#[test]
fn rescope_clears_when_touched_methods_are_under_max() {
    let mut module_abc = from_scores(
        &[
            offense_at(1, 10, 40, 0, 0),
            offense_at(20, 30, 0, 80, 0),
            offense_at(40, 50, 0, 0, 90),
        ],
        "app/models/user.rb",
        "",
        50.0,
    );
    rescope(&mut module_abc, 50.0, |o| o.line <= 10);
    assert_eq!(module_abc, None);
}