ryo-executor 0.2.0

[experimental] Mutation execution engine for RYO - parallel execution, conflict detection, workspace management
Documentation
//! V2 ASTRegApply implementation for NoOpArmToTodoMutation
//!
//! Replaces empty/noop match arms with todo!/unimplemented!/unreachable!:
//! - `_ => {}` → `_ => todo!()`
//! - `_ => ()` → `_ => todo!()`

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

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

impl ASTRegApply for NoOpArmToTodoMutation {
    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) {
                let changes = self.transform_block(&mut f.body);
                if changes > 0 {
                    ctx.emit_modified(target_id, ModificationType::BodyModified);
                    total_changes += changes;
                }
            }
        } 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) {
                    let changes = self.transform_block(&mut f.body);
                    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_block(&mut f.body);
                        }
                    }
                }
                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!(
                    "Replaced {} empty match arm(s) with {}!()",
                    total_changes, self.replacement
                )
            } else {
                "No empty match arms found".to_string()
            },
        }
    }
}

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

    #[test]
    fn test_v2_noop_arm_empty_block() {
        let mut ctx = ContextBuilder::new()
            .with_file(
                "src/lib.rs",
                r#"
fn process(x: Option<i32>) {
    match x {
        Some(v) => println!("{}", v),
        _ => {}
    }
}
"#,
            )
            .build();

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

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

    #[test]
    fn test_v2_noop_arm_unit_tuple() {
        let mut ctx = ContextBuilder::new()
            .with_file(
                "src/lib.rs",
                r#"
fn process(x: Option<i32>) {
    match x {
        Some(v) => println!("{}", v),
        None => ()
    }
}
"#,
            )
            .build();

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

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

    #[test]
    fn test_v2_noop_arm_with_replacement() {
        let mut ctx = ContextBuilder::new()
            .with_file(
                "src/lib.rs",
                r#"
fn process(x: Option<i32>) {
    match x {
        Some(v) => println!("{}", v),
        _ => {}
    }
}
"#,
            )
            .build();

        let mutation = NoOpArmToTodoMutation::new().with_replacement("unreachable");
        let result = ASTMutationEngine::execute_ast_reg(&mutation, &mut ctx);

        assert_eq!(result.result.changes, 1);
        assert!(result.result.description.contains("unreachable!()"));
    }

    #[test]
    fn test_v2_noop_arm_no_changes() {
        let mut ctx = ContextBuilder::new()
            .with_file(
                "src/lib.rs",
                r#"
fn process(x: Option<i32>) {
    match x {
        Some(v) => println!("{}", v),
        None => todo!()
    }
}
"#,
            )
            .build();

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

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

    #[test]
    fn test_v2_noop_arm_emits_event_on_positive_changes() {
        // Issue d223c4e4 regression pin: idiom mutations must emit
        // MutationEvent via ctx.emit_modified(...) whenever
        // transform_block returns changes > 0. Without the event,
        // blueprint_executor's sync_files_and_rebuild aggregates an
        // empty event set and the dry_run path reports 0 modified
        // files even though the AST changed in place.
        //
        // db8f9a8c added emit_modified to all 13 idiom impls
        // (noop_arm / collapsible_if / bool_simplify / manual_map /
        //  match_to_if_let / map_unwrap_or / filter_next /
        //  redundant_closure / unwrap_to_question /
        //  comparison_to_method / loop_to_iter / clone_on_copy /
        //  assign_op). This spec pins the contract on the
        // representative noop_arm path (the other 12 share the
        // same structural pattern).
        let mut ctx = ContextBuilder::new()
            .with_file(
                "src/lib.rs",
                r#"
fn process(x: Option<i32>) {
    match x {
        Some(v) => println!("{}", v),
        _ => {}
    }
}
"#,
            )
            .build();

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

        assert_eq!(result.result.changes, 1);
        assert!(
            !result.events.is_empty(),
            "events must be emitted when changes > 0 (issue d223c4e4 regression pin)"
        );
    }
}