1use crate::codebase_scanner::ScanResult;
4use crate::models::{DetectedPattern, PatternCategory};
5use crate::ResearchError;
6use std::collections::HashMap;
7
8#[derive(Debug, Clone)]
10pub struct PatternDetector {
11 pub confidence_threshold: f32,
13}
14
15impl PatternDetector {
16 pub fn new() -> Self {
18 Self {
19 confidence_threshold: 0.5,
20 }
21 }
22
23 pub fn with_threshold(confidence_threshold: f32) -> Self {
25 Self {
26 confidence_threshold: confidence_threshold.clamp(0.0, 1.0),
27 }
28 }
29
30 pub fn detect(&self, scan_result: &ScanResult) -> Result<Vec<DetectedPattern>, ResearchError> {
40 let mut patterns = Vec::new();
41
42 let arch_patterns = self.detect_architectural_patterns(scan_result)?;
44 patterns.extend(arch_patterns);
45
46 let coding_patterns = self.detect_coding_patterns(scan_result)?;
48 patterns.extend(coding_patterns);
49
50 patterns.retain(|p| p.confidence >= self.confidence_threshold);
52
53 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 fn detect_architectural_patterns(
65 &self,
66 scan_result: &ScanResult,
67 ) -> Result<Vec<DetectedPattern>, ResearchError> {
68 let mut patterns = Vec::new();
69
70 if let Some(pattern) = self.detect_layered_architecture(scan_result)? {
72 patterns.push(pattern);
73 }
74
75 if let Some(pattern) = self.detect_microservices_pattern(scan_result)? {
77 patterns.push(pattern);
78 }
79
80 if let Some(pattern) = self.detect_event_driven_pattern(scan_result)? {
82 patterns.push(pattern);
83 }
84
85 if let Some(pattern) = self.detect_monolithic_pattern(scan_result)? {
87 patterns.push(pattern);
88 }
89
90 Ok(patterns)
91 }
92
93 fn detect_coding_patterns(
95 &self,
96 scan_result: &ScanResult,
97 ) -> Result<Vec<DetectedPattern>, ResearchError> {
98 let mut patterns = Vec::new();
99
100 if let Some(pattern) = self.detect_factory_pattern(scan_result)? {
102 patterns.push(pattern);
103 }
104
105 if let Some(pattern) = self.detect_observer_pattern(scan_result)? {
107 patterns.push(pattern);
108 }
109
110 if let Some(pattern) = self.detect_strategy_pattern(scan_result)? {
112 patterns.push(pattern);
113 }
114
115 if let Some(pattern) = self.detect_singleton_pattern(scan_result)? {
117 patterns.push(pattern);
118 }
119
120 Ok(patterns)
121 }
122
123 fn detect_layered_architecture(
125 &self,
126 scan_result: &ScanResult,
127 ) -> Result<Option<DetectedPattern>, ResearchError> {
128 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 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 fn detect_microservices_pattern(
195 &self,
196 scan_result: &ScanResult,
197 ) -> Result<Option<DetectedPattern>, ResearchError> {
198 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 fn detect_event_driven_pattern(
232 &self,
233 scan_result: &ScanResult,
234 ) -> Result<Option<DetectedPattern>, ResearchError> {
235 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 fn detect_monolithic_pattern(
283 &self,
284 scan_result: &ScanResult,
285 ) -> Result<Option<DetectedPattern>, ResearchError> {
286 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 fn detect_factory_pattern(
330 &self,
331 scan_result: &ScanResult,
332 ) -> Result<Option<DetectedPattern>, ResearchError> {
333 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 fn detect_observer_pattern(
375 &self,
376 scan_result: &ScanResult,
377 ) -> Result<Option<DetectedPattern>, ResearchError> {
378 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 fn detect_strategy_pattern(
430 &self,
431 scan_result: &ScanResult,
432 ) -> Result<Option<DetectedPattern>, ResearchError> {
433 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 fn detect_singleton_pattern(
475 &self,
476 scan_result: &ScanResult,
477 ) -> Result<Option<DetectedPattern>, ResearchError> {
478 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}