ryo-mutations 0.1.0

[experimental] Code transformation primitives for Rust source code
Documentation
//! CloneOnCopyMutation: Remove unnecessary .clone() on Copy types
//!
//! Transforms:
//! - `x.clone()` → `x` (when x is a Copy type)
//!
//! This mutation detects Copy types from:
//! - Literals (integers, floats, bools, chars)
//! - Function parameters with known Copy types (i32, u32, f64, bool, etc.)
//! - Explicitly marked Copy variables
//!
//! Corresponds to Clippy lint: `clippy::clone_on_copy`

use ryo_source::pure::{PureBlock, PureExpr, PureFn, PureParam, PureStmt, PureType};
use ryo_symbol::SymbolId;
use std::collections::HashSet;

use crate::Mutation;

/// Remove unnecessary .clone() calls on Copy types
///
/// Since we don't have full type information, this mutation is conservative
/// and only removes .clone() in patterns where the type is clearly Copy:
/// - Literals (integers, floats, bools, chars)
/// - Known Copy types (primitives)
///
/// For more aggressive removal, use with Clippy integration which has
/// full type information.
///
/// # Example
///
/// ```rust,ignore
/// use ryo_mutations::idiom::CloneOnCopyMutation;
///
/// let mutation = CloneOnCopyMutation::new().aggressive();
/// // With aggressive mode, transforms: x.clone()
/// // Into:                             x
/// // (requires external type checking for safety)
/// ```
#[derive(Debug, Clone, Default)]
pub struct CloneOnCopyMutation {
    /// Target function SymbolId. If None, applies to all functions.
    pub target_fn: Option<SymbolId>,
    /// Aggressive mode: remove all .clone() calls (requires external type checking)
    pub aggressive: bool,
    /// Known Copy type variable names (for targeted removal)
    pub copy_vars: Vec<String>,
}

impl CloneOnCopyMutation {
    pub fn new() -> Self {
        Self::default()
    }

    /// Only apply in a specific function
    pub fn in_function(mut self, id: SymbolId) -> Self {
        self.target_fn = Some(id);
        self
    }

    /// Enable aggressive mode (remove all .clone() calls)
    ///
    /// Use with caution - this should be combined with Clippy's type analysis
    /// or external validation.
    pub fn aggressive(mut self) -> Self {
        self.aggressive = true;
        self
    }

    /// Add a variable known to be Copy type
    pub fn with_copy_var(mut self, var: impl Into<String>) -> Self {
        self.copy_vars.push(var.into());
        self
    }

    /// Check if expression is a known Copy literal
    fn is_copy_literal(expr: &PureExpr) -> bool {
        match expr {
            PureExpr::Lit(lit) => {
                // Integer literals
                lit.chars().all(|c| c.is_ascii_digit() || c == '_')
                    // Float literals
                    || lit.contains('.') && lit.chars().all(|c| c.is_ascii_digit() || c == '.' || c == '_')
                    // Bool literals
                    || lit == "true" || lit == "false"
                    // Char literals
                    || (lit.starts_with('\'') && lit.ends_with('\''))
            }
            PureExpr::Path(path) => {
                // Known Copy paths
                path == "true" || path == "false"
            }
            _ => false,
        }
    }

    /// Check if a path is a known Copy variable
    fn is_known_copy_var(&self, path: &str, fn_copy_vars: &HashSet<String>) -> bool {
        self.copy_vars.iter().any(|v| v == path) || fn_copy_vars.contains(path)
    }

