Skip to main content

ryo_mutations/debugger/
dbg_wrap.rs

1//! Wrap expressions with `dbg!()` for debugging.
2//!
3//! This mutation wraps specified expressions with `dbg!()` macro calls,
4//! allowing you to see the value and location of expressions.
5
6use ryo_source::pure::{MacroDelimiter, PureBlock, PureExpr, PureFn, PureStmt};
7
8use super::marker::DebugMarker;
9use crate::Mutation;
10
11/// Target for wrapping with dbg!().
12#[derive(Debug, Clone)]
13pub enum WrapTarget {
14    /// Wrap all method chains starting with a specific method.
15    MethodChain { starts_with: String },
16    /// Wrap method calls to a specific method.
17    MethodCall { method: String },
18    /// Wrap all variables with a specific name.
19    Variable { name: String },
20    /// Wrap the return expression of a function.
21    ReturnExpr { function: String },
22}
23
24/// Wrap expressions with `dbg!()` macro.
25///
26/// # Example
27///
28/// Before:
29/// ```ignore
30/// let result = items.iter().map(|x| x * 2).collect();
31/// ```
32///
33/// After (with `MethodChain { starts_with: "iter" }`):
34/// ```ignore
35/// let result = dbg!(/* ryo-debug:... */ items.iter().map(|x| x * 2).collect());
36/// ```
37#[derive(Debug, Clone)]
38pub struct DbgWrapMutation {
39    /// What to wrap.
40    pub target: WrapTarget,
41    /// Debug marker for tracking.
42    pub marker: DebugMarker,
43    /// Only apply in this function (if specified).
44    pub in_function: Option<String>,
45}
46
47impl DbgWrapMutation {
48    /// Create a new mutation to wrap method chains.
49    pub fn method_chain(starts_with: impl Into<String>, marker: DebugMarker) -> Self {
50        Self {
51            target: WrapTarget::MethodChain {
52                starts_with: starts_with.into(),
53            },
54            marker,
55            in_function: None,
56        }
57    }
58
59    /// Create a new mutation to wrap specific method calls.
60    pub fn method_call(method: impl Into<String>, marker: DebugMarker) -> Self {
61        Self {
62            target: WrapTarget::MethodCall {
63                method: method.into(),
64            },
65            marker,
66            in_function: None,
67        }
68    }
69
70    /// Create a new mutation to wrap variable references.
71    pub fn variable(name: impl Into<String>, marker: DebugMarker) -> Self {
72        Self {
73            target: WrapTarget::Variable { name: name.into() },
74            marker,
75            in_function: None,
76        }
77    }
78
79    /// Create a new mutation to wrap return expressions.
80    pub fn return_expr(function: impl Into<String>, marker: DebugMarker) -> Self {
81        Self {
82            target: WrapTarget::ReturnExpr {
83                function: function.into(),
84            },
85            marker,
86            in_function: None,
87        }
88    }
89
90    /// Only apply in a specific function.
91    pub fn in_function(mut self, name: impl Into<String>) -> Self {
92        self.in_function = Some(name.into());
93        self
94    }
95
96    /// Check if an expression matches the wrap target.
97    fn matches_target(&self, expr: &PureExpr) -> bool {
98        match &self.target {
99            WrapTarget::MethodChain { starts_with } => {
100                self.is_chain_starting_with(expr, starts_with)
101            }
102            WrapTarget::MethodCall { method } => {
103                matches!(expr, PureExpr::MethodCall { method: m, .. } if m == method)
104            }
105            WrapTarget::Variable { name } => {
106                matches!(expr, PureExpr::Path(p) if p == name)
107            }
108            WrapTarget::ReturnExpr { .. } => false, // Handled separately
109        }
110    }
111
112    /// Check if a method chain starts with a specific method.
113    fn is_chain_starting_with(&self, expr: &PureExpr, method: &str) -> bool {
114        match expr {
115            PureExpr::MethodCall {
116                receiver,
117                method: m,
118                ..
119            } => {
120                if m == method {
121                    // Check if receiver is not a method call (i.e., this is the first method)
122                    !matches!(receiver.as_ref(), PureExpr::MethodCall { .. })
123                } else {
124                    // Check the receiver
125                    self.is_chain_starting_with(receiver, method)
126                }
127            }
128            _ => false,
129        }
130    }
131
132    /// Build a dbg! wrapped expression.
133    fn wrap_with_dbg(&self, expr: PureExpr) -> PureExpr {
134        let marker_str = self.marker.to_marker_string();
135
136        PureExpr::Macro {
137            name: "dbg".to_string(),
138            delimiter: MacroDelimiter::Paren,
139            tokens: format!("\"{}\", {}", marker_str, expr_to_tokens(&expr)),
140        }
141    }
142
143    /// Transform an expression, wrapping matches.
144    fn transform_expr(&self, expr: &PureExpr) -> (PureExpr, usize) {
145        // Check if this expression should be wrapped
146        if self.matches_target(expr) {
147            return (self.wrap_with_dbg(expr.clone()), 1);
148        }
149
150        // Recursively transform nested expressions
151        match expr {
152            PureExpr::MethodCall {
153                receiver,
154                method,
155                args,
156                ..
157            } => {
158                let (new_receiver, c1) = self.transform_expr(receiver);
159                let mut total = c1;
160                let new_args: Vec<_> = args
161                    .iter()
162                    .map(|a| {
163                        let (new_a, c) = self.transform_expr(a);
164                        total += c;
165                        new_a
166                    })
167                    .collect();
168                (
169                    PureExpr::MethodCall {
170                        receiver: Box::new(new_receiver),
171                        method: method.clone(),
172                        turbofish: None,
173                        args: new_args,
174                    },
175                    total,
176                )
177            }
178
179            PureExpr::Call { func, args } => {
180                let (new_func, mut total) = self.transform_expr(func);
181                let new_args: Vec<_> = args
182                    .iter()
183                    .map(|a| {
184                        let (new_a, c) = self.transform_expr(a);
185                        total += c;
186                        new_a
187                    })
188                    .collect();
189                (
190                    PureExpr::Call {
191                        func: Box::new(new_func),
192                        args: new_args,
193                    },
194                    total,
195                )
196            }
197
198            PureExpr::Binary { op, left, right } => {
199                let (new_left, c1) = self.transform_expr(left);
200                let (new_right, c2) = self.transform_expr(right);
201                (
202                    PureExpr::Binary {
203                        op: op.clone(),
204                        left: Box::new(new_left),
205                        right: Box::new(new_right),
206                    },
207                    c1 + c2,
208                )
209            }
210
211            PureExpr::Block { label, block } => {
212                let (new_block, count) = self.transform_block(block);
213                (
214                    PureExpr::Block {
215                        label: label.clone(),
216                        block: new_block,
217                    },
218                    count,
219                )
220            }
221
222            PureExpr::If {
223                cond,
224                then_branch,
225                else_branch,
226            } => {
227                let (new_cond, c1) = self.transform_expr(cond);
228                let (new_then, c2) = self.transform_block(then_branch);
229                let (new_else, c3) = if let Some(e) = else_branch {
230                    let (ne, c) = self.transform_expr(e);
231                    (Some(Box::new(ne)), c)
232                } else {
233                    (None, 0)
234                };
235                (
236                    PureExpr::If {
237                        cond: Box::new(new_cond),
238                        then_branch: new_then,
239                        else_branch: new_else,
240                    },
241                    c1 + c2 + c3,
242                )
243            }
244
245            PureExpr::Closure {
246                params, ret, body, ..
247            } => {
248                let (new_body, count) = self.transform_expr(body);
249                (
250                    PureExpr::Closure {
251                        is_async: false,
252                        is_move: false,
253                        params: params.clone(),
254                        ret: ret.clone(),
255                        body: Box::new(new_body),
256                    },
257                    count,
258                )
259            }
260
261            _ => (expr.clone(), 0),
262        }
263    }
264
265    /// Transform a block.
266    fn transform_block(&self, block: &PureBlock) -> (PureBlock, usize) {
267        let mut count = 0;
268        let new_stmts: Vec<_> = block
269            .stmts
270            .iter()
271            .map(|stmt| {
272                let (new_stmt, c) = self.transform_stmt(stmt);
273                count += c;
274                new_stmt
275            })
276            .collect();
277        (PureBlock { stmts: new_stmts }, count)
278    }
279
280    /// Transform a statement.
281    fn transform_stmt(&self, stmt: &PureStmt) -> (PureStmt, usize) {
282        match stmt {
283            PureStmt::Local { pattern, ty, init } => {
284                if let Some(init_expr) = init {
285                    let (new_init, count) = self.transform_expr(init_expr);
286                    (
287                        PureStmt::Local {
288                            pattern: pattern.clone(),
289                            ty: ty.clone(),
290                            init: Some(new_init),
291                        },
292                        count,
293                    )
294                } else {
295                    (stmt.clone(), 0)
296                }
297            }
298            PureStmt::Expr(expr) => {
299                let (new_expr, count) = self.transform_expr(expr);
300                (PureStmt::Expr(new_expr), count)
301            }
302            PureStmt::Semi(expr) => {
303                let (new_expr, count) = self.transform_expr(expr);
304                (PureStmt::Semi(new_expr), count)
305            }
306            _ => (stmt.clone(), 0),
307        }
308    }
309
310    /// Transform a function, handling return expression wrapping.
311    pub fn transform_fn(&self, func: &PureFn) -> (PureFn, usize) {
312        // Check if we should only apply to a specific function
313        if let Some(ref target_fn) = self.in_function {
314            if &func.name != target_fn {
315                return (func.clone(), 0);
316            }
317        }
318
319        let mut new_func = func.clone();
320        let mut count = 0;
321
322        // Handle return expression wrapping
323        if let WrapTarget::ReturnExpr { function } = &self.target {
324            if &func.name == function {
325                // Wrap the last expression if it's the return value
326                if let Some(last_stmt) = new_func.body.stmts.last_mut() {
327                    if let PureStmt::Expr(expr) = last_stmt {
328                        *last_stmt = PureStmt::Expr(self.wrap_with_dbg(expr.clone()));
329                        count += 1;
330                    }
331                }
332                return (new_func, count);
333            }
334        }
335
336        // Regular transformation
337        let (new_body, block_count) = self.transform_block(&func.body);
338        new_func.body = new_body;
339        (new_func, count + block_count)
340    }
341}
342
343impl Mutation for DbgWrapMutation {
344    fn describe(&self) -> String {
345        let target_desc = match &self.target {
346            WrapTarget::MethodChain { starts_with } => {
347                format!("method chains starting with .{}()", starts_with)
348            }
349            WrapTarget::MethodCall { method } => {
350                format!(".{}() calls", method)
351            }
352            WrapTarget::Variable { name } => {
353                format!("variable '{}'", name)
354            }
355            WrapTarget::ReturnExpr { function } => {
356                format!("return expression in {}", function)
357            }
358        };
359        format!(
360            "Wrap {} with dbg!() [session: {}]",
361            target_desc, self.marker.session_id
362        )
363    }
364
365    fn mutation_type(&self) -> &'static str {
366        "DbgWrap"
367    }
368
369    fn box_clone(&self) -> Box<dyn Mutation> {
370        Box::new(self.clone())
371    }
372}
373
374/// Convert an expression to a token string (simplified).
375fn expr_to_tokens(expr: &PureExpr) -> String {
376    // This is a simplified version - the actual rendering would use to_source()
377    match expr {
378        PureExpr::Path(p) => p.clone(),
379        PureExpr::Lit(l) => l.clone(),
380        PureExpr::MethodCall {
381            receiver,
382            method,
383            args,
384            ..
385        } => {
386            let receiver_str = expr_to_tokens(receiver);
387            let args_str: Vec<_> = args.iter().map(expr_to_tokens).collect();
388            format!("{}.{}({})", receiver_str, method, args_str.join(", "))
389        }
390        PureExpr::Call { func, args } => {
391            let func_str = expr_to_tokens(func);
392            let args_str: Vec<_> = args.iter().map(expr_to_tokens).collect();
393            format!("{}({})", func_str, args_str.join(", "))
394        }
395        PureExpr::Binary { op, left, right } => {
396            format!("{} {} {}", expr_to_tokens(left), op, expr_to_tokens(right))
397        }
398        _ => "...".to_string(),
399    }
400}