ryo-executor 0.1.0

[experimental] Mutation execution engine for RYO - parallel execution, conflict detection, workspace management
Documentation
//! V2 ASTRegApply implementation for UnwrapToQuestionMutation
//!
//! Converts .unwrap()/.expect() to ? operator:
//! - `x.unwrap()` → `x?`
//! - `x.expect("msg")` → `x?`
//!
//! Only applies in functions that return Result/Option.

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

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

/// Check if a return type is Option or Result (compatible with ? operator)
fn returns_option_or_result(ret: &Option<PureType>) -> bool {
    match ret {
        Some(PureType::Path(path)) => {
            path.starts_with("Option<")
                || path.starts_with("Result<")
                || path == "Option"
                || path == "Result"
        }
        _ => false,
    }
}

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

        // 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) {
                // Only transform if function returns Option or Result
                if returns_option_or_result(&f.ret) {
                    total_changes += self.transform_block(&mut f.body);
                }
            }
        }

        // 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 {
            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 {
                        // Only transform if method returns Option or Result
                        if returns_option_or_result(&f.ret) {
                            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 {} .unwrap()/.expect() to ?", total_changes)
            } else {
                "No unwrap calls converted".to_string()
            },
        }
    }
}

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

    #[test]
    fn test_v2_unwrap_to_question_basic() {
        let mut ctx = ContextBuilder::new()
            .with_file(
                "src/lib.rs",
                r#"
fn process(opt: Option<i32>) -> Option<i32> {
    let x = opt.unwrap();
    Some(x + 1)
}
"#,
            )
            .build();

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

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

    #[test]
    fn test_v2_unwrap_to_question_non_option_return() {
        let mut ctx = ContextBuilder::new()
            .with_file(
                "src/lib.rs",
                r#"
fn process(opt: Option<i32>) -> i32 {
    opt.unwrap()
}
"#,
            )
            .build();

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

        // Should not convert because function doesn't return Option/Result
        assert_eq!(result.result.changes, 0);
    }

    #[test]
    fn test_v2_unwrap_to_question_expect() {
        let mut ctx = ContextBuilder::new()
            .with_file(
                "src/lib.rs",
                r#"
fn process(opt: Option<i32>) -> Option<i32> {
    let x = opt.expect("should not be none");
    Some(x + 1)
}
"#,
            )
            .build();

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

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