Skip to main content

ryo_mutations/idiom/
redundant_closure.rs

1//! RedundantClosureMutation: Simplify redundant closures
2//!
3//! Transforms:
4//! - `|x| foo(x)` → `foo`
5//! - `|x| x.method()` → `Type::method` (when unambiguous)
6//! - `|a, b| func(a, b)` → `func`
7//!
8//! Corresponds to Clippy lint: `clippy::redundant_closure`
9
10use ryo_source::pure::{PureBlock, PureClosureParam, PureExpr, PurePattern, PureStmt};
11use ryo_symbol::SymbolId;
12
13use crate::Mutation;
14
15/// Simplify redundant closures to function references
16///
17/// # Example
18///
19/// ```rust,ignore
20/// use ryo_mutations::idiom::RedundantClosureMutation;
21///
22/// let mutation = RedundantClosureMutation::new();
23/// // Transforms: items.map(|x| foo(x))
24/// // Into:       items.map(foo)
25/// ```
26#[derive(Debug, Clone, Default)]
27pub struct RedundantClosureMutation {
28    /// Target function SymbolId. If None, applies to all functions.
29    pub target_fn: Option<SymbolId>,
30}
31
32impl RedundantClosureMutation {
33    pub fn new() -> Self {
34        Self::default()
35    }
36
37    /// Only apply in a specific function
38    pub fn in_function(mut self, id: SymbolId) -> Self {
39        self.target_fn = Some(id);
40        self
41    }
42
43    /// Extract parameter names from closure
44    fn get_param_names(params: &[PureClosureParam]) -> Vec<String> {
45        params
46            .iter()
47            .filter_map(|p| match &p.pattern {
48                PurePattern::Ident { name, .. } => Some(name.clone()),
49                _ => None,
50            })
51            .collect()
52    }
53
54    /// Check if expression is a simple path reference to the given name
55    fn is_path_to(expr: &PureExpr, name: &str) -> bool {
56        matches!(expr, PureExpr::Path(p) if p == name)
57    }
58
59    /// Check if closure body is a simple function call with params in order
60    fn is_redundant_call(params: &[String], body: &PureExpr) -> Option<PureExpr> {
61        match body {
62            // |x| foo(x) or |a, b| foo(a, b)
63            PureExpr::Call { func, args } => {
64                if args.len() != params.len() {
65                    return None;
66                }
67
68                // Check each argument matches corresponding param in order
69                for (param, arg) in params.iter().zip(args.iter()) {
70                    if !Self::is_path_to(arg, param) {
71                        return None;
72                    }
73                }
74
75                // Return the function reference
76                Some(func.as_ref().clone())
77            }
78            // Handle blocks with single expression
79            PureExpr::Block { block, .. } => {
80                if block.stmts.len() == 1 {
81                    match &block.stmts[0] {
82                        PureStmt::Expr(e) => Self::is_redundant_call(params, e),
83                        _ => None,
84                    }
85                } else {
86                    None
87                }
88            }
89            _ => None,
90        }
91    }
92
93    /// Transform expressions, returns changes count
94    fn transform_expr(&self, expr: &mut PureExpr) -> usize {
95        let mut changes = 0;
96
97        // Check for redundant closure pattern
98        if let PureExpr::Closure { params, body, .. } = expr {
99            let param_names = Self::get_param_names(params);
100
101            // Only handle closures where all params are simple idents
102            if param_names.len() == params.len() {
103                if let Some(func_ref) = Self::is_redundant_call(&param_names, body) {
104                    *expr = func_ref;
105                    return 1;
106                }
107            }
108        }
109
110        // Recursively transform sub-expressions
111        match expr {
112            PureExpr::Binary { left, right, .. } => {
113                changes += self.transform_expr(left);
114                changes += self.transform_expr(right);
115            }
116            PureExpr::Unary { expr: inner, .. } => {
117                changes += self.transform_expr(inner);
118            }
119            PureExpr::Call { func, args } => {
120                changes += self.transform_expr(func);
121                for arg in args {
122                    changes += self.transform_expr(arg);
123                }
124            }
125            PureExpr::MethodCall { receiver, args, .. } => {
126                changes += self.transform_expr(receiver);
127                for arg in args {
128                    changes += self.transform_expr(arg);
129                }
130            }
131            PureExpr::Field { expr: inner, .. } => {
132                changes += self.transform_expr(inner);
133            }
134            PureExpr::Index { expr: inner, index } => {
135                changes += self.transform_expr(inner);
136                changes += self.transform_expr(index);
137            }
138            PureExpr::Block { block, .. } => {
139                changes += self.transform_block(block);
140            }
141            PureExpr::If {
142                cond,
143                then_branch,
144                else_branch,
145            } => {
146                changes += self.transform_expr(cond);
147                changes += self.transform_block(then_branch);
148                if let Some(else_expr) = else_branch {
149                    changes += self.transform_expr(else_expr);
150                }
151            }
152            PureExpr::Match { expr: e, arms } => {
153                changes += self.transform_expr(e);
154                for arm in arms {
155                    changes += self.transform_expr(&mut arm.body);
156                }
157            }
158            PureExpr::Loop { body: block, .. } | PureExpr::While { body: block, .. } => {
159                changes += self.transform_block(block);
160            }
161            PureExpr::For {
162                expr: iter_expr,
163                body,
164                ..
165            } => {
166                changes += self.transform_expr(iter_expr);
167                changes += self.transform_block(body);
168            }
169            PureExpr::Closure { body, .. } => {
170                changes += self.transform_expr(body);
171            }
172            PureExpr::Tuple(exprs) | PureExpr::Array(exprs) => {
173                for e in exprs {
174                    changes += self.transform_expr(e);
175                }
176            }
177            PureExpr::Struct { fields, .. } => {
178                for (_, e) in fields {
179                    changes += self.transform_expr(e);
180                }
181            }
182            PureExpr::Ref { expr: inner, .. } => {
183                changes += self.transform_expr(inner);
184            }
185            PureExpr::Return(Some(inner)) => {
186                changes += self.transform_expr(inner);
187            }
188            PureExpr::Try(inner) | PureExpr::Await(inner) => {
189                changes += self.transform_expr(inner);
190            }
191            _ => {}
192        }
193
194        changes
195    }
196
197    pub fn transform_block(&self, block: &mut PureBlock) -> usize {
198        let mut changes = 0;
199        for stmt in &mut block.stmts {
200            changes += self.transform_stmt(stmt);
201        }
202        changes
203    }
204
205    fn transform_stmt(&self, stmt: &mut PureStmt) -> usize {
206        match stmt {
207            PureStmt::Local { init: Some(e), .. } => self.transform_expr(e),
208            PureStmt::Semi(e) | PureStmt::Expr(e) => self.transform_expr(e),
209            _ => 0,
210        }
211    }
212}
213
214impl Mutation for RedundantClosureMutation {
215    fn describe(&self) -> String {
216        "Simplify redundant closures (|x| foo(x) → foo)".to_string()
217    }
218
219    fn mutation_type(&self) -> &'static str {
220        "RedundantClosure"
221    }
222
223    fn box_clone(&self) -> Box<dyn Mutation> {
224        Box::new(self.clone())
225    }
226}
227
228#[cfg(test)]
229mod tests {
230    use super::*;
231
232    #[test]
233    fn test_get_param_names() {
234        let params = vec![
235            PureClosureParam::untyped(PurePattern::Ident {
236                name: "x".to_string(),
237                is_mut: false,
238            }),
239            PureClosureParam::untyped(PurePattern::Ident {
240                name: "y".to_string(),
241                is_mut: false,
242            }),
243        ];
244        let names = RedundantClosureMutation::get_param_names(&params);
245        assert_eq!(names, vec!["x", "y"]);
246    }
247
248    #[test]
249    fn test_is_redundant_call_single_param() {
250        // |x| foo(x)
251        let params = vec!["x".to_string()];
252        let body = PureExpr::Call {
253            func: Box::new(PureExpr::Path("foo".to_string())),
254            args: vec![PureExpr::Path("x".to_string())],
255        };
256
257        let result = RedundantClosureMutation::is_redundant_call(&params, &body);
258        assert!(result.is_some());
259        assert!(matches!(result.unwrap(), PureExpr::Path(s) if s == "foo"));
260    }
261
262    #[test]
263    fn test_is_redundant_call_multi_param() {
264        // |a, b| func(a, b)
265        let params = vec!["a".to_string(), "b".to_string()];
266        let body = PureExpr::Call {
267            func: Box::new(PureExpr::Path("func".to_string())),
268            args: vec![
269                PureExpr::Path("a".to_string()),
270                PureExpr::Path("b".to_string()),
271            ],
272        };
273
274        let result = RedundantClosureMutation::is_redundant_call(&params, &body);
275        assert!(result.is_some());
276    }
277
278    #[test]
279    fn test_is_not_redundant_wrong_order() {
280        // |a, b| func(b, a) - params in wrong order
281        let params = vec!["a".to_string(), "b".to_string()];
282        let body = PureExpr::Call {
283            func: Box::new(PureExpr::Path("func".to_string())),
284            args: vec![
285                PureExpr::Path("b".to_string()),
286                PureExpr::Path("a".to_string()),
287            ],
288        };
289
290        let result = RedundantClosureMutation::is_redundant_call(&params, &body);
291        assert!(result.is_none());
292    }
293
294    #[test]
295    fn test_is_not_redundant_extra_args() {
296        // |x| foo(x, y) - extra argument
297        let params = vec!["x".to_string()];
298        let body = PureExpr::Call {
299            func: Box::new(PureExpr::Path("foo".to_string())),
300            args: vec![
301                PureExpr::Path("x".to_string()),
302                PureExpr::Path("y".to_string()),
303            ],
304        };
305
306        let result = RedundantClosureMutation::is_redundant_call(&params, &body);
307        assert!(result.is_none());
308    }
309}