ryo-mutations 0.1.0

[experimental] Code transformation primitives for Rust source code
Documentation
//! Remove debug logs inserted by ryo.
//!
//! This mutation removes debug logs that were previously inserted,
//! using the embedded markers to identify them.

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

use super::marker::{DebugMarker, MARKER_PREFIX};
use crate::Mutation;

/// Target for removal.
#[derive(Debug, Clone)]
pub enum RemovalTarget {
    /// Remove all ryo-inserted debug logs.
    All,
    /// Remove logs from a specific session.
    BySession(String),
    /// Remove logs older than a timestamp.
    OlderThan(u64),
    /// Remove logs matching a description pattern.
    ByDescription(String),
}

/// Remove debug logs inserted by ryo.
///
/// # Example
///
/// ```ignore
/// use ryo_mutations::debugger::{RemoveDebugLogsMutation, RemovalTarget};
///
/// // Remove all debug logs
/// let mutation = RemoveDebugLogsMutation::all();
///
/// // Remove logs from a specific session
/// let mutation = RemoveDebugLogsMutation::by_session("abc123");
///
/// // Remove logs older than an hour ago
/// let mutation = RemoveDebugLogsMutation::older_than(timestamp);
/// ```
#[derive(Debug, Clone)]
pub struct RemoveDebugLogsMutation {
    /// What to remove.
    pub target: RemovalTarget,
}

impl RemoveDebugLogsMutation {
    /// Remove all ryo-inserted debug logs.
    pub fn all() -> Self {
        Self {
            target: RemovalTarget::All,
        }
    }

    /// Remove logs from a specific session.
    pub fn by_session(session_id: impl Into<String>) -> Self {
        Self {
            target: RemovalTarget::BySession(session_id.into()),
        }
    }

    /// Remove logs older than a timestamp.
    pub fn older_than(timestamp: u64) -> Self {
        Self {
            target: RemovalTarget::OlderThan(timestamp),
        }
    }

    /// Remove logs matching a description pattern.
    pub fn by_description(pattern: impl Into<String>) -> Self {
        Self {
            target: RemovalTarget::ByDescription(pattern.into()),
        }
    }

    /// Check if a marker should be removed.
    fn should_remove(&self, marker: &DebugMarker) -> bool {
        match &self.target {
            RemovalTarget::All => true,
            RemovalTarget::BySession(session) => &marker.session_id == session,
            RemovalTarget::OlderThan(ts) => marker.timestamp < *ts,
            RemovalTarget::ByDescription(pattern) => marker
                .description
                .as_ref()
                .map(|d| d.contains(pattern))
                .unwrap_or(false),
        }
    }

    /// Check if an expression contains a ryo debug marker that should be removed.
    fn has_removable_marker(&self, expr: &PureExpr) -> bool {
        match expr {
            PureExpr::Macro { name, tokens, .. } => {
                if name == "dbg" && DebugMarker::contains_marker(tokens) {
                    // Parse the marker and check if it should be removed
                    if let Some(marker) = self.extract_marker_from_tokens(tokens) {
                        return self.should_remove(&marker);
                    }
                }
                false
            }
            _ => false,
        }
    }

    /// Check if a block contains a removable marker.
    fn block_has_removable_marker(&self, expr: &PureExpr) -> bool {
        match expr {
            PureExpr::Block { block, .. } => block.stmts.iter().any(|stmt| match stmt {
                PureStmt::Semi(e) | PureStmt::Expr(e) => self.has_removable_marker(e),
                _ => false,
            }),
            PureExpr::Macro { name, tokens, .. } => {
                name == "dbg" && DebugMarker::contains_marker(tokens) && {
                    self.extract_marker_from_tokens(tokens)
                        .map(|m| self.should_remove(&m))
                        .unwrap_or(false)
                }
            }
            _ => false,
        }
    }

    /// Extract a marker from macro tokens.
    fn extract_marker_from_tokens(&self, tokens: &str) -> Option<DebugMarker> {
        // Try string literal format first: "ryo-debug:..."
        let string_prefix = format!("\"{}:", MARKER_PREFIX);
        if let Some(start) = tokens.find(&string_prefix) {
            // Find the closing quote
            let rest = &tokens[start + 1..]; // skip opening quote
            if let Some(end) = rest.find('"') {
                let marker_content = &rest[..end];
                return DebugMarker::from_comment(marker_content);
            }
        }

        // Try comment format: /* ryo-debug:... */
        let comment_prefix = format!("/* {}:", MARKER_PREFIX);
        if let Some(start) = tokens.find(&comment_prefix) {
            if let Some(end_offset) = tokens[start..].find("*/") {
                let end = start + end_offset + 2;
                return DebugMarker::from_comment(&tokens[start..end]);
            }
        }

        None
    }

