descendit 0.0.2

Deterministic structural metrics and loss scoring for Rust code
Documentation
#![allow(
    clippy::expect_used,
    clippy::unwrap_used,
    clippy::too_many_lines,
    dead_code
)]

use std::fs;
use std::path::Path;

use descendit::{
    AnalysisReport, CompliancePolicy, ComplianceReport, analyze_path, compute_compliance,
};

pub struct BenchmarkCase {
    pub name: &'static str,
    pub before: &'static str,
    pub after: &'static str,
}

pub fn all_benchmark_cases() -> &'static [BenchmarkCase] {
    &[
        BenchmarkCase {
            name: "split_god_function",
            before: r#"
pub fn process_items(items: &[i32]) -> i32 {
    let mut total = 0;
    for item in items {
        if *item > 0 {
            total += item * 2;
        } else {
            total += item.abs();
        }
    }
    total
}
"#,
            after: r#"
pub fn process_items(items: &[i32]) -> i32 {
    let cleaned = normalize(items);
    accumulate(&cleaned)
}

fn normalize(items: &[i32]) -> Vec<i32> {
    let mut out = Vec::new();
    for item in items {
        if *item > 0 {
            out.push(item * 2);
        } else {
            out.push(item.abs());
        }
    }
    out
}

fn accumulate(items: &[i32]) -> i32 {
    let mut total = 0;
    for item in items {
        total += item;
    }
    total
}
"#,
        },
        BenchmarkCase {
            name: "merge_helpers_into_one_function",
            before: r#"
pub fn process_items(items: &[i32]) -> i32 {
    let cleaned = normalize(items);
    accumulate(&cleaned)
}

fn normalize(items: &[i32]) -> Vec<i32> {
    let mut out = Vec::new();
    for item in items {
        if *item > 0 {
            out.push(item * 2);
        } else {
            out.push(item.abs());
        }
    }
    out
}

fn accumulate(items: &[i32]) -> i32 {
    let mut total = 0;
    for item in items {
        total += item;
    }
    total
}
"#,
            after: r#"
pub fn process_items(items: &[i32]) -> i32 {
    let mut total = 0;
    for item in items {
        if *item > 0 {
            total += item * 2;
        } else {
            total += item.abs();
        }
    }
    total
}
"#,
        },
        BenchmarkCase {
            name: "add_duplication",
            before: r#"
fn compute(x: i32) -> i32 {
    let a = x + 1;
    let b = a * 2;
    let c = b - 3;
    if c > 10 {
        return c * 4;
    }
    let d = c + 5;
    d
}
"#,
            after: r#"
fn compute(x: i32) -> i32 {
    let a = x + 1;
    let b = a * 2;
    let c = b - 3;
    if c > 10 {
        return c * 4;
    }
    let d = c + 5;
    d
}

fn compute_copy(y: i32) -> i32 {
    let a = y + 1;
    let b = a * 2;
    let c = b - 3;
    if c > 10 {
        return c * 4;
    }
    let d = c + 5;
    d
}
"#,
        },
        BenchmarkCase {
            name: "replace_bool_soup_with_enum",
            before: r#"
struct Config {
    enabled: bool,
    verbose: bool,
    debug: bool,
    name: String,
}

fn use_config(c: &Config) -> i32 {
    if c.enabled { 1 } else { 0 }
}
"#,
            after: r#"
enum Mode {
    Disabled,
    Normal,
    Verbose,
    Debug,
}

struct Config {
    mode: Mode,
    name: String,
}

fn use_config(c: &Config) -> i32 {
    match c.mode {
        Mode::Disabled => 0,
        Mode::Normal => 1,
        Mode::Verbose => 2,
        Mode::Debug => 3,
    }
}
"#,
        },
        BenchmarkCase {
            name: "inflate_public_api",
            before: r#"
pub fn api_entry(x: i32) -> i32 {
    helper(x)
}

fn helper(x: i32) -> i32 {
    if x > 0 { x * 2 } else { x }
}
"#,
            after: r#"
pub fn api_entry(x: i32) -> i32 {
    helper(x)
}

pub fn helper(x: i32) -> i32 {
    if x > 0 { x * 2 } else { x }
}
"#,
        },
        BenchmarkCase {
            name: "hide_overhead_in_macro",
            before: r#"
pub fn api_entry(x: i32) -> i32 {
    let y = helper_a(x);
    helper_b(y)
}

fn helper_a(x: i32) -> i32 {
    if x > 0 { x * 2 } else { x }
}

fn helper_b(x: i32) -> i32 {
    match x {
        0 => 1,
        _ => x + 1,
    }
}
"#,
            after: r#"
pub fn api_entry(x: i32) -> i32 {
    let y = helper_a!(x);
    helper_b!(y)
}

macro_rules! helper_a {
    ($x:expr) => {
        if $x > 0 { $x * 2 } else { $x }
    };
}

macro_rules! helper_b {
    ($x:expr) => {
        match $x {
            0 => 1,
            _ => $x + 1,
        }
    };
}
"#,
        },
    ]
}

pub fn benchmark_case(name: &str) -> &'static BenchmarkCase {
    all_benchmark_cases()
        .iter()
        .find(|case| case.name == name)
        .unwrap_or_else(|| panic!("unknown benchmark case: {name}"))
}

fn write_source(source: &str, dir: &Path) {
    fs::write(dir.join("lib.rs"), source).expect("write benchmark case source");
}

pub fn analyze_case(
    case: &BenchmarkCase,
) -> (
    AnalysisReport,
    ComplianceReport,
    AnalysisReport,
    ComplianceReport,
) {
    let dir = tempfile::tempdir().expect("create benchmark tempdir");

    write_source(case.before, dir.path());
    let before_analysis = analyze_path(dir.path()).expect("analyze benchmark before");
    let before_compliance = compute_compliance(&before_analysis, &CompliancePolicy::default());

    write_source(case.after, dir.path());
    let after_analysis = analyze_path(dir.path()).expect("analyze benchmark after");
    let after_compliance = compute_compliance(&after_analysis, &CompliancePolicy::default());

    (
        before_analysis,
        before_compliance,
        after_analysis,
        after_compliance,
    )
}