ryo-executor 0.1.0

[experimental] Mutation execution engine for RYO - parallel execution, conflict detection, workspace management
Documentation
//! StmtConverter: Converts statement-level MutationSpec variants to Mutations
//!
//! Handles:
//! - ReplaceExpr
//! - RemoveStatement
//! - InsertStatement
//! - ReplaceStatement

use crate::engine::ASTRegApply;
use crate::executor::registry::converters::ResolveTargetSymbol;
use crate::executor::registry::{ConvertError, MutationConverter};
use crate::executor::spec::{MutationSpec, StmtInsertPosition};
use ryo_analysis::AnalysisContext;
use ryo_analysis::SymbolPath;
use ryo_mutations::basic::stmt::{
    InsertPosition, InsertStatementMutation, RemoveStatementMutation, ReplaceExprAtMutation,
    ReplaceExprMutation, ReplaceStatementMutation,
};
use ryo_source::pure::{PureExpr, PureStmt, ToPure};

/// Converter for statement-level mutations
#[derive(Debug, Clone, Default)]
pub struct StmtConverter;

impl StmtConverter {
    pub fn new() -> Self {
        Self
    }

    /// Parse a Rust expression string into PureExpr
    pub fn parse_expr(expr_str: &str) -> Result<PureExpr, ConvertError> {
        let expr: syn::Expr = syn::parse_str(expr_str).map_err(|e| {
            ConvertError::Parse(format!("Failed to parse expression '{}': {}", expr_str, e))
        })?;
        Ok(expr.to_pure())
    }

    /// Parse a Rust statement string into PureStmt
    pub fn parse_stmt(stmt_str: &str) -> Result<PureStmt, ConvertError> {
        // Try parsing as a statement first
        let stmt_with_semi = if stmt_str.ends_with(';') {
            stmt_str.to_string()
        } else {
            format!("{};", stmt_str)
        };

        let stmt: syn::Stmt = syn::parse_str(&stmt_with_semi).map_err(|e| {
            ConvertError::Parse(format!("Failed to parse statement '{}': {}", stmt_str, e))
        })?;

        // Use ToPure trait implementation for syn::Stmt
        Ok(stmt.to_pure())
    }

    /// Convert StmtInsertPosition to internal InsertPosition
    fn convert_position(pos: &StmtInsertPosition) -> InsertPosition {
        match pos {
            StmtInsertPosition::Start => InsertPosition::Start,
            StmtInsertPosition::End => InsertPosition::End,
            StmtInsertPosition::BeforePattern => InsertPosition::BeforePattern,
            StmtInsertPosition::AfterPattern => InsertPosition::AfterPattern,
        }
    }
}

// StmtConverter uses the default implementation of ResolveTargetSymbol
impl ResolveTargetSymbol for StmtConverter {}

