pretender 0.3.1

Multi-language structural code-quality checks — complexity, duplication, mutation testing, and CI integration
use anyhow::{anyhow, Result};

pub struct MetricDoc {
    pub name: &'static str,
    pub measures: &'static str,
    pub formula: &'static str,
    pub default_threshold: &'static str,
    pub citation: &'static str,
    pub improve: &'static str,
}

const CATALOG: &[MetricDoc] = &[
    MetricDoc {
        name: "cyclomatic",
        measures: "The number of independent execution paths through a function.",
        formula: "1 + decision points (if/elif/else-if/for/while/except/&&/||/ternary)",
        default_threshold: "10 (app), 8 (library exported), 3 (test)",
        citation: "McCabe (1976), \"A Complexity Measure\", IEEE TSE",
        improve: "Extract helper functions; replace nested conditionals with early returns or guard clauses.",
    },
    MetricDoc {
        name: "cognitive",
        measures: "The mental effort required to understand a function, penalising nesting and structural breaks in linear flow.",
        formula: "Sum of branch weights × (1 + nesting depth); logical sequences counted once",
        default_threshold: "15 (app), 5 (test)",
        citation: "Campbell (2018), \"Cognitive Complexity — A new way of measuring understandability\", SonarSource",
        improve: "Flatten nested conditionals; extract loops into named helpers; prefer early returns.",
    },
    MetricDoc {
        name: "abc",
        measures: "Composite size proxy: Assignments, Branches, and Calls weighted by smell.",
        formula: "√(A² + B² + C²) where A = assignments, B = branches, C = weighted call count",
        default_threshold: "30 (app)",
        citation: "Fitzpatrick (1997), \"Applying the ABC Metric to C, C++, and Java\"",
        improve: "Break large functions into smaller ones; reduce assignments and deep call chains.",
    },
    MetricDoc {
        name: "function_lines",
        measures: "Source lines spanned by a function or method, including signature and closing brace.",
        formula: "end_line − start_line + 1",
        default_threshold: "40 (app), 100 (script), 80 (test)",
        citation: "Clean Code (Martin, 2008) §3; SonarQube default 150 lines",
        improve: "Extract cohesive sub-routines; separate setup from logic; remove dead branches.",
    },
    MetricDoc {
        name: "file_lines",
        measures: "Total source lines in a file.",
        formula: "Line count of the file",
        default_threshold: "400 (app), 300 (script)",
        citation: "SonarQube default 750; Pretender uses a stricter 400 for application code.",
        improve: "Split large files by responsibility; move helper types or utility functions to separate modules.",
    },
    MetricDoc {
        name: "nesting_max",
        measures: "The maximum nesting depth of control-flow blocks inside a function.",
        formula: "Deepest block depth (0 = top-level statement inside the function body)",
        default_threshold: "3 (app/library), 2 (test)",
        citation: "Lanza & Marinescu (2006), \"Object-Oriented Metrics in Practice\"",
        improve: "Invert conditions to reduce nesting; extract inner blocks into helper functions.",
    },
    MetricDoc {
        name: "params",
        measures: "Number of declared parameters on a function or method.",
        formula: "Count of formal parameters",
        default_threshold: "4 (app), 3 (library exported), 2 (test)",
        citation: "Clean Code (Martin, 2008) §3 — \"Functions should have few arguments\"",
        improve: "Introduce a parameter object or builder; pass context structs instead of primitives.",
    },
    MetricDoc {
        name: "min_assertions",
        measures: "Minimum number of assertion calls required in a test function.",
        formula: "Count of assertion calls (assert_*, expect, assert!, etc.)",
        default_threshold: "1 (test role only)",
        citation: "xUnit patterns — every test must contain at least one verification.",
        improve: "Add at least one assert/expect call; split structural setup from behavioural assertions.",
    },
    MetricDoc {
        name: "exported_cyclomatic",
        measures: "Cyclomatic complexity of exported (public) functions in library code.",
        formula: "Same as cyclomatic, applied only to exported symbols",
        default_threshold: "8 (library role)",
        citation: "API design heuristic: public surfaces should be simpler than internals.",
        improve: "Delegate complexity to private helpers; keep the public API thin.",
    },
    MetricDoc {
        name: "exported_params",
        measures: "Number of parameters on exported functions in library code.",
        formula: "Count of formal parameters on exported symbols",
        default_threshold: "3 (library role)",
        citation: "API usability principle: callers should rarely need more than 3 arguments.",
        improve: "Use builder patterns or config structs for optional parameters.",
    },
    MetricDoc {
        name: "exported_lines",
        measures: "Line span of exported functions in library code.",
        formula: "Same as function_lines, applied only to exported symbols",
        default_threshold: "30 (library role)",
        citation: "API design: public functions should do one thing; longer ones hide complexity from callers.",
        improve: "Delegate to private helpers; split multi-phase exported functions.",
    },
];

