Skip to main content

ryo_mutations/idiom/
default.rs

1//! Default Pattern Mutations
2//!
3//! Mutations for implementing the Default trait:
4//!
5//! - `DefaultMutation`: Add Default to structs (derive or generate impl)
6//! - `DeriveDefaultMutation`: Convert manual impl Default to #[derive(Default)]
7//!
8//! # Example
9//!
10//! ```rust,ignore
11//! // Before (no Default)
12//! pub struct Config {
13//!     timeout: u32,
14//!     enabled: bool,
15//! }
16//!
17//! // After (with derive)
18//! #[derive(Default)]
19//! pub struct Config {
20//!     timeout: u32,
21//!     enabled: bool,
22//! }
23//!
24//! // Or with manual impl
25//! impl Default for Config {
26//!     fn default() -> Self {
27//!         Self { timeout: 0, enabled: false }
28//!     }
29//! }
30//! ```
31
32use ryo_source::pure::{
33    PureAttrMeta, PureAttribute, PureExpr, PureField, PureFields, PureFile, PureImpl, PureImplItem,
34    PureItem, PureStmt,
35};
36
37use super::detect::{Detect, DetectCategory, DetectLocation, DetectOperation, DetectOpportunity};
38use crate::Mutation;
39
40// ============================================================================
41// DefaultMutation: Add Default to structs
42// ============================================================================
43
44/// Add Default implementation to structs
45///
46/// This mutation adds `#[derive(Default)]` or generates `impl Default`
47/// for structs that don't have it.
48///
49/// # Example
50///
51/// ```rust,ignore
52/// use ryo_mutations::idiom::DefaultMutation;
53///
54/// let mutation = DefaultMutation::new();
55/// let result = mutation.apply(&mut file);
56/// ```
57#[derive(Debug, Clone)]
58pub struct DefaultMutation {
59    /// Only apply to specific struct
60    pub target_struct: Option<String>,
61    /// Use #[derive(Default)] instead of impl (default: true)
62    pub use_derive: bool,
63}
64
65impl Default for DefaultMutation {
66    fn default() -> Self {
67        Self {
68            target_struct: None,
69            use_derive: true,
70        }
71    }
72}
73
74impl DefaultMutation {
75    pub fn new() -> Self {
76        Self::default()
77    }
78
79    /// Only apply to a specific struct
80    pub fn for_struct(mut self, name: impl Into<String>) -> Self {
81        self.target_struct = Some(name.into());
82        self
83    }
84
85    /// Use #[derive(Default)] (true) or impl Default (false)
86    pub fn with_derive(mut self, use_derive: bool) -> Self {
87        self.use_derive = use_derive;
88        self
89    }
90
91    /// Get named fields from a struct
92    fn get_named_fields(fields: &PureFields) -> Option<&Vec<PureField>> {
93        match fields {
94            PureFields::Named(f) => Some(f),
95            _ => None,
96        }
97    }
98
99    /// Check if struct already has derive(Default)
100    fn has_derive_default(attrs: &[PureAttribute]) -> bool {
101        attrs.iter().any(|attr| {
102            attr.path == "derive"
103                && matches!(&attr.meta, PureAttrMeta::List(args) if args.contains("Default"))
104        })
105    }
106
107    /// Check if manual Default impl exists
108    fn has_manual_default_impl(file: &PureFile, struct_name: &str) -> bool {
109        file.items.iter().any(|item| {
110            if let PureItem::Impl(imp) = item {
111                imp.self_ty == struct_name && imp.trait_.as_deref() == Some("Default")
112            } else {
113                false
114            }
115        })
116    }
117}
118
119impl Mutation for DefaultMutation {
120    fn describe(&self) -> String {
121        if self.use_derive {
122            "Add #[derive(Default)] to structs".to_string()
123        } else {
124            "Generate impl Default for structs".to_string()
125        }
126    }
127
128    fn mutation_type(&self) -> &'static str {
129        "Default"
130    }
131
132    fn box_clone(&self) -> Box<dyn Mutation> {
133        Box::new(self.clone())
134    }
135}
136
137impl Detect for DefaultMutation {
138    fn detect(&self, file: &PureFile) -> Vec<DetectOpportunity> {
139        let mut opportunities = Vec::new();
140
141        for item in &file.items {
142            if let PureItem::Struct(s) = item {
143                if let Some(ref target) = self.target_struct {
144                    if &s.name != target {
145                        continue;
146                    }
147                }
148
149                // Skip if already has Default
150                if Self::has_derive_default(&s.attrs) {
151                    continue;
152                }
153                if Self::has_manual_default_impl(file, &s.name) {
154                    continue;
155                }
156
157                // Only named-field structs
158                if Self::get_named_fields(&s.fields).is_some() {
159                    opportunities.push(
160                        DetectOpportunity::new(
161                            DetectLocation::struct_item(&s.name),
162                            format!("Add Default implementation for '{}'", s.name),
163                        )
164                        .with_operations(vec![DetectOperation::Generate])
165                        .with_confidence(0.8),
166                    );
167                }
168            }
169        }
170
171        opportunities
172    }
173
174    fn category(&self) -> DetectCategory {
175        DetectCategory::Creational
176    }
177
178    fn detect_name(&self) -> &'static str {
179        "Default"
180    }
181
182    fn detect_description(&self) -> &str {
183        "Add Default implementation to structs"
184    }
185}
186
187// ============================================================================
188// DeriveDefaultMutation: Convert manual impl to derive
189// ============================================================================
190
191/// Convert manual Default implementations to #[derive(Default)]
192///
193/// This mutation:
194/// 1. Finds `impl Default for T` blocks
195/// 2. Checks if all field initializations use default values
196/// 3. Removes the impl block
197/// 4. Adds `#[derive(Default)]` to the struct
198///
199/// # Example
200///
201/// ```rust,ignore
202/// use ryo_mutations::idiom::DeriveDefaultMutation;
203///
204/// let mutation = DeriveDefaultMutation::new();
205/// let result = mutation.apply(&mut file);
206/// ```
207#[derive(Debug, Clone, Default)]
208pub struct DeriveDefaultMutation {
209    /// Only apply to specific type
210    pub target_type: Option<String>,
211}
212
213impl DeriveDefaultMutation {
214    pub fn new() -> Self {
215        Self::default()
216    }
217
218    /// Only apply to a specific type
219    pub fn for_type(mut self, type_name: impl Into<String>) -> Self {
220        self.target_type = Some(type_name.into());
221        self
222    }
223
224    /// Check if an impl block is a Default impl
225    fn is_default_impl(imp: &PureImpl) -> bool {
226        imp.trait_.as_deref() == Some("Default")
227    }
228
229    /// Check if Default impl is derivable (all fields use default values)
230    fn is_derivable_default(imp: &PureImpl) -> Option<String> {
231        // Find the default() method
232        let default_fn = imp.items.iter().find_map(|item| {
233            if let PureImplItem::Fn(f) = item {
234                if f.name == "default" {
235                    return Some(f);
236                }
237            }
238            None
239        })?;
240
241        // Check the function body - should be a single expression returning Self { ... }
242        if default_fn.body.stmts.len() != 1 {
243            return None;
244        }
245
246        let expr = match &default_fn.body.stmts[0] {
247            PureStmt::Expr(e) => e,
248            _ => return None,
249        };
250
251        // Should be a struct literal Self { field: value, ... }
252        let fields = match expr {
253            PureExpr::Struct { path, fields } => {
254                if path != "Self" && path != &imp.self_ty {
255                    return None;
256                }
257                fields
258            }
259            _ => return None,
260        };
261
262        // Check all field values are defaults
263        for (_, value) in fields {
264            if !Self::is_default_value(value) {
265                return None;
266            }
267        }
268
269        Some(imp.self_ty.clone())
270    }
271
272    /// Check if an expression is a default value
273    fn is_default_value(expr: &PureExpr) -> bool {
274        match expr {
275            PureExpr::Lit(lit) => Self::is_zero_literal(lit),
276            PureExpr::Path(p) => p == "None",
277            PureExpr::Call { func, args } => {
278                if args.is_empty() {
279                    if let PureExpr::Path(p) = func.as_ref() {
280                        return matches!(
281                            p.as_str(),
282                            "Default::default"
283                                | "Vec::new"
284                                | "String::new"
285                                | "HashMap::new"
286                                | "HashSet::new"
287                                | "BTreeMap::new"
288                                | "BTreeSet::new"
289                                | "PathBuf::new"
290                                | "OsString::new"
291                        );
292                    }
293                }
294                false
295            }
296            PureExpr::MethodCall { method, args, .. } => {
297                (method == "default" || method == "new") && args.is_empty()
298            }
299            _ => false,
300        }
301    }
302
303    /// Check if a literal is "zero" or "empty"
304    fn is_zero_literal(lit: &str) -> bool {
305        matches!(
306            lit.trim(),
307            "0" | "0i8"
308                | "0i16"
309                | "0i32"
310                | "0i64"
311                | "0i128"
312                | "0isize"
313                | "0u8"
314                | "0u16"
315                | "0u32"
316                | "0u64"
317                | "0u128"
318                | "0usize"
319                | "0.0"
320                | "0.0f32"
321                | "0.0f64"
322                | "false"
323                | "'\\0'"
324                | "\"\""
325        )
326    }
327}
328
329impl Mutation for DeriveDefaultMutation {
330    fn describe(&self) -> String {
331        "Convert manual Default implementations to #[derive(Default)]".to_string()
332    }
333
334    fn mutation_type(&self) -> &'static str {
335        "DeriveDefault"
336    }
337
338    fn box_clone(&self) -> Box<dyn Mutation> {
339        Box::new(self.clone())
340    }
341}
342
343impl Detect for DeriveDefaultMutation {
344    fn detect(&self, file: &PureFile) -> Vec<DetectOpportunity> {
345        let mut opportunities = Vec::new();
346
347        for item in &file.items {
348            if let PureItem::Impl(imp) = item {
349                if Self::is_default_impl(imp) {
350                    if let Some(ref target) = self.target_type {
351                        if &imp.self_ty != target {
352                            continue;
353                        }
354                    }
355
356                    if Self::is_derivable_default(imp).is_some() {
357                        opportunities.push(
358                            DetectOpportunity::new(
359                                DetectLocation::impl_item(&imp.self_ty),
360                                format!(
361                                    "Convert manual Default impl for '{}' to #[derive(Default)]",
362                                    imp.self_ty
363                                ),
364                            )
365                            .with_operations(vec![DetectOperation::Refactor])
366                            .with_confidence(0.95),
367                        );
368                    }
369                }
370            }
371        }
372
373        opportunities
374    }
375
376    fn category(&self) -> DetectCategory {
377        DetectCategory::Creational
378    }
379
380    fn detect_name(&self) -> &'static str {
381        "DeriveDefault"
382    }
383
384    fn detect_description(&self) -> &str {
385        "Convert manual Default implementations to #[derive(Default)]"
386    }
387}