Skip to main content

ryo_mutations/debugger/
inspect.rs

1//! Insert `.inspect()` calls into method chains for debugging.
2//!
3//! This mutation inserts `.inspect(|x| dbg!(x))` after a specified method
4//! in a method chain, allowing you to observe intermediate values.
5
6use ryo_source::pure::{
7    MacroDelimiter, PureBlock, PureClosureParam, PureExpr, PureFn, PurePattern, PureStmt,
8};
9
10use super::marker::DebugMarker;
11use crate::Mutation;
12
13/// Insert `.inspect()` after a method call in a chain.
14///
15/// # Example
16///
17/// Before:
18/// ```ignore
19/// items.iter().map(|x| x * 2).collect()
20/// ```
21///
22/// After (with `after_method = "iter"`):
23/// ```ignore
24/// items.iter().inspect(|x| { /* ryo-debug:... */ dbg!(x); }).map(|x| x * 2).collect()
25/// ```
26#[derive(Debug, Clone)]
27pub struct InsertInspectMutation {
28    /// Method name after which to insert inspect.
29    pub after_method: String,
30    /// Debug marker for tracking this insertion.
31    pub marker: DebugMarker,
32    /// Variable name to use in the closure (default: "_x").
33    pub var_name: String,
34    /// Only insert in function with this name (if specified).
35    pub in_function: Option<String>,
36}
37
38impl InsertInspectMutation {
39    /// Create a new mutation to insert inspect after a method.
40    pub fn new(after_method: impl Into<String>, marker: DebugMarker) -> Self {
41        Self {
42            after_method: after_method.into(),
43            marker,
44            var_name: "_x".to_string(),
45            in_function: None,
46        }
47    }
48
49    /// Set the variable name for the inspect closure.
50    pub fn with_var_name(mut self, name: impl Into<String>) -> Self {
51        self.var_name = name.into();
52        self
53    }
54
55    /// Only apply in a specific function.
56    pub fn in_function(mut self, name: impl Into<String>) -> Self {
57        self.in_function = Some(name.into());
58        self
59    }
60
61    /// Transform an expression, inserting inspect where appropriate.
62    fn transform_expr(&self, expr: &PureExpr) -> (PureExpr, usize) {
63        match expr {
64            PureExpr::MethodCall {
65                receiver,
66                method,
67                args,
68                ..
69            } => {
70                // First, transform the receiver
71                let (new_receiver, mut count) = self.transform_expr(receiver);
72
73                // Check if this is the method after which we should insert inspect
74                if method == &self.after_method {
75                    // Build: receiver.method(args).inspect(|_x| { /* marker */ dbg!(&_x); })
76                    let method_call = PureExpr::MethodCall {
77                        receiver: Box::new(new_receiver),
78                        method: method.clone(),
79                        turbofish: None,
80                        args: args.clone(),
81                    };
82
83                    let inspect_call = self.build_inspect_call(method_call);
84                    count += 1;
85                    (inspect_call, count)
86                } else {
87                    // Just rebuild with transformed receiver
88                    let new_expr = PureExpr::MethodCall {
89                        receiver: Box::new(new_receiver),
90                        method: method.clone(),
91                        turbofish: None,
92                        args: args.clone(),
93                    };
94                    (new_expr, count)
95                }
96            }
97
98            // Recursively transform nested expressions
99            PureExpr::Call { func, args } => {
100                let (new_func, mut count) = self.transform_expr(func);
101                let mut new_args = Vec::new();
102                for arg in args {
103                    let (new_arg, c) = self.transform_expr(arg);
104                    new_args.push(new_arg);
105                    count += c;
106                }
107                (
108                    PureExpr::Call {
109                        func: Box::new(new_func),
110                        args: new_args,
111                    },
112                    count,
113                )
114            }
115
116            PureExpr::Binary { op, left, right } => {
117                let (new_left, c1) = self.transform_expr(left);
118                let (new_right, c2) = self.transform_expr(right);
119                (
120                    PureExpr::Binary {
121                        op: op.clone(),
122                        left: Box::new(new_left),
123                        right: Box::new(new_right),
124                    },
125                    c1 + c2,
126                )
127            }
128
129            PureExpr::Unary { op, expr: inner } => {
130                let (new_inner, count) = self.transform_expr(inner);
131                (
132                    PureExpr::Unary {
133                        op: op.clone(),
134                        expr: Box::new(new_inner),
135                    },
136                    count,
137                )
138            }
139
140            PureExpr::Block { label, block } => {
141                let (new_block, count) = self.transform_block(block);
142                (
143                    PureExpr::Block {
144                        label: label.clone(),
145                        block: new_block,
146                    },
147                    count,
148                )
149            }
150
151            PureExpr::If {
152                cond,
153                then_branch,
154                else_branch,
155            } => {
156                let (new_cond, c1) = self.transform_expr(cond);
157                let (new_then, c2) = self.transform_block(then_branch);
158                let (new_else, c3) = if let Some(e) = else_branch {
159                    let (ne, c) = self.transform_expr(e);
160                    (Some(Box::new(ne)), c)
161                } else {
162                    (None, 0)
163                };
164                (
165                    PureExpr::If {
166                        cond: Box::new(new_cond),
167                        then_branch: new_then,
168                        else_branch: new_else,
169                    },
170                    c1 + c2 + c3,
171                )
172            }
173
174            PureExpr::Closure {
175                params, ret, body, ..
176            } => {
177                let (new_body, count) = self.transform_expr(body);
178                (
179                    PureExpr::Closure {
180                        is_async: false,
181                        is_move: false,
182                        params: params.clone(),
183                        ret: ret.clone(),
184                        body: Box::new(new_body),
185                    },
186                    count,
187                )
188            }
189
190            PureExpr::Match { expr: e, arms } => {
191                let (new_expr, mut count) = self.transform_expr(e);
192                let new_arms = arms
193                    .iter()
194                    .map(|arm| {
195                        let (new_body, c) = self.transform_expr(&arm.body);
196                        count += c;
197                        ryo_source::pure::PureMatchArm {
198                            pattern: arm.pattern.clone(),
199                            guard: arm.guard.clone(),
200                            body: new_body,
201                        }
202                    })
203                    .collect();
204                (
205                    PureExpr::Match {
206                        expr: Box::new(new_expr),
207                        arms: new_arms,
208                    },
209                    count,
210                )
211            }
212
213            // Expressions that don't need transformation
214            _ => (expr.clone(), 0),
215        }
216    }
217
218    /// Build the inspect call expression.
219    fn build_inspect_call(&self, receiver: PureExpr) -> PureExpr {
220        // Build: .inspect(|_x| { dbg!("ryo-debug:session:ts:desc", &_x); })
221        // We use a string literal as first arg to dbg! for tracking
222        let marker_str = self.marker.to_marker_string();
223
224        let closure = PureExpr::Closure {
225            is_async: false,
226            is_move: false,
227            params: vec![PureClosureParam::untyped(PurePattern::Ident {
228                name: self.var_name.clone(),
229                is_mut: false,
230            })],
231            ret: None,
232            body: Box::new(PureExpr::Block {
233                label: None,
234                block: PureBlock {
235                    stmts: vec![PureStmt::Semi(PureExpr::Macro {
236                        name: "dbg".to_string(),
237                        delimiter: MacroDelimiter::Paren,
238                        tokens: format!("\"{}\", &{}", marker_str, self.var_name),
239                    })],
240                },
241            }),
242        };
243
244        PureExpr::MethodCall {
245            receiver: Box::new(receiver),
246            method: "inspect".to_string(),
247            turbofish: None,
248            args: vec![closure],
249        }
250    }
251
252    /// Transform a block.
253    fn transform_block(&self, block: &PureBlock) -> (PureBlock, usize) {
254        let mut count = 0;
255        let new_stmts = block
256            .stmts
257            .iter()
258            .map(|stmt| {
259                let (new_stmt, c) = self.transform_stmt(stmt);
260                count += c;
261                new_stmt
262            })
263            .collect();
264        (PureBlock { stmts: new_stmts }, count)
265    }
266
267    /// Transform a statement.
268    fn transform_stmt(&self, stmt: &PureStmt) -> (PureStmt, usize) {
269        match stmt {
270            PureStmt::Local { pattern, ty, init } => {
271                if let Some(init_expr) = init {
272                    let (new_init, count) = self.transform_expr(init_expr);
273                    (
274                        PureStmt::Local {
275                            pattern: pattern.clone(),
276                            ty: ty.clone(),
277                            init: Some(new_init),
278                        },
279                        count,
280                    )
281                } else {
282                    (stmt.clone(), 0)
283                }
284            }
285            PureStmt::Expr(expr) => {
286                let (new_expr, count) = self.transform_expr(expr);
287                (PureStmt::Expr(new_expr), count)
288            }
289            PureStmt::Semi(expr) => {
290                let (new_expr, count) = self.transform_expr(expr);
291                (PureStmt::Semi(new_expr), count)
292            }
293            _ => (stmt.clone(), 0),
294        }
295    }
296
297    /// Transform a function.
298    pub fn transform_fn(&self, func: &PureFn) -> (PureFn, usize) {
299        // Check if we should only apply to a specific function
300        if let Some(ref target_fn) = self.in_function {
301            if &func.name != target_fn {
302                return (func.clone(), 0);
303            }
304        }
305
306        let (new_body, count) = self.transform_block(&func.body);
307        let mut new_func = func.clone();
308        new_func.body = new_body;
309        (new_func, count)
310    }
311}
312
313impl Mutation for InsertInspectMutation {
314    fn describe(&self) -> String {
315        format!(
316            "Insert .inspect() after .{}() [session: {}]",
317            self.after_method, self.marker.session_id
318        )
319    }
320
321    fn mutation_type(&self) -> &'static str {
322        "InsertInspect"
323    }
324
325    fn box_clone(&self) -> Box<dyn Mutation> {
326        Box::new(self.clone())
327    }
328}