ryo-executor 0.1.0

[experimental] Mutation execution engine for RYO - parallel execution, conflict detection, workspace management
Documentation
//! V2 ASTRegApply implementation for AssignOpMutation
//!
//! Converts assignment patterns to compound operators:
//! - `a = a + b` → `a += b`
//! - `a = a - b` → `a -= b`
//! - etc.

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

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

impl ASTRegApply for AssignOpMutation {
    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
            if let Some(PureItem::Fn(f)) = ctx.ast_registry.get_mut(target_id) {
                total_changes += self.transform_block(&mut f.body);
            }
        } else {
            // No target: process all 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) {
                    total_changes += self.transform_block(&mut f.body);
                }
            }

            // Process methods (from impl blocks)
            // New design: methods are registered directly as SymbolKind::Method
            let method_ids: Vec<_> = ctx
                .symbol_registry
                .iter()
                .filter(|(id, _)| matches!(ctx.symbol_registry.kind(*id), Some(SymbolKind::Method)))
                .map(|(id, _)| id)
                .collect();

            for id in method_ids {
                if let Some(PureItem::Fn(f)) = ctx.ast_registry.get_mut(id) {
                    total_changes += self.transform_block(&mut f.body);
                }
            }
        }

        MutationResult {
            mutation_type: self.mutation_type().to_string(),
            changes: total_changes,
            description: if total_changes > 0 {
                format!(
                    "Converted {} assignment(s) to compound operators",
                    total_changes
                )
            } else {
                "No assignments converted".to_string()
            },
        }
    }
}

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

    #[test]
    fn test_v2_assign_op_basic() {
        let mut ctx = ContextBuilder::new()
            .with_file(
                "src/lib.rs",
                r#"
fn increment(x: &mut i32) {
    *x = *x + 1;
}
"#,
            )
            .build();

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

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

    #[test]
    fn test_v2_assign_op_multiple() {
        let mut ctx = ContextBuilder::new()
            .with_file(
                "src/lib.rs",
                r#"
fn math(a: &mut i32, b: &mut i32) {
    *a = *a + 1;
    *b = *b * 2;
}
"#,
            )
            .build();

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

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

    #[test]
    fn test_v2_assign_op_in_method() {
        let mut ctx = ContextBuilder::new()
            .with_file(
                "src/lib.rs",
                r#"
struct Counter { value: i32 }

impl Counter {
    fn increment(&mut self) {
        self.value = self.value + 1;
    }
}
"#,
            )
            .build();

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

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

    #[test]
    fn test_v2_assign_op_target_fn() {
        let mut ctx = ContextBuilder::new()
            .with_file(
                "src/lib.rs",
                r#"
fn foo(x: &mut i32) {
    *x = *x + 1;
}

fn bar(x: &mut i32) {
    *x = *x + 2;
}
"#,
            )
            .build();

        // Find foo function's SymbolId
        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 = AssignOpMutation::new().in_function(foo_id);
        let result = ASTMutationEngine::execute_ast_reg(&mutation, &mut ctx);

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

    #[test]
    fn test_v2_assign_op_no_changes() {
        let mut ctx = ContextBuilder::new()
            .with_file(
                "src/lib.rs",
                r#"
fn already_compound(x: &mut i32) {
    *x += 1;
}
"#,
            )
            .build();

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

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