Skip to main content

ryo_mutations/idiom/
manual_map.rs

1//! ManualMapMutation: Convert manual Option/Result mapping patterns
2//!
3//! Transforms:
4//! - `match opt { Some(x) => Some(f(x)), None => None }` → `opt.map(|x| f(x))`
5//! - `if let Some(x) = opt { Some(f(x)) } else { None }` → `opt.map(|x| f(x))`
6//!
7//! Similar patterns for Result:
8//! - `match res { Ok(x) => Ok(f(x)), Err(e) => Err(e) }` → `res.map(|x| f(x))`
9//!
10//! Corresponds to Clippy lint: `clippy::manual_map`
11//!
12//! # Note
13//!
14//! This mutation currently has limited pattern support due to PurePattern
15//! not having a TupleStruct variant for patterns like `Some(x)`.
16//! Full support requires AST enhancement.
17
18use ryo_source::pure::{
19    PureBlock, PureClosureParam, PureExpr, PureMatchArm, PurePattern, PureStmt,
20};
21use ryo_symbol::SymbolId;
22
23use crate::Mutation;
24
25/// Convert manual Option/Result mapping to .map() method
26///
27/// # Example
28///
29/// ```rust,ignore
30/// use ryo_mutations::idiom::ManualMapMutation;
31///
32/// let mutation = ManualMapMutation::new();
33/// // Transforms:
34/// //   match opt {
35/// //       Some(x) => Some(x.to_string()),
36/// //       None => None,
37/// //   }
38/// // Into:
39/// //   opt.map(|x| x.to_string())
40/// ```
41///
42/// # Limitations
43///
44/// Currently only detects patterns where the AST represents `Some(x)` as
45/// a Struct pattern with path "Some". Full TupleStruct pattern support
46/// is planned for future AST enhancements.
47#[derive(Debug, Clone, Default)]
48pub struct ManualMapMutation {
49    /// Target function SymbolId. If None, applies to all functions.
50    pub target_fn: Option<SymbolId>,
51}
52
53impl ManualMapMutation {
54    pub fn new() -> Self {
55        Self::default()
56    }
57
58    /// Only apply in a specific function
59    pub fn in_function(mut self, id: SymbolId) -> Self {
60        self.target_fn = Some(id);
61        self
62    }
63
64    /// Check if pattern is Some(x) and extract the binding name
65    ///
66    /// Supports both Struct pattern `Some { 0: x }` (internal representation)
67    /// and various other representations of tuple struct patterns.
68    fn is_some_pattern(pattern: &PurePattern) -> Option<String> {
69        match pattern {
70            // Struct pattern: Some { 0: x } (how tuple struct patterns are represented)
71            PurePattern::Struct { path, fields, .. } => {
72                // Check for "Some" or "Option::Some" or "std::option::Option::Some"
73                if (path == "Some" || path.ends_with("::Some")) && fields.len() == 1 {
74                    // The field name is "0" for tuple structs
75                    if let Some((_, PurePattern::Ident { name, .. })) = fields.first() {
76                        return Some(name.clone());
77                    }
78                }
79                None
80            }
81            _ => None,
82        }
83    }
84
85    /// Check if pattern is None
86    ///
87    /// Note: `None` in a match arm is parsed by syn as `Pat::Ident` (not Pat::Path),
88    /// so we need to check both Ident and Path variants.
89    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    /// Check if pattern is Ok(x) and extract the binding name
98    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    /// Check if pattern is Err(e) and extract the binding name
113    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    /// Check if expression is Some(inner) and extract inner
128    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    /// Check if expression is None
143    fn is_none_expr(expr: &PureExpr) -> bool {
144        matches!(expr, PureExpr::Path(p) if p == "None" || p.ends_with("::None"))
145    }
146
147    /// Check if expression is Ok(inner) and extract inner
148    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    /// Check if expression is Err(e) where e matches the given name
161    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    /// Try to convert a match expression to .map() call
174    fn try_convert_match(scrutinee: &PureExpr, arms: &[PureMatchArm]) -> Option<PureExpr> {
175        if arms.len() != 2 {
176            return None;
177        }
178
179        // Try Option pattern: Some(x) => Some(f(x)), None => None
180        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        // Try reversed Option pattern: None => None, Some(x) => Some(f(x))
193        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        // Try Result pattern: Ok(x) => Ok(f(x)), Err(e) => Err(e)
206        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    /// Create a .map(|var| body) call
224    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    /// Transform expressions, returns changes count
244    fn transform_expr(&self, expr: &mut PureExpr) -> usize {
245        let mut changes = 0;
246
247        // Check for match pattern
248        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        // Note: if let pattern support requires IfLet variant in PureExpr
260        // which may not exist. For now, we focus on match expressions.
261        // Future: if let Some(x) = opt { Some(f(x)) } else { None } → opt.map(|x| f(x))
262
263        // Recursively transform sub-expressions
264        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        // PurePattern::Struct is used to represent Some(x) patterns
362        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}