Skip to main content

ryo_mutations/idiom/
clone_on_copy.rs

1//! CloneOnCopyMutation: Remove unnecessary .clone() on Copy types
2//!
3//! Transforms:
4//! - `x.clone()` → `x` (when x is a Copy type)
5//!
6//! This mutation detects Copy types from:
7//! - Literals (integers, floats, bools, chars)
8//! - Function parameters with known Copy types (i32, u32, f64, bool, etc.)
9//! - Explicitly marked Copy variables
10//!
11//! Corresponds to Clippy lint: `clippy::clone_on_copy`
12
13use ryo_source::pure::{PureBlock, PureExpr, PureFn, PureParam, PureStmt, PureType};
14use ryo_symbol::SymbolId;
15use std::collections::HashSet;
16
17use crate::Mutation;
18
19/// Remove unnecessary .clone() calls on Copy types
20///
21/// Since we don't have full type information, this mutation is conservative
22/// and only removes .clone() in patterns where the type is clearly Copy:
23/// - Literals (integers, floats, bools, chars)
24/// - Known Copy types (primitives)
25///
26/// For more aggressive removal, use with Clippy integration which has
27/// full type information.
28///
29/// # Example
30///
31/// ```rust,ignore
32/// use ryo_mutations::idiom::CloneOnCopyMutation;
33///
34/// let mutation = CloneOnCopyMutation::new().aggressive();
35/// // With aggressive mode, transforms: x.clone()
36/// // Into:                             x
37/// // (requires external type checking for safety)
38/// ```
39#[derive(Debug, Clone, Default)]
40pub struct CloneOnCopyMutation {
41    /// Target function SymbolId. If None, applies to all functions.
42    pub target_fn: Option<SymbolId>,
43    /// Aggressive mode: remove all .clone() calls (requires external type checking)
44    pub aggressive: bool,
45    /// Known Copy type variable names (for targeted removal)
46    pub copy_vars: Vec<String>,
47}
48
49impl CloneOnCopyMutation {
50    pub fn new() -> Self {
51        Self::default()
52    }
53
54    /// Only apply in a specific function
55    pub fn in_function(mut self, id: SymbolId) -> Self {
56        self.target_fn = Some(id);
57        self
58    }
59
60    /// Enable aggressive mode (remove all .clone() calls)
61    ///
62    /// Use with caution - this should be combined with Clippy's type analysis
63    /// or external validation.
64    pub fn aggressive(mut self) -> Self {
65        self.aggressive = true;
66        self
67    }
68
69    /// Add a variable known to be Copy type
70    pub fn with_copy_var(mut self, var: impl Into<String>) -> Self {
71        self.copy_vars.push(var.into());
72        self
73    }
74
75    /// Check if expression is a known Copy literal
76    fn is_copy_literal(expr: &PureExpr) -> bool {
77        match expr {
78            PureExpr::Lit(lit) => {
79                // Integer literals
80                lit.chars().all(|c| c.is_ascii_digit() || c == '_')
81                    // Float literals
82                    || lit.contains('.') && lit.chars().all(|c| c.is_ascii_digit() || c == '.' || c == '_')
83                    // Bool literals
84                    || lit == "true" || lit == "false"
85                    // Char literals
86                    || (lit.starts_with('\'') && lit.ends_with('\''))
87            }
88            PureExpr::Path(path) => {
89                // Known Copy paths
90                path == "true" || path == "false"
91            }
92            _ => false,
93        }
94    }
95
96    /// Check if a path is a known Copy variable
97    fn is_known_copy_var(&self, path: &str, fn_copy_vars: &HashSet<String>) -> bool {
98        self.copy_vars.iter().any(|v| v == path) || fn_copy_vars.contains(path)
99    }
100
101    /// Known primitive Copy types
102    const COPY_TYPES: &'static [&'static str] = &[
103        // Signed integers
104        "i8",
105        "i16",
106        "i32",
107        "i64",
108        "i128",
109        "isize",
110        // Unsigned integers
111        "u8",
112        "u16",
113        "u32",
114        "u64",
115        "u128",
116        "usize",
117        // Floating point
118        "f32",
119        "f64",
120        // Other primitives
121        "bool",
122        "char",
123        // Common Copy types
124        "NonZeroI8",
125        "NonZeroI16",
126        "NonZeroI32",
127        "NonZeroI64",
128        "NonZeroI128",
129        "NonZeroIsize",
130        "NonZeroU8",
131        "NonZeroU16",
132        "NonZeroU32",
133        "NonZeroU64",
134        "NonZeroU128",
135        "NonZeroUsize",
136    ];
137
138    /// Check if a type is a known Copy type
139    fn is_copy_type(ty: &PureType) -> bool {
140        match ty {
141            PureType::Path(path) => {
142                // Check if it's a primitive Copy type
143                Self::COPY_TYPES
144                    .iter()
145                    .any(|&t| path == t || path.ends_with(&format!("::{}", t)))
146            }
147            PureType::Ref { .. } => {
148                // References are always Copy
149                true
150            }
151            PureType::Tuple(types) => {
152                // Tuple is Copy if all elements are Copy
153                types.iter().all(Self::is_copy_type)
154            }
155            PureType::Array { ty, .. } => {
156                // Array is Copy if element type is Copy
157                Self::is_copy_type(ty)
158            }
159            _ => false,
160        }
161    }
162
163    /// Collect Copy variable names from function parameters
164    fn collect_copy_vars_from_params(params: &[PureParam]) -> HashSet<String> {
165        let mut copy_vars = HashSet::new();
166        for param in params {
167            if let PureParam::Typed { name, ty } = param {
168                if Self::is_copy_type(ty) {
169                    copy_vars.insert(name.clone());
170                }
171            }
172        }
173        copy_vars
174    }
175
176    /// Transform expressions, returns changes count
177    fn transform_expr(&self, expr: &mut PureExpr, fn_copy_vars: &HashSet<String>) -> usize {
178        let mut changes = 0;
179
180        // Check for .clone() call pattern
181        if let PureExpr::MethodCall {
182            receiver,
183            method,
184            args,
185            ..
186        } = expr
187        {
188            if method == "clone" && args.is_empty() {
189                let should_remove = if self.aggressive {
190                    true
191                } else {
192                    // Only remove if receiver is a known Copy type
193                    Self::is_copy_literal(receiver)
194                        || matches!(receiver.as_ref(), PureExpr::Path(p) if self.is_known_copy_var(p, fn_copy_vars))
195                };
196
197                if should_remove {
198                    // Replace .clone() call with just the receiver
199                    let inner = std::mem::replace(
200                        receiver.as_mut(),
201                        PureExpr::Path("__placeholder".to_string()),
202                    );
203                    *expr = inner;
204                    return 1;
205                }
206            }
207        }
208
209        // Recursively transform sub-expressions
210        match expr {
211            PureExpr::Binary { left, right, .. } => {
212                changes += self.transform_expr(left, fn_copy_vars);
213                changes += self.transform_expr(right, fn_copy_vars);
214            }
215            PureExpr::Unary { expr: inner, .. } => {
216                changes += self.transform_expr(inner, fn_copy_vars);
217            }
218            PureExpr::Call { func, args } => {
219                changes += self.transform_expr(func, fn_copy_vars);
220                for arg in args {
221                    changes += self.transform_expr(arg, fn_copy_vars);
222                }
223            }
224            PureExpr::MethodCall { receiver, args, .. } => {
225                changes += self.transform_expr(receiver, fn_copy_vars);
226                for arg in args {
227                    changes += self.transform_expr(arg, fn_copy_vars);
228                }
229            }
230            PureExpr::Field { expr: inner, .. } => {
231                changes += self.transform_expr(inner, fn_copy_vars);
232            }
233            PureExpr::Index { expr: inner, index } => {
234                changes += self.transform_expr(inner, fn_copy_vars);
235                changes += self.transform_expr(index, fn_copy_vars);
236            }
237            PureExpr::Block { block, .. } => {
238                changes += self.transform_block(block, fn_copy_vars);
239            }
240            PureExpr::If {
241                cond,
242                then_branch,
243                else_branch,
244            } => {
245                changes += self.transform_expr(cond, fn_copy_vars);
246                changes += self.transform_block(then_branch, fn_copy_vars);
247                if let Some(else_expr) = else_branch {
248                    changes += self.transform_expr(else_expr, fn_copy_vars);
249                }
250            }
251            PureExpr::Match { expr: e, arms } => {
252                changes += self.transform_expr(e, fn_copy_vars);
253                for arm in arms {
254                    changes += self.transform_expr(&mut arm.body, fn_copy_vars);
255                }
256            }
257            PureExpr::Loop { body: block, .. } | PureExpr::While { body: block, .. } => {
258                changes += self.transform_block(block, fn_copy_vars);
259            }
260            PureExpr::For {
261                expr: iter_expr,
262                body,
263                ..
264            } => {
265                changes += self.transform_expr(iter_expr, fn_copy_vars);
266                changes += self.transform_block(body, fn_copy_vars);
267            }
268            PureExpr::Closure { body, .. } => {
269                changes += self.transform_expr(body, fn_copy_vars);
270            }
271            PureExpr::Tuple(exprs) | PureExpr::Array(exprs) => {
272                for e in exprs {
273                    changes += self.transform_expr(e, fn_copy_vars);
274                }
275            }
276            PureExpr::Struct { fields, .. } => {
277                for (_, e) in fields {
278                    changes += self.transform_expr(e, fn_copy_vars);
279                }
280            }
281            PureExpr::Ref { expr: inner, .. } => {
282                changes += self.transform_expr(inner, fn_copy_vars);
283            }
284            PureExpr::Return(Some(inner)) => {
285                changes += self.transform_expr(inner, fn_copy_vars);
286            }
287            PureExpr::Try(inner) | PureExpr::Await(inner) => {
288                changes += self.transform_expr(inner, fn_copy_vars);
289            }
290            _ => {}
291        }
292
293        changes
294    }
295
296    fn transform_block(&self, block: &mut PureBlock, fn_copy_vars: &HashSet<String>) -> usize {
297        let mut changes = 0;
298        for stmt in &mut block.stmts {
299            changes += self.transform_stmt(stmt, fn_copy_vars);
300        }
301        changes
302    }
303
304    fn transform_stmt(&self, stmt: &mut PureStmt, fn_copy_vars: &HashSet<String>) -> usize {
305        match stmt {
306            PureStmt::Local { init: Some(e), .. } => self.transform_expr(e, fn_copy_vars),
307            PureStmt::Semi(e) | PureStmt::Expr(e) => self.transform_expr(e, fn_copy_vars),
308            _ => 0,
309        }
310    }
311
312    pub fn transform_fn(&self, func: &mut PureFn) -> usize {
313        // Note: target_fn filtering requires SymbolId comparison at executor layer.
314        // This method is called from executor with pre-filtered functions.
315        // Collect Copy variable names from function parameters
316        let fn_copy_vars = Self::collect_copy_vars_from_params(&func.params);
317        self.transform_block(&mut func.body, &fn_copy_vars)
318    }
319}
320
321impl Mutation for CloneOnCopyMutation {
322    fn describe(&self) -> String {
323        "Remove unnecessary .clone() on Copy types".to_string()
324    }
325
326    fn mutation_type(&self) -> &'static str {
327        "CloneOnCopy"
328    }
329
330    fn box_clone(&self) -> Box<dyn Mutation> {
331        Box::new(self.clone())
332    }
333}
334
335#[cfg(test)]
336mod tests {
337    use super::*;
338
339    #[test]
340    fn test_is_copy_literal_integer() {
341        assert!(CloneOnCopyMutation::is_copy_literal(&PureExpr::Lit(
342            "42".to_string()
343        )));
344        assert!(CloneOnCopyMutation::is_copy_literal(&PureExpr::Lit(
345            "1_000".to_string()
346        )));
347    }
348
349    #[test]
350    fn test_is_copy_literal_bool() {
351        assert!(CloneOnCopyMutation::is_copy_literal(&PureExpr::Lit(
352            "true".to_string()
353        )));
354        assert!(CloneOnCopyMutation::is_copy_literal(&PureExpr::Lit(
355            "false".to_string()
356        )));
357        assert!(CloneOnCopyMutation::is_copy_literal(&PureExpr::Path(
358            "true".to_string()
359        )));
360    }
361
362    #[test]
363    fn test_is_copy_literal_char() {
364        assert!(CloneOnCopyMutation::is_copy_literal(&PureExpr::Lit(
365            "'a'".to_string()
366        )));
367        assert!(CloneOnCopyMutation::is_copy_literal(&PureExpr::Lit(
368            "'\\n'".to_string()
369        )));
370    }
371
372    #[test]
373    fn test_is_not_copy_literal() {
374        assert!(!CloneOnCopyMutation::is_copy_literal(&PureExpr::Lit(
375            "\"string\"".to_string()
376        )));
377        assert!(!CloneOnCopyMutation::is_copy_literal(&PureExpr::Path(
378            "variable".to_string()
379        )));
380    }
381
382    #[test]
383    fn test_known_copy_var() {
384        let mutation = CloneOnCopyMutation::new()
385            .with_copy_var("x")
386            .with_copy_var("count");
387
388        let empty_set = std::collections::HashSet::new();
389        assert!(mutation.is_known_copy_var("x", &empty_set));
390        assert!(mutation.is_known_copy_var("count", &empty_set));
391        assert!(!mutation.is_known_copy_var("other", &empty_set));
392    }
393}