debtmap/testing/rust/
complexity_scorer.rs

1use syn::spanned::Spanned;
2use syn::visit::Visit;
3use syn::ItemFn;
4
5/// Scores test complexity using multiple factors
6pub struct ComplexityScorer {
7    conditionals: u32,
8    loops: u32,
9    assertions: u32,
10    nesting_depth: u32,
11    max_nesting: u32,
12    line_count: usize,
13}
14
15#[derive(Debug, Clone)]
16pub struct TestComplexityScore {
17    pub total_score: f32,
18    pub factors: ComplexityFactors,
19    pub maintainability_index: f32,
20}
21
22#[derive(Debug, Clone)]
23pub struct ComplexityFactors {
24    pub conditionals: u32,
25    pub loops: u32,
26    pub assertions: u32,
27    pub nesting_depth: u32,
28    pub line_count: usize,
29}
30
31impl ComplexityScorer {
32    /// Default complexity threshold for tests
33    pub const DEFAULT_THRESHOLD: f32 = 10.0;
34
35    pub fn new() -> Self {
36        Self {
37            conditionals: 0,
38            loops: 0,
39            assertions: 0,
40            nesting_depth: 0,
41            max_nesting: 0,
42            line_count: 0,
43        }
44    }
45
46    /// Calculate complexity score for a test function
47    pub fn calculate_complexity(
48        &mut self,
49        func: &ItemFn,
50        assertion_count: usize,
51    ) -> TestComplexityScore {
52        self.reset();
53        self.assertions = assertion_count as u32;
54        self.line_count = self.count_lines(func);
55
56        // Visit the function body to count complexity factors
57        self.visit_block(&func.block);
58
59        let total_score = self.compute_total_score();
60        let maintainability_index = self.compute_maintainability_index(total_score);
61
62        TestComplexityScore {
63            total_score,
64            factors: ComplexityFactors {
65                conditionals: self.conditionals,
66                loops: self.loops,
67                assertions: self.assertions,
68                nesting_depth: self.max_nesting,
69                line_count: self.line_count,
70            },
71            maintainability_index,
72        }
73    }
74
75    /// Reset all counters
76    fn reset(&mut self) {
77        self.conditionals = 0;
78        self.loops = 0;
79        self.assertions = 0;
80        self.nesting_depth = 0;
81        self.max_nesting = 0;
82        self.line_count = 0;
83    }
84
85    /// Compute total complexity score based on all factors
86    fn compute_total_score(&self) -> f32 {
87        let mut score = 0.0;
88
89        // Conditionals: +2 per if/match
90        score += self.conditionals as f32 * 2.0;
91
92        // Loops: +3 per loop
93        score += self.loops as f32 * 3.0;
94
95        // Assertions: +1 per assertion beyond 5
96        if self.assertions > 5 {
97            score += (self.assertions - 5) as f32;
98        }
99
100        // Nesting depth: +2 per level > 2
101        if self.max_nesting > 2 {
102            score += (self.max_nesting - 2) as f32 * 2.0;
103        }
104
105        // Line count: +(lines-30)/10 for tests > 30 lines
106        if self.line_count > 30 {
107            score += ((self.line_count - 30) as f32) / 10.0;
108        }
109
110        score
111    }
112
113    /// Compute maintainability index (inverse of complexity)
114    fn compute_maintainability_index(&self, total_score: f32) -> f32 {
115        100.0 - (total_score * 2.0).min(100.0)
116    }
117
118    /// Count lines in function
119    fn count_lines(&self, func: &ItemFn) -> usize {
120        let span = func.span();
121        let start_line = span.start().line;
122        let end_line = span.end().line;
123
124        if end_line >= start_line {
125            end_line - start_line + 1
126        } else {
127            1
128        }
129    }
130
131    /// Track nesting depth
132    fn enter_nested(&mut self) {
133        self.nesting_depth += 1;
134        if self.nesting_depth > self.max_nesting {
135            self.max_nesting = self.nesting_depth;
136        }
137    }
138
139    fn exit_nested(&mut self) {
140        if self.nesting_depth > 0 {
141            self.nesting_depth -= 1;
142        }
143    }
144}
145
146impl Default for ComplexityScorer {
147    fn default() -> Self {
148        Self::new()
149    }
150}
151
152impl<'ast> Visit<'ast> for ComplexityScorer {
153    fn visit_expr_if(&mut self, expr: &'ast syn::ExprIf) {
154        self.conditionals += 1;
155        self.enter_nested();
156        syn::visit::visit_expr_if(self, expr);
157        self.exit_nested();
158    }
159
160    fn visit_expr_match(&mut self, expr: &'ast syn::ExprMatch) {
161        self.conditionals += 1;
162        self.enter_nested();
163        syn::visit::visit_expr_match(self, expr);
164        self.exit_nested();
165    }
166
167    fn visit_expr_while(&mut self, expr: &'ast syn::ExprWhile) {
168        self.loops += 1;
169        self.enter_nested();
170        syn::visit::visit_expr_while(self, expr);
171        self.exit_nested();
172    }
173
174    fn visit_expr_for_loop(&mut self, expr: &'ast syn::ExprForLoop) {
175        self.loops += 1;
176        self.enter_nested();
177        syn::visit::visit_expr_for_loop(self, expr);
178        self.exit_nested();
179    }
180
181    fn visit_expr_loop(&mut self, expr: &'ast syn::ExprLoop) {
182        self.loops += 1;
183        self.enter_nested();
184        syn::visit::visit_expr_loop(self, expr);
185        self.exit_nested();
186    }
187}
188
189#[cfg(test)]
190mod tests {
191    use super::*;
192    use syn::parse_quote;
193
194    #[test]
195    fn test_simple_test_low_complexity() {
196        let func: ItemFn = parse_quote! {
197            #[test]
198            fn test_simple() {
199                let x = 42;
200                assert_eq!(x, 42);
201            }
202        };
203
204        let mut scorer = ComplexityScorer::new();
205        let score = scorer.calculate_complexity(&func, 1);
206        assert!(score.total_score < 5.0);
207    }
208
209    #[test]
210    fn test_conditional_increases_complexity() {
211        let func: ItemFn = parse_quote! {
212            #[test]
213            fn test_conditional() {
214                if true {
215                    assert!(true);
216                }
217            }
218        };
219
220        let mut scorer = ComplexityScorer::new();
221        let score = scorer.calculate_complexity(&func, 1);
222        assert_eq!(score.factors.conditionals, 1);
223        assert!(score.total_score >= 2.0);
224    }
225
226    #[test]
227    fn test_loop_increases_complexity() {
228        let func: ItemFn = parse_quote! {
229            #[test]
230            fn test_loop() {
231                for i in 0..10 {
232                    assert!(i < 10);
233                }
234            }
235        };
236
237        let mut scorer = ComplexityScorer::new();
238        let score = scorer.calculate_complexity(&func, 1);
239        assert_eq!(score.factors.loops, 1);
240        assert!(score.total_score >= 3.0);
241    }
242
243    #[test]
244    fn test_excessive_assertions() {
245        let func: ItemFn = parse_quote! {
246            #[test]
247            fn test_many_assertions() {
248                assert!(true);
249                assert!(true);
250                assert!(true);
251                assert!(true);
252                assert!(true);
253                assert!(true);
254                assert!(true);
255            }
256        };
257
258        let mut scorer = ComplexityScorer::new();
259        let score = scorer.calculate_complexity(&func, 7);
260        assert_eq!(score.factors.assertions, 7);
261        // Should add 2 points for assertions beyond 5
262        assert!(score.total_score >= 2.0);
263    }
264
265    #[test]
266    fn test_nested_complexity() {
267        let func: ItemFn = parse_quote! {
268            #[test]
269            fn test_nested() {
270                if true {
271                    for i in 0..10 {
272                        if i % 2 == 0 {
273                            assert!(true);
274                        }
275                    }
276                }
277            }
278        };
279
280        let mut scorer = ComplexityScorer::new();
281        let score = scorer.calculate_complexity(&func, 1);
282        assert!(score.factors.nesting_depth >= 3);
283        assert!(score.total_score > 5.0);
284    }
285
286    #[test]
287    fn test_maintainability_index() {
288        let func: ItemFn = parse_quote! {
289            #[test]
290            fn test_simple() {
291                assert!(true);
292            }
293        };
294
295        let mut scorer = ComplexityScorer::new();
296        let score = scorer.calculate_complexity(&func, 1);
297        // Simple tests should have high maintainability
298        assert!(score.maintainability_index > 90.0);
299    }
300
301    #[test]
302    fn test_complex_test_low_maintainability() {
303        let func: ItemFn = parse_quote! {
304            #[test]
305            fn test_complex() {
306                for i in 0..10 {
307                    if i % 2 == 0 {
308                        for j in 0..5 {
309                            if j > i {
310                                assert!(true);
311                            }
312                        }
313                    }
314                }
315            }
316        };
317
318        let mut scorer = ComplexityScorer::new();
319        let score = scorer.calculate_complexity(&func, 1);
320        // Complex tests should have low maintainability
321        assert!(score.maintainability_index < 80.0);
322    }
323}