Skip to main content

debtmap/complexity/
recursive_detector.rs

1use super::match_patterns::MatchExpressionRecognizer;
2use std::collections::HashMap;
3use syn::{
4    Block, Expr, ExprAsync, ExprBlock, ExprClosure, ExprForLoop, ExprIf, ExprLoop, ExprMatch,
5    ExprWhile, ImplItem, Item, Stmt, visit::Visit,
6};
7
8/// Location information for a match expression found in the AST
9#[derive(Debug, Clone)]
10pub struct MatchLocation {
11    pub line: usize,
12    pub arms: usize,
13    pub complexity: u32,
14    pub context: ComplexityContext,
15}
16
17/// Context information about where a match expression was found
18#[derive(Debug, Clone)]
19pub struct ComplexityContext {
20    pub in_closure: bool,
21    pub in_async: bool,
22    pub nesting_depth: u32,
23    pub function_role: FunctionRole,
24}
25
26#[derive(Debug, Clone, PartialEq)]
27pub enum FunctionRole {
28    EntryPoint,
29    CoreLogic,
30    Utility,
31    Test,
32    Unknown,
33}
34
35/// Maximum recursion depth to prevent stack overflow
36const MAX_RECURSION_DEPTH: u32 = 150;
37
38/// Cache key for match detection results
39#[derive(Debug, Clone, Hash, PartialEq, Eq)]
40pub struct CacheKey {
41    function_name: String,
42    file_path: String,
43}
44
45/// Recursively detects all match expressions throughout the entire function AST
46pub struct RecursiveMatchDetector {
47    pub matches_found: Vec<MatchLocation>,
48    depth_tracker: u32,
49    complexity_context: ComplexityContext,
50    in_closure: bool,
51    in_async: bool,
52    /// Cache for previously analyzed functions
53    cache: HashMap<CacheKey, Vec<MatchLocation>>,
54    /// Maximum depth reached during traversal
55    max_depth_reached: u32,
56}
57
58impl Default for RecursiveMatchDetector {
59    fn default() -> Self {
60        Self::new()
61    }
62}
63
64impl RecursiveMatchDetector {
65    pub fn new() -> Self {
66        Self {
67            matches_found: Vec::new(),
68            depth_tracker: 0,
69            complexity_context: ComplexityContext {
70                in_closure: false,
71                in_async: false,
72                nesting_depth: 0,
73                function_role: FunctionRole::Unknown,
74            },
75            in_closure: false,
76            in_async: false,
77            cache: HashMap::new(),
78            max_depth_reached: 0,
79        }
80    }
81
82    /// Create a new detector with pre-populated cache
83    pub fn with_cache(cache: HashMap<CacheKey, Vec<MatchLocation>>) -> Self {
84        let mut detector = Self::new();
85        detector.cache = cache;
86        detector
87    }
88
89    /// Get the current cache for reuse
90    pub fn get_cache(&self) -> &HashMap<CacheKey, Vec<MatchLocation>> {
91        &self.cache
92    }
93
94    /// Check if recursion depth is within safe limits
95    fn check_depth_limit(&mut self) -> bool {
96        if self.depth_tracker > MAX_RECURSION_DEPTH {
97            eprintln!(
98                "Warning: Maximum recursion depth {} reached, stopping traversal",
99                MAX_RECURSION_DEPTH
100            );
101            return false;
102        }
103        if self.depth_tracker > self.max_depth_reached {
104            self.max_depth_reached = self.depth_tracker;
105        }
106        true
107    }
108
109    /// Find all match expressions in a function item
110    pub fn find_all_matches(&mut self, item: &Item) -> Vec<MatchLocation> {
111        self.traverse_item_recursively(item);
112        self.matches_found.clone()
113    }
114
115    /// Find all match expressions in a block
116    pub fn find_matches_in_block(&mut self, block: &Block) -> Vec<MatchLocation> {
117        self.visit_block(block);
118        self.matches_found.clone()
119    }
120
121    fn traverse_item_recursively(&mut self, item: &Item) {
122        match item {
123            Item::Fn(func) => {
124                self.determine_function_role(&func.sig.ident.to_string());
125                self.visit_block(&func.block);
126            }
127            Item::Impl(impl_block) => {
128                for item in &impl_block.items {
129                    if let ImplItem::Fn(method) = item {
130                        self.determine_function_role(&method.sig.ident.to_string());
131                        self.visit_block(&method.block);
132                    }
133                }
134            }
135            _ => {}
136        }
137    }
138
139    fn determine_function_role(&mut self, name: &str) {
140        self.complexity_context.function_role = if name == "main" {
141            FunctionRole::EntryPoint
142        } else if name.starts_with("test_") || name.ends_with("_test") {
143            FunctionRole::Test
144        } else if name.starts_with("get_") || name.starts_with("set_") || name.starts_with("is_") {
145            FunctionRole::Utility
146        } else {
147            FunctionRole::CoreLogic
148        };
149    }
150
151    fn calculate_match_complexity(&self, match_expr: &ExprMatch) -> u32 {
152        let recognizer = MatchExpressionRecognizer::new();
153        let mut complexity = match_expr.arms.len() as u32;
154
155        // Check if arms are simple (reduces complexity)
156        let simple_arms = match_expr
157            .arms
158            .iter()
159            .all(|arm| recognizer.is_simple_arm(&arm.body));
160
161        if simple_arms {
162            // Apply logarithmic scaling for simple match patterns
163            complexity = (complexity as f32).log2().ceil() as u32;
164        }
165
166        // Add depth penalty
167        complexity += self.depth_tracker.min(3);
168
169        complexity
170    }
171
172    fn get_line_number(&self, _expr: &ExprMatch) -> usize {
173        // In a real implementation, we'd use span information
174        // For now, return a placeholder
175        1
176    }
177
178    fn visit_with_nested_depth(&mut self, visit_nested: impl FnOnce(&mut Self)) {
179        self.depth_tracker += 1;
180        if self.check_depth_limit() {
181            visit_nested(self);
182        }
183        self.depth_tracker -= 1;
184    }
185
186    fn record_match(&mut self, match_expr: &ExprMatch) {
187        self.matches_found.push(MatchLocation {
188            line: self.get_line_number(match_expr),
189            arms: match_expr.arms.len(),
190            complexity: self.calculate_match_complexity(match_expr),
191            context: self.complexity_context.clone(),
192        });
193    }
194
195    fn visit_match_expr(&mut self, match_expr: &ExprMatch) {
196        self.record_match(match_expr);
197        self.visit_with_nested_depth(|detector| {
198            for arm in &match_expr.arms {
199                detector.visit_expr(&arm.body);
200            }
201        });
202    }
203
204    fn visit_closure_expr(&mut self, closure: &ExprClosure) {
205        let was_in_closure = self.in_closure;
206        self.in_closure = true;
207        self.complexity_context.in_closure = true;
208
209        self.visit_with_nested_depth(|detector| {
210            detector.visit_expr(&closure.body);
211        });
212
213        self.complexity_context.in_closure = was_in_closure;
214        self.in_closure = was_in_closure;
215    }
216
217    fn visit_async_expr(&mut self, async_block: &ExprAsync) {
218        let was_in_async = self.in_async;
219        self.in_async = true;
220        self.complexity_context.in_async = true;
221
222        self.visit_with_nested_depth(|detector| {
223            detector.visit_block(&async_block.block);
224        });
225
226        self.complexity_context.in_async = was_in_async;
227        self.in_async = was_in_async;
228    }
229
230    fn visit_block_expr(&mut self, expr_block: &ExprBlock) {
231        self.visit_with_nested_depth(|detector| {
232            detector.visit_block(&expr_block.block);
233        });
234    }
235
236    fn visit_if_expr(&mut self, if_expr: &ExprIf) {
237        self.visit_with_nested_depth(|detector| {
238            detector.visit_expr(&if_expr.cond);
239            detector.visit_block(&if_expr.then_branch);
240            if let Some((_else_token, else_branch)) = &if_expr.else_branch {
241                detector.visit_expr(else_branch);
242            }
243        });
244    }
245
246    fn visit_while_expr(&mut self, while_expr: &ExprWhile) {
247        self.visit_with_nested_depth(|detector| {
248            detector.visit_expr(&while_expr.cond);
249            detector.visit_block(&while_expr.body);
250        });
251    }
252
253    fn visit_for_loop_expr(&mut self, for_loop: &ExprForLoop) {
254        self.visit_with_nested_depth(|detector| {
255            detector.visit_expr(&for_loop.expr);
256            detector.visit_block(&for_loop.body);
257        });
258    }
259
260    fn visit_loop_expr(&mut self, loop_expr: &ExprLoop) {
261        self.visit_with_nested_depth(|detector| {
262            detector.visit_block(&loop_expr.body);
263        });
264    }
265
266    fn visit_other_expr(&mut self, expr: &Expr) {
267        self.visit_with_nested_depth(|detector| {
268            detector.visit_other_expr_children(expr);
269        });
270    }
271
272    fn visit_other_expr_children(&mut self, expr: &Expr) {
273        match expr {
274            Expr::Binary(e) => {
275                self.visit_expr(&e.left);
276                self.visit_expr(&e.right);
277            }
278            Expr::Unary(e) => {
279                self.visit_expr(&e.expr);
280            }
281            Expr::Call(e) => {
282                self.visit_expr(&e.func);
283                for arg in &e.args {
284                    self.visit_expr(arg);
285                }
286            }
287            Expr::MethodCall(e) => {
288                self.visit_expr(&e.receiver);
289                for arg in &e.args {
290                    self.visit_expr(arg);
291                }
292            }
293            Expr::Field(e) => {
294                self.visit_expr(&e.base);
295            }
296            Expr::Index(e) => {
297                self.visit_expr(&e.expr);
298                self.visit_expr(&e.index);
299            }
300            Expr::Paren(e) => {
301                self.visit_expr(&e.expr);
302            }
303            Expr::Try(e) => {
304                self.visit_expr(&e.expr);
305            }
306            Expr::Await(e) => {
307                self.visit_expr(&e.base);
308            }
309            Expr::Lit(_) | Expr::Path(_) | Expr::Continue(_) | Expr::Break(_) => {}
310            _ => {
311                if self.depth_tracker < MAX_RECURSION_DEPTH / 2 {
312                    syn::visit::visit_expr(self, expr);
313                }
314            }
315        }
316    }
317}
318
319impl<'ast> Visit<'ast> for RecursiveMatchDetector {
320    fn visit_expr(&mut self, expr: &'ast Expr) {
321        // Check depth limit before processing
322        if !self.check_depth_limit() {
323            return;
324        }
325
326        match expr {
327            Expr::Match(match_expr) => self.visit_match_expr(match_expr),
328            Expr::Closure(closure) => self.visit_closure_expr(closure),
329            Expr::Async(async_block) => self.visit_async_expr(async_block),
330            Expr::Block(expr_block) => self.visit_block_expr(expr_block),
331            Expr::If(if_expr) => self.visit_if_expr(if_expr),
332            Expr::While(while_expr) => self.visit_while_expr(while_expr),
333            Expr::ForLoop(for_loop) => self.visit_for_loop_expr(for_loop),
334            Expr::Loop(loop_expr) => self.visit_loop_expr(loop_expr),
335            _ => self.visit_other_expr(expr),
336        }
337    }
338
339    fn visit_block(&mut self, block: &'ast Block) {
340        // Check depth before processing block
341        if !self.check_depth_limit() {
342            return;
343        }
344
345        for stmt in &block.stmts {
346            if !self.check_depth_limit() {
347                break;
348            }
349            self.visit_stmt(stmt);
350        }
351    }
352
353    fn visit_stmt(&mut self, stmt: &'ast Stmt) {
354        // Check depth before processing statement
355        if !self.check_depth_limit() {
356            return;
357        }
358
359        match stmt {
360            Stmt::Expr(expr, _) => {
361                self.visit_expr(expr);
362            }
363            Stmt::Macro(_) => {
364                // Macros can't be analyzed without expansion
365            }
366            Stmt::Local(local) => {
367                if let Some(init) = &local.init {
368                    self.visit_expr(&init.expr);
369                }
370            }
371            Stmt::Item(item) => {
372                // Handle nested items (like inner functions)
373                if let Item::Fn(func) = item {
374                    self.depth_tracker += 1;
375                    if self.check_depth_limit() {
376                        self.visit_block(&func.block);
377                    }
378                    self.depth_tracker -= 1;
379                }
380            }
381        }
382    }
383}
384
385#[cfg(test)]
386mod tests {
387    use super::*;
388    use syn::parse_quote;
389
390    #[test]
391    fn test_recursive_match_detection() {
392        let item: Item = parse_quote! {
393            fn process_value(val: Value) -> Result<String> {
394                let result = match val {
395                    Value::String(s) => s,
396                    Value::Number(n) => n.to_string(),
397                    Value::Bool(b) => b.to_string(),
398                    Value::Nested(inner) => {
399                        // Nested match expression
400                        match inner.kind {
401                            Kind::A => "type_a",
402                            Kind::B => "type_b",
403                            Kind::C => "type_c",
404                        }
405                    }
406                    _ => "unknown",
407                };
408
409                Ok(result)
410            }
411        };
412
413        let mut detector = RecursiveMatchDetector::new();
414        let matches = detector.find_all_matches(&item);
415
416        // Should find 2 match expressions (outer and nested)
417        assert_eq!(matches.len(), 2);
418        assert_eq!(matches[0].arms, 5); // Outer match
419        assert_eq!(matches[1].arms, 3); // Inner match
420    }
421
422    #[test]
423    fn test_match_in_closure() {
424        let block: Block = parse_quote! {{
425            let processor = |item| {
426                match item {
427                    Item::A => 1,
428                    Item::B => 2,
429                    Item::C => 3,
430                }
431            };
432        }};
433
434        let mut detector = RecursiveMatchDetector::new();
435        let matches = detector.find_matches_in_block(&block);
436
437        assert_eq!(matches.len(), 1);
438        assert!(matches[0].context.in_closure);
439    }
440
441    #[test]
442    fn test_match_in_async_block() {
443        let block: Block = parse_quote! {{
444            let result = async {
445                match fetch_data().await {
446                    Ok(data) => process(data),
447                    Err(e) => handle_error(e),
448                }
449            };
450        }};
451
452        let mut detector = RecursiveMatchDetector::new();
453        let matches = detector.find_matches_in_block(&block);
454
455        assert_eq!(matches.len(), 1);
456        assert!(matches[0].context.in_async);
457    }
458
459    #[test]
460    fn test_context_is_restored_after_closure_and_async() {
461        let block: Block = parse_quote! {{
462            let from_closure = |item| {
463                match item {
464                    Item::A => 1,
465                    _ => 2,
466                }
467            };
468
469            let from_async = async {
470                match fetch_data().await {
471                    Ok(data) => data,
472                    Err(_) => 0,
473                }
474            };
475
476            match current {
477                Current::Ready => 1,
478                _ => 0,
479            }
480        }};
481
482        let mut detector = RecursiveMatchDetector::new();
483        let matches = detector.find_matches_in_block(&block);
484
485        assert_eq!(matches.len(), 3);
486        assert!(matches[0].context.in_closure);
487        assert!(!matches[0].context.in_async);
488        assert!(!matches[1].context.in_closure);
489        assert!(matches[1].context.in_async);
490        assert!(!matches[2].context.in_closure);
491        assert!(!matches[2].context.in_async);
492    }
493
494    #[test]
495    fn test_default_fallback_finds_match_in_tuple_expression() {
496        let block: Block = parse_quote! {{
497            let pair = (
498                match left {
499                    Side::A => 1,
500                    _ => 0,
501                },
502                match right {
503                    Side::B => 2,
504                    _ => 0,
505                },
506            );
507        }};
508
509        let mut detector = RecursiveMatchDetector::new();
510        let matches = detector.find_matches_in_block(&block);
511
512        assert_eq!(matches.len(), 2);
513    }
514
515    #[test]
516    fn test_deeply_nested_matches() {
517        let block: Block = parse_quote! {{
518            if condition {
519                for item in items {
520                    while processing {
521                        match item.state {
522                            State::Init => {
523                                match item.sub_state {
524                                    SubState::Ready => "ready",
525                                    SubState::Waiting => "waiting",
526                                }
527                            }
528                            State::Done => "done",
529                        }
530                    }
531                }
532            }
533        }};
534
535        let mut detector = RecursiveMatchDetector::new();
536        let matches = detector.find_matches_in_block(&block);
537
538        assert_eq!(matches.len(), 2);
539        // The deeper match should have higher nesting depth
540        assert!(matches[1].complexity > matches[0].arms as u32);
541    }
542}