1use std::collections::{HashMap, HashSet};
46use std::path::{Path, PathBuf};
47use std::sync::Mutex;
48
49use fxhash::FxHashMap;
50use rayon::prelude::*;
51use serde::{Deserialize, Serialize};
52use tracing::{debug, trace};
53
54use crate::ast::{ClassInfo, FunctionInfo, ModuleInfo};
55use crate::callgraph::scanner::{ProjectScanner, ScanConfig};
56use crate::error::Result;
57use crate::lang::LanguageRegistry;
58
59#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
65#[serde(rename_all = "snake_case")]
66pub enum PatternCategory {
67 Creational,
69 Structural,
71 Behavioral,
73}
74
75impl std::fmt::Display for PatternCategory {
76 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
77 match self {
78 Self::Creational => write!(f, "Creational"),
79 Self::Structural => write!(f, "Structural"),
80 Self::Behavioral => write!(f, "Behavioral"),
81 }
82 }
83}
84
85#[derive(Debug, Clone, Serialize, Deserialize)]
87#[serde(tag = "type", rename_all = "snake_case")]
88pub enum DesignPattern {
89 Singleton {
96 class: String,
98 instance_method: String,
100 instance_field: Option<String>,
102 private_constructor: bool,
104 },
105
106 Factory {
113 class: String,
115 create_methods: Vec<String>,
117 products: Vec<String>,
119 is_abstract: bool,
121 },
122
123 Builder {
130 class: String,
132 build_method: String,
134 setters: Vec<String>,
136 target_type: Option<String>,
138 },
139
140 Adapter {
147 class: String,
149 adaptee: String,
151 target_interface: Option<String>,
153 },
154
155 Decorator {
162 class: String,
164 base_interface: String,
166 component_field: Option<String>,
168 },
169
170 Proxy {
177 class: String,
179 subject: String,
181 proxy_type: Option<String>,
183 },
184
185 Observer {
192 subject: String,
194 observers: Vec<String>,
196 notify_method: String,
198 subscribe_methods: Vec<String>,
200 },
201
202 Strategy {
209 interface: String,
211 implementations: Vec<String>,
213 context: Option<String>,
215 },
216
217 Command {
224 interface: String,
226 commands: Vec<String>,
228 execute_method: String,
230 has_undo: bool,
232 },
233
234 DependencyInjection {
241 class: String,
243 dependencies: Vec<(String, String)>,
245 },
246
247 Repository {
254 class: String,
256 entity_type: Option<String>,
258 methods: Vec<String>,
260 },
261}
262
263impl DesignPattern {
264 #[must_use]
266 pub fn name(&self) -> &'static str {
267 match self {
268 Self::Singleton { .. } => "Singleton",
269 Self::Factory { .. } => "Factory",
270 Self::Builder { .. } => "Builder",
271 Self::Adapter { .. } => "Adapter",
272 Self::Decorator { .. } => "Decorator",
273 Self::Proxy { .. } => "Proxy",
274 Self::Observer { .. } => "Observer",
275 Self::Strategy { .. } => "Strategy",
276 Self::Command { .. } => "Command",
277 Self::DependencyInjection { .. } => "Dependency Injection",
278 Self::Repository { .. } => "Repository",
279 }
280 }
281
282 #[must_use]
284 pub fn category(&self) -> PatternCategory {
285 match self {
286 Self::Singleton { .. } | Self::Factory { .. } | Self::Builder { .. } => {
287 PatternCategory::Creational
288 }
289 Self::Adapter { .. } | Self::Decorator { .. } | Self::Proxy { .. } => {
290 PatternCategory::Structural
291 }
292 Self::Observer { .. }
293 | Self::Strategy { .. }
294 | Self::Command { .. }
295 | Self::DependencyInjection { .. }
296 | Self::Repository { .. } => PatternCategory::Behavioral,
297 }
298 }
299
300 #[must_use]
302 pub fn primary_class(&self) -> &str {
303 match self {
304 Self::Singleton { class, .. }
305 | Self::Factory { class, .. }
306 | Self::Builder { class, .. }
307 | Self::Adapter { class, .. }
308 | Self::Decorator { class, .. }
309 | Self::Proxy { class, .. }
310 | Self::Observer { subject: class, .. }
311 | Self::DependencyInjection { class, .. }
312 | Self::Repository { class, .. } => class,
313 Self::Strategy { interface, .. } | Self::Command { interface, .. } => interface,
314 }
315 }
316}
317
318#[derive(Debug, Clone, Serialize, Deserialize)]
320pub struct Location {
321 pub file: PathBuf,
323 pub line: usize,
325 pub end_line: Option<usize>,
327 pub name: String,
329 pub kind: String,
331}
332
333impl Location {
334 #[must_use]
336 pub fn for_class(file: PathBuf, class: &ClassInfo) -> Self {
337 Self {
338 file,
339 line: class.line_number,
340 end_line: class.end_line_number,
341 name: class.name.clone(),
342 kind: "class".to_string(),
343 }
344 }
345
346 #[must_use]
348 pub fn for_function(file: PathBuf, func: &FunctionInfo) -> Self {
349 Self {
350 file,
351 line: func.line_number,
352 end_line: func.end_line_number,
353 name: func.name.clone(),
354 kind: if func.is_method { "method" } else { "function" }.to_string(),
355 }
356 }
357}
358
359#[derive(Debug, Clone, Serialize, Deserialize)]
361pub struct PatternMatch {
362 pub pattern: DesignPattern,
364 pub confidence: f64,
370 pub locations: Vec<Location>,
372 #[serde(skip_serializing_if = "Option::is_none")]
374 pub note: Option<String>,
375 #[serde(skip_serializing_if = "Vec::is_empty")]
377 pub evidence: Vec<String>,
378}
379
380impl PatternMatch {
381 #[must_use]
383 pub fn new(pattern: DesignPattern, confidence: f64) -> Self {
384 Self {
385 pattern,
386 confidence,
387 locations: Vec::new(),
388 note: None,
389 evidence: Vec::new(),
390 }
391 }
392
393 #[must_use]
395 pub fn with_location(mut self, location: Location) -> Self {
396 self.locations.push(location);
397 self
398 }
399
400 #[must_use]
402 pub fn with_locations(mut self, locations: Vec<Location>) -> Self {
403 self.locations.extend(locations);
404 self
405 }
406
407 #[must_use]
409 pub fn with_note(mut self, note: impl Into<String>) -> Self {
410 self.note = Some(note.into());
411 self
412 }
413
414 #[must_use]
416 pub fn with_evidence(mut self, evidence: Vec<String>) -> Self {
417 self.evidence = evidence;
418 self
419 }
420
421 #[must_use]
423 pub fn primary_location(&self) -> Option<&Location> {
424 self.locations.first()
425 }
426}
427
428#[derive(Debug, Clone, Serialize, Deserialize)]
434pub struct PatternConfig {
435 pub min_confidence: f64,
438
439 pub patterns: Vec<String>,
441
442 pub language: Option<String>,
444
445 pub max_file_size: u64,
447
448 pub include_tests: bool,
450
451 pub include_generated: bool,
453
454 pub detect_modern_patterns: bool,
456
457 pub language_specific: bool,
459}
460
461impl Default for PatternConfig {
462 fn default() -> Self {
463 Self {
464 min_confidence: 0.5,
465 patterns: Vec::new(),
466 language: None,
467 max_file_size: 1024 * 1024, include_tests: false,
469 include_generated: false,
470 detect_modern_patterns: true,
471 language_specific: true,
472 }
473 }
474}
475
476impl PatternConfig {
477 #[must_use]
479 pub fn for_pattern(pattern: impl Into<String>) -> Self {
480 Self {
481 patterns: vec![pattern.into()],
482 ..Default::default()
483 }
484 }
485
486 #[must_use]
488 pub fn with_min_confidence(mut self, confidence: f64) -> Self {
489 self.min_confidence = confidence.clamp(0.0, 1.0);
490 self
491 }
492
493 #[must_use]
495 pub fn with_language(mut self, lang: impl Into<String>) -> Self {
496 self.language = Some(lang.into());
497 self
498 }
499
500 #[must_use]
502 pub fn with_modern_patterns(mut self, enable: bool) -> Self {
503 self.detect_modern_patterns = enable;
504 self
505 }
506}
507
508#[derive(Debug, Clone, Default, Serialize, Deserialize)]
514pub struct PatternStats {
515 pub files_scanned: usize,
517 pub files_with_patterns: usize,
519 pub patterns_detected: usize,
521 pub by_category: HashMap<String, usize>,
523 pub by_type: HashMap<String, usize>,
525 pub average_confidence: f64,
527 pub files_skipped: usize,
529}
530
531#[derive(Debug, Clone, Serialize, Deserialize)]
537pub struct PatternAnalysis {
538 pub path: PathBuf,
540 pub patterns: Vec<PatternMatch>,
542 pub stats: PatternStats,
544 #[serde(skip_serializing_if = "Vec::is_empty")]
546 pub errors: Vec<String>,
547}
548
549impl PatternAnalysis {
550 #[must_use]
552 pub fn new(path: PathBuf) -> Self {
553 Self {
554 path,
555 patterns: Vec::new(),
556 stats: PatternStats::default(),
557 errors: Vec::new(),
558 }
559 }
560
561 #[must_use]
563 pub fn patterns_of_type(&self, pattern_name: &str) -> Vec<&PatternMatch> {
564 self.patterns
565 .iter()
566 .filter(|p| p.pattern.name().eq_ignore_ascii_case(pattern_name))
567 .collect()
568 }
569
570 #[must_use]
572 pub fn patterns_in_category(&self, category: PatternCategory) -> Vec<&PatternMatch> {
573 self.patterns
574 .iter()
575 .filter(|p| p.pattern.category() == category)
576 .collect()
577 }
578
579 #[must_use]
581 pub fn high_confidence_patterns(&self) -> Vec<&PatternMatch> {
582 self.patterns
583 .iter()
584 .filter(|p| p.confidence >= 0.7)
585 .collect()
586 }
587}
588
589#[derive(Debug, Clone, thiserror::Error)]
595pub enum PatternError {
596 #[error("Failed to scan project: {0}")]
598 ScanError(String),
599
600 #[error("Failed to parse file {path}: {message}")]
602 ParseError { path: PathBuf, message: String },
603
604 #[error("Invalid configuration: {0}")]
606 ConfigError(String),
607
608 #[error("IO error: {0}")]
610 IoError(String),
611}
612
613impl From<std::io::Error> for PatternError {
614 fn from(e: std::io::Error) -> Self {
615 Self::IoError(e.to_string())
616 }
617}
618
619pub struct PatternDetector {
625 config: PatternConfig,
626}
627
628impl PatternDetector {
629 #[must_use]
631 pub fn new(config: PatternConfig) -> Self {
632 Self { config }
633 }
634
635 pub fn detect(&self, path: impl AsRef<Path>) -> Result<PatternAnalysis> {
637 let path = path.as_ref();
638 let abs_path = if path.is_absolute() {
639 path.to_path_buf()
640 } else {
641 std::env::current_dir()?.join(path)
642 };
643
644 let mut analysis = PatternAnalysis::new(abs_path.clone());
645
646 if abs_path.is_file() {
647 self.detect_in_file(&abs_path, &mut analysis)?;
648 } else {
649 self.detect_in_directory(&abs_path, &mut analysis)?;
650 }
651
652 self.calculate_stats(&mut analysis);
654
655 analysis.patterns.retain(|p| p.confidence >= self.config.min_confidence);
657
658 if !self.config.patterns.is_empty() {
660 let patterns_lower: Vec<String> = self.config.patterns
661 .iter()
662 .map(|p| p.to_lowercase())
663 .collect();
664 analysis.patterns.retain(|p| {
665 patterns_lower.iter().any(|name| {
666 p.pattern.name().to_lowercase().contains(name)
667 })
668 });
669 }
670
671 analysis.patterns.sort_by(|a, b| {
673 b.confidence.partial_cmp(&a.confidence).unwrap_or(std::cmp::Ordering::Equal)
674 });
675
676 Ok(analysis)
677 }
678
679 fn detect_in_directory(&self, path: &Path, analysis: &mut PatternAnalysis) -> Result<()> {
681 let path_str = path
682 .to_str()
683 .ok_or_else(|| crate::error::BrrrError::InvalidArgument("Invalid path encoding".to_string()))?;
684
685 let scanner = ProjectScanner::new(path_str)?;
686
687 let scan_config = if let Some(ref lang) = self.config.language {
688 ScanConfig::for_language(lang)
689 } else {
690 ScanConfig::default()
691 };
692
693 let scan_result = scanner.scan_with_config(&scan_config)?;
694
695 let patterns: Mutex<Vec<PatternMatch>> = Mutex::new(Vec::new());
696 let errors: Mutex<Vec<String>> = Mutex::new(Vec::new());
697 let files_scanned: Mutex<usize> = Mutex::new(0);
698 let files_skipped: Mutex<usize> = Mutex::new(0);
699
700 scan_result.files.par_iter().for_each(|file| {
701 if !scan_result.metadata.is_empty() {
703 if let Some(file_meta) = scan_result.metadata.iter().find(|m| m.path == *file) {
704 if file_meta.size > self.config.max_file_size {
705 let mut skipped = files_skipped.lock().unwrap();
706 *skipped += 1;
707 return;
708 }
709 }
710 }
711
712 if !self.config.include_tests && is_test_file(file) {
714 let mut skipped = files_skipped.lock().unwrap();
715 *skipped += 1;
716 return;
717 }
718
719 if !self.config.include_generated && is_generated_file(file) {
721 let mut skipped = files_skipped.lock().unwrap();
722 *skipped += 1;
723 return;
724 }
725
726 let mut file_analysis = PatternAnalysis::new(file.clone());
727 match self.detect_in_file(file, &mut file_analysis) {
728 Ok(()) => {
729 let mut p = patterns.lock().unwrap();
730 p.extend(file_analysis.patterns);
731 let mut scanned = files_scanned.lock().unwrap();
732 *scanned += 1;
733 }
734 Err(e) => {
735 let mut errs = errors.lock().unwrap();
736 errs.push(format!("{}: {}", file.display(), e));
737 }
738 }
739 });
740
741 analysis.patterns = patterns.into_inner().unwrap();
742 analysis.errors = errors.into_inner().unwrap();
743 analysis.stats.files_scanned = *files_scanned.lock().unwrap();
744 analysis.stats.files_skipped = *files_skipped.lock().unwrap();
745
746 Ok(())
747 }
748
749 fn detect_in_file(&self, path: &Path, analysis: &mut PatternAnalysis) -> Result<()> {
751 let registry = LanguageRegistry::global();
752 let lang = registry.detect_language(path)
753 .or_else(|| self.config.language.as_ref().and_then(|l| registry.get_by_name(l)));
754
755 if lang.is_none() {
756 trace!("Unsupported language for file: {}", path.display());
757 return Ok(());
758 }
759
760 let module = match crate::ast::AstExtractor::extract_file(path) {
762 Ok(m) => m,
763 Err(e) => {
764 debug!("Failed to extract AST from {}: {}", path.display(), e);
765 return Ok(());
766 }
767 };
768
769 self.detect_in_module(&module, path, analysis);
771
772 Ok(())
773 }
774
775 fn detect_in_module(&self, module: &ModuleInfo, path: &Path, analysis: &mut PatternAnalysis) {
777 let lang = module.language.as_str();
778
779 for class in &module.classes {
781 if let Some(pattern_match) = self.detect_singleton(class, path, lang) {
782 analysis.patterns.push(pattern_match);
783 }
784
785 if let Some(pattern_match) = self.detect_builder(class, path, lang) {
787 analysis.patterns.push(pattern_match);
788 }
789
790 if let Some(pattern_match) = self.detect_factory(class, &module.classes, path, lang) {
792 analysis.patterns.push(pattern_match);
793 }
794
795 if let Some(pattern_match) = self.detect_observer(class, path, lang) {
797 analysis.patterns.push(pattern_match);
798 }
799
800 if let Some(pattern_match) = self.detect_decorator(class, &module.classes, path, lang) {
802 analysis.patterns.push(pattern_match);
803 }
804
805 if let Some(pattern_match) = self.detect_adapter(class, path, lang) {
807 analysis.patterns.push(pattern_match);
808 }
809
810 if let Some(pattern_match) = self.detect_proxy(class, path, lang) {
812 analysis.patterns.push(pattern_match);
813 }
814
815 if let Some(pattern_match) = self.detect_command(class, &module.classes, path, lang) {
817 analysis.patterns.push(pattern_match);
818 }
819
820 if self.config.detect_modern_patterns {
822 if let Some(pattern_match) = self.detect_dependency_injection(class, path, lang) {
824 analysis.patterns.push(pattern_match);
825 }
826
827 if let Some(pattern_match) = self.detect_repository(class, path, lang) {
829 analysis.patterns.push(pattern_match);
830 }
831 }
832 }
833
834 let strategies = self.detect_strategy(&module.classes, path, lang);
836 analysis.patterns.extend(strategies);
837 }
838
839 fn detect_singleton(&self, class: &ClassInfo, path: &Path, lang: &str) -> Option<PatternMatch> {
841 let mut confidence: f64 = 0.0;
842 let mut evidence = Vec::new();
843 let mut instance_method = String::new();
844 let mut instance_field = None;
845 let mut private_constructor = false;
846
847 let class_name_lower = class.name.to_lowercase();
848
849 if class_name_lower.contains("singleton") {
851 confidence += 0.3;
852 evidence.push("Class name contains 'singleton'".to_string());
853 }
854
855 let private_ctor = class.methods.iter().find(|m| {
857 let name = m.name.to_lowercase();
858 (name == "__init__" || name == "new" || name == class_name_lower ||
859 name == "init" || name == "constructor") &&
860 m.decorators.iter().any(|d| d.contains("private")) ||
861 matches!(lang, "java" | "typescript" | "csharp") &&
862 !m.params.iter().any(|p| !p.contains("self") && !p.contains("this"))
863 });
864
865 let python_new = class.methods.iter().find(|m| m.name == "__new__");
867 if python_new.is_some() && lang == "python" {
868 confidence += 0.2;
869 evidence.push("Has __new__ method (Python singleton idiom)".to_string());
870 }
871
872 if private_ctor.is_some() {
873 confidence += 0.2;
874 evidence.push("Has private constructor".to_string());
875 private_constructor = true;
876 }
877
878 let instance_field_names = ["_instance", "instance", "_singleton", "singleton",
880 "INSTANCE", "_INSTANCE", "shared", "sharedInstance"];
881 for field in &class.fields {
882 if instance_field_names.iter().any(|n| field.name.eq_ignore_ascii_case(n)) {
883 if field.is_static {
884 confidence += 0.25;
885 evidence.push(format!("Has static instance field: {}", field.name));
886 instance_field = Some(field.name.clone());
887 } else {
888 confidence += 0.1;
889 evidence.push(format!("Has instance field (non-static): {}", field.name));
890 instance_field = Some(field.name.clone());
891 }
892 }
893 }
894
895 let getter_patterns = ["getinstance", "get_instance", "instance", "shared",
897 "sharedinstance", "default", "current", "singleton"];
898 for method in &class.methods {
899 let method_lower = method.name.to_lowercase();
900 if getter_patterns.iter().any(|p| method_lower == *p || method_lower.contains(p)) {
901 let is_static = method.decorators.iter().any(|d| {
903 d.contains("static") || d.contains("classmethod")
904 }) || !method.params.iter().any(|p| p.contains("self"));
905
906 if is_static {
907 confidence += 0.3;
908 evidence.push(format!("Has static getter method: {}", method.name));
909 instance_method = method.name.clone();
910 } else {
911 confidence += 0.1;
912 evidence.push(format!("Has getter method (non-static): {}", method.name));
913 if instance_method.is_empty() {
914 instance_method = method.name.clone();
915 }
916 }
917 }
918 }
919
920 if lang == "rust" {
922 for field in &class.fields {
923 let field_type = field.field_type.as_deref().unwrap_or("");
924 if field_type.contains("Lazy") || field_type.contains("OnceCell") ||
925 field_type.contains("LazyLock") {
926 confidence += 0.3;
927 evidence.push(format!("Uses lazy initialization: {}", field_type));
928 }
929 }
930 }
931
932 if confidence >= 0.4 {
933 if instance_method.is_empty() {
934 instance_method = "getInstance".to_string();
935 }
936
937 let pattern = DesignPattern::Singleton {
938 class: class.name.clone(),
939 instance_method,
940 instance_field,
941 private_constructor,
942 };
943
944 let note = if confidence < 0.7 {
945 Some("Partial singleton implementation detected".to_string())
946 } else {
947 None
948 };
949
950 Some(PatternMatch::new(pattern, confidence.min(1.0))
951 .with_location(Location::for_class(path.to_path_buf(), class))
952 .with_note(note.unwrap_or_default())
953 .with_evidence(evidence))
954 } else {
955 None
956 }
957 }
958
959 fn detect_builder(&self, class: &ClassInfo, path: &Path, lang: &str) -> Option<PatternMatch> {
961 let mut confidence: f64 = 0.0;
962 let mut evidence = Vec::new();
963 let mut build_method = String::new();
964 let mut setters = Vec::new();
965 let mut target_type = None;
966
967 let class_name_lower = class.name.to_lowercase();
968
969 if class_name_lower.ends_with("builder") {
971 confidence += 0.35;
972 evidence.push("Class name ends with 'Builder'".to_string());
973 let target = class.name.strip_suffix("Builder")
975 .or_else(|| class.name.strip_suffix("builder"));
976 if let Some(t) = target {
977 if !t.is_empty() {
978 target_type = Some(t.to_string());
979 }
980 }
981 }
982
983 let build_methods = ["build", "create", "make", "construct", "get_result", "getResult"];
985 for method in &class.methods {
986 let method_lower = method.name.to_lowercase();
987 if build_methods.iter().any(|b| method_lower == *b) {
988 confidence += 0.25;
989 evidence.push(format!("Has build method: {}", method.name));
990 build_method = method.name.clone();
991
992 if let Some(ref ret) = method.return_type {
994 if !ret.contains("Self") && !ret.contains(&class.name) {
995 target_type = Some(ret.clone());
996 }
997 }
998 }
999 }
1000
1001 let setter_prefixes = ["with_", "set_", "add_", "set", "with", "add"];
1003 for method in &class.methods {
1004 let method_lower = method.name.to_lowercase();
1005 let is_setter = setter_prefixes.iter().any(|p| method_lower.starts_with(p));
1006
1007 let returns_self = method.return_type.as_ref().map(|r| {
1009 r.contains("Self") || r.contains(&class.name) || r == "&mut Self"
1010 }).unwrap_or(false);
1011
1012 if is_setter && returns_self {
1013 confidence += 0.05;
1014 setters.push(method.name.clone());
1015 } else if is_setter {
1016 setters.push(method.name.clone());
1018 }
1019 }
1020
1021 if setters.len() >= 3 {
1023 confidence += 0.15;
1024 evidence.push(format!("Has {} setter/configuration methods", setters.len()));
1025 }
1026
1027 if class.bases.iter().any(|b| b.to_lowercase().contains("step")) {
1029 confidence += 0.1;
1030 evidence.push("Implements step builder interface".to_string());
1031 }
1032
1033 if confidence >= 0.4 && !setters.is_empty() {
1034 if build_method.is_empty() {
1035 build_method = "build".to_string();
1036 }
1037
1038 let pattern = DesignPattern::Builder {
1039 class: class.name.clone(),
1040 build_method,
1041 setters,
1042 target_type,
1043 };
1044
1045 Some(PatternMatch::new(pattern, confidence.min(1.0))
1046 .with_location(Location::for_class(path.to_path_buf(), class))
1047 .with_evidence(evidence))
1048 } else {
1049 None
1050 }
1051 }
1052
1053 fn detect_factory(&self, class: &ClassInfo, all_classes: &[ClassInfo],
1055 path: &Path, _lang: &str) -> Option<PatternMatch> {
1056 let mut confidence: f64 = 0.0;
1057 let mut evidence = Vec::new();
1058 let mut create_methods = Vec::new();
1059 let mut products = Vec::new();
1060 let is_abstract = class.decorators.iter().any(|d| d.contains("abstract")) ||
1061 class.bases.iter().any(|b| b.contains("ABC") || b.contains("Abstract"));
1062
1063 let class_name_lower = class.name.to_lowercase();
1064
1065 if class_name_lower.ends_with("factory") {
1067 confidence += 0.35;
1068 evidence.push("Class name ends with 'Factory'".to_string());
1069 }
1070
1071 let factory_prefixes = ["create", "make", "build", "new", "get", "produce", "manufacture"];
1073 for method in &class.methods {
1074 let method_lower = method.name.to_lowercase();
1075 if factory_prefixes.iter().any(|p| method_lower.starts_with(p)) {
1076 if let Some(ref ret_type) = method.return_type {
1078 let ret_lower = ret_type.to_lowercase();
1079 let returns_concrete = all_classes.iter()
1081 .any(|c| c.name.eq_ignore_ascii_case(ret_type));
1082
1083 if !returns_concrete ||
1084 ret_lower.contains("interface") ||
1085 ret_lower.contains("abstract") ||
1086 ret_lower.contains("protocol") {
1087 confidence += 0.15;
1088 create_methods.push(method.name.clone());
1089 if !products.contains(ret_type) {
1090 products.push(ret_type.clone());
1091 }
1092 }
1093 }
1094
1095 if !create_methods.contains(&method.name) {
1097 create_methods.push(method.name.clone());
1098 confidence += 0.05;
1099 }
1100 }
1101 }
1102
1103 evidence.push(format!("Has {} factory methods", create_methods.len()));
1104
1105 if is_abstract && !create_methods.is_empty() {
1107 confidence += 0.2;
1108 evidence.push("Abstract factory (abstract class with factory methods)".to_string());
1109 }
1110
1111 let has_type_param = class.methods.iter().any(|m| {
1113 m.params.iter().any(|p| {
1114 let p_lower = p.to_lowercase();
1115 p_lower.contains("type") || p_lower.contains("kind") || p_lower.contains("variant")
1116 })
1117 });
1118 if has_type_param {
1119 confidence += 0.1;
1120 evidence.push("Has type/kind parameter (parameterized factory)".to_string());
1121 }
1122
1123 if confidence >= 0.4 && !create_methods.is_empty() {
1124 let pattern = DesignPattern::Factory {
1125 class: class.name.clone(),
1126 create_methods,
1127 products,
1128 is_abstract,
1129 };
1130
1131 Some(PatternMatch::new(pattern, confidence.min(1.0))
1132 .with_location(Location::for_class(path.to_path_buf(), class))
1133 .with_evidence(evidence))
1134 } else {
1135 None
1136 }
1137 }
1138
1139 fn detect_observer(&self, class: &ClassInfo, path: &Path, _lang: &str) -> Option<PatternMatch> {
1141 let mut confidence: f64 = 0.0;
1142 let mut evidence = Vec::new();
1143 let mut observers = Vec::new();
1144 let mut notify_method = String::new();
1145 let mut subscribe_methods = Vec::new();
1146
1147 let class_name_lower = class.name.to_lowercase();
1148
1149 if class_name_lower.contains("observable") ||
1151 class_name_lower.contains("subject") ||
1152 class_name_lower.contains("publisher") ||
1153 class_name_lower.contains("emitter") {
1154 confidence += 0.25;
1155 evidence.push("Class name suggests observable pattern".to_string());
1156 }
1157
1158 let collection_patterns = ["listener", "observer", "subscriber", "handler", "callback"];
1160 for field in &class.fields {
1161 let field_lower = field.name.to_lowercase();
1162 let type_lower = field.field_type.as_ref()
1163 .map(|t| t.to_lowercase())
1164 .unwrap_or_default();
1165
1166 if collection_patterns.iter().any(|p| field_lower.contains(p)) ||
1167 (type_lower.contains("list") || type_lower.contains("vec") ||
1168 type_lower.contains("set") || type_lower.contains("array")) &&
1169 collection_patterns.iter().any(|p| type_lower.contains(p)) {
1170 confidence += 0.2;
1171 evidence.push(format!("Has observer collection: {}", field.name));
1172
1173 if let Some(ref ft) = field.field_type {
1175 if let Some(start) = ft.find('<') {
1177 if let Some(end) = ft.find('>') {
1178 let inner = &ft[start+1..end];
1179 if !observers.contains(&inner.to_string()) {
1180 observers.push(inner.to_string());
1181 }
1182 }
1183 }
1184 }
1185 }
1186 }
1187
1188 let subscribe_patterns = ["add", "register", "subscribe", "attach", "on"];
1190 let unsubscribe_patterns = ["remove", "unregister", "unsubscribe", "detach", "off"];
1191
1192 for method in &class.methods {
1193 let method_lower = method.name.to_lowercase();
1194
1195 if subscribe_patterns.iter().any(|p| method_lower.contains(p)) &&
1196 collection_patterns.iter().any(|p| method_lower.contains(p)) {
1197 confidence += 0.15;
1198 subscribe_methods.push(method.name.clone());
1199 evidence.push(format!("Has subscribe method: {}", method.name));
1200 }
1201
1202 if unsubscribe_patterns.iter().any(|p| method_lower.contains(p)) &&
1203 collection_patterns.iter().any(|p| method_lower.contains(p)) {
1204 confidence += 0.1;
1205 subscribe_methods.push(method.name.clone());
1206 }
1207 }
1208
1209 let notify_patterns = ["notify", "emit", "fire", "trigger", "dispatch", "publish", "broadcast"];
1211 for method in &class.methods {
1212 let method_lower = method.name.to_lowercase();
1213 if notify_patterns.iter().any(|p| method_lower.contains(p)) {
1214 confidence += 0.2;
1215 evidence.push(format!("Has notify method: {}", method.name));
1216 notify_method = method.name.clone();
1217 }
1218 }
1219
1220 if confidence >= 0.4 && !notify_method.is_empty() {
1221 let pattern = DesignPattern::Observer {
1222 subject: class.name.clone(),
1223 observers,
1224 notify_method,
1225 subscribe_methods,
1226 };
1227
1228 Some(PatternMatch::new(pattern, confidence.min(1.0))
1229 .with_location(Location::for_class(path.to_path_buf(), class))
1230 .with_evidence(evidence))
1231 } else {
1232 None
1233 }
1234 }
1235
1236 fn detect_decorator(&self, class: &ClassInfo, all_classes: &[ClassInfo],
1238 path: &Path, _lang: &str) -> Option<PatternMatch> {
1239 let mut confidence: f64 = 0.0;
1240 let mut evidence = Vec::new();
1241 let mut base_interface = String::new();
1242 let mut component_field = None;
1243
1244 let class_name_lower = class.name.to_lowercase();
1245
1246 if class_name_lower.contains("decorator") || class_name_lower.contains("wrapper") {
1248 confidence += 0.25;
1249 evidence.push("Class name suggests decorator pattern".to_string());
1250 }
1251
1252 for base in &class.bases {
1254 for field in &class.fields {
1256 if let Some(ref field_type) = field.field_type {
1257 if field_type == base ||
1258 field_type.contains(base) ||
1259 base.contains(field_type) {
1260 confidence += 0.35;
1261 evidence.push(format!("Has wrapped component field: {} of type {}",
1262 field.name, field_type));
1263 base_interface = base.clone();
1264 component_field = Some(field.name.clone());
1265 break;
1266 }
1267 }
1268 }
1269
1270 for method in &class.methods {
1272 let is_ctor = method.name == "__init__" || method.name == "new" ||
1273 method.name.to_lowercase() == class_name_lower ||
1274 method.name == "constructor";
1275 if is_ctor {
1276 for param in &method.params {
1277 if param.contains(base) {
1278 confidence += 0.2;
1279 evidence.push(format!("Constructor accepts base type: {}", base));
1280 }
1281 }
1282 }
1283 }
1284 }
1285
1286 if !base_interface.is_empty() {
1289 if let Some(base_class) = all_classes.iter().find(|c| c.name == base_interface) {
1290 let base_method_names: HashSet<_> = base_class.methods.iter()
1291 .map(|m| m.name.as_str())
1292 .collect();
1293 let class_method_names: HashSet<_> = class.methods.iter()
1294 .map(|m| m.name.as_str())
1295 .collect();
1296
1297 let shared_methods: usize = base_method_names.intersection(&class_method_names).count();
1298 if shared_methods >= 2 {
1299 confidence += 0.1;
1300 evidence.push(format!("Shares {} methods with base interface", shared_methods));
1301 }
1302 }
1303 }
1304
1305 if confidence >= 0.4 && !base_interface.is_empty() {
1306 let pattern = DesignPattern::Decorator {
1307 class: class.name.clone(),
1308 base_interface,
1309 component_field,
1310 };
1311
1312 Some(PatternMatch::new(pattern, confidence.min(1.0))
1313 .with_location(Location::for_class(path.to_path_buf(), class))
1314 .with_evidence(evidence))
1315 } else {
1316 None
1317 }
1318 }
1319
1320 fn detect_adapter(&self, class: &ClassInfo, path: &Path, _lang: &str) -> Option<PatternMatch> {
1322 let mut confidence: f64 = 0.0;
1323 let mut evidence = Vec::new();
1324 let mut adaptee = String::new();
1325 let mut target_interface = None;
1326
1327 let class_name_lower = class.name.to_lowercase();
1328
1329 if class_name_lower.contains("adapter") {
1331 confidence += 0.3;
1332 evidence.push("Class name contains 'Adapter'".to_string());
1333 }
1334
1335 for field in &class.fields {
1338 if let Some(ref field_type) = field.field_type {
1339 let field_type_lower = field_type.to_lowercase();
1340
1341 let is_different_from_bases = class.bases.iter()
1343 .all(|b| !field_type.contains(b) && !b.contains(field_type));
1344
1345 if is_different_from_bases && !class.bases.is_empty() {
1346 confidence += 0.25;
1347 evidence.push(format!("Wraps {} while implementing different interface", field_type));
1348 adaptee = field_type.clone();
1349 target_interface = class.bases.first().cloned();
1350 }
1351
1352 if field_type_lower.contains("adaptee") ||
1354 field.name.to_lowercase().contains("adaptee") ||
1355 field.name.to_lowercase().contains("wrapped") {
1356 confidence += 0.2;
1357 evidence.push(format!("Has adaptee field: {}", field.name));
1358 adaptee = field_type.clone();
1359 }
1360 }
1361 }
1362
1363 if confidence >= 0.4 && !adaptee.is_empty() {
1364 let pattern = DesignPattern::Adapter {
1365 class: class.name.clone(),
1366 adaptee,
1367 target_interface,
1368 };
1369
1370 Some(PatternMatch::new(pattern, confidence.min(1.0))
1371 .with_location(Location::for_class(path.to_path_buf(), class))
1372 .with_evidence(evidence))
1373 } else {
1374 None
1375 }
1376 }
1377
1378 fn detect_proxy(&self, class: &ClassInfo, path: &Path, _lang: &str) -> Option<PatternMatch> {
1380 let mut confidence: f64 = 0.0;
1381 let mut evidence = Vec::new();
1382 let mut subject = String::new();
1383 let mut proxy_type = None;
1384
1385 let class_name_lower = class.name.to_lowercase();
1386
1387 if class_name_lower.contains("proxy") {
1389 confidence += 0.3;
1390 evidence.push("Class name contains 'Proxy'".to_string());
1391 }
1392
1393 if class_name_lower.contains("lazy") {
1395 proxy_type = Some("lazy".to_string());
1396 confidence += 0.1;
1397 } else if class_name_lower.contains("remote") {
1398 proxy_type = Some("remote".to_string());
1399 confidence += 0.1;
1400 } else if class_name_lower.contains("protection") || class_name_lower.contains("secure") {
1401 proxy_type = Some("protection".to_string());
1402 confidence += 0.1;
1403 } else if class_name_lower.contains("cache") || class_name_lower.contains("cached") {
1404 proxy_type = Some("caching".to_string());
1405 confidence += 0.1;
1406 } else if class_name_lower.contains("virtual") {
1407 proxy_type = Some("virtual".to_string());
1408 confidence += 0.1;
1409 }
1410
1411 for field in &class.fields {
1413 if let Some(ref field_type) = field.field_type {
1414 for base in &class.bases {
1416 if field_type == base || field_type.contains(base) {
1417 confidence += 0.3;
1418 evidence.push(format!("Has subject field: {} of type {}", field.name, field_type));
1419 subject = field_type.clone();
1420 break;
1421 }
1422 }
1423 }
1424
1425 let field_lower = field.name.to_lowercase();
1427 if field_lower.contains("subject") || field_lower.contains("real") ||
1428 field_lower.contains("target") || field_lower.contains("wrapped") {
1429 confidence += 0.15;
1430 if let Some(ref ft) = field.field_type {
1431 subject = ft.clone();
1432 }
1433 }
1434 }
1435
1436 let has_lazy = class.methods.iter().any(|m| {
1438 let name_lower = m.name.to_lowercase();
1439 name_lower.contains("lazy") || name_lower.contains("ensure") ||
1440 name_lower.contains("initialize") || name_lower.contains("load")
1441 });
1442 if has_lazy {
1443 confidence += 0.1;
1444 if proxy_type.is_none() {
1445 proxy_type = Some("lazy".to_string());
1446 }
1447 }
1448
1449 let has_access_control = class.methods.iter().any(|m| {
1451 let name_lower = m.name.to_lowercase();
1452 name_lower.contains("check") || name_lower.contains("authorize") ||
1453 name_lower.contains("validate") || name_lower.contains("permission")
1454 });
1455 if has_access_control {
1456 confidence += 0.1;
1457 if proxy_type.is_none() {
1458 proxy_type = Some("protection".to_string());
1459 }
1460 }
1461
1462 if confidence >= 0.4 && !subject.is_empty() {
1463 let pattern = DesignPattern::Proxy {
1464 class: class.name.clone(),
1465 subject,
1466 proxy_type,
1467 };
1468
1469 Some(PatternMatch::new(pattern, confidence.min(1.0))
1470 .with_location(Location::for_class(path.to_path_buf(), class))
1471 .with_evidence(evidence))
1472 } else {
1473 None
1474 }
1475 }
1476
1477 fn detect_command(&self, class: &ClassInfo, all_classes: &[ClassInfo],
1479 path: &Path, _lang: &str) -> Option<PatternMatch> {
1480 let mut confidence: f64 = 0.0;
1481 let mut evidence = Vec::new();
1482 let mut execute_method = String::new();
1483 let mut has_undo = false;
1484
1485 let class_name_lower = class.name.to_lowercase();
1486
1487 if class_name_lower.ends_with("command") || class_name_lower.ends_with("action") ||
1489 class_name_lower.ends_with("handler") {
1490 confidence += 0.25;
1491 evidence.push("Class name suggests command pattern".to_string());
1492 }
1493
1494 let execute_patterns = ["execute", "run", "invoke", "call", "handle", "perform", "do"];
1496 for method in &class.methods {
1497 let method_lower = method.name.to_lowercase();
1498 if execute_patterns.iter().any(|p| method_lower == *p || method_lower.starts_with(p)) {
1499 confidence += 0.25;
1500 evidence.push(format!("Has execute method: {}", method.name));
1501 execute_method = method.name.clone();
1502 }
1503
1504 if method_lower == "undo" || method_lower == "revert" || method_lower == "rollback" {
1506 has_undo = true;
1507 confidence += 0.15;
1508 evidence.push(format!("Has undo method: {}", method.name));
1509 }
1510 }
1511
1512 let has_receiver = class.fields.iter().any(|f| {
1514 let name_lower = f.name.to_lowercase();
1515 name_lower.contains("receiver") || name_lower.contains("target") ||
1516 name_lower.contains("handler")
1517 });
1518 if has_receiver {
1519 confidence += 0.1;
1520 evidence.push("Has receiver field".to_string());
1521 }
1522
1523 let implements_command = class.bases.iter().any(|b| {
1525 let b_lower = b.to_lowercase();
1526 b_lower.contains("command") || b_lower.contains("handler") ||
1527 b_lower.contains("action")
1528 });
1529 if implements_command {
1530 confidence += 0.2;
1531 evidence.push(format!("Implements command interface: {:?}", class.bases));
1532 }
1533
1534 if confidence >= 0.4 && !execute_method.is_empty() {
1535 let interface = class.bases.first().cloned().unwrap_or_else(|| "Command".to_string());
1537 let commands: Vec<String> = all_classes.iter()
1538 .filter(|c| c.name != class.name && c.bases.contains(&interface))
1539 .map(|c| c.name.clone())
1540 .collect();
1541
1542 let pattern = DesignPattern::Command {
1543 interface,
1544 commands,
1545 execute_method,
1546 has_undo,
1547 };
1548
1549 Some(PatternMatch::new(pattern, confidence.min(1.0))
1550 .with_location(Location::for_class(path.to_path_buf(), class))
1551 .with_evidence(evidence))
1552 } else {
1553 None
1554 }
1555 }
1556
1557 fn detect_strategy(&self, classes: &[ClassInfo], path: &Path, _lang: &str) -> Vec<PatternMatch> {
1559 let mut patterns = Vec::new();
1560
1561 let mut interface_impls: FxHashMap<String, Vec<&ClassInfo>> = FxHashMap::default();
1563 for class in classes {
1564 for base in &class.bases {
1565 interface_impls.entry(base.clone())
1566 .or_default()
1567 .push(class);
1568 }
1569 }
1570
1571 for (interface, implementations) in interface_impls {
1573 if implementations.len() < 2 {
1574 continue;
1575 }
1576
1577 let mut confidence: f64 = 0.0;
1578 let mut evidence = Vec::new();
1579
1580 let interface_lower = interface.to_lowercase();
1582 if interface_lower.contains("strategy") || interface_lower.contains("policy") ||
1583 interface_lower.contains("algorithm") || interface_lower.contains("handler") {
1584 confidence += 0.3;
1585 evidence.push("Interface name suggests strategy pattern".to_string());
1586 }
1587
1588 confidence += 0.1 * (implementations.len().min(5) as f64);
1590 evidence.push(format!("{} implementations found", implementations.len()));
1591
1592 if implementations.len() >= 2 {
1594 let first_methods: HashSet<_> = implementations[0].methods.iter()
1595 .map(|m| m.name.as_str())
1596 .collect();
1597 let all_have_same_methods = implementations.iter().skip(1).all(|impl_class| {
1598 let impl_methods: HashSet<_> = impl_class.methods.iter()
1599 .map(|m| m.name.as_str())
1600 .collect();
1601 !first_methods.is_disjoint(&impl_methods)
1602 });
1603 if all_have_same_methods {
1604 confidence += 0.2;
1605 evidence.push("Implementations share method signatures".to_string());
1606 }
1607 }
1608
1609 let context = classes.iter().find(|c| {
1611 c.fields.iter().any(|f| {
1612 f.field_type.as_ref().map(|t| t == &interface).unwrap_or(false)
1613 }) ||
1614 c.methods.iter().any(|m| {
1615 m.params.iter().any(|p| p.contains(&interface))
1616 })
1617 });
1618
1619 if context.is_some() {
1620 confidence += 0.15;
1621 evidence.push("Found context class using strategy".to_string());
1622 }
1623
1624 if confidence >= 0.4 {
1625 let impl_names: Vec<String> = implementations.iter()
1626 .map(|c| c.name.clone())
1627 .collect();
1628
1629 let pattern = DesignPattern::Strategy {
1630 interface: interface.clone(),
1631 implementations: impl_names,
1632 context: context.map(|c| c.name.clone()),
1633 };
1634
1635 let mut locations = Vec::new();
1636 for impl_class in &implementations {
1637 locations.push(Location::for_class(path.to_path_buf(), impl_class));
1638 }
1639
1640 patterns.push(PatternMatch::new(pattern, confidence.min(1.0))
1641 .with_locations(locations)
1642 .with_evidence(evidence));
1643 }
1644 }
1645
1646 patterns
1647 }
1648
1649 fn detect_dependency_injection(&self, class: &ClassInfo, path: &Path, _lang: &str) -> Option<PatternMatch> {
1651 let mut confidence: f64 = 0.0;
1652 let mut evidence = Vec::new();
1653 let mut dependencies: Vec<(String, String)> = Vec::new();
1654
1655 let constructor = class.methods.iter().find(|m| {
1657 let name = m.name.to_lowercase();
1658 name == "__init__" || name == "new" || name == "constructor" ||
1659 name == class.name.to_lowercase()
1660 });
1661
1662 if let Some(ctor) = constructor {
1663 let meaningful_params: Vec<_> = ctor.params.iter()
1665 .filter(|p| !p.contains("self") && !p.contains("this"))
1666 .collect();
1667
1668 for param in &meaningful_params {
1669 let parts: Vec<&str> = param.split(':').collect();
1672 let (name, type_hint) = if parts.len() >= 2 {
1673 (parts[0].trim().to_string(), Some(parts[1].trim().to_string()))
1674 } else {
1675 (param.trim().to_string(), None)
1676 };
1677
1678 if let Some(ref t) = type_hint {
1679 let t_lower = t.to_lowercase();
1681 let is_primitive = ["int", "str", "string", "bool", "boolean", "float",
1682 "double", "void", "none", "null"].iter()
1683 .any(|p| t_lower == *p);
1684
1685 if !is_primitive && !t.starts_with(char::is_lowercase) {
1686 dependencies.push((name, t.clone()));
1687 confidence += 0.15;
1688 }
1689 }
1690 }
1691 }
1692
1693 let di_decorators = ["inject", "autowired", "autowire", "dependency", "injectable"];
1695 for decorator in &class.decorators {
1696 let dec_lower = decorator.to_lowercase();
1697 if di_decorators.iter().any(|d| dec_lower.contains(d)) {
1698 confidence += 0.25;
1699 evidence.push(format!("Has DI decorator: {}", decorator));
1700 }
1701 }
1702
1703 let setter_injection = class.methods.iter().filter(|m| {
1705 let name_lower = m.name.to_lowercase();
1706 (name_lower.starts_with("set_") || name_lower.starts_with("inject_")) &&
1707 m.params.len() >= 2 }).count();
1709
1710 if setter_injection > 0 {
1711 confidence += 0.1 * setter_injection as f64;
1712 evidence.push(format!("{} setter injection methods", setter_injection));
1713 }
1714
1715 for field in &class.fields {
1717 for annotation in &field.annotations {
1718 let ann_lower = annotation.to_lowercase();
1719 if di_decorators.iter().any(|d| ann_lower.contains(d)) {
1720 confidence += 0.2;
1721 if let Some(ref ft) = field.field_type {
1722 dependencies.push((field.name.clone(), ft.clone()));
1723 }
1724 }
1725 }
1726 }
1727
1728 evidence.push(format!("{} dependencies detected", dependencies.len()));
1729
1730 if confidence >= 0.4 && !dependencies.is_empty() {
1731 let pattern = DesignPattern::DependencyInjection {
1732 class: class.name.clone(),
1733 dependencies,
1734 };
1735
1736 Some(PatternMatch::new(pattern, confidence.min(1.0))
1737 .with_location(Location::for_class(path.to_path_buf(), class))
1738 .with_evidence(evidence))
1739 } else {
1740 None
1741 }
1742 }
1743
1744 fn detect_repository(&self, class: &ClassInfo, path: &Path, _lang: &str) -> Option<PatternMatch> {
1746 let mut confidence: f64 = 0.0;
1747 let mut evidence = Vec::new();
1748 let mut entity_type = None;
1749 let mut crud_methods = Vec::new();
1750
1751 let class_name_lower = class.name.to_lowercase();
1752
1753 if class_name_lower.ends_with("repository") || class_name_lower.ends_with("repo") {
1755 confidence += 0.35;
1756 evidence.push("Class name suggests repository pattern".to_string());
1757
1758 let entity = class_name_lower
1760 .strip_suffix("repository")
1761 .or_else(|| class_name_lower.strip_suffix("repo"));
1762 if let Some(e) = entity {
1763 if !e.is_empty() {
1764 let mut chars = e.chars();
1766 if let Some(first) = chars.next() {
1767 entity_type = Some(format!("{}{}", first.to_uppercase(), chars.collect::<String>()));
1768 }
1769 }
1770 }
1771 }
1772
1773 if class_name_lower.ends_with("dao") {
1775 confidence += 0.25;
1776 evidence.push("Class name ends with 'DAO'".to_string());
1777 }
1778
1779 let crud_patterns = [
1781 ("find", "read"), ("get", "read"), ("fetch", "read"), ("load", "read"),
1782 ("save", "write"), ("create", "write"), ("add", "write"), ("insert", "write"),
1783 ("update", "write"), ("modify", "write"),
1784 ("delete", "write"), ("remove", "write"),
1785 ("list", "read"), ("all", "read"), ("count", "read"),
1786 ];
1787
1788 for method in &class.methods {
1789 let method_lower = method.name.to_lowercase();
1790 for (pattern, _op_type) in &crud_patterns {
1791 if method_lower.contains(pattern) {
1792 crud_methods.push(method.name.clone());
1793 confidence += 0.05;
1794 break;
1795 }
1796 }
1797 }
1798
1799 let unique_operations: HashSet<_> = crud_methods.iter()
1801 .filter_map(|m| {
1802 let m_lower = m.to_lowercase();
1803 if m_lower.contains("find") || m_lower.contains("get") || m_lower.contains("load") {
1804 Some("read")
1805 } else if m_lower.contains("save") || m_lower.contains("create") || m_lower.contains("insert") {
1806 Some("create")
1807 } else if m_lower.contains("update") || m_lower.contains("modify") {
1808 Some("update")
1809 } else if m_lower.contains("delete") || m_lower.contains("remove") {
1810 Some("delete")
1811 } else {
1812 None
1813 }
1814 })
1815 .collect();
1816
1817 if unique_operations.len() >= 3 {
1818 confidence += 0.2;
1819 evidence.push(format!("Has {} CRUD operations: {:?}", unique_operations.len(), unique_operations));
1820 }
1821
1822 if entity_type.is_none() {
1824 for method in &class.methods {
1825 if let Some(ref ret) = method.return_type {
1826 let ret_lower = ret.to_lowercase();
1827 if !ret_lower.contains("list") && !ret_lower.contains("vec") &&
1828 !ret_lower.contains("option") && ret.starts_with(char::is_uppercase) {
1829 entity_type = Some(ret.clone());
1830 break;
1831 }
1832 }
1833 }
1834 }
1835
1836 if confidence >= 0.4 && !crud_methods.is_empty() {
1837 let pattern = DesignPattern::Repository {
1838 class: class.name.clone(),
1839 entity_type,
1840 methods: crud_methods,
1841 };
1842
1843 Some(PatternMatch::new(pattern, confidence.min(1.0))
1844 .with_location(Location::for_class(path.to_path_buf(), class))
1845 .with_evidence(evidence))
1846 } else {
1847 None
1848 }
1849 }
1850
1851 fn calculate_stats(&self, analysis: &mut PatternAnalysis) {
1853 let mut by_category: HashMap<String, usize> = HashMap::new();
1854 let mut by_type: HashMap<String, usize> = HashMap::new();
1855 let mut total_confidence = 0.0;
1856 let mut files_with_patterns: HashSet<&Path> = HashSet::new();
1857
1858 for pattern_match in &analysis.patterns {
1859 let category = pattern_match.pattern.category().to_string();
1860 *by_category.entry(category).or_insert(0) += 1;
1861
1862 let pattern_name = pattern_match.pattern.name().to_string();
1863 *by_type.entry(pattern_name).or_insert(0) += 1;
1864
1865 total_confidence += pattern_match.confidence;
1866
1867 for loc in &pattern_match.locations {
1868 files_with_patterns.insert(&loc.file);
1869 }
1870 }
1871
1872 analysis.stats.patterns_detected = analysis.patterns.len();
1873 analysis.stats.files_with_patterns = files_with_patterns.len();
1874 analysis.stats.by_category = by_category;
1875 analysis.stats.by_type = by_type;
1876 analysis.stats.average_confidence = if analysis.patterns.is_empty() {
1877 0.0
1878 } else {
1879 total_confidence / analysis.patterns.len() as f64
1880 };
1881 }
1882}
1883
1884fn is_test_file(path: &Path) -> bool {
1890 let path_str = path.to_string_lossy().to_lowercase();
1891 path_str.contains("/test/") ||
1892 path_str.contains("/tests/") ||
1893 path_str.contains("_test.") ||
1894 path_str.contains("_spec.") ||
1895 path_str.contains(".test.") ||
1896 path_str.contains(".spec.") ||
1897 path_str.contains("/test_") ||
1898 path_str.contains("/__tests__/")
1899}
1900
1901fn is_generated_file(path: &Path) -> bool {
1903 let path_str = path.to_string_lossy().to_lowercase();
1904 path_str.contains("/generated/") ||
1905 path_str.contains("/gen/") ||
1906 path_str.contains(".generated.") ||
1907 path_str.contains(".g.") ||
1908 path_str.contains("_generated") ||
1909 path_str.contains("/build/") ||
1910 path_str.contains("/dist/") ||
1911 path_str.contains("/node_modules/") ||
1912 path_str.contains("__pycache__")
1913}
1914
1915pub fn detect_patterns(
1944 path: impl AsRef<Path>,
1945 pattern_filter: Option<&str>,
1946 config: Option<PatternConfig>,
1947) -> Result<PatternAnalysis> {
1948 let mut config = config.unwrap_or_default();
1949 if let Some(filter) = pattern_filter {
1950 config.patterns = vec![filter.to_string()];
1951 }
1952
1953 let detector = PatternDetector::new(config);
1954 detector.detect(path)
1955}
1956
1957pub fn format_pattern_summary(analysis: &PatternAnalysis) -> String {
1959 let mut output = String::new();
1960
1961 output.push_str(&format!("Design Pattern Analysis: {}\n", analysis.path.display()));
1962 output.push_str(&format!("Files scanned: {}\n", analysis.stats.files_scanned));
1963 output.push_str(&format!("Files with patterns: {}\n", analysis.stats.files_with_patterns));
1964 output.push_str(&format!("Total patterns detected: {}\n", analysis.stats.patterns_detected));
1965 output.push_str(&format!("Average confidence: {:.1}%\n\n", analysis.stats.average_confidence * 100.0));
1966
1967 if !analysis.stats.by_category.is_empty() {
1968 output.push_str("By Category:\n");
1969 for (category, count) in &analysis.stats.by_category {
1970 output.push_str(&format!(" {}: {}\n", category, count));
1971 }
1972 output.push('\n');
1973 }
1974
1975 if !analysis.stats.by_type.is_empty() {
1976 output.push_str("By Pattern Type:\n");
1977 for (pattern_type, count) in &analysis.stats.by_type {
1978 output.push_str(&format!(" {}: {}\n", pattern_type, count));
1979 }
1980 output.push('\n');
1981 }
1982
1983 output.push_str("Detected Patterns:\n");
1984 for pattern_match in &analysis.patterns {
1985 output.push_str(&format!("\n {} (confidence: {:.1}%)\n",
1986 pattern_match.pattern.name(),
1987 pattern_match.confidence * 100.0));
1988
1989 output.push_str(&format!(" Category: {}\n", pattern_match.pattern.category()));
1990 output.push_str(&format!(" Primary class: {}\n", pattern_match.pattern.primary_class()));
1991
1992 if let Some(loc) = pattern_match.primary_location() {
1993 output.push_str(&format!(" Location: {}:{}\n", loc.file.display(), loc.line));
1994 }
1995
1996 if let Some(ref note) = pattern_match.note {
1997 if !note.is_empty() {
1998 output.push_str(&format!(" Note: {}\n", note));
1999 }
2000 }
2001
2002 if !pattern_match.evidence.is_empty() {
2003 output.push_str(" Evidence:\n");
2004 for ev in &pattern_match.evidence {
2005 output.push_str(&format!(" - {}\n", ev));
2006 }
2007 }
2008 }
2009
2010 if !analysis.errors.is_empty() {
2011 output.push_str("\nErrors:\n");
2012 for error in &analysis.errors {
2013 output.push_str(&format!(" - {}\n", error));
2014 }
2015 }
2016
2017 output
2018}
2019
2020#[cfg(test)]
2025mod tests {
2026 use super::*;
2027 use crate::ast::FieldInfo;
2028
2029 #[test]
2030 fn test_pattern_category_display() {
2031 assert_eq!(PatternCategory::Creational.to_string(), "Creational");
2032 assert_eq!(PatternCategory::Structural.to_string(), "Structural");
2033 assert_eq!(PatternCategory::Behavioral.to_string(), "Behavioral");
2034 }
2035
2036 #[test]
2037 fn test_design_pattern_name() {
2038 let singleton = DesignPattern::Singleton {
2039 class: "Config".to_string(),
2040 instance_method: "getInstance".to_string(),
2041 instance_field: None,
2042 private_constructor: true,
2043 };
2044 assert_eq!(singleton.name(), "Singleton");
2045 assert_eq!(singleton.category(), PatternCategory::Creational);
2046 assert_eq!(singleton.primary_class(), "Config");
2047 }
2048
2049 #[test]
2050 fn test_pattern_match_builder() {
2051 let pattern = DesignPattern::Builder {
2052 class: "RequestBuilder".to_string(),
2053 build_method: "build".to_string(),
2054 setters: vec!["with_url".to_string(), "with_headers".to_string()],
2055 target_type: Some("Request".to_string()),
2056 };
2057
2058 let match_ = PatternMatch::new(pattern, 0.85)
2059 .with_location(Location {
2060 file: PathBuf::from("/src/builder.rs"),
2061 line: 10,
2062 end_line: Some(50),
2063 name: "RequestBuilder".to_string(),
2064 kind: "class".to_string(),
2065 })
2066 .with_evidence(vec!["Has build method".to_string()]);
2067
2068 assert_eq!(match_.confidence, 0.85);
2069 assert!(match_.primary_location().is_some());
2070 assert_eq!(match_.evidence.len(), 1);
2071 }
2072
2073 #[test]
2074 fn test_pattern_config_defaults() {
2075 let config = PatternConfig::default();
2076 assert_eq!(config.min_confidence, 0.5);
2077 assert!(config.patterns.is_empty());
2078 assert!(config.language.is_none());
2079 assert!(config.detect_modern_patterns);
2080 }
2081
2082 #[test]
2083 fn test_pattern_config_builder() {
2084 let config = PatternConfig::for_pattern("singleton")
2085 .with_min_confidence(0.8)
2086 .with_language("rust")
2087 .with_modern_patterns(false);
2088
2089 assert_eq!(config.patterns, vec!["singleton"]);
2090 assert_eq!(config.min_confidence, 0.8);
2091 assert_eq!(config.language, Some("rust".to_string()));
2092 assert!(!config.detect_modern_patterns);
2093 }
2094
2095 #[test]
2096 fn test_is_test_file() {
2097 assert!(is_test_file(Path::new("/src/tests/test_foo.py")));
2098 assert!(is_test_file(Path::new("/src/foo_test.go")));
2099 assert!(is_test_file(Path::new("/src/foo.spec.ts")));
2100 assert!(is_test_file(Path::new("/src/__tests__/foo.js")));
2101 assert!(!is_test_file(Path::new("/src/foo.py")));
2102 }
2103
2104 #[test]
2105 fn test_is_generated_file() {
2106 assert!(is_generated_file(Path::new("/generated/foo.rs")));
2107 assert!(is_generated_file(Path::new("/src/foo.generated.ts")));
2108 assert!(is_generated_file(Path::new("/node_modules/foo/index.js")));
2109 assert!(is_generated_file(Path::new("/__pycache__/foo.pyc")));
2110 assert!(!is_generated_file(Path::new("/src/foo.rs")));
2111 }
2112
2113 #[test]
2114 fn test_pattern_analysis_filters() {
2115 let mut analysis = PatternAnalysis::new(PathBuf::from("."));
2116
2117 analysis.patterns.push(PatternMatch::new(
2118 DesignPattern::Singleton {
2119 class: "Config".to_string(),
2120 instance_method: "getInstance".to_string(),
2121 instance_field: None,
2122 private_constructor: true,
2123 },
2124 0.9,
2125 ));
2126
2127 analysis.patterns.push(PatternMatch::new(
2128 DesignPattern::Factory {
2129 class: "CarFactory".to_string(),
2130 create_methods: vec!["createCar".to_string()],
2131 products: vec!["Car".to_string()],
2132 is_abstract: false,
2133 },
2134 0.5,
2135 ));
2136
2137 assert_eq!(analysis.patterns_of_type("singleton").len(), 1);
2138 assert_eq!(analysis.patterns_in_category(PatternCategory::Creational).len(), 2);
2139 assert_eq!(analysis.high_confidence_patterns().len(), 1);
2140 }
2141
2142 #[test]
2143 fn test_singleton_detection_heuristics() {
2144 let class = ClassInfo {
2146 name: "DatabaseConnection".to_string(),
2147 methods: vec![
2148 FunctionInfo {
2149 name: "__init__".to_string(),
2150 params: vec!["self".to_string()],
2151 decorators: vec!["@private".to_string()],
2152 ..Default::default()
2153 },
2154 FunctionInfo {
2155 name: "get_instance".to_string(),
2156 params: vec![],
2157 decorators: vec!["@staticmethod".to_string()],
2158 return_type: Some("DatabaseConnection".to_string()),
2159 ..Default::default()
2160 },
2161 ],
2162 fields: vec![
2163 FieldInfo {
2164 name: "_instance".to_string(),
2165 is_static: true,
2166 field_type: Some("DatabaseConnection".to_string()),
2167 ..Default::default()
2168 },
2169 ],
2170 language: "python".to_string(),
2171 ..Default::default()
2172 };
2173
2174 let detector = PatternDetector::new(PatternConfig::default());
2175 let result = detector.detect_singleton(&class, Path::new("/test.py"), "python");
2176
2177 assert!(result.is_some());
2178 let pattern_match = result.unwrap();
2179 assert!(pattern_match.confidence >= 0.5);
2180 assert!(matches!(pattern_match.pattern, DesignPattern::Singleton { .. }));
2181 }
2182
2183 #[test]
2184 fn test_builder_detection_heuristics() {
2185 let class = ClassInfo {
2187 name: "RequestBuilder".to_string(),
2188 methods: vec![
2189 FunctionInfo {
2190 name: "with_url".to_string(),
2191 params: vec!["self".to_string(), "url: str".to_string()],
2192 return_type: Some("Self".to_string()),
2193 ..Default::default()
2194 },
2195 FunctionInfo {
2196 name: "with_headers".to_string(),
2197 params: vec!["self".to_string(), "headers: dict".to_string()],
2198 return_type: Some("Self".to_string()),
2199 ..Default::default()
2200 },
2201 FunctionInfo {
2202 name: "build".to_string(),
2203 params: vec!["self".to_string()],
2204 return_type: Some("Request".to_string()),
2205 ..Default::default()
2206 },
2207 ],
2208 language: "python".to_string(),
2209 ..Default::default()
2210 };
2211
2212 let detector = PatternDetector::new(PatternConfig::default());
2213 let result = detector.detect_builder(&class, Path::new("/test.py"), "python");
2214
2215 assert!(result.is_some());
2216 let pattern_match = result.unwrap();
2217 assert!(pattern_match.confidence >= 0.5);
2218 if let DesignPattern::Builder { setters, build_method, .. } = &pattern_match.pattern {
2219 assert_eq!(*build_method, "build");
2220 assert!(setters.contains(&"with_url".to_string()));
2221 assert!(setters.contains(&"with_headers".to_string()));
2222 } else {
2223 panic!("Expected Builder pattern");
2224 }
2225 }
2226
2227 #[test]
2228 fn test_observer_detection_heuristics() {
2229 let class = ClassInfo {
2230 name: "EventEmitter".to_string(),
2231 methods: vec![
2232 FunctionInfo {
2233 name: "add_listener".to_string(),
2234 params: vec!["self".to_string(), "listener: Listener".to_string()],
2235 ..Default::default()
2236 },
2237 FunctionInfo {
2238 name: "remove_listener".to_string(),
2239 params: vec!["self".to_string(), "listener: Listener".to_string()],
2240 ..Default::default()
2241 },
2242 FunctionInfo {
2243 name: "notify_all".to_string(),
2244 params: vec!["self".to_string()],
2245 ..Default::default()
2246 },
2247 ],
2248 fields: vec![
2249 FieldInfo {
2250 name: "listeners".to_string(),
2251 field_type: Some("List<Listener>".to_string()),
2252 ..Default::default()
2253 },
2254 ],
2255 language: "python".to_string(),
2256 ..Default::default()
2257 };
2258
2259 let detector = PatternDetector::new(PatternConfig::default());
2260 let result = detector.detect_observer(&class, Path::new("/test.py"), "python");
2261
2262 assert!(result.is_some());
2263 let pattern_match = result.unwrap();
2264 assert!(pattern_match.confidence >= 0.5);
2265 assert!(matches!(pattern_match.pattern, DesignPattern::Observer { .. }));
2266 }
2267
2268 #[test]
2269 fn test_repository_detection_heuristics() {
2270 let class = ClassInfo {
2271 name: "UserRepository".to_string(),
2272 methods: vec![
2273 FunctionInfo {
2274 name: "find_by_id".to_string(),
2275 params: vec!["self".to_string(), "id: int".to_string()],
2276 return_type: Some("User".to_string()),
2277 ..Default::default()
2278 },
2279 FunctionInfo {
2280 name: "save".to_string(),
2281 params: vec!["self".to_string(), "user: User".to_string()],
2282 ..Default::default()
2283 },
2284 FunctionInfo {
2285 name: "delete".to_string(),
2286 params: vec!["self".to_string(), "id: int".to_string()],
2287 ..Default::default()
2288 },
2289 FunctionInfo {
2290 name: "find_all".to_string(),
2291 params: vec!["self".to_string()],
2292 return_type: Some("List<User>".to_string()),
2293 ..Default::default()
2294 },
2295 ],
2296 language: "python".to_string(),
2297 ..Default::default()
2298 };
2299
2300 let detector = PatternDetector::new(PatternConfig::default());
2301 let result = detector.detect_repository(&class, Path::new("/test.py"), "python");
2302
2303 assert!(result.is_some());
2304 let pattern_match = result.unwrap();
2305 assert!(pattern_match.confidence >= 0.5);
2306 if let DesignPattern::Repository { entity_type, methods, .. } = &pattern_match.pattern {
2307 assert_eq!(*entity_type, Some("User".to_string()));
2308 assert!(methods.len() >= 3);
2309 } else {
2310 panic!("Expected Repository pattern");
2311 }
2312 }
2313
2314 #[test]
2315 fn test_format_pattern_summary() {
2316 let mut analysis = PatternAnalysis::new(PathBuf::from("/test"));
2317 analysis.stats.files_scanned = 10;
2318 analysis.stats.patterns_detected = 2;
2319 analysis.stats.average_confidence = 0.75;
2320
2321 analysis.patterns.push(PatternMatch::new(
2322 DesignPattern::Singleton {
2323 class: "Config".to_string(),
2324 instance_method: "getInstance".to_string(),
2325 instance_field: None,
2326 private_constructor: true,
2327 },
2328 0.9,
2329 ).with_location(Location {
2330 file: PathBuf::from("/test/config.py"),
2331 line: 10,
2332 end_line: Some(30),
2333 name: "Config".to_string(),
2334 kind: "class".to_string(),
2335 }));
2336
2337 let summary = format_pattern_summary(&analysis);
2338 assert!(summary.contains("Singleton"));
2339 assert!(summary.contains("Config"));
2340 assert!(summary.contains("90.0%"));
2341 }
2342}