1use ryo_source::pure::PureFile;
7use serde::{Deserialize, Serialize};
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
11pub enum DetectCategory {
12 Creational,
14 Structural,
16 Behavioural,
18 Performance,
20 Safety,
22 Clippy,
24 Refactor,
26}
27
28impl DetectCategory {
29 pub fn as_str(&self) -> &'static str {
30 match self {
31 DetectCategory::Creational => "creational",
32 DetectCategory::Structural => "structural",
33 DetectCategory::Behavioural => "behavioural",
34 DetectCategory::Performance => "performance",
35 DetectCategory::Safety => "safety",
36 DetectCategory::Clippy => "clippy",
37 DetectCategory::Refactor => "refactor",
38 }
39 }
40
41 pub fn short_code(&self) -> &'static str {
43 match self {
44 DetectCategory::Creational => "C",
45 DetectCategory::Structural => "S",
46 DetectCategory::Behavioural => "B",
47 DetectCategory::Performance => "P",
48 DetectCategory::Safety => "Y",
49 DetectCategory::Clippy => "L",
50 DetectCategory::Refactor => "R",
51 }
52 }
53}
54
55#[derive(Debug, Clone, Serialize, Deserialize)]
57pub struct DetectLocation {
58 pub item_kind: String,
60 pub item_name: String,
62 #[serde(default, skip_serializing_if = "Option::is_none")]
64 pub symbol_path: Option<String>,
65}
66
67impl DetectLocation {
68 pub fn new(kind: impl Into<String>, name: impl Into<String>) -> Self {
69 Self {
70 item_kind: kind.into(),
71 item_name: name.into(),
72 symbol_path: None,
73 }
74 }
75
76 pub fn struct_item(name: impl Into<String>) -> Self {
77 Self::new("struct", name)
78 }
79
80 pub fn impl_item(name: impl Into<String>) -> Self {
81 Self::new("impl", name)
82 }
83
84 pub fn fn_item(name: impl Into<String>) -> Self {
85 Self::new("fn", name)
86 }
87
88 pub fn with_symbol_path(mut self, path: impl Into<String>) -> Self {
89 self.symbol_path = Some(path.into());
90 self
91 }
92}
93
94#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
96pub enum DetectOperation {
97 Generate,
99 Refactor,
101}
102
103#[derive(Debug, Clone, Serialize, Deserialize)]
105pub struct DetectOpportunity {
106 pub location: DetectLocation,
108 pub operations: Vec<DetectOperation>,
110 pub suggestion: String,
112 pub confidence: f32,
114 pub context: Option<String>,
116}
117
118impl DetectOpportunity {
119 pub fn new(location: DetectLocation, suggestion: impl Into<String>) -> Self {
120 Self {
121 location,
122 operations: vec![DetectOperation::Generate],
123 suggestion: suggestion.into(),
124 confidence: 1.0,
125 context: None,
126 }
127 }
128
129 pub fn with_operations(mut self, ops: Vec<DetectOperation>) -> Self {
130 self.operations = ops;
131 self
132 }
133
134 pub fn with_confidence(mut self, confidence: f32) -> Self {
135 self.confidence = confidence.clamp(0.0, 1.0);
136 self
137 }
138
139 pub fn with_context(mut self, context: impl Into<String>) -> Self {
140 self.context = Some(context.into());
141 self
142 }
143
144 pub fn can_generate(&self) -> bool {
145 self.operations.contains(&DetectOperation::Generate)
146 }
147
148 pub fn can_refactor(&self) -> bool {
149 self.operations.contains(&DetectOperation::Refactor)
150 }
151}
152
153pub trait Detect: Send + Sync {
158 fn detect(&self, file: &PureFile) -> Vec<DetectOpportunity>;
160
161 fn category(&self) -> DetectCategory;
163
164 fn detect_name(&self) -> &'static str;
166
167 fn detect_description(&self) -> &str {
169 ""
170 }
171}
172
173#[derive(Default)]
175pub struct DetectRegistry {
176 detectors: Vec<Box<dyn Detect>>,
177}
178
179impl DetectRegistry {
180 pub fn new() -> Self {
181 Self::default()
182 }
183
184 pub fn register<D: Detect + 'static>(&mut self, detector: D) {
186 self.detectors.push(Box::new(detector));
187 }
188
189 pub fn all(&self) -> &[Box<dyn Detect>] {
191 &self.detectors
192 }
193
194 pub fn find(&self, name: &str) -> Option<&dyn Detect> {
196 self.detectors
197 .iter()
198 .find(|d| d.detect_name().eq_ignore_ascii_case(name))
199 .map(|d| d.as_ref())
200 }
201
202 pub fn by_category(&self, category: DetectCategory) -> Vec<&dyn Detect> {
204 self.detectors
205 .iter()
206 .filter(|d| d.category() == category)
207 .map(|d| d.as_ref())
208 .collect()
209 }
210
211 pub fn detect_all(&self, file: &PureFile) -> Vec<(&dyn Detect, DetectOpportunity)> {
213 self.detectors
214 .iter()
215 .flat_map(|d| d.detect(file).into_iter().map(move |opp| (d.as_ref(), opp)))
216 .collect()
217 }
218}
219
220pub fn create_default_registry() -> DetectRegistry {
222 use super::{
223 DefaultMutation, DeriveDefaultMutation, LockScopeMutation, UseAtomicMutation,
224 UseRwLockMutation,
225 };
226
227 let mut registry = DetectRegistry::new();
228
229 registry.register(DefaultMutation::new());
231 registry.register(DeriveDefaultMutation::new());
232
233 registry.register(UseAtomicMutation::new());
235 registry.register(UseRwLockMutation::new());
236 registry.register(LockScopeMutation::new());
237
238 registry
239}
240
241#[cfg(test)]
242mod tests {
243 use super::*;
244
245 #[test]
246 fn test_detect_location() {
247 let loc = DetectLocation::struct_item("Config");
248 assert_eq!(loc.item_kind, "struct");
249 assert_eq!(loc.item_name, "Config");
250 }
251
252 #[test]
253 fn test_detect_opportunity() {
254 let opp = DetectOpportunity::new(
255 DetectLocation::struct_item("Config"),
256 "Generate Builder pattern",
257 )
258 .with_operations(vec![DetectOperation::Generate])
259 .with_confidence(0.9);
260
261 assert!(opp.can_generate());
262 assert!(!opp.can_refactor());
263 assert_eq!(opp.confidence, 0.9);
264 }
265
266 #[test]
267 fn test_detect_category() {
268 assert_eq!(DetectCategory::Creational.as_str(), "creational");
269 assert_eq!(DetectCategory::Clippy.as_str(), "clippy");
270 }
271
272 #[test]
275 fn test_all_detectors_are_registered() {
276 use super::super::{
277 DefaultMutation, DeriveDefaultMutation, LockScopeMutation, UseAtomicMutation,
278 UseRwLockMutation,
279 };
280 use std::collections::HashSet;
281
282 let registry = create_default_registry();
283 let registered_names: HashSet<&str> =
284 registry.all().iter().map(|d| d.detect_name()).collect();
285
286 let expected_detectors: Vec<(&str, Box<dyn Detect>)> = vec![
288 ("DefaultMutation", Box::new(DefaultMutation::new())),
289 (
290 "DeriveDefaultMutation",
291 Box::new(DeriveDefaultMutation::new()),
292 ),
293 ("UseAtomicMutation", Box::new(UseAtomicMutation::new())),
294 ("UseRwLockMutation", Box::new(UseRwLockMutation::new())),
295 ("LockScopeMutation", Box::new(LockScopeMutation::new())),
296 ];
297
298 for (label, detector) in expected_detectors {
299 let name = detector.detect_name();
300 assert!(
301 registered_names.contains(name),
302 "{} (detect_name='{}') is NOT registered in create_default_registry(). \
303 Add: registry.register({}::new());",
304 label,
305 name,
306 label
307 );
308 }
309
310 assert_eq!(
312 registry.all().len(),
313 5,
314 "Expected 5 detectors registered, found {}",
315 registry.all().len()
316 );
317 }
318}