Skip to main content

ryo_mutations/idiom/
map_unwrap_or.rs

1//! MapUnwrapOrMutation: Convert `.map().unwrap_or()` to `.map_or()`
2//!
3//! Transforms:
4//! - `opt.map(f).unwrap_or(default)` -> `opt.map_or(default, f)`
5//! - `opt.map(f).unwrap_or_else(g)` -> `opt.map_or_else(g, f)`
6//!
7//! Corresponds to Clippy lints: `clippy::map_unwrap_or`, `clippy::option_map_or_none`
8
9use ryo_source::pure::{PureBlock, PureExpr, PureStmt};
10use ryo_symbol::SymbolId;
11
12use crate::Mutation;
13
14/// Convert `.map().unwrap_or()` chains to `.map_or()`
15///
16/// # Example
17///
18/// ```rust,ignore
19/// use ryo_mutations::idiom::MapUnwrapOrMutation;
20///
21/// let mutation = MapUnwrapOrMutation::new();
22/// // Transforms: opt.map(|x| x + 1).unwrap_or(0)
23/// // Into:       opt.map_or(0, |x| x + 1)
24/// ```
25#[derive(Debug, Clone, Default)]
26pub struct MapUnwrapOrMutation {
27    /// Target function SymbolId. If None, applies to all functions.
28    pub target_fn: Option<SymbolId>,
29}
30
31impl MapUnwrapOrMutation {
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 `.map(f).unwrap_or(default)` pattern
43    /// Returns (receiver, map_fn, default_value, is_lazy) if matched
44    fn is_map_unwrap_or_pattern(expr: &PureExpr) -> Option<(PureExpr, PureExpr, PureExpr, bool)> {
45        if let PureExpr::MethodCall {
46            receiver,
47            method,
48            args,
49            ..
50        } = expr
51        {
52            // Check for .unwrap_or(default) or .unwrap_or_else(f)
53            let (is_lazy, default_arg) = if method == "unwrap_or" && args.len() == 1 {
54                (false, &args[0])
55            } else if method == "unwrap_or_else" && args.len() == 1 {
56                (true, &args[0])
57            } else {
58                return None;
59            };
60
61            // Check receiver is .map(f)
62            if let PureExpr::MethodCall {
63                receiver: map_receiver,
64                method: map_method,
65                args: map_args,
66                ..
67            } = receiver.as_ref()
68            {
69                if map_method == "map" && map_args.len() == 1 {
70                    return Some((
71                        map_receiver.as_ref().clone(),
72                        map_args[0].clone(),
73                        default_arg.clone(),
74                        is_lazy,
75                    ));
76                }
77            }
78        }
79        None
80    }
81
82    /// Check if expression is a `.map(f).unwrap_or(None)` pattern (-> and_then)
83    /// Returns (receiver, map_fn) if matched
84    fn is_map_or_none_pattern(expr: &PureExpr) -> Option<(PureExpr, PureExpr)> {
85        if let PureExpr::MethodCall {
86            receiver,
87            method,
88            args,
89            ..
90        } = expr
91        {
92            // Check for .unwrap_or(None)
93            if method == "unwrap_or"
94                && args.len() == 1
95                && matches!(&args[0], PureExpr::Path(p) if p == "None")
96            {
97                // Check receiver is .map(f)
98                if let PureExpr::MethodCall {
99                    receiver: map_receiver,
100                    method: map_method,
101                    args: map_args,
102                    ..
103                } = receiver.as_ref()
104                {
105                    if map_method == "map" && map_args.len() == 1 {
106                        return Some((map_receiver.as_ref().clone(), map_args[0].clone()));
107                    }
108                }
109            }
110        }
111        None
112    }
113
114    /// Transform an expression, returns changes count
115    fn transform_expr(&self, expr: &mut PureExpr) -> usize {
116        let mut changes = 0;
117
118        // Check for map().unwrap_or(None) -> and_then pattern first
119        if let Some((receiver, map_fn)) = Self::is_map_or_none_pattern(expr) {
120            *expr = PureExpr::MethodCall {
121                receiver: Box::new(receiver),
122                method: "and_then".to_string(),
123                turbofish: None,
124                args: vec![map_fn],
125            };
126            return 1;
127        }
128
129        // Check for map().unwrap_or() pattern
130        if let Some((receiver, map_fn, default_value, is_lazy)) =
131            Self::is_map_unwrap_or_pattern(expr)
132        {
133            let method = if is_lazy { "map_or_else" } else { "map_or" };
134            *expr = PureExpr::MethodCall {
135                receiver: Box::new(receiver),
136                method: method.to_string(),
137                turbofish: None,
138                // Note: map_or takes (default, f), map_or_else takes (default_fn, f)
139                args: vec![default_value, map_fn],
140            };
141            return 1;
142        }
143
144        // Recursively transform sub-expressions
145        match expr {
146            PureExpr::Binary { left, right, .. } => {
147                changes += self.transform_expr(left);
148                changes += self.transform_expr(right);
149            }
150            PureExpr::Unary { expr: inner, .. } => {
151                changes += self.transform_expr(inner);
152            }
153            PureExpr::Call { func, args } => {
154                changes += self.transform_expr(func);
155                for arg in args {
156                    changes += self.transform_expr(arg);
157                }
158            }
159            PureExpr::MethodCall { receiver, args, .. } => {
160                changes += self.transform_expr(receiver);
161                for arg in args {
162                    changes += self.transform_expr(arg);
163                }
164            }
165            PureExpr::Field { expr: inner, .. } => {
166                changes += self.transform_expr(inner);
167            }
168            PureExpr::Index { expr: inner, index } => {
169                changes += self.transform_expr(inner);
170                changes += self.transform_expr(index);
171            }
172            PureExpr::Block { block, .. } => {
173                changes += self.transform_block(block);
174            }
175            PureExpr::If {
176                cond,
177                then_branch,
178                else_branch,
179            } => {
180                changes += self.transform_expr(cond);
181                changes += self.transform_block(then_branch);
182                if let Some(else_expr) = else_branch {
183                    changes += self.transform_expr(else_expr);
184                }
185            }
186            PureExpr::Match { expr: e, arms } => {
187                changes += self.transform_expr(e);
188                for arm in arms {
189                    changes += self.transform_expr(&mut arm.body);
190                }
191            }
192            PureExpr::Loop { body: block, .. } | PureExpr::While { body: block, .. } => {
193                changes += self.transform_block(block);
194            }
195            PureExpr::For {
196                expr: iter_expr,
197                body,
198                ..
199            } => {
200                changes += self.transform_expr(iter_expr);
201                changes += self.transform_block(body);
202            }
203            PureExpr::Closure { body, .. } => {
204                changes += self.transform_expr(body);
205            }
206            PureExpr::Tuple(exprs) | PureExpr::Array(exprs) => {
207                for e in exprs {
208                    changes += self.transform_expr(e);
209                }
210            }
211            PureExpr::Struct { fields, .. } => {
212                for (_, e) in fields {
213                    changes += self.transform_expr(e);
214                }
215            }
216            PureExpr::Ref { expr: inner, .. } => {
217                changes += self.transform_expr(inner);
218            }
219            PureExpr::Return(Some(inner)) => {
220                changes += self.transform_expr(inner);
221            }
222            PureExpr::Try(inner) | PureExpr::Await(inner) => {
223                changes += self.transform_expr(inner);
224            }
225            _ => {}
226        }
227
228        changes
229    }
230
231    pub fn transform_block(&self, block: &mut PureBlock) -> usize {
232        let mut changes = 0;
233        for stmt in &mut block.stmts {
234            changes += self.transform_stmt(stmt);
235        }
236        changes
237    }
238
239    fn transform_stmt(&self, stmt: &mut PureStmt) -> usize {
240        match stmt {
241            PureStmt::Local { init: Some(e), .. } => self.transform_expr(e),
242            PureStmt::Semi(e) | PureStmt::Expr(e) => self.transform_expr(e),
243            _ => 0,
244        }
245    }
246}
247
248impl Mutation for MapUnwrapOrMutation {
249    fn describe(&self) -> String {
250        "Convert .map().unwrap_or() to .map_or()".to_string()
251    }
252
253    fn mutation_type(&self) -> &'static str {
254        "MapUnwrapOr"
255    }
256
257    fn box_clone(&self) -> Box<dyn Mutation> {
258        Box::new(self.clone())
259    }
260}
261
262#[cfg(test)]
263mod tests {
264    use super::*;
265
266    #[test]
267    fn test_is_map_unwrap_or_pattern() {
268        // opt.map(f).unwrap_or(default)
269        let expr = PureExpr::MethodCall {
270            receiver: Box::new(PureExpr::MethodCall {
271                receiver: Box::new(PureExpr::Path("opt".to_string())),
272                method: "map".to_string(),
273                turbofish: None,
274                args: vec![PureExpr::Path("f".to_string())],
275            }),
276            method: "unwrap_or".to_string(),
277            turbofish: None,
278            args: vec![PureExpr::Lit("0".to_string())],
279        };
280
281        let result = MapUnwrapOrMutation::is_map_unwrap_or_pattern(&expr);
282        assert!(result.is_some());
283        let (_, _, _, is_lazy) = result.unwrap();
284        assert!(!is_lazy);
285    }
286
287    #[test]
288    fn test_is_map_unwrap_or_else_pattern() {
289        // opt.map(f).unwrap_or_else(g)
290        let expr = PureExpr::MethodCall {
291            receiver: Box::new(PureExpr::MethodCall {
292                receiver: Box::new(PureExpr::Path("opt".to_string())),
293                method: "map".to_string(),
294                turbofish: None,
295                args: vec![PureExpr::Path("f".to_string())],
296            }),
297            method: "unwrap_or_else".to_string(),
298            turbofish: None,
299            args: vec![PureExpr::Path("g".to_string())],
300        };
301
302        let result = MapUnwrapOrMutation::is_map_unwrap_or_pattern(&expr);
303        assert!(result.is_some());
304        let (_, _, _, is_lazy) = result.unwrap();
305        assert!(is_lazy);
306    }
307
308    #[test]
309    fn test_is_map_or_none_pattern() {
310        // opt.map(f).unwrap_or(None)
311        let expr = PureExpr::MethodCall {
312            receiver: Box::new(PureExpr::MethodCall {
313                receiver: Box::new(PureExpr::Path("opt".to_string())),
314                method: "map".to_string(),
315                turbofish: None,
316                args: vec![PureExpr::Path("f".to_string())],
317            }),
318            method: "unwrap_or".to_string(),
319            turbofish: None,
320            args: vec![PureExpr::Path("None".to_string())],
321        };
322
323        assert!(MapUnwrapOrMutation::is_map_or_none_pattern(&expr).is_some());
324    }
325}