ryo-mutations 0.1.0

[experimental] Code transformation primitives for Rust source code
Documentation
//! MatchToIfLetMutation: Convert match expressions to if let
//!
//! Transforms:
//! - `match opt { Some(x) => body, None => {} }` → `if let Some(x) = opt { body }`
//! - `match res { Ok(x) => body, Err(_) => {} }` → `if let Ok(x) = res { body }`
//!
//! Corresponds to Clippy lint: `clippy::single_match`

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

use crate::Mutation;

/// Convert simple match expressions to if let
///
/// # Example
///
/// ```rust,ignore
/// use ryo_mutations::idiom::MatchToIfLetMutation;
///
/// let mutation = MatchToIfLetMutation::new();
/// // Transforms:
/// //   match opt {
/// //       Some(x) => println!("{}", x),
/// //       None => {}
/// //   }
/// // Into:
/// //   if let Some(x) = opt {
/// //       println!("{}", x);
/// //   }
/// ```
#[derive(Debug, Clone, Default)]
pub struct MatchToIfLetMutation {
    /// Target function SymbolId. If None, applies to all functions.
    pub target_fn: Option<SymbolId>,
}

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

    pub fn in_function(mut self, id: SymbolId) -> Self {
        self.target_fn = Some(id);
        self
    }

    /// Check if pattern is Some(x) and extract the binding name
    fn is_some_pattern(pattern: &PurePattern) -> Option<String> {
        match pattern {
            PurePattern::Struct { path, fields, .. } => {
                if (path == "Some" || path.ends_with("::Some")) && fields.len() == 1 {
                    if let Some((_, PurePattern::Ident { name, .. })) = fields.first() {
                        return Some(name.clone());
                    }
                }
                None
            }
            _ => None,
        }
    }

    /// Check if pattern is None
    fn is_none_pattern(pattern: &PurePattern) -> bool {
        match pattern {
            PurePattern::Path(p) => p == "None" || p.ends_with("::None"),
            PurePattern::Ident { name, .. } => name == "None",
            _ => false,
        }
    }

    /// Check if pattern is Ok(x) and extract the binding name
    fn is_ok_pattern(pattern: &PurePattern) -> Option<String> {
        match pattern {
            PurePattern::Struct { path, fields, .. } => {
                if (path == "Ok" || path.ends_with("::Ok")) && fields.len() == 1 {
                    if let Some((_, PurePattern::Ident { name, .. })) = fields.first() {
                        return Some(name.clone());
                    }
                }
                None
            }
            _ => None,
        }
    }

    /// Check if pattern is Err(_) or Err(e)
    fn is_err_pattern(pattern: &PurePattern) -> bool {
        match pattern {
            PurePattern::Struct { path, fields, .. } => {
                if (path == "Err" || path.ends_with("::Err")) && fields.len() == 1 {
                    // Accept any binding including wildcard
                    return true;
                }
                false
            }
            _ => false,
        }
    }

    /// Check if body is empty (empty block or unit expression)
    fn is_empty_body(expr: &PureExpr) -> bool {
        match expr {
            PureExpr::Block { block, .. } => block.stmts.is_empty(),
            PureExpr::Tuple(elems) if elems.is_empty() => true, // Unit ()
            PureExpr::Path(p) if p == "()" => true,
            _ => false,
        }
    }

    /// Try to convert a match expression to if let
    fn try_convert_match(scrutinee: &PureExpr, arms: &[PureMatchArm]) -> Option<PureExpr> {
        if arms.len() != 2 {
            return None;
        }

        // Try pattern: Some(x) => body, None => {}
        if let Some(var_name) = Self::is_some_pattern(&arms[0].pattern) {
            if Self::is_none_pattern(&arms[1].pattern) && Self::is_empty_body(&arms[1].body) {
                return Some(Self::create_if_let(
                    scrutinee.clone(),
                    "Some".to_string(),
                    var_name,
                    arms[0].body.clone(),
                ));
            }
        }

        // Try reversed: None => {}, Some(x) => body
        if Self::is_none_pattern(&arms[0].pattern) && Self::is_empty_body(&arms[0].body) {
            if let Some(var_name) = Self::is_some_pattern(&arms[1].pattern) {
                return Some(Self::create_if_let(
                    scrutinee.clone(),
                    "Some".to_string(),
                    var_name,
                    arms[1].body.clone(),
                ));
            }
        }

        // Try pattern: Ok(x) => body, Err(_) => {}
        if let Some(var_name) = Self::is_ok_pattern(&arms[0].pattern) {
            if Self::is_err_pattern(&arms[1].pattern) && Self::is_empty_body(&arms[1].body) {
                return Some(Self::create_if_let(
                    scrutinee.clone(),
                    "Ok".to_string(),
                    var_name,
                    arms[0].body.clone(),
                ));
            }
        }

        // Try reversed: Err(_) => {}, Ok(x) => body
        if Self::is_err_pattern(&arms[0].pattern) && Self::is_empty_body(&arms[0].body) {
            if let Some(var_name) = Self::is_ok_pattern(&arms[1].pattern) {
                return Some(Self::create_if_let(
                    scrutinee.clone(),
                    "Ok".to_string(),
                    var_name,
                    arms[1].body.clone(),
                ));
            }
        }

        None
    }

    /// Create an if let expression
    ///
    /// `if let Some(x) = opt { body }` is represented as:
    /// ```text
    /// If {
    ///     cond: Let { pattern: Some(x), expr: opt },
    ///     then_branch: body,
    ///     else_branch: None,
    /// }
    /// ```
    fn create_if_let(
        scrutinee: PureExpr,
        variant: String,
        var_name: String,
        body: PureExpr,
    ) -> PureExpr {
        // Convert body to block if needed
        let then_block = match body {
            PureExpr::Block { block, .. } => block,
            other => PureBlock {
                stmts: vec![PureStmt::Expr(other)],
            },
        };

        // Create the let expression: `let Some(x) = scrutinee`
        let let_expr = PureExpr::Let {
            pattern: PurePattern::Struct {
                path: variant,
                fields: vec![(
                    "0".to_string(),
                    PurePattern::Ident {
                        name: var_name,
                        is_mut: false,
                    },
                )],
                rest: false,
            },
            expr: Box::new(scrutinee),
        };

        // Create the if expression with the let as condition
        PureExpr::If {
            cond: Box::new(let_expr),
            then_branch: then_block,
            else_branch: None,
        }
    }

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

        // Check for match pattern
        if let PureExpr::Match {
            expr: scrutinee,
            arms,
        } = expr
        {
            if let Some(if_let) = Self::try_convert_match(scrutinee, arms) {
                *expr = if_let;
                return 1;
            }
        }

        // Recursively transform sub-expressions
        match expr {
            PureExpr::Binary { left, right, .. } => {
                changes += self.transform_expr(left);
                changes += self.transform_expr(right);
            }
            PureExpr::Unary { expr: inner, .. } => {
                changes += self.transform_expr(inner);
            }
            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::Block { block, .. } => {
                changes += self.transform_block(block);
            }
            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::Match { expr: e, arms } => {
                changes += self.transform_expr(e);
                for arm in arms {
                    changes += self.transform_expr(&mut arm.body);
                }
            }
            PureExpr::Loop { body: block, .. } | PureExpr::While { body: block, .. } => {
                changes += self.transform_block(block);
            }
            PureExpr::For {
                expr: iter_expr,
                body,
                ..
            } => {
                changes += self.transform_expr(iter_expr);
                changes += self.transform_block(body);
            }
            PureExpr::Closure { body, .. } => {
                changes += self.transform_expr(body);
            }
            _ => {}
        }

        changes
    }

    pub fn transform_block(&self, block: &mut PureBlock) -> usize {
        let mut changes = 0;
        for stmt in &mut block.stmts {
            changes += self.transform_stmt(stmt);
        }
        changes
    }

    fn transform_stmt(&self, stmt: &mut PureStmt) -> usize {
        match stmt {
            PureStmt::Local { init: Some(e), .. } => self.transform_expr(e),
            PureStmt::Semi(e) | PureStmt::Expr(e) => self.transform_expr(e),
            _ => 0,
        }
    }
}

impl Mutation for MatchToIfLetMutation {
    fn describe(&self) -> String {
        "Convert match to if let".to_string()
    }

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

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

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

    #[test]
    fn test_is_some_pattern() {
        let pattern = PurePattern::Struct {
            path: "Some".to_string(),
            fields: vec![(
                "0".to_string(),
                PurePattern::Ident {
                    name: "x".to_string(),
                    is_mut: false,
                },
            )],
            rest: false,
        };
        assert_eq!(
            MatchToIfLetMutation::is_some_pattern(&pattern),
            Some("x".to_string())
        );
    }

    #[test]
    fn test_is_none_pattern_ident() {
        let pattern = PurePattern::Ident {
            name: "None".to_string(),
            is_mut: false,
        };
        assert!(MatchToIfLetMutation::is_none_pattern(&pattern));
    }

    #[test]
    fn test_is_empty_body() {
        let empty_block = PureExpr::Block {
            label: None,
            block: PureBlock { stmts: vec![] },
        };
        assert!(MatchToIfLetMutation::is_empty_body(&empty_block));
    }
}