debtmap/testing/rust/
complexity_scorer.rs1use syn::spanned::Spanned;
2use syn::visit::Visit;
3use syn::ItemFn;
4
5pub 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 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 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 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 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 fn compute_total_score(&self) -> f32 {
87 let mut score = 0.0;
88
89 score += self.conditionals as f32 * 2.0;
91
92 score += self.loops as f32 * 3.0;
94
95 if self.assertions > 5 {
97 score += (self.assertions - 5) as f32;
98 }
99
100 if self.max_nesting > 2 {
102 score += (self.max_nesting - 2) as f32 * 2.0;
103 }
104
105 if self.line_count > 30 {
107 score += ((self.line_count - 30) as f32) / 10.0;
108 }
109
110 score
111 }
112
113 fn compute_maintainability_index(&self, total_score: f32) -> f32 {
115 100.0 - (total_score * 2.0).min(100.0)
116 }
117
118 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 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 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 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 assert!(score.maintainability_index < 80.0);
322 }
323}