Skip to main content

ryo_mutations/debugger/
remover.rs

1//! Remove debug logs inserted by ryo.
2//!
3//! This mutation removes debug logs that were previously inserted,
4//! using the embedded markers to identify them.
5
6use ryo_source::pure::{PureBlock, PureExpr, PureFn, PureStmt};
7
8use super::marker::{DebugMarker, MARKER_PREFIX};
9use crate::Mutation;
10
11/// Target for removal.
12#[derive(Debug, Clone)]
13pub enum RemovalTarget {
14    /// Remove all ryo-inserted debug logs.
15    All,
16    /// Remove logs from a specific session.
17    BySession(String),
18    /// Remove logs older than a timestamp.
19    OlderThan(u64),
20    /// Remove logs matching a description pattern.
21    ByDescription(String),
22}
23
24/// Remove debug logs inserted by ryo.
25///
26/// # Example
27///
28/// ```ignore
29/// use ryo_mutations::debugger::{RemoveDebugLogsMutation, RemovalTarget};
30///
31/// // Remove all debug logs
32/// let mutation = RemoveDebugLogsMutation::all();
33///
34/// // Remove logs from a specific session
35/// let mutation = RemoveDebugLogsMutation::by_session("abc123");
36///
37/// // Remove logs older than an hour ago
38/// let mutation = RemoveDebugLogsMutation::older_than(timestamp);
39/// ```
40#[derive(Debug, Clone)]
41pub struct RemoveDebugLogsMutation {
42    /// What to remove.
43    pub target: RemovalTarget,
44}
45
46impl RemoveDebugLogsMutation {
47    /// Remove all ryo-inserted debug logs.
48    pub fn all() -> Self {
49        Self {
50            target: RemovalTarget::All,
51        }
52    }
53
54    /// Remove logs from a specific session.
55    pub fn by_session(session_id: impl Into<String>) -> Self {
56        Self {
57            target: RemovalTarget::BySession(session_id.into()),
58        }
59    }
60
61    /// Remove logs older than a timestamp.
62    pub fn older_than(timestamp: u64) -> Self {
63        Self {
64            target: RemovalTarget::OlderThan(timestamp),
65        }
66    }
67
68    /// Remove logs matching a description pattern.
69    pub fn by_description(pattern: impl Into<String>) -> Self {
70        Self {
71            target: RemovalTarget::ByDescription(pattern.into()),
72        }
73    }
74
75    /// Check if a marker should be removed.
76    fn should_remove(&self, marker: &DebugMarker) -> bool {
77        match &self.target {
78            RemovalTarget::All => true,
79            RemovalTarget::BySession(session) => &marker.session_id == session,
80            RemovalTarget::OlderThan(ts) => marker.timestamp < *ts,
81            RemovalTarget::ByDescription(pattern) => marker
82                .description
83                .as_ref()
84                .map(|d| d.contains(pattern))
85                .unwrap_or(false),
86        }
87    }
88
89    /// Check if an expression contains a ryo debug marker that should be removed.
90    fn has_removable_marker(&self, expr: &PureExpr) -> bool {
91        match expr {
92            PureExpr::Macro { name, tokens, .. } => {
93                if name == "dbg" && DebugMarker::contains_marker(tokens) {
94                    // Parse the marker and check if it should be removed
95                    if let Some(marker) = self.extract_marker_from_tokens(tokens) {
96                        return self.should_remove(&marker);
97                    }
98                }
99                false
100            }
101            _ => false,
102        }
103    }
104
105    /// Check if a block contains a removable marker.
106    fn block_has_removable_marker(&self, expr: &PureExpr) -> bool {
107        match expr {
108            PureExpr::Block { block, .. } => block.stmts.iter().any(|stmt| match stmt {
109                PureStmt::Semi(e) | PureStmt::Expr(e) => self.has_removable_marker(e),
110                _ => false,
111            }),
112            PureExpr::Macro { name, tokens, .. } => {
113                name == "dbg" && DebugMarker::contains_marker(tokens) && {
114                    self.extract_marker_from_tokens(tokens)
115                        .map(|m| self.should_remove(&m))
116                        .unwrap_or(false)
117                }
118            }
119            _ => false,
120        }
121    }
122
123    /// Extract a marker from macro tokens.
124    fn extract_marker_from_tokens(&self, tokens: &str) -> Option<DebugMarker> {
125        // Try string literal format first: "ryo-debug:..."
126        let string_prefix = format!("\"{}:", MARKER_PREFIX);
127        if let Some(start) = tokens.find(&string_prefix) {
128            // Find the closing quote
129            let rest = &tokens[start + 1..]; // skip opening quote
130            if let Some(end) = rest.find('"') {
131                let marker_content = &rest[..end];
132                return DebugMarker::from_comment(marker_content);
133            }
134        }
135
136        // Try comment format: /* ryo-debug:... */
137        let comment_prefix = format!("/* {}:", MARKER_PREFIX);
138        if let Some(start) = tokens.find(&comment_prefix) {
139            if let Some(end_offset) = tokens[start..].find("*/") {
140                let end = start + end_offset + 2;
141                return DebugMarker::from_comment(&tokens[start..end]);
142            }
143        }
144
145        None
146    }
147
148    /// Extract the inner expression from a dbg! macro.
149    fn extract_dbg_inner(&self, tokens: &str) -> Option<String> {
150        // Format is: "ryo-debug:...", expression
151        // We need to extract the expression part after the comma
152
153        // Try string literal format first
154        let string_prefix = format!("\"{}:", MARKER_PREFIX);
155        if let Some(start) = tokens.find(&string_prefix) {
156            let rest = &tokens[start + 1..]; // skip opening quote
157            if let Some(quote_end) = rest.find('"') {
158                // Skip past the closing quote and comma
159                let after_marker = &rest[quote_end + 1..].trim_start();
160                if let Some(after_comma) = after_marker.strip_prefix(',') {
161                    let expr = after_comma.trim();
162                    if !expr.is_empty() {
163                        return Some(expr.to_string());
164                    }
165                }
166            }
167        }
168
169        // Try comment format: /* ryo-debug:... */ expression
170        let comment_prefix = format!("/* {}:", MARKER_PREFIX);
171        if let Some(start) = tokens.find(&comment_prefix) {
172            if let Some(end_offset) = tokens[start..].find("*/") {
173                let end = start + end_offset + 2;
174                let rest = tokens[end..].trim();
175                if !rest.is_empty() {
176                    return Some(rest.to_string());
177                }
178            }
179        }
180
181        None
182    }
183
184    /// Transform an expression, removing debug logs.
185    fn transform_expr(&self, expr: &PureExpr) -> (PureExpr, usize) {
186        match expr {
187            // Handle dbg! macros
188            PureExpr::Macro { name, tokens, .. } if name == "dbg" => {
189                if self.has_removable_marker(expr) {
190                    // Try to extract the inner expression
191                    if let Some(inner) = self.extract_dbg_inner(tokens) {
192                        // Parse the inner expression (simplified - just return as Other)
193                        return (PureExpr::Other(inner), 1);
194                    }
195                }
196                (expr.clone(), 0)
197            }
198
199            // Handle method chains with inspect
200            PureExpr::MethodCall {
201                receiver,
202                method,
203                args,
204                ..
205            } => {
206                // First transform the receiver
207                let (new_receiver, mut count) = self.transform_expr(receiver);
208
209                // Check if this is a removable inspect
210                if method == "inspect" && args.len() == 1 {
211                    if let PureExpr::Closure { body, .. } = &args[0] {
212                        if self.block_has_removable_marker(body) {
213                            // Skip this inspect, return the receiver
214                            return (new_receiver, count + 1);
215                        }
216                    }
217                }
218
219                // Transform args
220                let new_args: Vec<_> = args
221                    .iter()
222                    .map(|a| {
223                        let (new_a, c) = self.transform_expr(a);
224                        count += c;
225                        new_a
226                    })
227                    .collect();
228
229                (
230                    PureExpr::MethodCall {
231                        receiver: Box::new(new_receiver),
232                        method: method.clone(),
233                        turbofish: None,
234                        args: new_args,
235                    },
236                    count,
237                )
238            }
239
240            // Recursively transform other expressions
241            PureExpr::Call { func, args } => {
242                let (new_func, mut count) = self.transform_expr(func);
243                let new_args: Vec<_> = args
244                    .iter()
245                    .map(|a| {
246                        let (new_a, c) = self.transform_expr(a);
247                        count += c;
248                        new_a
249                    })
250                    .collect();
251                (
252                    PureExpr::Call {
253                        func: Box::new(new_func),
254                        args: new_args,
255                    },
256                    count,
257                )
258            }
259
260            PureExpr::Binary { op, left, right } => {
261                let (new_left, c1) = self.transform_expr(left);
262                let (new_right, c2) = self.transform_expr(right);
263                (
264                    PureExpr::Binary {
265                        op: op.clone(),
266                        left: Box::new(new_left),
267                        right: Box::new(new_right),
268                    },
269                    c1 + c2,
270                )
271            }
272
273            PureExpr::Block { label, block } => {
274                let (new_block, count) = self.transform_block(block);
275                (
276                    PureExpr::Block {
277                        label: label.clone(),
278                        block: new_block,
279                    },
280                    count,
281                )
282            }
283
284            PureExpr::If {
285                cond,
286                then_branch,
287                else_branch,
288            } => {
289                let (new_cond, c1) = self.transform_expr(cond);
290                let (new_then, c2) = self.transform_block(then_branch);
291                let (new_else, c3) = if let Some(e) = else_branch {
292                    let (ne, c) = self.transform_expr(e);
293                    (Some(Box::new(ne)), c)
294                } else {
295                    (None, 0)
296                };
297                (
298                    PureExpr::If {
299                        cond: Box::new(new_cond),
300                        then_branch: new_then,
301                        else_branch: new_else,
302                    },
303                    c1 + c2 + c3,
304                )
305            }
306
307            PureExpr::Closure {
308                params, ret, body, ..
309            } => {
310                let (new_body, count) = self.transform_expr(body);
311                (
312                    PureExpr::Closure {
313                        is_async: false,
314                        is_move: false,
315                        params: params.clone(),
316                        ret: ret.clone(),
317                        body: Box::new(new_body),
318                    },
319                    count,
320                )
321            }
322
323            _ => (expr.clone(), 0),
324        }
325    }
326
327    /// Transform a block.
328    fn transform_block(&self, block: &PureBlock) -> (PureBlock, usize) {
329        let mut count = 0;
330        let new_stmts: Vec<_> = block
331            .stmts
332            .iter()
333            .map(|stmt| {
334                let (new_stmt, c) = self.transform_stmt(stmt);
335                count += c;
336                new_stmt
337            })
338            .collect();
339        (PureBlock { stmts: new_stmts }, count)
340    }
341
342    /// Transform a statement.
343    fn transform_stmt(&self, stmt: &PureStmt) -> (PureStmt, usize) {
344        match stmt {
345            PureStmt::Local { pattern, ty, init } => {
346                if let Some(init_expr) = init {
347                    let (new_init, count) = self.transform_expr(init_expr);
348                    (
349                        PureStmt::Local {
350                            pattern: pattern.clone(),
351                            ty: ty.clone(),
352                            init: Some(new_init),
353                        },
354                        count,
355                    )
356                } else {
357                    (stmt.clone(), 0)
358                }
359            }
360            PureStmt::Expr(expr) => {
361                let (new_expr, count) = self.transform_expr(expr);
362                (PureStmt::Expr(new_expr), count)
363            }
364            PureStmt::Semi(expr) => {
365                let (new_expr, count) = self.transform_expr(expr);
366                (PureStmt::Semi(new_expr), count)
367            }
368            _ => (stmt.clone(), 0),
369        }
370    }
371
372    /// Transform a function.
373    pub fn transform_fn(&self, func: &PureFn) -> (PureFn, usize) {
374        let (new_body, count) = self.transform_block(&func.body);
375        let mut new_func = func.clone();
376        new_func.body = new_body;
377        (new_func, count)
378    }
379}
380
381impl Mutation for RemoveDebugLogsMutation {
382    fn describe(&self) -> String {
383        match &self.target {
384            RemovalTarget::All => "Remove all ryo debug logs".to_string(),
385            RemovalTarget::BySession(s) => format!("Remove debug logs from session '{}'", s),
386            RemovalTarget::OlderThan(ts) => format!("Remove debug logs older than {}", ts),
387            RemovalTarget::ByDescription(p) => format!("Remove debug logs matching '{}'", p),
388        }
389    }
390
391    fn mutation_type(&self) -> &'static str {
392        "RemoveDebugLogs"
393    }
394
395    fn box_clone(&self) -> Box<dyn Mutation> {
396        Box::new(self.clone())
397    }
398}