ryo-executor 0.2.0

[experimental] Mutation execution engine for RYO - parallel execution, conflict detection, workspace management
Documentation
//! V2 ASTRegApply implementation for CloneOnCopyMutation
//!
//! Removes unnecessary .clone() calls on Copy types:
//! - `x.clone()` → `x` (when x is a Copy type)

use ryo_analysis::SymbolKind;
use ryo_mutations::idiom::CloneOnCopyMutation;
use ryo_mutations::{Mutation, MutationResult};
use ryo_source::pure::{PureImplItem, PureItem};

use crate::engine::{ASTMutationContext, ASTRegApply, ModificationType};

impl ASTRegApply for CloneOnCopyMutation {
    fn apply_to_registry(&self, ctx: &mut ASTMutationContext) -> MutationResult {
        let mut total_changes = 0;

        if let Some(target_id) = self.target_fn {
            // Target function specified: direct lookup, transform only that fn.
            // Matches convention of every other scoped idiom mutation: standalone
            // Function only. Methods inside Impl blocks are not handled in scoped
            // mode (pre-existing limitation across all idioms).
            if let Some(PureItem::Fn(f)) = ctx.ast_registry.get_mut(target_id) {
                let changes = self.transform_fn(f);
                if changes > 0 {
                    ctx.emit_modified(target_id, ModificationType::BodyModified);
                    total_changes += changes;
                }
            }
        } else {
            // Process standalone functions
            let fn_ids: Vec<_> = ctx
                .symbol_registry
                .iter()
                .filter(|(id, _)| {
                    matches!(ctx.symbol_registry.kind(*id), Some(SymbolKind::Function))
                })
                .map(|(id, _)| id)
                .collect();

            for id in fn_ids {
                if let Some(PureItem::Fn(f)) = ctx.ast_registry.get_mut(id) {
                    let changes = self.transform_fn(f);
                    if changes > 0 {
                        ctx.emit_modified(id, ModificationType::BodyModified);
                        total_changes += changes;
                    }
                }
            }

            // Process impl blocks (methods)
            let impl_ids: Vec<_> = ctx
                .symbol_registry
                .iter()
                .filter(|(id, _)| matches!(ctx.symbol_registry.kind(*id), Some(SymbolKind::Impl)))
                .map(|(id, _)| id)
                .collect();

            for id in impl_ids {
                let mut impl_changes = 0;
                if let Some(PureItem::Impl(imp)) = ctx.ast_registry.get_mut(id) {
                    for impl_item in &mut imp.items {
                        if let PureImplItem::Fn(f) = impl_item {
                            impl_changes += self.transform_fn(f);
                        }
                    }
                }
                if impl_changes > 0 {
                    ctx.emit_modified(id, ModificationType::BodyModified);
                    total_changes += impl_changes;
                }
            }
        }

        MutationResult {
            mutation_type: self.mutation_type().to_string(),
            changes: total_changes,
            description: if total_changes > 0 {
                format!("Removed {} unnecessary .clone() call(s)", total_changes)
            } else {
                "No .clone() calls removed".to_string()
            },
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::engine::ASTMutationEngine;
    use ryo_analysis::testing::ContextBuilder;

    #[test]
    fn test_v2_clone_on_copy_target_fn_scopes_to_target_only() {
        // Boundary regression: target_fn must restrict transform to ONE fn.
        // Pre-fix the impl ignored target_fn entirely → both foo and bar would
        // be transformed (2 changes). Post-fix: only foo (1 change).
        let mut ctx = ContextBuilder::new()
            .with_file(
                "src/lib.rs",
                r#"
fn foo(x: i32) -> i32 {
    x.clone()
}

fn bar(y: i32) -> i32 {
    y.clone()
}
"#,
            )
            .build();

        let foo_id = ctx
            .registry
            .iter()
            .find(|(id, path)| {
                matches!(ctx.registry.kind(*id), Some(SymbolKind::Function)) && path.name() == "foo"
            })
            .map(|(id, _)| id)
            .expect("foo function not found");

        let mutation = CloneOnCopyMutation::new().in_function(foo_id);
        let result = ASTMutationEngine::execute_ast_reg(&mutation, &mut ctx);

        assert_eq!(result.result.changes, 1, "target_fn must scope to foo only");
    }

    #[test]
    fn test_v2_clone_on_copy_literal() {
        let mut ctx = ContextBuilder::new()
            .with_file(
                "src/lib.rs",
                r#"
fn process() -> i32 {
    let x = 42.clone();
    x
}
"#,
            )
            .build();

        let mutation = CloneOnCopyMutation::new();
        let result = ASTMutationEngine::execute_ast_reg(&mutation, &mut ctx);

        assert_eq!(result.result.changes, 1);
    }

    #[test]
    fn test_v2_clone_on_copy_param() {
        let mut ctx = ContextBuilder::new()
            .with_file(
                "src/lib.rs",
                r#"
fn process(x: i32) -> i32 {
    x.clone()
}
"#,
            )
            .build();

        let mutation = CloneOnCopyMutation::new();
        let result = ASTMutationEngine::execute_ast_reg(&mutation, &mut ctx);

        // Should remove because x is declared as i32 (Copy type)
        assert_eq!(result.result.changes, 1);
    }

    #[test]
    fn test_v2_clone_on_copy_no_changes() {
        let mut ctx = ContextBuilder::new()
            .with_file(
                "src/lib.rs",
                r#"
fn process(s: String) -> String {
    s.clone()
}
"#,
            )
            .build();

        let mutation = CloneOnCopyMutation::new();
        let result = ASTMutationEngine::execute_ast_reg(&mutation, &mut ctx);

        // String is not Copy, should not remove
        assert_eq!(result.result.changes, 0);
    }

    #[test]
    fn test_v2_clone_on_copy_aggressive() {
        let mut ctx = ContextBuilder::new()
            .with_file(
                "src/lib.rs",
                r#"
fn process(s: String) -> String {
    s.clone()
}
"#,
            )
            .build();

        let mutation = CloneOnCopyMutation::new().aggressive();
        let result = ASTMutationEngine::execute_ast_reg(&mutation, &mut ctx);

        // With aggressive mode, removes all .clone() calls
        assert_eq!(result.result.changes, 1);
    }
}