ryo-mutations 0.1.0

[experimental] Code transformation primitives for Rust source code
Documentation
//! FilterNextMutation: Convert `.filter().next()` to `.find()`
//!
//! Transforms:
//! - `iter.filter(|x| predicate(x)).next()` -> `iter.find(|x| predicate(x))`
//! - `iter.filter(predicate).next()` -> `iter.find(predicate)`
//!
//! Corresponds to Clippy lint: `clippy::filter_next`

use ryo_source::pure::{PureBlock, PureExpr, PureStmt};
use ryo_symbol::SymbolId;

use crate::Mutation;

/// Convert `.filter().next()` chains to `.find()`
///
/// # Example
///
/// ```rust,ignore
/// use ryo_mutations::idiom::FilterNextMutation;
///
/// let mutation = FilterNextMutation::new();
/// // Transforms: items.iter().filter(|x| x > 0).next()
/// // Into:       items.iter().find(|x| x > 0)
/// ```
#[derive(Debug, Clone, Default)]
pub struct FilterNextMutation {
    /// Target function SymbolId. If None, applies to all functions.
    pub target_fn: Option<SymbolId>,
}

impl FilterNextMutation {
    pub fn new() -> Self {
        Self::default()
    }

    /// Only apply in a specific function
    pub fn in_function(mut self, id: SymbolId) -> Self {
        self.target_fn = Some(id);
        self
    }

    /// Check if expression is a `.filter(predicate).next()` pattern
    /// Returns (receiver before filter, predicate) if matched
    fn is_filter_next_pattern(expr: &PureExpr) -> Option<(PureExpr, PureExpr)> {
        // Pattern: receiver.filter(predicate).next()
        if let PureExpr::MethodCall {
            receiver,
            method,
            args,
            ..
        } = expr
        {
            // Check for .next()
            if method == "next" && args.is_empty() {
                // Check receiver is .filter(predicate)
                if let PureExpr::MethodCall {
                    receiver: filter_receiver,
                    method: filter_method,
                    args: filter_args,
                    ..
                } = receiver.as_ref()
                {
                    if filter_method == "filter" && filter_args.len() == 1 {
                        return Some((filter_receiver.as_ref().clone(), filter_args[0].clone()));
                    }
                }
            }
        }
        None
    }

    /// Transform an expression, returns changes count
    fn transform_expr(&self, expr: &mut PureExpr) -> usize {
        let mut changes = 0;

        // Check for filter().next() pattern
        if let Some((receiver, predicate)) = Self::is_filter_next_pattern(expr) {
            *expr = PureExpr::MethodCall {
                receiver: Box::new(receiver),
                method: "find".to_string(),
                turbofish: None,
                args: vec![predicate],
            };
            return 1;
        }

        // Recursively transform sub-expressions
        match expr {
            PureExpr::Binary { left, right, .. } => {
                changes += self.transform_expr(left);
                changes += self.transform_expr(right);
            }
            PureExpr::Unary { expr: inner, .. } => {
                changes += self.transform_expr(inner);
            }
            PureExpr::Call { func, args } => {
                changes += self.transform_expr(func);
                for arg in args {
                    changes += self.transform_expr(arg);
                }
            }
            PureExpr::MethodCall { receiver, args, .. } => {
                changes += self.transform_expr(receiver);
                for arg in args {
                    changes += self.transform_expr(arg);
                }
            }
            PureExpr::Field { expr: inner, .. } => {
                changes += self.transform_expr(inner);
            }
            PureExpr::Index { expr: inner, index } => {
                changes += self.transform_expr(inner);
                changes += self.transform_expr(index);
            }
            PureExpr::Block { block, .. } => {
                changes += self.transform_block(block);
            }
            PureExpr::If {
                cond,
                then_branch,
                else_branch,
            } => {
                changes += self.transform_expr(cond);
                changes += self.transform_block(then_branch);
                if let Some(else_expr) = else_branch {
                    changes += self.transform_expr(else_expr);
                }
            }
            PureExpr::Match { expr: e, arms } => {
                changes += self.transform_expr(e);
                for arm in arms {
                    changes += self.transform_expr(&mut arm.body);
                }
            }
            PureExpr::Loop { body: block, .. } | PureExpr::While { body: block, .. } => {
                changes += self.transform_block(block);
            }
            PureExpr::For {
                expr: iter_expr,
                body,
                ..
            } => {
                changes += self.transform_expr(iter_expr);
                changes += self.transform_block(body);
            }
            PureExpr::Closure { body, .. } => {
                changes += self.transform_expr(body);
            }
            PureExpr::Tuple(exprs) | PureExpr::Array(exprs) => {
                for e in exprs {
                    changes += self.transform_expr(e);
                }
            }
            PureExpr::Struct { fields, .. } => {
                for (_, e) in fields {
                    changes += self.transform_expr(e);
                }
            }
            PureExpr::Ref { expr: inner, .. } => {
                changes += self.transform_expr(inner);
            }
            PureExpr::Return(Some(inner)) => {
                changes += self.transform_expr(inner);
            }
            PureExpr::Try(inner) | PureExpr::Await(inner) => {
                changes += self.transform_expr(inner);
            }
            _ => {}
        }

        changes
    }

    pub fn transform_block(&self, block: &mut PureBlock) -> usize {
        let mut changes = 0;
        for stmt in &mut block.stmts {
            changes += self.transform_stmt(stmt);
        }
        changes
    }

    fn transform_stmt(&self, stmt: &mut PureStmt) -> usize {
        match stmt {
            PureStmt::Local { init: Some(e), .. } => self.transform_expr(e),
            PureStmt::Semi(e) | PureStmt::Expr(e) => self.transform_expr(e),
            _ => 0,
        }
    }
}

impl Mutation for FilterNextMutation {
    fn describe(&self) -> String {
        "Convert .filter().next() to .find()".to_string()
    }

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

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

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_is_filter_next_pattern_basic() {
        // iter.filter(|x| x > 0).next()
        let expr = PureExpr::MethodCall {
            receiver: Box::new(PureExpr::MethodCall {
                receiver: Box::new(PureExpr::Path("iter".to_string())),
                method: "filter".to_string(),
                turbofish: None,
                args: vec![PureExpr::Closure {
                    params: vec![],
                    ret: None,
                    body: Box::new(PureExpr::Path("predicate".to_string())),
                    is_async: false,
                    is_move: false,
                }],
            }),
            method: "next".to_string(),
            turbofish: None,
            args: vec![],
        };

        assert!(FilterNextMutation::is_filter_next_pattern(&expr).is_some());
    }

    #[test]
    fn test_is_filter_next_pattern_not_matching() {
        // iter.map(f).next() - not filter
        let expr = PureExpr::MethodCall {
            receiver: Box::new(PureExpr::MethodCall {
                receiver: Box::new(PureExpr::Path("iter".to_string())),
                method: "map".to_string(),
                turbofish: None,
                args: vec![PureExpr::Path("f".to_string())],
            }),
            method: "next".to_string(),
            turbofish: None,
            args: vec![],
        };

        assert!(FilterNextMutation::is_filter_next_pattern(&expr).is_none());
    }

    #[test]
    fn test_is_filter_next_pattern_filter_without_next() {
        // iter.filter(predicate) - no next
        let expr = PureExpr::MethodCall {
            receiver: Box::new(PureExpr::Path("iter".to_string())),
            method: "filter".to_string(),
            turbofish: None,
            args: vec![PureExpr::Path("predicate".to_string())],
        };

        assert!(FilterNextMutation::is_filter_next_pattern(&expr).is_none());
    }
}