    /// Extract the inner expression from a dbg! macro.
    fn extract_dbg_inner(&self, tokens: &str) -> Option<String> {
        // Format is: "ryo-debug:...", expression
        // We need to extract the expression part after the comma

        // Try string literal format first
        let string_prefix = format!("\"{}:", MARKER_PREFIX);
        if let Some(start) = tokens.find(&string_prefix) {
            let rest = &tokens[start + 1..]; // skip opening quote
            if let Some(quote_end) = rest.find('"') {
                // Skip past the closing quote and comma
                let after_marker = &rest[quote_end + 1..].trim_start();
                if let Some(after_comma) = after_marker.strip_prefix(',') {
                    let expr = after_comma.trim();
                    if !expr.is_empty() {
                        return Some(expr.to_string());
                    }
                }
            }
        }

        // Try comment format: /* ryo-debug:... */ expression
        let comment_prefix = format!("/* {}:", MARKER_PREFIX);
        if let Some(start) = tokens.find(&comment_prefix) {
            if let Some(end_offset) = tokens[start..].find("*/") {
                let end = start + end_offset + 2;
                let rest = tokens[end..].trim();
                if !rest.is_empty() {
                    return Some(rest.to_string());
                }
            }
        }

        None
    }

    /// Transform an expression, removing debug logs.
    fn transform_expr(&self, expr: &PureExpr) -> (PureExpr, usize) {
        match expr {
            // Handle dbg! macros
            PureExpr::Macro { name, tokens, .. } if name == "dbg" => {
                if self.has_removable_marker(expr) {
                    // Try to extract the inner expression
                    if let Some(inner) = self.extract_dbg_inner(tokens) {
                        // Parse the inner expression (simplified - just return as Other)
                        return (PureExpr::Other(inner), 1);
                    }
                }
                (expr.clone(), 0)
            }

            // Handle method chains with inspect
            PureExpr::MethodCall {
                receiver,
                method,
                args,
                ..
            } => {
                // First transform the receiver
                let (new_receiver, mut count) = self.transform_expr(receiver);

                // Check if this is a removable inspect
                if method == "inspect" && args.len() == 1 {
                    if let PureExpr::Closure { body, .. } = &args[0] {
                        if self.block_has_removable_marker(body) {
                            // Skip this inspect, return the receiver
                            return (new_receiver, count + 1);
                        }
                    }
                }

                // Transform args
                let new_args: Vec<_> = args
                    .iter()
                    .map(|a| {
                        let (new_a, c) = self.transform_expr(a);
                        count += c;
                        new_a
                    })
                    .collect();

                (
                    PureExpr::MethodCall {
                        receiver: Box::new(new_receiver),
                        method: method.clone(),
                        turbofish: None,
                        args: new_args,
                    },
                    count,
                )
            }

            // Recursively transform other expressions
            PureExpr::Call { func, args } => {
                let (new_func, mut count) = self.transform_expr(func);
                let new_args: Vec<_> = args
                    .iter()
                    .map(|a| {
                        let (new_a, c) = self.transform_expr(a);
                        count += c;
                        new_a
                    })
                    .collect();
                (
                    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::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,
                )
            }

            _ => (expr.clone(), 0),
        }
    }

    /// Transform a block.
    fn transform_block(&self, block: &PureBlock) -> (PureBlock, usize) {
        let mut count = 0;
        let new_stmts: Vec<_> = 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) {
        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 RemoveDebugLogsMutation {
    fn describe(&self) -> String {
        match &self.target {
            RemovalTarget::All => "Remove all ryo debug logs".to_string(),
            RemovalTarget::BySession(s) => format!("Remove debug logs from session '{}'", s),
            RemovalTarget::OlderThan(ts) => format!("Remove debug logs older than {}", ts),
            RemovalTarget::ByDescription(p) => format!("Remove debug logs matching '{}'", p),
        }
    }

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

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