ryo_mutations/idiom/
redundant_closure.rs1use ryo_source::pure::{PureBlock, PureClosureParam, PureExpr, PurePattern, PureStmt};
11use ryo_symbol::SymbolId;
12
13use crate::Mutation;
14
15#[derive(Debug, Clone, Default)]
27pub struct RedundantClosureMutation {
28 pub target_fn: Option<SymbolId>,
30}
31
32impl RedundantClosureMutation {
33 pub fn new() -> Self {
34 Self::default()
35 }
36
37 pub fn in_function(mut self, id: SymbolId) -> Self {
39 self.target_fn = Some(id);
40 self
41 }
42
43 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 fn is_path_to(expr: &PureExpr, name: &str) -> bool {
56 matches!(expr, PureExpr::Path(p) if p == name)
57 }
58
59 fn is_redundant_call(params: &[String], body: &PureExpr) -> Option<PureExpr> {
61 match body {
62 PureExpr::Call { func, args } => {
64 if args.len() != params.len() {
65 return None;
66 }
67
68 for (param, arg) in params.iter().zip(args.iter()) {
70 if !Self::is_path_to(arg, param) {
71 return None;
72 }
73 }
74
75 Some(func.as_ref().clone())
77 }
78 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 fn transform_expr(&self, expr: &mut PureExpr) -> usize {
95 let mut changes = 0;
96
97 if let PureExpr::Closure { params, body, .. } = expr {
99 let param_names = Self::get_param_names(params);
100
101 if param_names.len() == params.len() {
103 if let Some(func_ref) = Self::is_redundant_call(¶m_names, body) {
104 *expr = func_ref;
105 return 1;
106 }
107 }
108 }
109
110 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(¶ms);
245 assert_eq!(names, vec!["x", "y"]);
246 }
247
248 #[test]
249 fn test_is_redundant_call_single_param() {
250 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(¶ms, &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 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(¶ms, &body);
275 assert!(result.is_some());
276 }
277
278 #[test]
279 fn test_is_not_redundant_wrong_order() {
280 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(¶ms, &body);
291 assert!(result.is_none());
292 }
293
294 #[test]
295 fn test_is_not_redundant_extra_args() {
296 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(¶ms, &body);
307 assert!(result.is_none());
308 }
309}