trusty-analyze 0.1.6

Sidecar code-analysis daemon for trusty-search: complexity, smells, quality, facts
Documentation
//! Complexity metrics passed between trusty-search and trusty-analyzer.
//!
//! Wire-compatible with `trusty_search_core::complexity::ComplexityMetrics`.
//! The variant names on `ComplexityGrade` and `CodeSmell` match exactly so
//! serde round-trips JSON produced by the search daemon without translation.

use serde::{Deserialize, Serialize};
use std::fmt;

/// Bundle of per-chunk complexity numbers and detected smells.
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
pub struct ComplexityMetrics {
    #[serde(default)]
    pub cyclomatic: u32,
    #[serde(default)]
    pub cognitive: u32,
    #[serde(default)]
    pub grade: ComplexityGrade,
    #[serde(default)]
    pub smells: Vec<CodeSmell>,
}

/// Letter grade derived from cyclomatic complexity. A is best, F is worst.
///
/// Note: `E` is intentionally absent. The grading bands match trusty-search's
/// thresholds exactly (A/B/C/D/F), and the variant set is part of the wire
/// format — adding `E` would break JSON compatibility with existing payloads
/// produced by the search daemon.
///
/// Variants are declared in natural order (A < B < C < D < F), so the derived
/// `PartialOrd`/`Ord` impls let callers compare grades directly (e.g. to flag
/// any chunk worse than `B`).
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
pub enum ComplexityGrade {
    #[default]
    A,
    B,
    C,
    D,
    F,
}

impl fmt::Display for ComplexityGrade {
    /// Why: Lets callers render the grade as a single letter for HTTP/MCP
    /// responses and CLI output without each call site re-implementing the
    /// match.
    /// What: Writes one of `"A"`, `"B"`, `"C"`, `"D"`, `"F"`.
    /// Test: `assert_eq!(ComplexityGrade::B.to_string(), "B")`.
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let s = match self {
            ComplexityGrade::A => "A",
            ComplexityGrade::B => "B",
            ComplexityGrade::C => "C",
            ComplexityGrade::D => "D",
            ComplexityGrade::F => "F",
        };
        f.write_str(s)
    }
}

impl ComplexityGrade {
    /// Map a cyclomatic complexity number to a letter grade. Bands match
    /// trusty-search exactly so the same chunk receives the same grade in
    /// both projects.
    pub fn from_cyclomatic(v: u32) -> Self {
        match v {
            0..=5 => Self::A,
            6..=10 => Self::B,
            11..=15 => Self::C,
            16..=20 => Self::D,
            _ => Self::F,
        }
    }
}

/// A single detected code smell within a chunk.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum CodeSmell {
    LongFunction { lines: usize },
    DeepNesting { max_depth: u8 },
    TooManyParams { count: usize },
    MissingDocstring,
}

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

    #[test]
    fn grade_from_cyclomatic_thresholds() {
        assert_eq!(ComplexityGrade::from_cyclomatic(0), ComplexityGrade::A);
        assert_eq!(ComplexityGrade::from_cyclomatic(5), ComplexityGrade::A);
        assert_eq!(ComplexityGrade::from_cyclomatic(6), ComplexityGrade::B);
        assert_eq!(ComplexityGrade::from_cyclomatic(11), ComplexityGrade::C);
        assert_eq!(ComplexityGrade::from_cyclomatic(16), ComplexityGrade::D);
        assert_eq!(ComplexityGrade::from_cyclomatic(50), ComplexityGrade::F);
    }

    #[test]
    fn grade_display_is_single_letter() {
        assert_eq!(ComplexityGrade::A.to_string(), "A");
        assert_eq!(ComplexityGrade::B.to_string(), "B");
        assert_eq!(ComplexityGrade::C.to_string(), "C");
        assert_eq!(ComplexityGrade::D.to_string(), "D");
        assert_eq!(ComplexityGrade::F.to_string(), "F");
    }

    #[test]
    fn grade_orders_a_through_f() {
        assert!(ComplexityGrade::A < ComplexityGrade::B);
        assert!(ComplexityGrade::B < ComplexityGrade::C);
        assert!(ComplexityGrade::C < ComplexityGrade::D);
        assert!(ComplexityGrade::D < ComplexityGrade::F);
    }

    #[test]
    fn metrics_round_trip_json() {
        let m = ComplexityMetrics {
            cyclomatic: 7,
            cognitive: 12,
            grade: ComplexityGrade::B,
            smells: vec![CodeSmell::LongFunction { lines: 80 }],
        };
        let s = serde_json::to_string(&m).unwrap();
        let back: ComplexityMetrics = serde_json::from_str(&s).unwrap();
        assert_eq!(m, back);
    }

    #[test]
    fn metrics_default_when_field_missing() {
        // Forward-compat: trusty-search may emit chunks without a
        // `complexity` field. Default deserialization must not fail.
        let s = r#"{}"#;
        let m: ComplexityMetrics = serde_json::from_str(s).unwrap();
        assert_eq!(m, ComplexityMetrics::default());
    }
}