Skip to main content

debtmap/complexity/
nested_callables.rs

1//! Nested callable complexity summaries and scoring rollups.
2//!
3//! Parent function metrics exclude nested function/lambda bodies to avoid double
4//! counting. Summaries are stored as `detected_patterns` and rolled into effective
5//! complexity for hotspot detection.
6
7use serde::{Deserialize, Serialize};
8
9/// Aggregated complexity of nested functions/callbacks inside a parent body.
10#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
11pub struct NestedCallableSummary {
12    pub count: u32,
13    pub cyclomatic: u32,
14    pub cognitive: u32,
15    pub max_nesting: u32,
16}
17
18impl NestedCallableSummary {
19    pub fn is_empty(&self) -> bool {
20        self.count == 0
21    }
22}
23
24/// Encode a nested summary as `detected_patterns` entries.
25pub fn nested_callable_patterns(summary: &NestedCallableSummary) -> Vec<String> {
26    if summary.is_empty() {
27        return Vec::new();
28    }
29
30    vec![
31        format!("nested-functions:count={}", summary.count),
32        format!("nested-functions:cyclomatic={}", summary.cyclomatic),
33        format!("nested-functions:cognitive={}", summary.cognitive),
34        format!("nested-functions:max-nesting={}", summary.max_nesting),
35    ]
36}
37
38/// Parse nested summary from `detected_patterns` (returns default if absent).
39pub fn parse_nested_callable_patterns(patterns: Option<&[String]>) -> NestedCallableSummary {
40    let Some(patterns) = patterns else {
41        return NestedCallableSummary::default();
42    };
43
44    let mut summary = NestedCallableSummary::default();
45    for pattern in patterns {
46        if let Some(value) = pattern.strip_prefix("nested-functions:count=") {
47            summary.count = value.parse().unwrap_or(0);
48        } else if let Some(value) = pattern.strip_prefix("nested-functions:cyclomatic=") {
49            summary.cyclomatic = value.parse().unwrap_or(0);
50        } else if let Some(value) = pattern.strip_prefix("nested-functions:cognitive=") {
51            summary.cognitive = value.parse().unwrap_or(0);
52        } else if let Some(value) = pattern.strip_prefix("nested-functions:max-nesting=") {
53            summary.max_nesting = value.parse().unwrap_or(0);
54        }
55    }
56    summary
57}
58
59/// Parent metrics plus nested bodies for threshold and hotspot decisions.
60pub fn scoring_complexity(
61    cyclomatic: u32,
62    cognitive: u32,
63    patterns: Option<&[String]>,
64) -> (u32, u32) {
65    let nested = parse_nested_callable_patterns(patterns);
66    (
67        cyclomatic.saturating_add(nested.cyclomatic),
68        cognitive.saturating_add(nested.cognitive),
69    )
70}
71
72#[cfg(test)]
73mod tests {
74    use super::*;
75
76    #[test]
77    fn round_trips_nested_patterns() {
78        let summary = NestedCallableSummary {
79            count: 2,
80            cyclomatic: 4,
81            cognitive: 2,
82            max_nesting: 1,
83        };
84        let patterns = nested_callable_patterns(&summary);
85        let parsed = parse_nested_callable_patterns(Some(&patterns));
86        assert_eq!(parsed, summary);
87    }
88
89    #[test]
90    fn scoring_complexity_includes_nested_totals() {
91        let patterns = nested_callable_patterns(&NestedCallableSummary {
92            count: 1,
93            cyclomatic: 3,
94            cognitive: 5,
95            max_nesting: 2,
96        });
97        let (cyc, cog) = scoring_complexity(2, 1, Some(&patterns));
98        assert_eq!(cyc, 5);
99        assert_eq!(cog, 6);
100    }
101}