ryo-mutations 0.1.0

[experimental] Code transformation primitives for Rust source code
Documentation
//! Insert `.inspect()` calls into method chains for debugging.
//!
//! This mutation inserts `.inspect(|x| dbg!(x))` after a specified method
//! in a method chain, allowing you to observe intermediate values.

use ryo_source::pure::{
    MacroDelimiter, PureBlock, PureClosureParam, PureExpr, PureFn, PurePattern, PureStmt,
};

use super::marker::DebugMarker;
use crate::Mutation;

/// Insert `.inspect()` after a method call in a chain.
///
/// # Example
///
/// Before:
/// ```ignore
/// items.iter().map(|x| x * 2).collect()
/// ```
///
/// After (with `after_method = "iter"`):
/// ```ignore
/// items.iter().inspect(|x| { /* ryo-debug:... */ dbg!(x); }).map(|x| x * 2).collect()
/// ```
#[derive(Debug, Clone)]
pub struct InsertInspectMutation {
    /// Method name after which to insert inspect.
    pub after_method: String,
    /// Debug marker for tracking this insertion.
    pub marker: DebugMarker,
    /// Variable name to use in the closure (default: "_x").
    pub var_name: String,
    /// Only insert in function with this name (if specified).
    pub in_function: Option<String>,
}

impl InsertInspectMutation {
    /// Create a new mutation to insert inspect after a method.
    pub fn new(after_method: impl Into<String>, marker: DebugMarker) -> Self {
        Self {
            after_method: after_method.into(),
            marker,
            var_name: "_x".to_string(),
            in_function: None,
        }
    }

    /// Set the variable name for the inspect closure.
    pub fn with_var_name(mut self, name: impl Into<String>) -> Self {
        self.var_name = name.into();
        self
    }

    /// Only apply in a specific function.
    pub fn in_function(mut self, name: impl Into<String>) -> Self {
        self.in_function = Some(name.into());
        self
    }

    /// Transform an expression, inserting inspect where appropriate.
    fn transform_expr(&self, expr: &PureExpr) -> (PureExpr, usize) {
        match expr {
            PureExpr::MethodCall {
                receiver,
                method,
                args,
                ..
            } => {
                // First, transform the receiver
                let (new_receiver, mut count) = self.transform_expr(receiver);

                // Check if this is the method after which we should insert inspect
                if method == &self.after_method {
                    // Build: receiver.method(args).inspect(|_x| { /* marker */ dbg!(&_x); })
                    let method_call = PureExpr::MethodCall {
                        receiver: Box::new(new_receiver),
                        method: method.clone(),
                        turbofish: None,
                        args: args.clone(),
                    };

                    let inspect_call = self.build_inspect_call(method_call);
                    count += 1;
                    (inspect_call, count)
                } else {
                    // Just rebuild with transformed receiver
                    let new_expr = PureExpr::MethodCall {
                        receiver: Box::new(new_receiver),
                        method: method.clone(),
                        turbofish: None,
                        args: args.clone(),
                    };
                    (new_expr, count)
                }
            }

            // Recursively transform nested expressions
            PureExpr::Call { func, args } => {
                let (new_func, mut count) = self.transform_expr(func);
                let mut new_args = Vec::new();
                for arg in args {
                    let (new_arg, c) = self.transform_expr(arg);
                    new_args.push(new_arg);
                    count += c;
                }
                (
                    PureExpr::Call {
                        func: Box::new(new_func),
                        args: new_args,
                    },
                    count,
                )
            }

            PureExpr::Binary { op, left, right } => {
                let (new_left, c1) = self.transform_expr(left);
                let (new_right, c2) = self.transform_expr(right);
                (
                    PureExpr::Binary {
                        op: op.clone(),
                        left: Box::new(new_left),
                        right: Box::new(new_right),
                    },
                    c1 + c2,
                )
            }

            PureExpr::Unary { op, expr: inner } => {
                let (new_inner, count) = self.transform_expr(inner);
                (
                    PureExpr::Unary {
                        op: op.clone(),
                        expr: Box::new(new_inner),
                    },
                    count,
                )
            }

            PureExpr::Block { label, block } => {
                let (new_block, count) = self.transform_block(block);
                (
                    PureExpr::Block {
                        label: label.clone(),
                        block: new_block,
                    },
                    count,
                )
            }

            PureExpr::If {
                cond,
                then_branch,
                else_branch,
            } => {
                let (new_cond, c1) = self.transform_expr(cond);
                let (new_then, c2) = self.transform_block(then_branch);
                let (new_else, c3) = if let Some(e) = else_branch {
                    let (ne, c) = self.transform_expr(e);
                    (Some(Box::new(ne)), c)
                } else {
                    (None, 0)
                };
                (
                    PureExpr::If {
                        cond: Box::new(new_cond),
                        then_branch: new_then,
                        else_branch: new_else,
                    },
                    c1 + c2 + c3,
                )
            }

            PureExpr::Closure {
                params, ret, body, ..
            } => {
                let (new_body, count) = self.transform_expr(body);
                (
                    PureExpr::Closure {
                        is_async: false,
                        is_move: false,
                        params: params.clone(),
                        ret: ret.clone(),
                        body: Box::new(new_body),
                    },
                    count,
                )
            }

            PureExpr::Match { expr: e, arms } => {
                let (new_expr, mut count) = self.transform_expr(e);
                let new_arms = arms
                    .iter()
                    .map(|arm| {
                        let (new_body, c) = self.transform_expr(&arm.body);
                        count += c;
                        ryo_source::pure::PureMatchArm {
                            pattern: arm.pattern.clone(),
                            guard: arm.guard.clone(),
                            body: new_body,
                        }
                    })
                    .collect();
                (
                    PureExpr::Match {
                        expr: Box::new(new_expr),
                        arms: new_arms,
                    },
                    count,
                )
            }

            // Expressions that don't need transformation
            _ => (expr.clone(), 0),
        }
    }

