codegraph_parser_api/
complexity.rs

1//! Code complexity metrics for functions and modules.
2//!
3//! This module provides structures and utilities for tracking code complexity metrics
4//! such as cyclomatic complexity, nesting depth, and decision point counts.
5
6use serde::{Deserialize, Serialize};
7
8/// Complexity metrics for a function or method.
9///
10/// These metrics help identify code that may be difficult to understand,
11/// test, or maintain.
12#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
13pub struct ComplexityMetrics {
14    /// McCabe's Cyclomatic Complexity (CC)
15    ///
16    /// CC = 1 + number of decision points
17    /// - 1-5: Simple, low risk
18    /// - 6-10: Moderate complexity
19    /// - 11-20: Complex, moderate risk
20    /// - 21-50: Very complex, high risk
21    /// - 51+: Untestable, very high risk
22    pub cyclomatic_complexity: u32,
23
24    /// Number of branch statements (if, else if, else, switch/match cases)
25    pub branches: u32,
26
27    /// Number of loop constructs (for, while, loop, do-while)
28    pub loops: u32,
29
30    /// Number of logical operators (&& / || / and / or)
31    pub logical_operators: u32,
32
33    /// Maximum nesting depth of control structures
34    pub max_nesting_depth: u32,
35
36    /// Number of exception handlers (catch, except, rescue)
37    pub exception_handlers: u32,
38
39    /// Number of early returns (return statements not at the end)
40    pub early_returns: u32,
41}
42
43impl ComplexityMetrics {
44    /// Create a new ComplexityMetrics with default values (base complexity of 1)
45    pub fn new() -> Self {
46        Self {
47            cyclomatic_complexity: 1,
48            ..Default::default()
49        }
50    }
51
52    /// Calculate the cyclomatic complexity from the component counts
53    ///
54    /// CC = 1 + branches + loops + logical_operators + exception_handlers
55    pub fn calculate_cyclomatic(&mut self) {
56        self.cyclomatic_complexity =
57            1 + self.branches + self.loops + self.logical_operators + self.exception_handlers;
58    }
59
60    /// Get a letter grade based on cyclomatic complexity
61    ///
62    /// - A: 1-5 (Simple, low risk)
63    /// - B: 6-10 (Moderate complexity)
64    /// - C: 11-20 (Complex, moderate risk)
65    /// - D: 21-50 (Very complex, high risk)
66    /// - F: 51+ (Untestable, very high risk)
67    pub fn grade(&self) -> char {
68        match self.cyclomatic_complexity {
69            1..=5 => 'A',
70            6..=10 => 'B',
71            11..=20 => 'C',
72            21..=50 => 'D',
73            _ => 'F',
74        }
75    }
76
77    /// Check if complexity exceeds a threshold
78    pub fn exceeds_threshold(&self, threshold: u32) -> bool {
79        self.cyclomatic_complexity > threshold
80    }
81
82    /// Check if the function has high nesting (> 4 levels)
83    pub fn has_high_nesting(&self) -> bool {
84        self.max_nesting_depth > 4
85    }
86
87    /// Merge metrics from a nested scope (used when traversing nested functions)
88    pub fn merge_nested(&mut self, nested: &ComplexityMetrics) {
89        self.branches += nested.branches;
90        self.loops += nested.loops;
91        self.logical_operators += nested.logical_operators;
92        self.exception_handlers += nested.exception_handlers;
93        self.early_returns += nested.early_returns;
94        // max_nesting_depth should be tracked separately during traversal
95    }
96
97    // Builder methods
98
99    pub fn with_branches(mut self, count: u32) -> Self {
100        self.branches = count;
101        self
102    }
103
104    pub fn with_loops(mut self, count: u32) -> Self {
105        self.loops = count;
106        self
107    }
108
109    pub fn with_logical_operators(mut self, count: u32) -> Self {
110        self.logical_operators = count;
111        self
112    }
113
114    pub fn with_nesting_depth(mut self, depth: u32) -> Self {
115        self.max_nesting_depth = depth;
116        self
117    }
118
119    pub fn with_exception_handlers(mut self, count: u32) -> Self {
120        self.exception_handlers = count;
121        self
122    }
123
124    pub fn with_early_returns(mut self, count: u32) -> Self {
125        self.early_returns = count;
126        self
127    }
128
129    /// Finalize and calculate the cyclomatic complexity
130    pub fn finalize(mut self) -> Self {
131        self.calculate_cyclomatic();
132        self
133    }
134}
135
136/// Builder for incrementally tracking complexity during AST traversal
137#[derive(Debug, Default)]
138pub struct ComplexityBuilder {
139    metrics: ComplexityMetrics,
140    current_nesting: u32,
141}
142
143impl ComplexityBuilder {
144    pub fn new() -> Self {
145        Self {
146            metrics: ComplexityMetrics::new(),
147            current_nesting: 0,
148        }
149    }
150
151    /// Record a branch (if, else if, case, etc.)
152    pub fn add_branch(&mut self) {
153        self.metrics.branches += 1;
154    }
155
156    /// Record a loop (for, while, loop, etc.)
157    pub fn add_loop(&mut self) {
158        self.metrics.loops += 1;
159    }
160
161    /// Record a logical operator (&& or ||)
162    pub fn add_logical_operator(&mut self) {
163        self.metrics.logical_operators += 1;
164    }
165
166    /// Record an exception handler (catch, except, etc.)
167    pub fn add_exception_handler(&mut self) {
168        self.metrics.exception_handlers += 1;
169    }
170
171    /// Record an early return
172    pub fn add_early_return(&mut self) {
173        self.metrics.early_returns += 1;
174    }
175
176    /// Enter a nested scope (increases nesting depth)
177    pub fn enter_scope(&mut self) {
178        self.current_nesting += 1;
179        if self.current_nesting > self.metrics.max_nesting_depth {
180            self.metrics.max_nesting_depth = self.current_nesting;
181        }
182    }
183
184    /// Exit a nested scope (decreases nesting depth)
185    pub fn exit_scope(&mut self) {
186        self.current_nesting = self.current_nesting.saturating_sub(1);
187    }
188
189    /// Get the current nesting depth
190    pub fn current_depth(&self) -> u32 {
191        self.current_nesting
192    }
193
194    /// Build the final ComplexityMetrics
195    pub fn build(mut self) -> ComplexityMetrics {
196        self.metrics.calculate_cyclomatic();
197        self.metrics
198    }
199
200    /// Get a reference to the current metrics (without finalizing)
201    pub fn current(&self) -> &ComplexityMetrics {
202        &self.metrics
203    }
204}
205
206#[cfg(test)]
207mod tests {
208    use super::*;
209
210    #[test]
211    fn test_new_has_base_complexity() {
212        let metrics = ComplexityMetrics::new();
213        assert_eq!(metrics.cyclomatic_complexity, 1);
214    }
215
216    #[test]
217    fn test_grade_simple() {
218        let metrics = ComplexityMetrics {
219            cyclomatic_complexity: 3,
220            ..Default::default()
221        };
222        assert_eq!(metrics.grade(), 'A');
223    }
224
225    #[test]
226    fn test_grade_moderate() {
227        let metrics = ComplexityMetrics {
228            cyclomatic_complexity: 8,
229            ..Default::default()
230        };
231        assert_eq!(metrics.grade(), 'B');
232    }
233
234    #[test]
235    fn test_grade_complex() {
236        let metrics = ComplexityMetrics {
237            cyclomatic_complexity: 15,
238            ..Default::default()
239        };
240        assert_eq!(metrics.grade(), 'C');
241    }
242
243    #[test]
244    fn test_grade_very_complex() {
245        let metrics = ComplexityMetrics {
246            cyclomatic_complexity: 35,
247            ..Default::default()
248        };
249        assert_eq!(metrics.grade(), 'D');
250    }
251
252    #[test]
253    fn test_grade_untestable() {
254        let metrics = ComplexityMetrics {
255            cyclomatic_complexity: 60,
256            ..Default::default()
257        };
258        assert_eq!(metrics.grade(), 'F');
259    }
260
261    #[test]
262    fn test_calculate_cyclomatic() {
263        let mut metrics = ComplexityMetrics::new()
264            .with_branches(3)
265            .with_loops(2)
266            .with_logical_operators(1);
267        metrics.calculate_cyclomatic();
268        // CC = 1 + 3 + 2 + 1 = 7
269        assert_eq!(metrics.cyclomatic_complexity, 7);
270    }
271
272    #[test]
273    fn test_builder_basic() {
274        let mut builder = ComplexityBuilder::new();
275        builder.add_branch();
276        builder.add_branch();
277        builder.add_loop();
278
279        let metrics = builder.build();
280        // CC = 1 + 2 branches + 1 loop = 4
281        assert_eq!(metrics.cyclomatic_complexity, 4);
282    }
283
284    #[test]
285    fn test_builder_nesting() {
286        let mut builder = ComplexityBuilder::new();
287        builder.enter_scope();
288        builder.add_branch();
289        builder.enter_scope();
290        builder.add_loop();
291        builder.enter_scope();
292        builder.exit_scope();
293        builder.exit_scope();
294        builder.exit_scope();
295
296        let metrics = builder.build();
297        assert_eq!(metrics.max_nesting_depth, 3);
298    }
299
300    #[test]
301    fn test_exceeds_threshold() {
302        let metrics = ComplexityMetrics {
303            cyclomatic_complexity: 15,
304            ..Default::default()
305        };
306        assert!(metrics.exceeds_threshold(10));
307        assert!(!metrics.exceeds_threshold(20));
308    }
309
310    #[test]
311    fn test_has_high_nesting() {
312        let low_nesting = ComplexityMetrics {
313            max_nesting_depth: 3,
314            ..Default::default()
315        };
316        assert!(!low_nesting.has_high_nesting());
317
318        let high_nesting = ComplexityMetrics {
319            max_nesting_depth: 5,
320            ..Default::default()
321        };
322        assert!(high_nesting.has_high_nesting());
323    }
324}