Skip to main content

ryo_mutations/idiom/
detect.rs

1//! Detection trait and types for pattern/idiom detection
2//!
3//! Provides a unified interface for detecting transformation opportunities
4//! in Rust source code.
5
6use ryo_source::pure::PureFile;
7use serde::{Deserialize, Serialize};
8
9/// Category for detected opportunities
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
11pub enum DetectCategory {
12    /// Creational patterns: Builder, Default, Factory
13    Creational,
14    /// Structural patterns: From/Into, Adapter
15    Structural,
16    /// Behavioural patterns: Strategy, Observer
17    Behavioural,
18    /// Performance patterns: Atomic, RwLock
19    Performance,
20    /// Safety patterns: LockScope, etc.
21    Safety,
22    /// Clippy-style lints: bool_comparison, redundant_closure
23    Clippy,
24    /// Refactoring: extract, inline, organize
25    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    /// Short code for display (C/S/B/P/Y/L/R)
42    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/// Location in the AST where an opportunity was detected
56#[derive(Debug, Clone, Serialize, Deserialize)]
57pub struct DetectLocation {
58    /// Item kind (struct, impl, fn, etc.)
59    pub item_kind: String,
60    /// Item name
61    pub item_name: String,
62    /// Optional symbol path (e.g., "test_crate::config::Config")
63    #[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/// Available operations for a detected opportunity
95#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
96pub enum DetectOperation {
97    /// Generate new code
98    Generate,
99    /// Refactor existing code
100    Refactor,
101}
102
103/// A detected opportunity for transformation
104#[derive(Debug, Clone, Serialize, Deserialize)]
105pub struct DetectOpportunity {
106    /// Where the opportunity was detected
107    pub location: DetectLocation,
108    /// What operations are available
109    pub operations: Vec<DetectOperation>,
110    /// Human-readable suggestion
111    pub suggestion: String,
112    /// Confidence score (0.0 - 1.0)
113    pub confidence: f32,
114    /// Additional context (mutation-specific data)
115    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
153/// Trait for detecting transformation opportunities
154///
155/// Mutations can optionally implement this trait to provide detection
156/// capabilities for suggestion engines.
157pub trait Detect: Send + Sync {
158    /// Detect opportunities in a file
159    fn detect(&self, file: &PureFile) -> Vec<DetectOpportunity>;
160
161    /// Get the category for detected opportunities
162    fn category(&self) -> DetectCategory;
163
164    /// Get the mutation/pattern name
165    fn detect_name(&self) -> &'static str;
166
167    /// Get description
168    fn detect_description(&self) -> &str {
169        ""
170    }
171}
172
173/// Registry of detectable mutations
174#[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    /// Register a detector
185    pub fn register<D: Detect + 'static>(&mut self, detector: D) {
186        self.detectors.push(Box::new(detector));
187    }
188
189    /// Get all registered detectors
190    pub fn all(&self) -> &[Box<dyn Detect>] {
191        &self.detectors
192    }
193
194    /// Find detector by name
195    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    /// Get detectors by category
203    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    /// Detect all opportunities in a file
212    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
220/// Create a registry with all built-in detectors
221pub 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    // Tier 3: Code generation
230    registry.register(DefaultMutation::new());
231    registry.register(DeriveDefaultMutation::new());
232
233    // Tier 4: Performance/Safety
234    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    /// Ensures all Detect implementations are registered in create_default_registry().
273    /// This test prevents the "forgot to register" bug.
274    #[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        // All Detect implementations must be registered
287        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        // Verify count matches (removed BuilderMutation - now Intent-based)
311        assert_eq!(
312            registry.all().len(),
313            5,
314            "Expected 5 detectors registered, found {}",
315            registry.all().len()
316        );
317    }
318}