ryo-mutations 0.1.0

[experimental] Code transformation primitives for Rust source code
Documentation
//! CollapsibleIfMutation: Merge nested if statements
//!
//! Transforms:
//! - `if a { if b { body } }` → `if a && b { body }`
//!
//! Corresponds to Clippy lint: `clippy::collapsible_if`

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

use crate::Mutation;

/// Merge nested if statements using && operator
///
/// # Example
///
/// ```rust,ignore
/// use ryo_mutations::idiom::CollapsibleIfMutation;
///
/// let mutation = CollapsibleIfMutation::new();
/// // Transforms:
/// //   if a {
/// //       if b {
/// //           do_something();
/// //       }
/// //   }
/// // Into:
/// //   if a && b {
/// //       do_something();
/// //   }
/// ```
#[derive(Debug, Clone, Default)]
pub struct CollapsibleIfMutation {
    /// Target function SymbolId. If None, applies to all functions.
    pub target_fn: Option<SymbolId>,
}

impl CollapsibleIfMutation {
    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
    }

    /// Check if a block contains only a single if statement with no else
    fn is_single_if_block(block: &PureBlock) -> Option<(&PureExpr, &PureBlock)> {
        if block.stmts.len() != 1 {
            return None;
        }

        let expr = match &block.stmts[0] {
            PureStmt::Expr(e) | PureStmt::Semi(e) => e,
            _ => return None,
        };

        match expr {
            PureExpr::If {
                cond,
                then_branch,
                else_branch: None,
            } => Some((cond.as_ref(), then_branch)),
            _ => None,
        }
    }

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

        // Check for collapsible if pattern
        if let PureExpr::If {
            cond,
            then_branch,
            else_branch: None,
        } = expr
        {
            // Recursively transform inner first
            changes += self.transform_expr(cond);
            changes += self.transform_block(then_branch);

            // Check if then_branch is a single if with no else
            if let Some((inner_cond, inner_body)) = Self::is_single_if_block(then_branch) {
                // Collapse: if a { if b { body } } => if a && b { body }
                let outer_cond =
                    std::mem::replace(cond.as_mut(), PureExpr::Path("__placeholder".to_string()));
                let inner_cond = inner_cond.clone();
                let inner_body = inner_body.clone();

                let new_cond = PureExpr::Binary {
                    op: "&&".to_string(),
                    left: Box::new(outer_cond),
                    right: Box::new(inner_cond),
                };

                *expr = PureExpr::If {
                    cond: Box::new(new_cond),
                    then_branch: inner_body,
                    else_branch: None,
                };

                return changes + 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 CollapsibleIfMutation {
    fn describe(&self) -> String {
        "Collapse nested if statements (if a { if b { } } → if a && b { })".to_string()
    }

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

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

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

    #[test]
    fn test_is_single_if_block() {
        // Single if with no else
        let block = PureBlock {
            stmts: vec![PureStmt::Expr(PureExpr::If {
                cond: Box::new(PureExpr::Path("b".to_string())),
                then_branch: PureBlock { stmts: vec![] },
                else_branch: None,
            })],
        };
        assert!(CollapsibleIfMutation::is_single_if_block(&block).is_some());

        // If with else - not collapsible
        let block2 = PureBlock {
            stmts: vec![PureStmt::Expr(PureExpr::If {
                cond: Box::new(PureExpr::Path("b".to_string())),
                then_branch: PureBlock { stmts: vec![] },
                else_branch: Some(Box::new(PureExpr::Block {
                    label: None,
                    block: PureBlock { stmts: vec![] },
                })),
            })],
        };
        assert!(CollapsibleIfMutation::is_single_if_block(&block2).is_none());

        // Multiple statements
        let block3 = PureBlock {
            stmts: vec![
                PureStmt::Expr(PureExpr::Path("x".to_string())),
                PureStmt::Expr(PureExpr::Path("y".to_string())),
            ],
        };
        assert!(CollapsibleIfMutation::is_single_if_block(&block3).is_none());
    }
}