ryo-mutations 0.1.0

[experimental] Code transformation primitives for Rust source code
Documentation
//! NoOpArmToTodoMutation: Replace empty/noop match arms with todo!/unimplemented!/unreachable!
//!
//! Transforms:
//! - `_ => {}` → `_ => todo!()`
//! - `_ => ()` → `_ => todo!()`
//! - `Variant => {}` → `Variant => todo!()`
//!
//! This helps identify unhandled cases that might be silently ignored.

use ryo_source::pure::{MacroDelimiter, PureBlock, PureExpr, PureMatchArm, PureStmt};
use ryo_symbol::SymbolId;

use crate::Mutation;

/// Replace empty/noop match arms with explicit placeholders
///
/// # Example
///
/// ```rust,ignore
/// use ryo_mutations::idiom::NoOpArmToTodoMutation;
///
/// let mutation = NoOpArmToTodoMutation::new();
/// // Transforms:
/// //   match x {
/// //       Some(v) => process(v),
/// //       _ => {}
/// //   }
/// // Into:
/// //   match x {
/// //       Some(v) => process(v),
/// //       _ => todo!()
/// //   }
/// ```
#[derive(Debug, Clone)]
pub struct NoOpArmToTodoMutation {
    /// Target function SymbolId. If None, applies to all functions.
    pub target_fn: Option<SymbolId>,
    /// Replacement macro: "todo", "unimplemented", or "unreachable"
    pub replacement: String,
}

impl Default for NoOpArmToTodoMutation {
    fn default() -> Self {
        Self {
            target_fn: None,
            replacement: "todo".to_string(),
        }
    }
}

impl NoOpArmToTodoMutation {
    pub fn new() -> Self {
        Self::default()
    }

    /// Only apply in a specific function
    pub fn in_function(mut self, id: SymbolId) -> Self {
        self.target_fn = Some(id);
        self
    }

    /// Set replacement macro
    pub fn with_replacement(mut self, replacement: impl Into<String>) -> Self {
        self.replacement = replacement.into();
        self
    }

    /// Check if a match arm body is a noop (empty block or unit tuple)
    fn is_noop_body(expr: &PureExpr) -> bool {
        match expr {
            // Empty block: {}
            PureExpr::Block { block, .. } => block.stmts.is_empty(),
            // Unit tuple: ()
            PureExpr::Tuple(items) => items.is_empty(),
            _ => false,
        }
    }

    /// Create a macro call expression for the replacement
    fn create_replacement_expr(&self) -> PureExpr {
        PureExpr::Macro {
            name: self.replacement.clone(),
            delimiter: MacroDelimiter::Paren,
            tokens: "".to_string(),
        }
    }

    /// Transform match arms, returns number of changes
    fn transform_arms(&self, arms: &mut [PureMatchArm]) -> usize {
        let mut changes = 0;

        for arm in arms.iter_mut() {
            // First, recursively transform the body (in case there's a nested match)
            changes += self.transform_expr(&mut arm.body);

            // Then, check if this arm's body is a noop
            if Self::is_noop_body(&arm.body) {
                arm.body = self.create_replacement_expr();
                changes += 1;
            }
        }

        changes
    }

    /// Transform an expression, returns changes count
    fn transform_expr(&self, expr: &mut PureExpr) -> usize {
        let mut changes = 0;

        match expr {
            PureExpr::Match { arms, expr: inner } => {
                // Recursively transform the match scrutinee
                changes += self.transform_expr(inner);
                // Transform the arms
                changes += self.transform_arms(arms);
            }
            PureExpr::If {
                cond,
                then_branch,
                else_branch,
            } => {
                changes += self.transform_expr(cond);
                changes += self.transform_block(then_branch);
                if let Some(else_expr) = else_branch {
                    changes += self.transform_expr(else_expr);
                }
            }
            PureExpr::Block { block, .. } => {
                changes += self.transform_block(block);
            }
            PureExpr::Call { func, args } => {
                changes += self.transform_expr(func);
                for arg in args {
                    changes += self.transform_expr(arg);
                }
            }
            PureExpr::MethodCall { receiver, args, .. } => {
                changes += self.transform_expr(receiver);
                for arg in args {
                    changes += self.transform_expr(arg);
                }
            }
            PureExpr::Binary { left, right, .. } => {
                changes += self.transform_expr(left);
                changes += self.transform_expr(right);
            }
            PureExpr::Unary { expr: inner, .. } => {
                changes += self.transform_expr(inner);
            }
            PureExpr::Closure { body, .. } => {
                changes += self.transform_expr(body);
            }
            PureExpr::While { cond, body, .. } => {
                changes += self.transform_expr(cond);
                changes += self.transform_block(body);
            }
            PureExpr::Loop { body, .. } => {
                changes += self.transform_block(body);
            }
            PureExpr::For { expr, body, .. } => {
                changes += self.transform_expr(expr);
                changes += self.transform_block(body);
            }
            PureExpr::Tuple(items) | PureExpr::Array(items) => {
                for item in items {
                    changes += self.transform_expr(item);
                }
            }
            PureExpr::Struct { fields, .. } => {
                for (_, value) in fields {
                    changes += self.transform_expr(value);
                }
            }
            PureExpr::Index { expr, index } => {
                changes += self.transform_expr(expr);
                changes += self.transform_expr(index);
            }
            PureExpr::Field { expr, .. } => {
                changes += self.transform_expr(expr);
            }
            PureExpr::Cast { expr, .. } => {
                changes += self.transform_expr(expr);
            }
            PureExpr::Return(Some(inner))
            | PureExpr::Break {
                expr: Some(inner), ..
            }
            | PureExpr::Await(inner)
            | PureExpr::Try(inner)
            | PureExpr::Ref { expr: inner, .. }
            | PureExpr::Let { expr: inner, .. } => {
                changes += self.transform_expr(inner);
            }
            PureExpr::Range { start, end, .. } => {
                if let Some(s) = start {
                    changes += self.transform_expr(s);
                }
                if let Some(e) = end {
                    changes += self.transform_expr(e);
                }
            }
            PureExpr::Unsafe(block) | PureExpr::Async { body: block, .. } => {
                changes += self.transform_block(block);
            }
            PureExpr::Repeat { expr, len } => {
                changes += self.transform_expr(expr);
                changes += self.transform_expr(len);
            }
            // Leaf nodes - no recursion needed
            _ => {}
        }

        changes
    }

