Skip to main content

geographdb_core/algorithms/
complexity.rs

1//! Cyclomatic Complexity Calculation from Branch Counts
2//!
3//! This module calculates cyclomatic complexity using the Z-coordinate
4//! (branch_count) from the 3D spatial representation of CFG blocks.
5//!
6//! # Formula
7//!
8//! Cyclomatic Complexity M = E - N + 2P where:
9//! - E = number of edges
10//! - N = number of nodes
11//! - P = number of connected components (usually 1 for a function)
12//!
13//! Using spatial coordinates:
14//! M = 1 + sum(branch_count) for all blocks
15//!
16//! # Complexity
17//!
18//! - Time: O(n) single pass through blocks
19//! - Space: O(1) constant space
20//!
21//! # Example
22//!
23//! ```rust
24//! use geographdb_core::algorithms::complexity::{calculate_cyclomatic_complexity, ComplexityBlock};
25//!
26//! let blocks = vec![
27//!     ComplexityBlock { id: 0, z: 0.0 },  // Entry (no branches)
28//!     ComplexityBlock { id: 1, z: 1.0 },  // If statement (1 branch)
29//!     ComplexityBlock { id: 2, z: 0.0 },  // Sequential
30//!     ComplexityBlock { id: 3, z: 1.0 },  // Another if (1 branch)
31//! ];
32//!
33//! let result = calculate_cyclomatic_complexity(&blocks);
34//! assert_eq!(result.cyclomatic_complexity, 3);  // 1 + 1 + 1 = 3
35//! ```
36
37/// A block for complexity calculation
38#[derive(Debug, Clone)]
39pub struct ComplexityBlock {
40    pub id: u64,
41    pub z: f32, // branch_count
42}
43
44/// Result of complexity analysis
45#[derive(Debug, Clone)]
46pub struct ComplexityResult {
47    /// Cyclomatic complexity (McCabe number)
48    pub cyclomatic_complexity: u32,
49    /// Number of blocks analyzed
50    pub block_count: usize,
51    /// Total branch count
52    pub total_branches: u32,
53    /// Average branches per block
54    pub avg_branches_per_block: f32,
55    /// Complexity rating (Low/Medium/High/VeryHigh)
56    pub rating: ComplexityRating,
57}
58
59/// Complexity rating based on McCabe number
60#[derive(Debug, Clone, Copy, PartialEq, Eq)]
61pub enum ComplexityRating {
62    /// 1-10: Simple, well-structured code
63    Low,
64    /// 11-20: Moderate complexity
65    Medium,
66    /// 21-50: Complex, difficult to test
67    High,
68    /// 51+: Very complex, should be refactored
69    VeryHigh,
70}
71
72impl ComplexityRating {
73    pub fn as_str(&self) -> &'static str {
74        match self {
75            ComplexityRating::Low => "Low (1-10)",
76            ComplexityRating::Medium => "Medium (11-20)",
77            ComplexityRating::High => "High (21-50)",
78            ComplexityRating::VeryHigh => "Very High (51+)",
79        }
80    }
81
82    pub fn from_complexity(complexity: u32) -> Self {
83        match complexity {
84            0..=10 => ComplexityRating::Low,
85            11..=20 => ComplexityRating::Medium,
86            21..=50 => ComplexityRating::High,
87            _ => ComplexityRating::VeryHigh,
88        }
89    }
90}
91
92/// Calculate cyclomatic complexity from CFG blocks
93///
94/// Uses the formula: M = 1 + sum(branch_count)
95/// This is equivalent to the traditional formula M = E - N + 2P
96/// when branch_count represents the number of decision points.
97pub fn calculate_cyclomatic_complexity(blocks: &[ComplexityBlock]) -> ComplexityResult {
98    let block_count = blocks.len();
99    let total_branches: u32 = blocks.iter().map(|b| b.z as u32).sum();
100
101    // Cyclomatic complexity = 1 + number of decision points
102    let cyclomatic_complexity = 1 + total_branches;
103
104    let avg_branches = if block_count > 0 {
105        total_branches as f32 / block_count as f32
106    } else {
107        0.0
108    };
109
110    let rating = ComplexityRating::from_complexity(cyclomatic_complexity);
111
112    ComplexityResult {
113        cyclomatic_complexity,
114        block_count,
115        total_branches,
116        avg_branches_per_block: avg_branches,
117        rating,
118    }
119}
120
121/// Calculate complexity for each block individually
122pub fn calculate_per_block_complexity(blocks: &[ComplexityBlock]) -> Vec<BlockComplexity> {
123    blocks
124        .iter()
125        .map(|b| {
126            BlockComplexity {
127                block_id: b.id,
128                branch_count: b.z as u32,
129                // Individual block complexity is 1 + branches at that block
130                local_complexity: 1 + b.z as u32,
131            }
132        })
133        .collect()
134}
135
136/// Complexity metrics for a single block
137#[derive(Debug, Clone)]
138pub struct BlockComplexity {
139    pub block_id: u64,
140    pub branch_count: u32,
141    pub local_complexity: u32,
142}
143
144/// Identify the most complex blocks (top N by branch count)
145pub fn find_most_complex_blocks(blocks: &[ComplexityBlock], n: usize) -> Vec<BlockComplexity> {
146    let mut complexities: Vec<BlockComplexity> = calculate_per_block_complexity(blocks);
147
148    // Sort by local complexity descending
149    complexities.sort_by_key(|block| std::cmp::Reverse(block.local_complexity));
150
151    // Take top N
152    complexities.into_iter().take(n).collect()
153}
154
155/// Check if complexity is within acceptable limits
156pub fn is_complexity_acceptable(complexity: u32, threshold: u32) -> bool {
157    complexity <= threshold
158}
159
160/// Get complexity trend description
161pub fn get_complexity_trend(complexity: u32) -> &'static str {
162    match complexity {
163        0..=5 => "Very simple - easy to understand and test",
164        6..=10 => "Simple - well-structured code",
165        11..=15 => "Moderate - consider adding comments",
166        16..=20 => "Complex - consider refactoring",
167        21..=30 => "Very complex - should be refactored",
168        _ => "Extremely complex - immediate refactoring recommended",
169    }
170}
171
172#[cfg(test)]
173mod tests {
174    use super::*;
175
176    #[test]
177    fn test_cyclomatic_complexity_simple() {
178        // Simple function with no branches
179        let blocks = vec![
180            ComplexityBlock { id: 0, z: 0.0 },
181            ComplexityBlock { id: 1, z: 0.0 },
182            ComplexityBlock { id: 2, z: 0.0 },
183        ];
184
185        let result = calculate_cyclomatic_complexity(&blocks);
186
187        assert_eq!(
188            result.cyclomatic_complexity, 1,
189            "Simple function should have complexity 1"
190        );
191        assert_eq!(result.block_count, 3);
192        assert_eq!(result.total_branches, 0);
193        assert_eq!(result.rating, ComplexityRating::Low);
194    }
195
196    #[test]
197    fn test_cyclomatic_complexity_single_if() {
198        // Function with single if statement
199        let blocks = vec![
200            ComplexityBlock { id: 0, z: 0.0 }, // Entry
201            ComplexityBlock { id: 1, z: 1.0 }, // If (1 branch)
202            ComplexityBlock { id: 2, z: 0.0 }, // Then
203            ComplexityBlock { id: 3, z: 0.0 }, // Exit
204        ];
205
206        let result = calculate_cyclomatic_complexity(&blocks);
207
208        assert_eq!(
209            result.cyclomatic_complexity, 2,
210            "Single if should have complexity 2"
211        );
212        assert_eq!(result.total_branches, 1);
213    }
214
215    #[test]
216    fn test_cyclomatic_complexity_nested_if() {
217        // Function with nested if statements
218        let blocks = vec![
219            ComplexityBlock { id: 0, z: 0.0 }, // Entry
220            ComplexityBlock { id: 1, z: 1.0 }, // Outer if
221            ComplexityBlock { id: 2, z: 1.0 }, // Inner if
222            ComplexityBlock { id: 3, z: 0.0 }, // Then
223            ComplexityBlock { id: 4, z: 0.0 }, // Exit
224        ];
225
226        let result = calculate_cyclomatic_complexity(&blocks);
227
228        assert_eq!(
229            result.cyclomatic_complexity, 3,
230            "Nested if should have complexity 3"
231        );
232        assert_eq!(result.total_branches, 2);
233    }
234
235    #[test]
236    fn test_cyclomatic_complexity_multiple_branches() {
237        // Function with multiple decision points
238        let blocks = vec![
239            ComplexityBlock { id: 0, z: 0.0 }, // Entry
240            ComplexityBlock { id: 1, z: 1.0 }, // If 1
241            ComplexityBlock { id: 2, z: 1.0 }, // If 2
242            ComplexityBlock { id: 3, z: 1.0 }, // If 3
243            ComplexityBlock { id: 4, z: 0.0 }, // Exit
244        ];
245
246        let result = calculate_cyclomatic_complexity(&blocks);
247
248        assert_eq!(
249            result.cyclomatic_complexity, 4,
250            "Three ifs should have complexity 4"
251        );
252    }
253
254    #[test]
255    fn test_complexity_rating_low() {
256        let blocks: Vec<ComplexityBlock> =
257            (0..10).map(|i| ComplexityBlock { id: i, z: 0.0 }).collect();
258        let result = calculate_cyclomatic_complexity(&blocks);
259        assert_eq!(result.rating, ComplexityRating::Low);
260    }
261
262    #[test]
263    fn test_complexity_rating_medium() {
264        // Create blocks with 15 branches (complexity = 16)
265        let blocks: Vec<ComplexityBlock> =
266            (0..15).map(|i| ComplexityBlock { id: i, z: 1.0 }).collect();
267        let result = calculate_cyclomatic_complexity(&blocks);
268        assert_eq!(result.rating, ComplexityRating::Medium);
269    }
270
271    #[test]
272    fn test_complexity_rating_high() {
273        // Create blocks with 30 branches (complexity = 31)
274        let blocks: Vec<ComplexityBlock> =
275            (0..30).map(|i| ComplexityBlock { id: i, z: 1.0 }).collect();
276        let result = calculate_cyclomatic_complexity(&blocks);
277        assert_eq!(result.rating, ComplexityRating::High);
278    }
279
280    #[test]
281    fn test_complexity_rating_very_high() {
282        // Create blocks with 60 branches (complexity = 61)
283        let blocks: Vec<ComplexityBlock> =
284            (0..60).map(|i| ComplexityBlock { id: i, z: 1.0 }).collect();
285        let result = calculate_cyclomatic_complexity(&blocks);
286        assert_eq!(result.rating, ComplexityRating::VeryHigh);
287    }
288
289    #[test]
290    fn test_per_block_complexity() {
291        let blocks = vec![
292            ComplexityBlock { id: 0, z: 0.0 },
293            ComplexityBlock { id: 1, z: 2.0 }, // Switch with 2 cases
294            ComplexityBlock { id: 2, z: 0.0 },
295        ];
296
297        let complexities = calculate_per_block_complexity(&blocks);
298
299        assert_eq!(complexities.len(), 3);
300        assert_eq!(complexities[1].local_complexity, 3); // 1 + 2 branches
301    }
302
303    #[test]
304    fn test_find_most_complex_blocks() {
305        let blocks = vec![
306            ComplexityBlock { id: 0, z: 0.0 },
307            ComplexityBlock { id: 1, z: 3.0 }, // Most complex
308            ComplexityBlock { id: 2, z: 1.0 },
309            ComplexityBlock { id: 3, z: 2.0 }, // Second most
310            ComplexityBlock { id: 4, z: 0.0 },
311        ];
312
313        let top = find_most_complex_blocks(&blocks, 2);
314
315        assert_eq!(top.len(), 2);
316        assert_eq!(top[0].block_id, 1); // Most complex first
317        assert_eq!(top[1].block_id, 3); // Second most complex
318    }
319
320    #[test]
321    fn test_is_complexity_acceptable() {
322        assert!(
323            is_complexity_acceptable(5, 10),
324            "5 should be acceptable with threshold 10"
325        );
326        assert!(
327            !is_complexity_acceptable(15, 10),
328            "15 should not be acceptable with threshold 10"
329        );
330        assert!(
331            is_complexity_acceptable(10, 10),
332            "10 should be acceptable with threshold 10"
333        );
334    }
335
336    #[test]
337    fn test_get_complexity_trend() {
338        assert!(get_complexity_trend(3).contains("simple"));
339        assert!(get_complexity_trend(25).contains("complex"));
340        assert!(get_complexity_trend(60).contains("refactoring"));
341    }
342
343    #[test]
344    fn test_avg_branches_per_block() {
345        let blocks = vec![
346            ComplexityBlock { id: 0, z: 0.0 },
347            ComplexityBlock { id: 1, z: 2.0 },
348            ComplexityBlock { id: 2, z: 4.0 },
349        ];
350
351        let result = calculate_cyclomatic_complexity(&blocks);
352
353        // Total branches = 6, blocks = 3, avg = 2.0
354        assert!((result.avg_branches_per_block - 2.0).abs() < 0.001);
355    }
356}