Skip to main content

debtmap/analysis/patterns/
singleton.rs

1//! Singleton pattern recognition
2//!
3//! Detects the Singleton pattern in code by identifying:
4//! - Module-level class instances
5//! - Class-level static instances
6//! - Singleton instance methods being called
7
8use 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}