ricecoder_research/
pattern_detector.rs

1//! Pattern detection for identifying coding and architectural patterns in codebases
2
3use crate::codebase_scanner::ScanResult;
4use crate::models::{DetectedPattern, PatternCategory};
5use crate::ResearchError;
6use std::collections::HashMap;
7
8/// Detects coding patterns and architectural patterns in the codebase
9#[derive(Debug, Clone)]
10pub struct PatternDetector {
11    /// Minimum confidence threshold for pattern detection (0.0 to 1.0)
12    pub confidence_threshold: f32,
13}
14
15impl PatternDetector {
16    /// Creates a new PatternDetector with default settings
17    pub fn new() -> Self {
18        Self {
19            confidence_threshold: 0.5,
20        }
21    }
22
23    /// Creates a new PatternDetector with a custom confidence threshold
24    pub fn with_threshold(confidence_threshold: f32) -> Self {
25        Self {
26            confidence_threshold: confidence_threshold.clamp(0.0, 1.0),
27        }
28    }
29
30    /// Detects patterns in the provided scan result
31    ///
32    /// # Arguments
33    ///
34    /// * `scan_result` - The result of a codebase scan containing files and symbols
35    ///
36    /// # Returns
37    ///
38    /// A vector of detected patterns, or an error if detection fails
39    pub fn detect(&self, scan_result: &ScanResult) -> Result<Vec<DetectedPattern>, ResearchError> {
40        let mut patterns = Vec::new();
41
42        // Detect architectural patterns
43        let arch_patterns = self.detect_architectural_patterns(scan_result)?;
44        patterns.extend(arch_patterns);
45
46        // Detect coding patterns
47        let coding_patterns = self.detect_coding_patterns(scan_result)?;
48        patterns.extend(coding_patterns);
49
50        // Filter patterns by confidence threshold
51        patterns.retain(|p| p.confidence >= self.confidence_threshold);
52
53        // Sort by confidence (highest first)
54        patterns.sort_by(|a, b| {
55            b.confidence
56                .partial_cmp(&a.confidence)
57                .unwrap_or(std::cmp::Ordering::Equal)
58        });
59
60        Ok(patterns)
61    }
62
63    /// Detects architectural patterns in the codebase
64    fn detect_architectural_patterns(
65        &self,
66        scan_result: &ScanResult,
67    ) -> Result<Vec<DetectedPattern>, ResearchError> {
68        let mut patterns = Vec::new();
69
70        // Detect layered architecture
71        if let Some(pattern) = self.detect_layered_architecture(scan_result)? {
72            patterns.push(pattern);
73        }
74
75        // Detect microservices pattern
76        if let Some(pattern) = self.detect_microservices_pattern(scan_result)? {
77            patterns.push(pattern);
78        }
79
80        // Detect event-driven pattern
81        if let Some(pattern) = self.detect_event_driven_pattern(scan_result)? {
82            patterns.push(pattern);
83        }
84
85        // Detect monolithic pattern
86        if let Some(pattern) = self.detect_monolithic_pattern(scan_result)? {
87            patterns.push(pattern);
88        }
89
90        Ok(patterns)
91    }
92
93    /// Detects coding patterns in the codebase
94    fn detect_coding_patterns(
95        &self,
96        scan_result: &ScanResult,
97    ) -> Result<Vec<DetectedPattern>, ResearchError> {
98        let mut patterns = Vec::new();
99
100        // Detect factory pattern
101        if let Some(pattern) = self.detect_factory_pattern(scan_result)? {
102            patterns.push(pattern);
103        }
104
105        // Detect observer pattern
106        if let Some(pattern) = self.detect_observer_pattern(scan_result)? {
107            patterns.push(pattern);
108        }
109
110        // Detect strategy pattern
111        if let Some(pattern) = self.detect_strategy_pattern(scan_result)? {
112            patterns.push(pattern);
113        }
114
115        // Detect singleton pattern
116        if let Some(pattern) = self.detect_singleton_pattern(scan_result)? {
117            patterns.push(pattern);
118        }
119
120        Ok(patterns)
121    }
122
123    /// Detects layered architecture pattern
124    fn detect_layered_architecture(
125        &self,
126        scan_result: &ScanResult,
127    ) -> Result<Option<DetectedPattern>, ResearchError> {
128        // Check for common layered architecture directory structure
129        let has_domain = scan_result.files.iter().any(|f| {
130            f.path
131                .components()
132                .any(|c| c.as_os_str().to_string_lossy().contains("domain"))
133        });
134
135        let has_application = scan_result.files.iter().any(|f| {
136            f.path
137                .components()
138                .any(|c| c.as_os_str().to_string_lossy().contains("application"))
139        });
140
141        let has_infrastructure = scan_result.files.iter().any(|f| {
142            f.path
143                .components()
144                .any(|c| c.as_os_str().to_string_lossy().contains("infrastructure"))
145        });
146
147        let has_interfaces = scan_result.files.iter().any(|f| {
148            f.path
149                .components()
150                .any(|c| c.as_os_str().to_string_lossy().contains("interfaces"))
151        });
152
153        // Calculate confidence based on how many layers are present
154        let layer_count = [
155            has_domain,
156            has_application,
157            has_infrastructure,
158            has_interfaces,
159        ]
160        .iter()
161        .filter(|&&x| x)
162        .count();
163
164        if layer_count >= 2 {
165            let confidence = (layer_count as f32) / 4.0;
166            let locations = scan_result
167                .files
168                .iter()
169                .filter(|f| {
170                    f.path.components().any(|c| {
171                        let name = c.as_os_str().to_string_lossy();
172                        name.contains("domain")
173                            || name.contains("application")
174                            || name.contains("infrastructure")
175                            || name.contains("interfaces")
176                    })
177                })
178                .map(|f| f.path.clone())
179                .collect();
180
181            return Ok(Some(DetectedPattern {
182                name: "Layered Architecture".to_string(),
183                category: PatternCategory::Architectural,
184                confidence,
185                locations,
186                description: "Project uses layered architecture with domain, application, infrastructure, and/or interfaces layers".to_string(),
187            }));
188        }
189
190        Ok(None)
191    }
192
193    /// Detects microservices pattern
194    fn detect_microservices_pattern(
195        &self,
196        scan_result: &ScanResult,
197    ) -> Result<Option<DetectedPattern>, ResearchError> {
198        // Check for multiple service directories or modules
199        let mut service_dirs = HashMap::new();
200        for file in &scan_result.files {
201            if let Some(parent) = file.path.parent() {
202                let parent_name = parent
203                    .file_name()
204                    .and_then(|n| n.to_str())
205                    .unwrap_or("")
206                    .to_string();
207
208                if parent_name.contains("service") || parent_name.contains("services") {
209                    *service_dirs.entry(parent.to_path_buf()).or_insert(0) += 1;
210                }
211            }
212        }
213
214        if service_dirs.len() >= 2 {
215            let confidence = 0.6;
216            let locations: Vec<_> = service_dirs.keys().cloned().collect();
217
218            return Ok(Some(DetectedPattern {
219                name: "Microservices Pattern".to_string(),
220                category: PatternCategory::Architectural,
221                confidence,
222                locations,
223                description: "Project appears to use microservices architecture with multiple service modules".to_string(),
224            }));
225        }
226
227        Ok(None)
228    }
229
230    /// Detects event-driven pattern
231    fn detect_event_driven_pattern(
232        &self,
233        scan_result: &ScanResult,
234    ) -> Result<Option<DetectedPattern>, ResearchError> {
235        // Check for event-related naming patterns
236        let event_count = scan_result
237            .files
238            .iter()
239            .filter(|f| {
240                let name = f
241                    .path
242                    .file_name()
243                    .and_then(|n| n.to_str())
244                    .unwrap_or("")
245                    .to_lowercase();
246                name.contains("event") || name.contains("handler") || name.contains("listener")
247            })
248            .count();
249
250        if event_count >= 3 {
251            let confidence = 0.65;
252            let locations = scan_result
253                .files
254                .iter()
255                .filter(|f| {
256                    let name = f
257                        .path
258                        .file_name()
259                        .and_then(|n| n.to_str())
260                        .unwrap_or("")
261                        .to_lowercase();
262                    name.contains("event") || name.contains("handler") || name.contains("listener")
263                })
264                .map(|f| f.path.clone())
265                .collect();
266
267            return Ok(Some(DetectedPattern {
268                name: "Event-Driven Pattern".to_string(),
269                category: PatternCategory::Architectural,
270                confidence,
271                locations,
272                description:
273                    "Project uses event-driven architecture with event handlers and listeners"
274                        .to_string(),
275            }));
276        }
277
278        Ok(None)
279    }
280
281    /// Detects monolithic pattern
282    fn detect_monolithic_pattern(
283        &self,
284        scan_result: &ScanResult,
285    ) -> Result<Option<DetectedPattern>, ResearchError> {
286        // Check if project has a single main entry point and no service separation
287        let has_main = scan_result.files.iter().any(|f| {
288            f.path
289                .file_name()
290                .and_then(|n| n.to_str())
291                .map(|n| n == "main.rs" || n == "main.py" || n == "main.go" || n == "main.java")
292                .unwrap_or(false)
293        });
294
295        let service_count = scan_result
296            .files
297            .iter()
298            .filter(|f| {
299                f.path
300                    .file_name()
301                    .and_then(|n| n.to_str())
302                    .map(|n| n.contains("service"))
303                    .unwrap_or(false)
304            })
305            .count();
306
307        if has_main && service_count < 2 {
308            let confidence = 0.55;
309            let locations = vec![scan_result
310                .files
311                .first()
312                .map(|f| f.path.clone())
313                .unwrap_or_default()];
314
315            return Ok(Some(DetectedPattern {
316                name: "Monolithic Architecture".to_string(),
317                category: PatternCategory::Architectural,
318                confidence,
319                locations,
320                description: "Project appears to be monolithic with a single entry point"
321                    .to_string(),
322            }));
323        }
324
325        Ok(None)
326    }
327
328    /// Detects factory pattern
329    fn detect_factory_pattern(
330        &self,
331        scan_result: &ScanResult,
332    ) -> Result<Option<DetectedPattern>, ResearchError> {
333        // Check for factory-related naming
334        let factory_count = scan_result
335            .files
336            .iter()
337            .filter(|f| {
338                f.path
339                    .file_name()
340                    .and_then(|n| n.to_str())
341                    .map(|n| n.to_lowercase().contains("factory"))
342                    .unwrap_or(false)
343            })
344            .count();
345
346        if factory_count >= 1 {
347            let confidence = 0.7;
348            let locations = scan_result
349                .files
350                .iter()
351                .filter(|f| {
352                    f.path
353                        .file_name()
354                        .and_then(|n| n.to_str())
355                        .map(|n| n.to_lowercase().contains("factory"))
356                        .unwrap_or(false)
357                })
358                .map(|f| f.path.clone())
359                .collect();
360
361            return Ok(Some(DetectedPattern {
362                name: "Factory Pattern".to_string(),
363                category: PatternCategory::Design,
364                confidence,
365                locations,
366                description: "Project uses factory pattern for object creation".to_string(),
367            }));
368        }
369
370        Ok(None)
371    }
372
373    /// Detects observer pattern
374    fn detect_observer_pattern(
375        &self,
376        scan_result: &ScanResult,
377    ) -> Result<Option<DetectedPattern>, ResearchError> {
378        // Check for observer-related naming
379        let observer_count = scan_result
380            .files
381            .iter()
382            .filter(|f| {
383                f.path
384                    .file_name()
385                    .and_then(|n| n.to_str())
386                    .map(|n| {
387                        let lower = n.to_lowercase();
388                        lower.contains("observer")
389                            || lower.contains("listener")
390                            || lower.contains("subscriber")
391                    })
392                    .unwrap_or(false)
393            })
394            .count();
395
396        if observer_count >= 1 {
397            let confidence = 0.65;
398            let locations = scan_result
399                .files
400                .iter()
401                .filter(|f| {
402                    f.path
403                        .file_name()
404                        .and_then(|n| n.to_str())
405                        .map(|n| {
406                            let lower = n.to_lowercase();
407                            lower.contains("observer")
408                                || lower.contains("listener")
409                                || lower.contains("subscriber")
410                        })
411                        .unwrap_or(false)
412                })
413                .map(|f| f.path.clone())
414                .collect();
415
416            return Ok(Some(DetectedPattern {
417                name: "Observer Pattern".to_string(),
418                category: PatternCategory::Design,
419                confidence,
420                locations,
421                description: "Project uses observer pattern for event notification".to_string(),
422            }));
423        }
424
425        Ok(None)
426    }
427
428    /// Detects strategy pattern
429    fn detect_strategy_pattern(
430        &self,
431        scan_result: &ScanResult,
432    ) -> Result<Option<DetectedPattern>, ResearchError> {
433        // Check for strategy-related naming
434        let strategy_count = scan_result
435            .files
436            .iter()
437            .filter(|f| {
438                f.path
439                    .file_name()
440                    .and_then(|n| n.to_str())
441                    .map(|n| n.to_lowercase().contains("strategy"))
442                    .unwrap_or(false)
443            })
444            .count();
445
446        if strategy_count >= 1 {
447            let confidence = 0.7;
448            let locations = scan_result
449                .files
450                .iter()
451                .filter(|f| {
452                    f.path
453                        .file_name()
454                        .and_then(|n| n.to_str())
455                        .map(|n| n.to_lowercase().contains("strategy"))
456                        .unwrap_or(false)
457                })
458                .map(|f| f.path.clone())
459                .collect();
460
461            return Ok(Some(DetectedPattern {
462                name: "Strategy Pattern".to_string(),
463                category: PatternCategory::Design,
464                confidence,
465                locations,
466                description: "Project uses strategy pattern for algorithm selection".to_string(),
467            }));
468        }
469
470        Ok(None)
471    }
472
473    /// Detects singleton pattern
474    fn detect_singleton_pattern(
475        &self,
476        scan_result: &ScanResult,
477    ) -> Result<Option<DetectedPattern>, ResearchError> {
478        // Check for singleton-related naming
479        let singleton_count = scan_result
480            .files
481            .iter()
482            .filter(|f| {
483                f.path
484                    .file_name()
485                    .and_then(|n| n.to_str())
486                    .map(|n| n.to_lowercase().contains("singleton"))
487                    .unwrap_or(false)
488            })
489            .count();
490
491        if singleton_count >= 1 {
492            let confidence = 0.75;
493            let locations = scan_result
494                .files
495                .iter()
496                .filter(|f| {
497                    f.path
498                        .file_name()
499                        .and_then(|n| n.to_str())
500                        .map(|n| n.to_lowercase().contains("singleton"))
501                        .unwrap_or(false)
502                })
503                .map(|f| f.path.clone())
504                .collect();
505
506            return Ok(Some(DetectedPattern {
507                name: "Singleton Pattern".to_string(),
508                category: PatternCategory::Design,
509                confidence,
510                locations,
511                description: "Project uses singleton pattern for single instance management"
512                    .to_string(),
513            }));
514        }
515
516        Ok(None)
517    }
518}
519
520impl Default for PatternDetector {
521    fn default() -> Self {
522        Self::new()
523    }
524}
525
526#[cfg(test)]
527mod tests {
528    use super::*;
529
530    #[test]
531    fn test_pattern_detector_creation() {
532        let detector = PatternDetector::new();
533        assert_eq!(detector.confidence_threshold, 0.5);
534    }
535
536    #[test]
537    fn test_pattern_detector_with_threshold() {
538        let detector = PatternDetector::with_threshold(0.8);
539        assert_eq!(detector.confidence_threshold, 0.8);
540    }
541
542    #[test]
543    fn test_pattern_detector_threshold_clamping() {
544        let detector_low = PatternDetector::with_threshold(-0.5);
545        assert_eq!(detector_low.confidence_threshold, 0.0);
546
547        let detector_high = PatternDetector::with_threshold(1.5);
548        assert_eq!(detector_high.confidence_threshold, 1.0);
549    }
550
551    #[test]
552    fn test_pattern_detector_default() {
553        let detector = PatternDetector::default();
554        assert_eq!(detector.confidence_threshold, 0.5);
555    }
556}