    /// Transform a block, returns changes count
    pub fn transform_block(&self, block: &mut PureBlock) -> usize {
        let mut changes = 0;

        for stmt in &mut block.stmts {
            match stmt {
                PureStmt::Local {
                    init: Some(init_expr),
                    ..
                } => {
                    changes += self.transform_expr(init_expr);
                }
                PureStmt::Expr(expr) | PureStmt::Semi(expr) => {
                    changes += self.transform_expr(expr);
                }
                _ => {}
            }
        }

        changes
    }
}

impl Mutation for NoOpArmToTodoMutation {
    fn describe(&self) -> String {
        format!(
            "Replace empty match arms with {}!() (_ => {{}} → _ => {}!())",
            self.replacement, self.replacement
        )
    }

    fn mutation_type(&self) -> &'static str {
        "NoOpArmToTodo"
    }

    fn box_clone(&self) -> Box<dyn Mutation> {
        Box::new(self.clone())
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use ryo_source::pure::PurePattern;

    fn create_empty_block() -> PureExpr {
        PureExpr::Block {
            label: None,
            block: PureBlock { stmts: vec![] },
        }
    }

    fn create_unit_tuple() -> PureExpr {
        PureExpr::Tuple(vec![])
    }

    #[test]
    fn test_is_noop_body_empty_block() {
        assert!(NoOpArmToTodoMutation::is_noop_body(&create_empty_block()));
    }

    #[test]
    fn test_is_noop_body_unit_tuple() {
        assert!(NoOpArmToTodoMutation::is_noop_body(&create_unit_tuple()));
    }

    #[test]
    fn test_is_noop_body_non_empty() {
        let non_empty = PureExpr::Path("something".to_string());
        assert!(!NoOpArmToTodoMutation::is_noop_body(&non_empty));
    }

    #[test]
    fn test_create_replacement_expr_default() {
        let mutation = NoOpArmToTodoMutation::new();
        let expr = mutation.create_replacement_expr();
        match expr {
            PureExpr::Macro { name, tokens, .. } => {
                assert_eq!(name, "todo");
                assert_eq!(tokens, "");
            }
            _ => panic!("Expected macro expression"),
        }
    }

    #[test]
    fn test_create_replacement_expr_custom() {
        let mutation = NoOpArmToTodoMutation::new().with_replacement("unreachable");
        let expr = mutation.create_replacement_expr();
        match expr {
            PureExpr::Macro { name, tokens, .. } => {
                assert_eq!(name, "unreachable");
                assert_eq!(tokens, "");
            }
            _ => panic!("Expected macro expression"),
        }
    }

    #[test]
    fn test_transform_match_arms() {
        let mut arms = vec![
            PureMatchArm {
                pattern: PurePattern::Wild,
                guard: None,
                body: create_empty_block(),
            },
            PureMatchArm {
                pattern: PurePattern::Wild,
                guard: None,
                body: PureExpr::Path("existing".to_string()),
            },
        ];

        let mutation = NoOpArmToTodoMutation::new();
        let changes = mutation.transform_arms(&mut arms);

        assert_eq!(changes, 1);
        // First arm should be transformed
        match &arms[0].body {
            PureExpr::Macro { name, .. } => assert_eq!(name, "todo"),
            _ => panic!("Expected macro"),
        }
        // Second arm should remain unchanged
        match &arms[1].body {
            PureExpr::Path(p) => assert_eq!(p, "existing"),
            _ => panic!("Expected path"),
        }
    }
}