debtmap/analysis/patterns/
singleton.rs1use super::{Implementation, PatternInstance, PatternRecognizer, PatternType};
9use crate::core::{FileMetrics, FunctionMetrics};
10
11pub struct SingletonPatternRecognizer;
12
13impl SingletonPatternRecognizer {
14 pub fn new() -> Self {
15 Self
16 }
17}
18
19impl Default for SingletonPatternRecognizer {
20 fn default() -> Self {
21 Self::new()
22 }
23}
24
25impl PatternRecognizer for SingletonPatternRecognizer {
26 fn name(&self) -> &str {
27 "Singleton"
28 }
29
30 fn detect(&self, file_metrics: &FileMetrics) -> Vec<PatternInstance> {
31 let mut patterns = Vec::new();
32
33 if let Some(module_scope) = &file_metrics.module_scope {
34 for singleton in &module_scope.singleton_instances {
35 patterns.push(PatternInstance {
36 pattern_type: PatternType::Singleton,
37 confidence: 0.9,
38 base_class: Some(singleton.class_name.clone()),
39 implementations: vec![Implementation {
40 file: file_metrics.path.clone(),
41 class_name: Some(singleton.class_name.clone()),
42 function_name: singleton.variable_name.clone(),
43 line: singleton.line,
44 }],
45 usage_sites: Vec::new(),
46 reasoning: format!(
47 "Module-level singleton: {} = {}()",
48 singleton.variable_name, singleton.class_name
49 ),
50 });
51 }
52 }
53
54 patterns
55 }
56
57 fn is_function_used_by_pattern(
58 &self,
59 function: &FunctionMetrics,
60 file_metrics: &FileMetrics,
61 ) -> Option<PatternInstance> {
62 let parts: Vec<&str> = function.name.split("::").collect();
63 let class_name = if parts.len() >= 2 {
64 parts[0]
65 } else {
66 return None;
67 };
68
69 if let Some(module_scope) = &file_metrics.module_scope {
70 if module_scope
71 .singleton_instances
72 .iter()
73 .any(|s| s.class_name == class_name)
74 {
75 return Some(PatternInstance {
76 pattern_type: PatternType::Singleton,
77 confidence: 0.85,
78 base_class: Some(class_name.to_string()),
79 implementations: vec![Implementation {
80 file: file_metrics.path.clone(),
81 class_name: Some(class_name.to_string()),
82 function_name: function.name.clone(),
83 line: function.line,
84 }],
85 usage_sites: Vec::new(),
86 reasoning: format!("Method on singleton class {}", class_name),
87 });
88 }
89 }
90
91 None
92 }
93}
94
95#[cfg(test)]
96mod tests {
97 use super::*;
98 use crate::core::{
99 ast::ModuleScopeAnalysis, ast::SingletonInstance, ComplexityMetrics, Language,
100 };
101 use std::path::PathBuf;
102
103 fn create_test_file_metrics_with_singleton(singleton: SingletonInstance) -> FileMetrics {
104 FileMetrics {
105 path: PathBuf::from("test.py"),
106 language: Language::Python,
107 complexity: ComplexityMetrics::default(),
108 debt_items: vec![],
109 dependencies: vec![],
110 duplications: vec![],
111 total_lines: 0,
112 module_scope: Some(ModuleScopeAnalysis {
113 assignments: vec![],
114 singleton_instances: vec![singleton],
115 }),
116 classes: None,
117 }
118 }
119
120 #[test]
121 fn test_singleton_detection() {
122 let singleton = SingletonInstance {
123 variable_name: "manager".to_string(),
124 class_name: "Manager".to_string(),
125 line: 10,
126 };
127
128 let file_metrics = create_test_file_metrics_with_singleton(singleton);
129 let recognizer = SingletonPatternRecognizer::new();
130 let patterns = recognizer.detect(&file_metrics);
131
132 assert_eq!(patterns.len(), 1);
133 assert_eq!(patterns[0].pattern_type, PatternType::Singleton);
134 assert_eq!(patterns[0].confidence, 0.9);
135 assert_eq!(patterns[0].base_class, Some("Manager".to_string()));
136 }
137
138 #[test]
139 fn test_is_function_used_by_singleton() {
140 let singleton = SingletonInstance {
141 variable_name: "manager".to_string(),
142 class_name: "Manager".to_string(),
143 line: 10,
144 };
145
146 let file_metrics = create_test_file_metrics_with_singleton(singleton);
147
148 let function = FunctionMetrics {
149 name: "Manager::process".to_string(),
150 file: PathBuf::from("test.py"),
151 line: 15,
152 cyclomatic: 1,
153 cognitive: 0,
154 nesting: 0,
155 length: 5,
156 is_test: false,
157 visibility: None,
158 is_trait_method: false,
159 in_test_module: false,
160 entropy_score: None,
161 is_pure: None,
162 purity_confidence: None,
163 purity_reason: None,
164 call_dependencies: None,
165 detected_patterns: None,
166 upstream_callers: None,
167 downstream_callees: None,
168 mapping_pattern_result: None,
169 adjusted_complexity: None,
170 composition_metrics: None,
171 language_specific: None,
172 purity_level: None,
173 error_swallowing_count: None,
174 error_swallowing_patterns: None,
175 entropy_analysis: None,
176 };
177
178 let recognizer = SingletonPatternRecognizer::new();
179 let result = recognizer.is_function_used_by_pattern(&function, &file_metrics);
180
181 assert!(result.is_some());
182 let pattern = result.unwrap();
183 assert_eq!(pattern.pattern_type, PatternType::Singleton);
184 assert_eq!(pattern.confidence, 0.85);
185 }
186
187 #[test]
188 fn test_singleton_name() {
189 let recognizer = SingletonPatternRecognizer::new();
190 assert_eq!(recognizer.name(), "Singleton");
191 }
192}