    /// Known primitive Copy types
    const COPY_TYPES: &'static [&'static str] = &[
        // Signed integers
        "i8",
        "i16",
        "i32",
        "i64",
        "i128",
        "isize",
        // Unsigned integers
        "u8",
        "u16",
        "u32",
        "u64",
        "u128",
        "usize",
        // Floating point
        "f32",
        "f64",
        // Other primitives
        "bool",
        "char",
        // Common Copy types
        "NonZeroI8",
        "NonZeroI16",
        "NonZeroI32",
        "NonZeroI64",
        "NonZeroI128",
        "NonZeroIsize",
        "NonZeroU8",
        "NonZeroU16",
        "NonZeroU32",
        "NonZeroU64",
        "NonZeroU128",
        "NonZeroUsize",
    ];

    /// Check if a type is a known Copy type
    fn is_copy_type(ty: &PureType) -> bool {
        match ty {
            PureType::Path(path) => {
                // Check if it's a primitive Copy type
                Self::COPY_TYPES
                    .iter()
                    .any(|&t| path == t || path.ends_with(&format!("::{}", t)))
            }
            PureType::Ref { .. } => {
                // References are always Copy
                true
            }
            PureType::Tuple(types) => {
                // Tuple is Copy if all elements are Copy
                types.iter().all(Self::is_copy_type)
            }
            PureType::Array { ty, .. } => {
                // Array is Copy if element type is Copy
                Self::is_copy_type(ty)
            }
            _ => false,
        }
    }

    /// Collect Copy variable names from function parameters
    fn collect_copy_vars_from_params(params: &[PureParam]) -> HashSet<String> {
        let mut copy_vars = HashSet::new();
        for param in params {
            if let PureParam::Typed { name, ty } = param {
                if Self::is_copy_type(ty) {
                    copy_vars.insert(name.clone());
                }
            }
        }
        copy_vars
    }

    /// Transform expressions, returns changes count
    fn transform_expr(&self, expr: &mut PureExpr, fn_copy_vars: &HashSet<String>) -> usize {
        let mut changes = 0;

        // Check for .clone() call pattern
        if let PureExpr::MethodCall {
            receiver,
            method,
            args,
            ..
        } = expr
        {
            if method == "clone" && args.is_empty() {
                let should_remove = if self.aggressive {
                    true
                } else {
                    // Only remove if receiver is a known Copy type
                    Self::is_copy_literal(receiver)
                        || matches!(receiver.as_ref(), PureExpr::Path(p) if self.is_known_copy_var(p, fn_copy_vars))
                };

                if should_remove {
                    // Replace .clone() call with just the receiver
                    let inner = std::mem::replace(
                        receiver.as_mut(),
                        PureExpr::Path("__placeholder".to_string()),
                    );
                    *expr = inner;
                    return 1;
                }
            }
        }

        // Recursively transform sub-expressions
        match expr {
            PureExpr::Binary { left, right, .. } => {
                changes += self.transform_expr(left, fn_copy_vars);
                changes += self.transform_expr(right, fn_copy_vars);
            }
            PureExpr::Unary { expr: inner, .. } => {
                changes += self.transform_expr(inner, fn_copy_vars);
            }
            PureExpr::Call { func, args } => {
                changes += self.transform_expr(func, fn_copy_vars);
                for arg in args {
                    changes += self.transform_expr(arg, fn_copy_vars);
                }
            }
            PureExpr::MethodCall { receiver, args, .. } => {
                changes += self.transform_expr(receiver, fn_copy_vars);
                for arg in args {
                    changes += self.transform_expr(arg, fn_copy_vars);
                }
            }
            PureExpr::Field { expr: inner, .. } => {
                changes += self.transform_expr(inner, fn_copy_vars);
            }
            PureExpr::Index { expr: inner, index } => {
                changes += self.transform_expr(inner, fn_copy_vars);
                changes += self.transform_expr(index, fn_copy_vars);
            }
            PureExpr::Block { block, .. } => {
                changes += self.transform_block(block, fn_copy_vars);
            }
            PureExpr::If {
                cond,
                then_branch,
                else_branch,
            } => {
                changes += self.transform_expr(cond, fn_copy_vars);
                changes += self.transform_block(then_branch, fn_copy_vars);
                if let Some(else_expr) = else_branch {
                    changes += self.transform_expr(else_expr, fn_copy_vars);
                }
            }
            PureExpr::Match { expr: e, arms } => {
                changes += self.transform_expr(e, fn_copy_vars);
                for arm in arms {
                    changes += self.transform_expr(&mut arm.body, fn_copy_vars);
                }
            }
            PureExpr::Loop { body: block, .. } | PureExpr::While { body: block, .. } => {
                changes += self.transform_block(block, fn_copy_vars);
            }
            PureExpr::For {
                expr: iter_expr,
                body,
                ..
            } => {
                changes += self.transform_expr(iter_expr, fn_copy_vars);
                changes += self.transform_block(body, fn_copy_vars);
            }
            PureExpr::Closure { body, .. } => {
                changes += self.transform_expr(body, fn_copy_vars);
            }
            PureExpr::Tuple(exprs) | PureExpr::Array(exprs) => {
                for e in exprs {
                    changes += self.transform_expr(e, fn_copy_vars);
                }
            }
            PureExpr::Struct { fields, .. } => {
                for (_, e) in fields {
                    changes += self.transform_expr(e, fn_copy_vars);
                }
            }
            PureExpr::Ref { expr: inner, .. } => {
                changes += self.transform_expr(inner, fn_copy_vars);
            }
            PureExpr::Return(Some(inner)) => {
                changes += self.transform_expr(inner, fn_copy_vars);
            }
            PureExpr::Try(inner) | PureExpr::Await(inner) => {
                changes += self.transform_expr(inner, fn_copy_vars);
            }
            _ => {}
        }

        changes
    }

    fn transform_block(&self, block: &mut PureBlock, fn_copy_vars: &HashSet<String>) -> usize {
        let mut changes = 0;
        for stmt in &mut block.stmts {
            changes += self.transform_stmt(stmt, fn_copy_vars);
        }
        changes
    }

    fn transform_stmt(&self, stmt: &mut PureStmt, fn_copy_vars: &HashSet<String>) -> usize {
        match stmt {
            PureStmt::Local { init: Some(e), .. } => self.transform_expr(e, fn_copy_vars),
            PureStmt::Semi(e) | PureStmt::Expr(e) => self.transform_expr(e, fn_copy_vars),
            _ => 0,
        }
    }

    pub fn transform_fn(&self, func: &mut PureFn) -> usize {
        // Note: target_fn filtering requires SymbolId comparison at executor layer.
        // This method is called from executor with pre-filtered functions.
        // Collect Copy variable names from function parameters
        let fn_copy_vars = Self::collect_copy_vars_from_params(&func.params);
        self.transform_block(&mut func.body, &fn_copy_vars)
    }
}

