Skip to main content

ryo_mutations/idiom/
filter_next.rs

1//! FilterNextMutation: Convert `.filter().next()` to `.find()`
2//!
3//! Transforms:
4//! - `iter.filter(|x| predicate(x)).next()` -> `iter.find(|x| predicate(x))`
5//! - `iter.filter(predicate).next()` -> `iter.find(predicate)`
6//!
7//! Corresponds to Clippy lint: `clippy::filter_next`
8
9use ryo_source::pure::{PureBlock, PureExpr, PureStmt};
10use ryo_symbol::SymbolId;
11
12use crate::Mutation;
13
14/// Convert `.filter().next()` chains to `.find()`
15///
16/// # Example
17///
18/// ```rust,ignore
19/// use ryo_mutations::idiom::FilterNextMutation;
20///
21/// let mutation = FilterNextMutation::new();
22/// // Transforms: items.iter().filter(|x| x > 0).next()
23/// // Into:       items.iter().find(|x| x > 0)
24/// ```
25#[derive(Debug, Clone, Default)]
26pub struct FilterNextMutation {
27    /// Target function SymbolId. If None, applies to all functions.
28    pub target_fn: Option<SymbolId>,
29}
30
31impl FilterNextMutation {
32    pub fn new() -> Self {
33        Self::default()
34    }
35
36    /// Only apply in a specific function
37    pub fn in_function(mut self, id: SymbolId) -> Self {
38        self.target_fn = Some(id);
39        self
40    }
41
42    /// Check if expression is a `.filter(predicate).next()` pattern
43    /// Returns (receiver before filter, predicate) if matched
44    fn is_filter_next_pattern(expr: &PureExpr) -> Option<(PureExpr, PureExpr)> {
45        // Pattern: receiver.filter(predicate).next()
46        if let PureExpr::MethodCall {
47            receiver,
48            method,
49            args,
50            ..
51        } = expr
52        {
53            // Check for .next()
54            if method == "next" && args.is_empty() {
55                // Check receiver is .filter(predicate)
56                if let PureExpr::MethodCall {
57                    receiver: filter_receiver,
58                    method: filter_method,
59                    args: filter_args,
60                    ..
61                } = receiver.as_ref()
62                {
63                    if filter_method == "filter" && filter_args.len() == 1 {
64                        return Some((filter_receiver.as_ref().clone(), filter_args[0].clone()));
65                    }
66                }
67            }
68        }
69        None
70    }
71
72    /// Transform an expression, returns changes count
73    fn transform_expr(&self, expr: &mut PureExpr) -> usize {
74        let mut changes = 0;
75
76        // Check for filter().next() pattern
77        if let Some((receiver, predicate)) = Self::is_filter_next_pattern(expr) {
78            *expr = PureExpr::MethodCall {
79                receiver: Box::new(receiver),
80                method: "find".to_string(),
81                turbofish: None,
82                args: vec![predicate],
83            };
84            return 1;
85        }
86
87        // Recursively transform sub-expressions
88        match expr {
89            PureExpr::Binary { left, right, .. } => {
90                changes += self.transform_expr(left);
91                changes += self.transform_expr(right);
92            }
93            PureExpr::Unary { expr: inner, .. } => {
94                changes += self.transform_expr(inner);
95            }
96            PureExpr::Call { func, args } => {
97                changes += self.transform_expr(func);
98                for arg in args {
99                    changes += self.transform_expr(arg);
100                }
101            }
102            PureExpr::MethodCall { receiver, args, .. } => {
103                changes += self.transform_expr(receiver);
104                for arg in args {
105                    changes += self.transform_expr(arg);
106                }
107            }
108            PureExpr::Field { expr: inner, .. } => {
109                changes += self.transform_expr(inner);
110            }
111            PureExpr::Index { expr: inner, index } => {
112                changes += self.transform_expr(inner);
113                changes += self.transform_expr(index);
114            }
115            PureExpr::Block { block, .. } => {
116                changes += self.transform_block(block);
117            }
118            PureExpr::If {
119                cond,
120                then_branch,
121                else_branch,
122            } => {
123                changes += self.transform_expr(cond);
124                changes += self.transform_block(then_branch);
125                if let Some(else_expr) = else_branch {
126                    changes += self.transform_expr(else_expr);
127                }
128            }
129            PureExpr::Match { expr: e, arms } => {
130                changes += self.transform_expr(e);
131                for arm in arms {
132                    changes += self.transform_expr(&mut arm.body);
133                }
134            }
135            PureExpr::Loop { body: block, .. } | PureExpr::While { body: block, .. } => {
136                changes += self.transform_block(block);
137            }
138            PureExpr::For {
139                expr: iter_expr,
140                body,
141                ..
142            } => {
143                changes += self.transform_expr(iter_expr);
144                changes += self.transform_block(body);
145            }
146            PureExpr::Closure { body, .. } => {
147                changes += self.transform_expr(body);
148            }
149            PureExpr::Tuple(exprs) | PureExpr::Array(exprs) => {
150                for e in exprs {
151                    changes += self.transform_expr(e);
152                }
153            }
154            PureExpr::Struct { fields, .. } => {
155                for (_, e) in fields {
156                    changes += self.transform_expr(e);
157                }
158            }
159            PureExpr::Ref { expr: inner, .. } => {
160                changes += self.transform_expr(inner);
161            }
162            PureExpr::Return(Some(inner)) => {
163                changes += self.transform_expr(inner);
164            }
165            PureExpr::Try(inner) | PureExpr::Await(inner) => {
166                changes += self.transform_expr(inner);
167            }
168            _ => {}
169        }
170
171        changes
172    }
173
174    pub fn transform_block(&self, block: &mut PureBlock) -> usize {
175        let mut changes = 0;
176        for stmt in &mut block.stmts {
177            changes += self.transform_stmt(stmt);
178        }
179        changes
180    }
181
182    fn transform_stmt(&self, stmt: &mut PureStmt) -> usize {
183        match stmt {
184            PureStmt::Local { init: Some(e), .. } => self.transform_expr(e),
185            PureStmt::Semi(e) | PureStmt::Expr(e) => self.transform_expr(e),
186            _ => 0,
187        }
188    }
189}
190
191impl Mutation for FilterNextMutation {
192    fn describe(&self) -> String {
193        "Convert .filter().next() to .find()".to_string()
194    }
195
196    fn mutation_type(&self) -> &'static str {
197        "FilterNext"
198    }
199
200    fn box_clone(&self) -> Box<dyn Mutation> {
201        Box::new(self.clone())
202    }
203}
204
205#[cfg(test)]
206mod tests {
207    use super::*;
208
209    #[test]
210    fn test_is_filter_next_pattern_basic() {
211        // iter.filter(|x| x > 0).next()
212        let expr = PureExpr::MethodCall {
213            receiver: Box::new(PureExpr::MethodCall {
214                receiver: Box::new(PureExpr::Path("iter".to_string())),
215                method: "filter".to_string(),
216                turbofish: None,
217                args: vec![PureExpr::Closure {
218                    params: vec![],
219                    ret: None,
220                    body: Box::new(PureExpr::Path("predicate".to_string())),
221                    is_async: false,
222                    is_move: false,
223                }],
224            }),
225            method: "next".to_string(),
226            turbofish: None,
227            args: vec![],
228        };
229
230        assert!(FilterNextMutation::is_filter_next_pattern(&expr).is_some());
231    }
232
233    #[test]
234    fn test_is_filter_next_pattern_not_matching() {
235        // iter.map(f).next() - not filter
236        let expr = PureExpr::MethodCall {
237            receiver: Box::new(PureExpr::MethodCall {
238                receiver: Box::new(PureExpr::Path("iter".to_string())),
239                method: "map".to_string(),
240                turbofish: None,
241                args: vec![PureExpr::Path("f".to_string())],
242            }),
243            method: "next".to_string(),
244            turbofish: None,
245            args: vec![],
246        };
247
248        assert!(FilterNextMutation::is_filter_next_pattern(&expr).is_none());
249    }
250
251    #[test]
252    fn test_is_filter_next_pattern_filter_without_next() {
253        // iter.filter(predicate) - no next
254        let expr = PureExpr::MethodCall {
255            receiver: Box::new(PureExpr::Path("iter".to_string())),
256            method: "filter".to_string(),
257            turbofish: None,
258            args: vec![PureExpr::Path("predicate".to_string())],
259        };
260
261        assert!(FilterNextMutation::is_filter_next_pattern(&expr).is_none());
262    }
263}