    /// Build the inspect call expression.
    fn build_inspect_call(&self, receiver: PureExpr) -> PureExpr {
        // Build: .inspect(|_x| { dbg!("ryo-debug:session:ts:desc", &_x); })
        // We use a string literal as first arg to dbg! for tracking
        let marker_str = self.marker.to_marker_string();

        let closure = PureExpr::Closure {
            is_async: false,
            is_move: false,
            params: vec![PureClosureParam::untyped(PurePattern::Ident {
                name: self.var_name.clone(),
                is_mut: false,
            })],
            ret: None,
            body: Box::new(PureExpr::Block {
                label: None,
                block: PureBlock {
                    stmts: vec![PureStmt::Semi(PureExpr::Macro {
                        name: "dbg".to_string(),
                        delimiter: MacroDelimiter::Paren,
                        tokens: format!("\"{}\", &{}", marker_str, self.var_name),
                    })],
                },
            }),
        };

        PureExpr::MethodCall {
            receiver: Box::new(receiver),
            method: "inspect".to_string(),
            turbofish: None,
            args: vec![closure],
        }
    }

    /// Transform a block.
    fn transform_block(&self, block: &PureBlock) -> (PureBlock, usize) {
        let mut count = 0;
        let new_stmts = block
            .stmts
            .iter()
            .map(|stmt| {
                let (new_stmt, c) = self.transform_stmt(stmt);
                count += c;
                new_stmt
            })
            .collect();
        (PureBlock { stmts: new_stmts }, count)
    }

    /// Transform a statement.
    fn transform_stmt(&self, stmt: &PureStmt) -> (PureStmt, usize) {
        match stmt {
            PureStmt::Local { pattern, ty, init } => {
                if let Some(init_expr) = init {
                    let (new_init, count) = self.transform_expr(init_expr);
                    (
                        PureStmt::Local {
                            pattern: pattern.clone(),
                            ty: ty.clone(),
                            init: Some(new_init),
                        },
                        count,
                    )
                } else {
                    (stmt.clone(), 0)
                }
            }
            PureStmt::Expr(expr) => {
                let (new_expr, count) = self.transform_expr(expr);
                (PureStmt::Expr(new_expr), count)
            }
            PureStmt::Semi(expr) => {
                let (new_expr, count) = self.transform_expr(expr);
                (PureStmt::Semi(new_expr), count)
            }
            _ => (stmt.clone(), 0),
        }
    }

    /// Transform a function.
    pub fn transform_fn(&self, func: &PureFn) -> (PureFn, usize) {
        // Check if we should only apply to a specific function
        if let Some(ref target_fn) = self.in_function {
            if &func.name != target_fn {
                return (func.clone(), 0);
            }
        }

        let (new_body, count) = self.transform_block(&func.body);
        let mut new_func = func.clone();
        new_func.body = new_body;
        (new_func, count)
    }
}

impl Mutation for InsertInspectMutation {
    fn describe(&self) -> String {
        format!(
            "Insert .inspect() after .{}() [session: {}]",
            self.after_method, self.marker.session_id
        )
    }

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

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