impl Mutation for CloneOnCopyMutation {
    fn describe(&self) -> String {
        "Remove unnecessary .clone() on Copy types".to_string()
    }

    fn mutation_type(&self) -> &'static str {
        "CloneOnCopy"
    }

    fn box_clone(&self) -> Box<dyn Mutation> {
        Box::new(self.clone())
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_is_copy_literal_integer() {
        assert!(CloneOnCopyMutation::is_copy_literal(&PureExpr::Lit(
            "42".to_string()
        )));
        assert!(CloneOnCopyMutation::is_copy_literal(&PureExpr::Lit(
            "1_000".to_string()
        )));
    }

    #[test]
    fn test_is_copy_literal_bool() {
        assert!(CloneOnCopyMutation::is_copy_literal(&PureExpr::Lit(
            "true".to_string()
        )));
        assert!(CloneOnCopyMutation::is_copy_literal(&PureExpr::Lit(
            "false".to_string()
        )));
        assert!(CloneOnCopyMutation::is_copy_literal(&PureExpr::Path(
            "true".to_string()
        )));
    }

    #[test]
    fn test_is_copy_literal_char() {
        assert!(CloneOnCopyMutation::is_copy_literal(&PureExpr::Lit(
            "'a'".to_string()
        )));
        assert!(CloneOnCopyMutation::is_copy_literal(&PureExpr::Lit(
            "'\\n'".to_string()
        )));
    }

    #[test]
    fn test_is_not_copy_literal() {
        assert!(!CloneOnCopyMutation::is_copy_literal(&PureExpr::Lit(
            "\"string\"".to_string()
        )));
        assert!(!CloneOnCopyMutation::is_copy_literal(&PureExpr::Path(
            "variable".to_string()
        )));
    }

    #[test]
    fn test_known_copy_var() {
        let mutation = CloneOnCopyMutation::new()
            .with_copy_var("x")
            .with_copy_var("count");

        let empty_set = std::collections::HashSet::new();
        assert!(mutation.is_known_copy_var("x", &empty_set));
        assert!(mutation.is_known_copy_var("count", &empty_set));
        assert!(!mutation.is_known_copy_var("other", &empty_set));
    }
}