1use ryo_source::pure::{
19 PureBlock, PureClosureParam, PureExpr, PureMatchArm, PurePattern, PureStmt,
20};
21use ryo_symbol::SymbolId;
22
23use crate::Mutation;
24
25#[derive(Debug, Clone, Default)]
48pub struct ManualMapMutation {
49 pub target_fn: Option<SymbolId>,
51}
52
53impl ManualMapMutation {
54 pub fn new() -> Self {
55 Self::default()
56 }
57
58 pub fn in_function(mut self, id: SymbolId) -> Self {
60 self.target_fn = Some(id);
61 self
62 }
63
64 fn is_some_pattern(pattern: &PurePattern) -> Option<String> {
69 match pattern {
70 PurePattern::Struct { path, fields, .. } => {
72 if (path == "Some" || path.ends_with("::Some")) && fields.len() == 1 {
74 if let Some((_, PurePattern::Ident { name, .. })) = fields.first() {
76 return Some(name.clone());
77 }
78 }
79 None
80 }
81 _ => None,
82 }
83 }
84
85 fn is_none_pattern(pattern: &PurePattern) -> bool {
90 match pattern {
91 PurePattern::Path(p) => p == "None" || p.ends_with("::None"),
92 PurePattern::Ident { name, .. } => name == "None",
93 _ => false,
94 }
95 }
96
97 fn is_ok_pattern(pattern: &PurePattern) -> Option<String> {
99 match pattern {
100 PurePattern::Struct { path, fields, .. } => {
101 if path == "Ok" && fields.len() == 1 {
102 if let (_, PurePattern::Ident { name, .. }) = &fields[0] {
103 return Some(name.clone());
104 }
105 }
106 None
107 }
108 _ => None,
109 }
110 }
111
112 fn is_err_pattern(pattern: &PurePattern) -> Option<String> {
114 match pattern {
115 PurePattern::Struct { path, fields, .. } => {
116 if path == "Err" && fields.len() == 1 {
117 if let (_, PurePattern::Ident { name, .. }) = &fields[0] {
118 return Some(name.clone());
119 }
120 }
121 None
122 }
123 _ => None,
124 }
125 }
126
127 fn is_some_expr(expr: &PureExpr) -> Option<&PureExpr> {
129 match expr {
130 PureExpr::Call { func, args } => {
131 if matches!(func.as_ref(), PureExpr::Path(p) if p == "Some" || p.ends_with("::Some"))
132 && args.len() == 1
133 {
134 return Some(&args[0]);
135 }
136 None
137 }
138 _ => None,
139 }
140 }
141
142 fn is_none_expr(expr: &PureExpr) -> bool {
144 matches!(expr, PureExpr::Path(p) if p == "None" || p.ends_with("::None"))
145 }
146
147 fn is_ok_expr(expr: &PureExpr) -> Option<&PureExpr> {
149 match expr {
150 PureExpr::Call { func, args } => {
151 if matches!(func.as_ref(), PureExpr::Path(p) if p == "Ok") && args.len() == 1 {
152 return Some(&args[0]);
153 }
154 None
155 }
156 _ => None,
157 }
158 }
159
160 fn is_err_passthrough(expr: &PureExpr, err_name: &str) -> bool {
162 match expr {
163 PureExpr::Call { func, args } => {
164 if matches!(func.as_ref(), PureExpr::Path(p) if p == "Err") && args.len() == 1 {
165 return matches!(&args[0], PureExpr::Path(p) if p == err_name);
166 }
167 false
168 }
169 _ => false,
170 }
171 }
172
173 fn try_convert_match(scrutinee: &PureExpr, arms: &[PureMatchArm]) -> Option<PureExpr> {
175 if arms.len() != 2 {
176 return None;
177 }
178
179 if let Some(var_name) = Self::is_some_pattern(&arms[0].pattern) {
181 if Self::is_none_pattern(&arms[1].pattern) && Self::is_none_expr(&arms[1].body) {
182 if let Some(inner) = Self::is_some_expr(&arms[0].body) {
183 return Some(Self::create_map_call(
184 scrutinee.clone(),
185 var_name,
186 inner.clone(),
187 ));
188 }
189 }
190 }
191
192 if Self::is_none_pattern(&arms[0].pattern) && Self::is_none_expr(&arms[0].body) {
194 if let Some(var_name) = Self::is_some_pattern(&arms[1].pattern) {
195 if let Some(inner) = Self::is_some_expr(&arms[1].body) {
196 return Some(Self::create_map_call(
197 scrutinee.clone(),
198 var_name,
199 inner.clone(),
200 ));
201 }
202 }
203 }
204
205 if let Some(ok_var) = Self::is_ok_pattern(&arms[0].pattern) {
207 if let Some(err_var) = Self::is_err_pattern(&arms[1].pattern) {
208 if Self::is_err_passthrough(&arms[1].body, &err_var) {
209 if let Some(inner) = Self::is_ok_expr(&arms[0].body) {
210 return Some(Self::create_map_call(
211 scrutinee.clone(),
212 ok_var,
213 inner.clone(),
214 ));
215 }
216 }
217 }
218 }
219
220 None
221 }
222
223 fn create_map_call(receiver: PureExpr, var_name: String, body: PureExpr) -> PureExpr {
225 PureExpr::MethodCall {
226 receiver: Box::new(receiver),
227 method: "map".to_string(),
228 turbofish: None,
229 args: vec![PureExpr::Closure {
230 is_async: false,
231 is_move: false,
232 params: vec![PureClosureParam::untyped(PurePattern::Ident {
233 name: var_name,
234 is_mut: false,
235 by_ref: false,
236 })],
237 ret: None,
238 body: Box::new(body),
239 }],
240 }
241 }
242
243 fn transform_expr(&self, expr: &mut PureExpr) -> usize {
245 let mut changes = 0;
246
247 if let PureExpr::Match {
249 expr: scrutinee,
250 arms,
251 } = expr
252 {
253 if let Some(map_call) = Self::try_convert_match(scrutinee, arms) {
254 *expr = map_call;
255 return 1;
256 }
257 }
258
259 match expr {
265 PureExpr::Binary { left, right, .. } => {
266 changes += self.transform_expr(left);
267 changes += self.transform_expr(right);
268 }
269 PureExpr::Unary { expr: inner, .. } => {
270 changes += self.transform_expr(inner);
271 }
272 PureExpr::Call { func, args } => {
273 changes += self.transform_expr(func);
274 for arg in args {
275 changes += self.transform_expr(arg);
276 }
277 }
278 PureExpr::MethodCall { receiver, args, .. } => {
279 changes += self.transform_expr(receiver);
280 for arg in args {
281 changes += self.transform_expr(arg);
282 }
283 }
284 PureExpr::Block { block, .. } => {
285 changes += self.transform_block(block);
286 }
287 PureExpr::If {
288 cond,
289 then_branch,
290 else_branch,
291 } => {
292 changes += self.transform_expr(cond);
293 changes += self.transform_block(then_branch);
294 if let Some(else_expr) = else_branch {
295 changes += self.transform_expr(else_expr);
296 }
297 }
298 PureExpr::Match { expr: e, arms } => {
299 changes += self.transform_expr(e);
300 for arm in arms {
301 changes += self.transform_expr(&mut arm.body);
302 }
303 }
304 PureExpr::Loop { body: block, .. } | PureExpr::While { body: block, .. } => {
305 changes += self.transform_block(block);
306 }
307 PureExpr::For {
308 expr: iter_expr,
309 body,
310 ..
311 } => {
312 changes += self.transform_expr(iter_expr);
313 changes += self.transform_block(body);
314 }
315 PureExpr::Closure { body, .. } => {
316 changes += self.transform_expr(body);
317 }
318 _ => {}
319 }
320
321 changes
322 }
323
324 pub fn transform_block(&self, block: &mut PureBlock) -> usize {
325 let mut changes = 0;
326 for stmt in &mut block.stmts {
327 changes += self.transform_stmt(stmt);
328 }
329 changes
330 }
331
332 fn transform_stmt(&self, stmt: &mut PureStmt) -> usize {
333 match stmt {
334 PureStmt::Local { init: Some(e), .. } => self.transform_expr(e),
335 PureStmt::Semi(e) | PureStmt::Expr(e) => self.transform_expr(e),
336 _ => 0,
337 }
338 }
339}
340
341impl Mutation for ManualMapMutation {
342 fn describe(&self) -> String {
343 "Convert manual Option/Result map patterns to .map()".to_string()
344 }
345
346 fn mutation_type(&self) -> &'static str {
347 "ManualMap"
348 }
349
350 fn box_clone(&self) -> Box<dyn Mutation> {
351 Box::new(self.clone())
352 }
353}
354
355#[cfg(test)]
356mod tests {
357 use super::*;
358
359 #[test]
360 fn test_is_some_pattern_struct() {
361 let pattern = PurePattern::Struct {
363 path: "Some".to_string(),
364 fields: vec![(
365 "0".to_string(),
366 PurePattern::Ident {
367 name: "x".to_string(),
368 is_mut: false,
369 by_ref: false,
370 },
371 )],
372 rest: false,
373 };
374 assert_eq!(
375 ManualMapMutation::is_some_pattern(&pattern),
376 Some("x".to_string())
377 );
378 }
379
380 #[test]
381 fn test_is_none_pattern() {
382 let pattern = PurePattern::Path("None".to_string());
383 assert!(ManualMapMutation::is_none_pattern(&pattern));
384 }
385
386 #[test]
387 fn test_is_some_expr() {
388 let expr = PureExpr::Call {
389 func: Box::new(PureExpr::Path("Some".to_string())),
390 args: vec![PureExpr::Path("value".to_string())],
391 };
392 assert!(ManualMapMutation::is_some_expr(&expr).is_some());
393 }
394
395 #[test]
396 fn test_is_none_expr() {
397 let expr = PureExpr::Path("None".to_string());
398 assert!(ManualMapMutation::is_none_expr(&expr));
399 }
400
401 #[test]
402 fn test_try_convert_match_option() {
403 let scrutinee = PureExpr::Path("opt".to_string());
404 let arms = vec![
405 PureMatchArm {
406 pattern: PurePattern::Struct {
407 path: "Some".to_string(),
408 fields: vec![(
409 "0".to_string(),
410 PurePattern::Ident {
411 name: "x".to_string(),
412 is_mut: false,
413 by_ref: false,
414 },
415 )],
416 rest: false,
417 },
418 guard: None,
419 body: PureExpr::Call {
420 func: Box::new(PureExpr::Path("Some".to_string())),
421 args: vec![PureExpr::Binary {
422 op: "+".to_string(),
423 left: Box::new(PureExpr::Path("x".to_string())),
424 right: Box::new(PureExpr::Lit("1".to_string())),
425 }],
426 },
427 },
428 PureMatchArm {
429 pattern: PurePattern::Path("None".to_string()),
430 guard: None,
431 body: PureExpr::Path("None".to_string()),
432 },
433 ];
434
435 let result = ManualMapMutation::try_convert_match(&scrutinee, &arms);
436 assert!(result.is_some());
437
438 if let Some(PureExpr::MethodCall { method, .. }) = result {
439 assert_eq!(method, "map");
440 } else {
441 panic!("Expected MethodCall");
442 }
443 }
444}