pub fn lookup(name: &str) -> Option<&'static MetricDoc> {
    CATALOG.iter().find(|m| m.name == name)
}

pub fn all_names() -> Vec<&'static str> {
    CATALOG.iter().map(|m| m.name).collect()
}

pub fn print_doc(doc: &MetricDoc) -> Result<()> {
    let mut out = std::io::stdout().lock();
    use std::io::Write;
    writeln!(out, "{}", doc.name)?;
    writeln!(out, "  Measures:   {}", doc.measures)?;
    writeln!(out, "  Formula:    {}", doc.formula)?;
    writeln!(out, "  Threshold:  {}", doc.default_threshold)?;
    writeln!(out, "  Citation:   {}", doc.citation)?;
    writeln!(out, "  Improve:    {}", doc.improve)?;
    Ok(())
}

pub fn run(metric: &str) -> Result<()> {
    match lookup(metric) {
        Some(doc) => print_doc(doc),
        None => {
            let names = all_names().join(", ");
            Err(anyhow!("unknown metric: {metric}\nAvailable: {names}"))
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn cyclomatic_has_mccabe_citation() {
        let doc = lookup("cyclomatic").expect("cyclomatic must be in catalog");
        assert!(doc.citation.contains("McCabe"));
        assert!(doc.default_threshold.contains("10"));
    }

    #[test]
    fn cognitive_has_campbell_citation() {
        let doc = lookup("cognitive").expect("cognitive must be in catalog");
        assert!(doc.citation.contains("Campbell"));
        assert!(doc.default_threshold.contains("15"));
    }

    #[test]
    fn abc_has_fitzpatrick_citation() {
        let doc = lookup("abc").expect("abc must be in catalog");
        assert!(doc.citation.contains("Fitzpatrick"));
        assert!(doc.formula.contains("√"));
    }

    #[test]
    fn all_catalog_entries_have_non_empty_fields() {
        for doc in CATALOG {
            assert!(!doc.name.is_empty(), "name empty");
            assert!(!doc.measures.is_empty(), "{}: measures empty", doc.name);
            assert!(!doc.formula.is_empty(), "{}: formula empty", doc.name);
            assert!(
                !doc.default_threshold.is_empty(),
                "{}: threshold empty",
                doc.name
            );
            assert!(!doc.citation.is_empty(), "{}: citation empty", doc.name);
            assert!(!doc.improve.is_empty(), "{}: improve empty", doc.name);
        }
    }

    #[test]
    fn unknown_metric_returns_error() {
        assert!(run("not_a_metric").is_err());
    }

    #[test]
    fn error_message_lists_available_metrics() {
        let err = run("bogus").unwrap_err().to_string();
        assert!(err.contains("cyclomatic"));
        assert!(err.contains("cognitive"));
    }

    #[test]
    fn all_names_returns_every_catalog_entry() {
        let names = all_names();
        assert!(names.contains(&"cyclomatic"));
        assert!(names.contains(&"abc"));
        assert!(names.contains(&"nesting_max"));
        assert_eq!(names.len(), CATALOG.len());
    }
}