impl MutationConverter for StmtConverter {
    fn spec_kinds(&self) -> &'static [&'static str] {
        &[
            "ReplaceExpr",
            "RemoveStatement",
            "InsertStatement",
            "ReplaceStatement",
        ]
    }

    fn convert_v2(
        &self,
        spec: &MutationSpec,
        ctx: &AnalysisContext,
    ) -> Result<Vec<Box<dyn ASTRegApply>>, ConvertError> {
        match spec {
            MutationSpec::ReplaceExpr {
                fn_id,
                old_expr,
                new_expr,
                replace_all,
                symbol_path,
                ..
            } => {
                let new = Self::parse_expr(new_expr)?;

                // If symbol_path is provided, use position-based replacement
                if let Some(path_str) = symbol_path {
                    let path = SymbolPath::parse(path_str).map_err(|e| {
                        ConvertError::Parse(format!("Invalid symbol_path '{}': {}", path_str, e))
                    })?;

                    if !path.has_body_segment() {
                        return Err(ConvertError::Parse(format!(
                            "symbol_path '{}' must contain $body segment for position-based replacement",
                            path_str
                        )));
                    }

                    let (fn_path, indices) = path.split_at_body().ok_or_else(|| {
                        ConvertError::Parse(format!(
                            "Failed to extract function path from symbol_path '{}'",
                            path_str
                        ))
                    })?;

                    // Lookup SymbolId from function path
                    let fn_id = ctx.registry.lookup(&fn_path).ok_or_else(|| {
                        ConvertError::TargetNotFound(format!(
                            "Function '{}' not found in registry",
                            fn_path
                        ))
                    })?;

                    let mutation = ReplaceExprAtMutation::new(fn_id, indices, new);
                    return Ok(vec![Box::new(mutation)]);
                }

                let old = Self::parse_expr(old_expr)?;

                // If fn_id is None, generate mutations for all functions
                let mutations: Vec<Box<dyn ASTRegApply>> = if let Some(id) = fn_id {
                    let mut mutation = ReplaceExprMutation::new(old.clone(), new.clone(), *id);
                    mutation.replace_all = *replace_all;
                    vec![Box::new(mutation)]
                } else {
                    use ryo_analysis::SymbolKind;
                    ctx.registry
                        .iter()
                        .filter(|(id, _)| {
                            matches!(ctx.registry.kind(*id), Some(SymbolKind::Function))
                        })
                        .map(|(id, _)| {
                            let mut mutation =
                                ReplaceExprMutation::new(old.clone(), new.clone(), id);
                            mutation.replace_all = *replace_all;
                            Box::new(mutation) as Box<dyn ASTRegApply>
                        })
                        .collect()
                };

                Ok(mutations)
            }

            MutationSpec::RemoveStatement {
                fn_id,
                pattern,
                remove_all,
                ..
            } => {
                let target_stmt = Self::parse_stmt(pattern)?;

                // If fn_id is None, generate mutations for all functions
                let mutations: Vec<Box<dyn ASTRegApply>> = if let Some(id) = fn_id {
                    let mut mutation =
                        RemoveStatementMutation::new(target_stmt.clone(), pattern.clone(), *id);
                    mutation.remove_all = *remove_all;
                    vec![Box::new(mutation)]
                } else {
                    use ryo_analysis::SymbolKind;
                    ctx.registry
                        .iter()
                        .filter(|(id, _)| {
                            matches!(ctx.registry.kind(*id), Some(SymbolKind::Function))
                        })
                        .map(|(id, _)| {
                            let mut mutation = RemoveStatementMutation::new(
                                target_stmt.clone(),
                                pattern.clone(),
                                id,
                            );
                            mutation.remove_all = *remove_all;
                            Box::new(mutation) as Box<dyn ASTRegApply>
                        })
                        .collect()
                };

                Ok(mutations)
            }

            MutationSpec::InsertStatement {
                fn_id,
                stmt,
                position,
                reference_pattern,
                ..
            } => {
                let pure_stmt = Self::parse_stmt(stmt)?;
                let reference_stmt = reference_pattern
                    .as_ref()
                    .map(|p| Self::parse_stmt(p))
                    .transpose()?;

                let mut mutation = InsertStatementMutation::new(pure_stmt, *fn_id);
                mutation.position = Self::convert_position(position);
                mutation.reference_stmt = reference_stmt;

                Ok(vec![Box::new(mutation)])
            }

            MutationSpec::ReplaceStatement {
                fn_id,
                old_stmt,
                new_stmt,
                ..
            } => {
                let old = Self::parse_stmt(old_stmt)?;
                let new = Self::parse_stmt(new_stmt)?;

                // If fn_id is None, generate mutations for all functions
                let mutations: Vec<Box<dyn ASTRegApply>> = if let Some(id) = fn_id {
                    vec![Box::new(ReplaceStatementMutation::new(
                        old.clone(),
                        new.clone(),
                        *id,
                    ))]
                } else {
                    use ryo_analysis::SymbolKind;
                    ctx.registry
                        .iter()
                        .filter(|(id, _)| {
                            matches!(ctx.registry.kind(*id), Some(SymbolKind::Function))
                        })
                        .map(|(id, _)| {
                            Box::new(ReplaceStatementMutation::new(old.clone(), new.clone(), id))
                                as Box<dyn ASTRegApply>
                        })
                        .collect()
                };

                Ok(mutations)
            }

            _ => Err(ConvertError::TypeMismatch {
                expected: "ReplaceExpr|RemoveStatement|InsertStatement|ReplaceStatement",
                actual: spec.kind_name().to_string(),
            }),
        }
    }
}

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

    #[test]
    fn test_stmt_converter_spec_kinds() {
        let converter = StmtConverter::new();
        let kinds = converter.spec_kinds();
        assert!(kinds.contains(&"ReplaceExpr"));
        assert!(kinds.contains(&"RemoveStatement"));
        assert!(kinds.contains(&"InsertStatement"));
        assert!(kinds.contains(&"ReplaceStatement"));
    }

    #[test]
    fn test_stmt_converter_can_handle() {
        let converter = StmtConverter::new();

        let replace_expr_spec = MutationSpec::ReplaceExpr {
            module_id: None,
            fn_id: None,
            old_expr: "a + b".into(),
            new_expr: "c + d".into(),
            replace_all: true,
            symbol_path: None,
        };
        assert!(converter.can_handle(&replace_expr_spec));

        let remove_stmt_spec = MutationSpec::RemoveStatement {
            module_id: None,
            fn_id: None,
            pattern: "println!".into(),
            remove_all: true,
            symbol_path: None,
        };
        assert!(converter.can_handle(&remove_stmt_spec));

        let insert_stmt_spec = MutationSpec::InsertStatement {
            module_id: None,
            fn_id: SymbolId::default(),
            stmt: "let x = 1".into(),
            position: StmtInsertPosition::Start,
            reference_pattern: None,
            symbol_path: None,
        };
        assert!(converter.can_handle(&insert_stmt_spec));

        let replace_stmt_spec = MutationSpec::ReplaceStatement {
            module_id: None,
            fn_id: None,
            old_stmt: "let x = 1".into(),
            new_stmt: "let x = 2".into(),
            symbol_path: None,
        };
        assert!(converter.can_handle(&replace_stmt_spec));
    }
}