Skip to main content

debtmap/analysis/attribution/
pattern_tracker.rs

1use super::{
2    AttributedComplexity, CodeLocation, ComplexityComponent, ComplexitySourceType,
3    RecognizedPattern,
4};
5use crate::core::FunctionMetrics;
6use std::collections::HashMap;
7
8/// Pattern analysis and tracking
9pub struct PatternTracker {
10    pattern_detectors: Vec<Box<dyn PatternDetector>>,
11}
12
13impl Default for PatternTracker {
14    fn default() -> Self {
15        Self {
16            pattern_detectors: vec![
17                Box::new(ErrorHandlingDetector::new()),
18                Box::new(ValidationDetector::new()),
19                Box::new(DataTransformationDetector::new()),
20                Box::new(StateManagementDetector::new()),
21                Box::new(IteratorDetector::new()),
22            ],
23        }
24    }
25}
26
27impl PatternTracker {
28    /// Creates a new pattern tracker with default pattern detectors.
29    ///
30    /// Includes detectors for error handling, validation, data transformation,
31    /// state management, and iterator patterns.
32    pub fn new() -> Self {
33        Self::default()
34    }
35
36    /// Analyzes functions for recognized patterns and returns attributed complexity.
37    ///
38    /// Scans each function through all registered pattern detectors, accumulating
39    /// complexity adjustments based on detected patterns. Returns an attribution
40    /// with a confidence score based on pattern consistency across the codebase.
41    pub fn analyze_patterns(&self, functions: &[FunctionMetrics]) -> AttributedComplexity {
42        let mut total = 0u32;
43        let mut breakdown = Vec::new();
44        let mut pattern_counts = HashMap::new();
45
46        for func in functions {
47            for detector in &self.pattern_detectors {
48                if let Some(pattern_info) = detector.detect(func) {
49                    *pattern_counts
50                        .entry(pattern_info.pattern_type.clone())
51                        .or_insert(0) += 1;
52
53                    // Patterns typically reduce perceived complexity
54                    let adjustment =
55                        (func.cyclomatic as f32 * pattern_info.adjustment_factor) as u32;
56                    let reduction = func.cyclomatic.saturating_sub(adjustment);
57
58                    if reduction > 0 {
59                        breakdown.push(ComplexityComponent {
60                            source_type: ComplexitySourceType::PatternRecognition {
61                                pattern_type: pattern_info.pattern_type,
62                                adjustment_factor: pattern_info.adjustment_factor,
63                            },
64                            contribution: reduction,
65                            location: CodeLocation {
66                                file: func.file.to_string_lossy().to_string(),
67                                line: func.line as u32,
68                                column: 0,
69                                span: Some((func.line as u32, (func.line + func.length) as u32)),
70                            },
71                            description: format!("{} pattern in {}", pattern_info.name, func.name),
72                            suggestions: pattern_info.optimization_suggestions,
73                        });
74
75                        total += reduction;
76                    }
77                }
78            }
79        }
80
81        // Calculate confidence based on pattern consistency
82        let confidence = if !pattern_counts.is_empty() {
83            let max_count = *pattern_counts.values().max().unwrap_or(&0);
84            let pattern_variety = pattern_counts.len();
85
86            // Higher confidence when we see consistent patterns
87            let consistency_score = (max_count as f32 / functions.len().max(1) as f32).min(1.0);
88            let variety_score = (pattern_variety as f32 / 5.0).min(1.0);
89
90            (consistency_score * 0.6 + variety_score * 0.4).min(0.9)
91        } else {
92            0.3 // Low confidence when no patterns detected
93        };
94
95        AttributedComplexity {
96            total,
97            breakdown,
98            confidence,
99        }
100    }
101}
102
103/// Trait for pattern detection
104trait PatternDetector: Send + Sync {
105    fn detect(&self, func: &FunctionMetrics) -> Option<PatternInfo>;
106}
107
108/// Information about a detected pattern
109struct PatternInfo {
110    name: String,
111    pattern_type: RecognizedPattern,
112    adjustment_factor: f32,
113    optimization_suggestions: Vec<String>,
114}
115
116/// Detector for error handling patterns
117struct ErrorHandlingDetector;
118
119impl ErrorHandlingDetector {
120    fn new() -> Self {
121        Self
122    }
123}
124
125impl PatternDetector for ErrorHandlingDetector {
126    fn detect(&self, func: &FunctionMetrics) -> Option<PatternInfo> {
127        // Simple heuristic: functions with "error", "handle", "catch" in name
128        let name_lower = func.name.to_lowercase();
129        if name_lower.contains("error")
130            || name_lower.contains("handle")
131            || name_lower.contains("catch")
132        {
133            Some(PatternInfo {
134                name: "Error Handling".to_string(),
135                pattern_type: RecognizedPattern::ErrorHandling,
136                adjustment_factor: 0.8, // Error handling is expected complexity
137                optimization_suggestions: vec![
138                    "Consider using Result type consistently".to_string(),
139                    "Extract error transformation logic".to_string(),
140                ],
141            })
142        } else {
143            None
144        }
145    }
146}
147
148/// Detector for validation patterns
149struct ValidationDetector;
150
151impl ValidationDetector {
152    fn new() -> Self {
153        Self
154    }
155}
156
157impl PatternDetector for ValidationDetector {
158    fn detect(&self, func: &FunctionMetrics) -> Option<PatternInfo> {
159        let name_lower = func.name.to_lowercase();
160        if name_lower.contains("valid")
161            || name_lower.contains("check")
162            || name_lower.contains("verify")
163        {
164            Some(PatternInfo {
165                name: "Validation".to_string(),
166                pattern_type: RecognizedPattern::Validation,
167                adjustment_factor: 0.85,
168                optimization_suggestions: vec![
169                    "Consider using validation libraries".to_string(),
170                    "Extract validation rules into constants".to_string(),
171                ],
172            })
173        } else {
174            None
175        }
176    }
177}
178
179/// Detector for data transformation patterns
180struct DataTransformationDetector;
181
182impl DataTransformationDetector {
183    fn new() -> Self {
184        Self
185    }
186}
187
188impl PatternDetector for DataTransformationDetector {
189    fn detect(&self, func: &FunctionMetrics) -> Option<PatternInfo> {
190        let name_lower = func.name.to_lowercase();
191        if name_lower.contains("transform")
192            || name_lower.contains("convert")
193            || name_lower.contains("map")
194            || name_lower.contains("parse")
195        {
196            Some(PatternInfo {
197                name: "Data Transformation".to_string(),
198                pattern_type: RecognizedPattern::DataTransformation,
199                adjustment_factor: 0.9,
200                optimization_suggestions: vec![
201                    "Consider using functional transformations".to_string(),
202                    "Extract transformation logic into pure functions".to_string(),
203                ],
204            })
205        } else {
206            None
207        }
208    }
209}
210
211/// Detector for state management patterns
212struct StateManagementDetector;
213
214impl StateManagementDetector {
215    fn new() -> Self {
216        Self
217    }
218}
219
220impl PatternDetector for StateManagementDetector {
221    fn detect(&self, func: &FunctionMetrics) -> Option<PatternInfo> {
222        let name_lower = func.name.to_lowercase();
223        if name_lower.contains("state")
224            || name_lower.contains("update")
225            || name_lower.contains("sync")
226        {
227            Some(PatternInfo {
228                name: "State Management".to_string(),
229                pattern_type: RecognizedPattern::StateManagement,
230                adjustment_factor: 0.75, // State management can be complex
231                optimization_suggestions: vec![
232                    "Consider immutable state updates".to_string(),
233                    "Use state machines for complex state logic".to_string(),
234                ],
235            })
236        } else {
237            None
238        }
239    }
240}
241
242/// Detector for iterator patterns
243struct IteratorDetector;
244
245impl IteratorDetector {
246    fn new() -> Self {
247        Self
248    }
249}
250
251impl PatternDetector for IteratorDetector {
252    fn detect(&self, func: &FunctionMetrics) -> Option<PatternInfo> {
253        let name_lower = func.name.to_lowercase();
254        if name_lower.contains("iter")
255            || name_lower.contains("next")
256            || name_lower.contains("collect")
257            || name_lower.contains("fold")
258        {
259            Some(PatternInfo {
260                name: "Iterator".to_string(),
261                pattern_type: RecognizedPattern::Iterator,
262                adjustment_factor: 0.95,
263                optimization_suggestions: vec![
264                    "Consider using iterator combinators".to_string(),
265                    "Avoid unnecessary intermediate collections".to_string(),
266                ],
267            })
268        } else {
269            None
270        }
271    }
272}
273
274#[cfg(test)]
275mod tests {
276    use super::*;
277    use std::path::PathBuf;
278
279    #[test]
280    fn test_pattern_tracker_new() {
281        let tracker = PatternTracker::new();
282        assert!(!tracker.pattern_detectors.is_empty());
283    }
284
285    #[test]
286    fn test_error_handling_detection() {
287        let detector = ErrorHandlingDetector::new();
288
289        let func = FunctionMetrics {
290            name: "handle_error".to_string(),
291            file: PathBuf::from("test.rs"),
292            line: 10,
293            cyclomatic: 5,
294            cognitive: 3,
295            nesting: 1,
296            length: 20,
297            is_test: false,
298            visibility: None,
299            is_trait_method: false,
300            in_test_module: false,
301            entropy_score: None,
302            is_pure: None,
303            purity_confidence: None,
304            purity_reason: None,
305            call_dependencies: None,
306            detected_patterns: None,
307            upstream_callers: None,
308            downstream_callees: None,
309            mapping_pattern_result: None,
310            adjusted_complexity: None,
311            composition_metrics: None,
312            language_specific: None,
313            purity_level: None,
314            error_swallowing_count: None,
315            error_swallowing_patterns: None,
316            entropy_analysis: None,
317        };
318
319        let pattern = detector.detect(&func);
320        assert!(pattern.is_some());
321
322        let info = pattern.unwrap();
323        assert_eq!(info.pattern_type, RecognizedPattern::ErrorHandling);
324        assert_eq!(info.adjustment_factor, 0.8);
325    }
326
327    #[test]
328    fn test_validation_detection() {
329        let detector = ValidationDetector::new();
330
331        let func = FunctionMetrics {
332            name: "validate_input".to_string(),
333            file: PathBuf::from("test.rs"),
334            line: 20,
335            cyclomatic: 8,
336            cognitive: 5,
337            nesting: 2,
338            length: 30,
339            is_test: false,
340            visibility: None,
341            is_trait_method: false,
342            in_test_module: false,
343            entropy_score: None,
344            is_pure: None,
345            purity_confidence: None,
346            purity_reason: None,
347            call_dependencies: None,
348            detected_patterns: None,
349            upstream_callers: None,
350            downstream_callees: None,
351            mapping_pattern_result: None,
352            adjusted_complexity: None,
353            composition_metrics: None,
354            language_specific: None,
355            purity_level: None,
356            error_swallowing_count: None,
357            error_swallowing_patterns: None,
358            entropy_analysis: None,
359        };
360
361        let pattern = detector.detect(&func);
362        assert!(pattern.is_some());
363
364        let info = pattern.unwrap();
365        assert_eq!(info.pattern_type, RecognizedPattern::Validation);
366    }
367
368    #[test]
369    fn test_no_pattern_detection() {
370        let detector = ErrorHandlingDetector::new();
371
372        let func = FunctionMetrics {
373            name: "calculate_sum".to_string(),
374            file: PathBuf::from("test.rs"),
375            line: 30,
376            cyclomatic: 2,
377            cognitive: 1,
378            nesting: 0,
379            length: 10,
380            is_test: false,
381            visibility: None,
382            is_trait_method: false,
383            in_test_module: false,
384            entropy_score: None,
385            is_pure: None,
386            purity_confidence: None,
387            purity_reason: None,
388            call_dependencies: None,
389            detected_patterns: None,
390            upstream_callers: None,
391            downstream_callees: None,
392            mapping_pattern_result: None,
393            adjusted_complexity: None,
394            composition_metrics: None,
395            language_specific: None,
396            purity_level: None,
397            error_swallowing_count: None,
398            error_swallowing_patterns: None,
399            entropy_analysis: None,
400        };
401
402        let pattern = detector.detect(&func);
403        assert!(pattern.is_none());
404    }
405
406    #[test]
407    fn test_analyze_patterns_empty() {
408        let tracker = PatternTracker::new();
409        let functions = vec![];
410
411        let result = tracker.analyze_patterns(&functions);
412        assert_eq!(result.total, 0);
413        assert_eq!(result.confidence, 0.3);
414    }
415
416    #[test]
417    fn test_analyze_patterns_with_functions() {
418        let tracker = PatternTracker::new();
419        let functions = vec![FunctionMetrics {
420            name: "handle_request_error".to_string(),
421            file: PathBuf::from("test.rs"),
422            line: 10,
423            cyclomatic: 10,
424            cognitive: 8,
425            nesting: 2,
426            length: 40,
427            is_test: false,
428            visibility: None,
429            is_trait_method: false,
430            in_test_module: false,
431            entropy_score: None,
432            is_pure: None,
433            purity_confidence: None,
434            purity_reason: None,
435            call_dependencies: None,
436            detected_patterns: None,
437            upstream_callers: None,
438            downstream_callees: None,
439            mapping_pattern_result: None,
440            adjusted_complexity: None,
441            composition_metrics: None,
442            language_specific: None,
443            purity_level: None,
444            error_swallowing_count: None,
445            error_swallowing_patterns: None,
446            entropy_analysis: None,
447        }];
448
449        let result = tracker.analyze_patterns(&functions);
450        assert!(result.total > 0);
451        assert!(!result.breakdown.is_empty());
452    }
453}