Skip to main content

ryo_mutations/idiom/
match_to_if_let.rs

1//! MatchToIfLetMutation: Convert match expressions to if let
2//!
3//! Transforms:
4//! - `match opt { Some(x) => body, None => {} }` → `if let Some(x) = opt { body }`
5//! - `match res { Ok(x) => body, Err(_) => {} }` → `if let Ok(x) = res { body }`
6//!
7//! Corresponds to Clippy lint: `clippy::single_match`
8
9use ryo_source::pure::{PureBlock, PureExpr, PureMatchArm, PurePattern, PureStmt};
10use ryo_symbol::SymbolId;
11
12use crate::Mutation;
13
14/// Convert simple match expressions to if let
15///
16/// # Example
17///
18/// ```rust,ignore
19/// use ryo_mutations::idiom::MatchToIfLetMutation;
20///
21/// let mutation = MatchToIfLetMutation::new();
22/// // Transforms:
23/// //   match opt {
24/// //       Some(x) => println!("{}", x),
25/// //       None => {}
26/// //   }
27/// // Into:
28/// //   if let Some(x) = opt {
29/// //       println!("{}", x);
30/// //   }
31/// ```
32#[derive(Debug, Clone, Default)]
33pub struct MatchToIfLetMutation {
34    /// Target function SymbolId. If None, applies to all functions.
35    pub target_fn: Option<SymbolId>,
36}
37
38impl MatchToIfLetMutation {
39    pub fn new() -> Self {
40        Self::default()
41    }
42
43    pub fn in_function(mut self, id: SymbolId) -> Self {
44        self.target_fn = Some(id);
45        self
46    }
47
48    /// Check if pattern is Some(x) and extract the binding name
49    fn is_some_pattern(pattern: &PurePattern) -> Option<String> {
50        match pattern {
51            PurePattern::Struct { path, fields, .. } => {
52                if (path == "Some" || path.ends_with("::Some")) && fields.len() == 1 {
53                    if let Some((_, PurePattern::Ident { name, .. })) = fields.first() {
54                        return Some(name.clone());
55                    }
56                }
57                None
58            }
59            _ => None,
60        }
61    }
62
63    /// Check if pattern is None
64    fn is_none_pattern(pattern: &PurePattern) -> bool {
65        match pattern {
66            PurePattern::Path(p) => p == "None" || p.ends_with("::None"),
67            PurePattern::Ident { name, .. } => name == "None",
68            _ => false,
69        }
70    }
71
72    /// Check if pattern is Ok(x) and extract the binding name
73    fn is_ok_pattern(pattern: &PurePattern) -> Option<String> {
74        match pattern {
75            PurePattern::Struct { path, fields, .. } => {
76                if (path == "Ok" || path.ends_with("::Ok")) && fields.len() == 1 {
77                    if let Some((_, PurePattern::Ident { name, .. })) = fields.first() {
78                        return Some(name.clone());
79                    }
80                }
81                None
82            }
83            _ => None,
84        }
85    }
86
87    /// Check if pattern is Err(_) or Err(e)
88    fn is_err_pattern(pattern: &PurePattern) -> bool {
89        match pattern {
90            PurePattern::Struct { path, fields, .. } => {
91                if (path == "Err" || path.ends_with("::Err")) && fields.len() == 1 {
92                    // Accept any binding including wildcard
93                    return true;
94                }
95                false
96            }
97            _ => false,
98        }
99    }
100
101    /// Check if body is empty (empty block or unit expression)
102    fn is_empty_body(expr: &PureExpr) -> bool {
103        match expr {
104            PureExpr::Block { block, .. } => block.stmts.is_empty(),
105            PureExpr::Tuple(elems) if elems.is_empty() => true, // Unit ()
106            PureExpr::Path(p) if p == "()" => true,
107            _ => false,
108        }
109    }
110
111    /// Try to convert a match expression to if let
112    fn try_convert_match(scrutinee: &PureExpr, arms: &[PureMatchArm]) -> Option<PureExpr> {
113        if arms.len() != 2 {
114            return None;
115        }
116
117        // Try pattern: Some(x) => body, None => {}
118        if let Some(var_name) = Self::is_some_pattern(&arms[0].pattern) {
119            if Self::is_none_pattern(&arms[1].pattern) && Self::is_empty_body(&arms[1].body) {
120                return Some(Self::create_if_let(
121                    scrutinee.clone(),
122                    "Some".to_string(),
123                    var_name,
124                    arms[0].body.clone(),
125                ));
126            }
127        }
128
129        // Try reversed: None => {}, Some(x) => body
130        if Self::is_none_pattern(&arms[0].pattern) && Self::is_empty_body(&arms[0].body) {
131            if let Some(var_name) = Self::is_some_pattern(&arms[1].pattern) {
132                return Some(Self::create_if_let(
133                    scrutinee.clone(),
134                    "Some".to_string(),
135                    var_name,
136                    arms[1].body.clone(),
137                ));
138            }
139        }
140
141        // Try pattern: Ok(x) => body, Err(_) => {}
142        if let Some(var_name) = Self::is_ok_pattern(&arms[0].pattern) {
143            if Self::is_err_pattern(&arms[1].pattern) && Self::is_empty_body(&arms[1].body) {
144                return Some(Self::create_if_let(
145                    scrutinee.clone(),
146                    "Ok".to_string(),
147                    var_name,
148                    arms[0].body.clone(),
149                ));
150            }
151        }
152
153        // Try reversed: Err(_) => {}, Ok(x) => body
154        if Self::is_err_pattern(&arms[0].pattern) && Self::is_empty_body(&arms[0].body) {
155            if let Some(var_name) = Self::is_ok_pattern(&arms[1].pattern) {
156                return Some(Self::create_if_let(
157                    scrutinee.clone(),
158                    "Ok".to_string(),
159                    var_name,
160                    arms[1].body.clone(),
161                ));
162            }
163        }
164
165        None
166    }
167
168    /// Create an if let expression
169    ///
170    /// `if let Some(x) = opt { body }` is represented as:
171    /// ```text
172    /// If {
173    ///     cond: Let { pattern: Some(x), expr: opt },
174    ///     then_branch: body,
175    ///     else_branch: None,
176    /// }
177    /// ```
178    fn create_if_let(
179        scrutinee: PureExpr,
180        variant: String,
181        var_name: String,
182        body: PureExpr,
183    ) -> PureExpr {
184        // Convert body to block if needed
185        let then_block = match body {
186            PureExpr::Block { block, .. } => block,
187            other => PureBlock {
188                stmts: vec![PureStmt::Expr(other)],
189            },
190        };
191
192        // Create the let expression: `let Some(x) = scrutinee`
193        let let_expr = PureExpr::Let {
194            pattern: PurePattern::Struct {
195                path: variant,
196                fields: vec![(
197                    "0".to_string(),
198                    PurePattern::Ident {
199                        name: var_name,
200                        is_mut: false,
201                        by_ref: false,
202                    },
203                )],
204                rest: false,
205            },
206            expr: Box::new(scrutinee),
207        };
208
209        // Create the if expression with the let as condition
210        PureExpr::If {
211            cond: Box::new(let_expr),
212            then_branch: then_block,
213            else_branch: None,
214        }
215    }
216
217    /// Transform expressions, returns changes count
218    fn transform_expr(&self, expr: &mut PureExpr) -> usize {
219        let mut changes = 0;
220
221        // Check for match pattern
222        if let PureExpr::Match {
223            expr: scrutinee,
224            arms,
225        } = expr
226        {
227            if let Some(if_let) = Self::try_convert_match(scrutinee, arms) {
228                *expr = if_let;
229                return 1;
230            }
231        }
232
233        // Recursively transform sub-expressions
234        match expr {
235            PureExpr::Binary { left, right, .. } => {
236                changes += self.transform_expr(left);
237                changes += self.transform_expr(right);
238            }
239            PureExpr::Unary { expr: inner, .. } => {
240                changes += self.transform_expr(inner);
241            }
242            PureExpr::Call { func, args } => {
243                changes += self.transform_expr(func);
244                for arg in args {
245                    changes += self.transform_expr(arg);
246                }
247            }
248            PureExpr::MethodCall { receiver, args, .. } => {
249                changes += self.transform_expr(receiver);
250                for arg in args {
251                    changes += self.transform_expr(arg);
252                }
253            }
254            PureExpr::Block { block, .. } => {
255                changes += self.transform_block(block);
256            }
257            PureExpr::If {
258                cond,
259                then_branch,
260                else_branch,
261            } => {
262                changes += self.transform_expr(cond);
263                changes += self.transform_block(then_branch);
264                if let Some(else_expr) = else_branch {
265                    changes += self.transform_expr(else_expr);
266                }
267            }
268            PureExpr::Match { expr: e, arms } => {
269                changes += self.transform_expr(e);
270                for arm in arms {
271                    changes += self.transform_expr(&mut arm.body);
272                }
273            }
274            PureExpr::Loop { body: block, .. } | PureExpr::While { body: block, .. } => {
275                changes += self.transform_block(block);
276            }
277            PureExpr::For {
278                expr: iter_expr,
279                body,
280                ..
281            } => {
282                changes += self.transform_expr(iter_expr);
283                changes += self.transform_block(body);
284            }
285            PureExpr::Closure { body, .. } => {
286                changes += self.transform_expr(body);
287            }
288            _ => {}
289        }
290
291        changes
292    }
293
294    pub fn transform_block(&self, block: &mut PureBlock) -> usize {
295        let mut changes = 0;
296        for stmt in &mut block.stmts {
297            changes += self.transform_stmt(stmt);
298        }
299        changes
300    }
301
302    fn transform_stmt(&self, stmt: &mut PureStmt) -> usize {
303        match stmt {
304            PureStmt::Local { init: Some(e), .. } => self.transform_expr(e),
305            PureStmt::Semi(e) | PureStmt::Expr(e) => self.transform_expr(e),
306            _ => 0,
307        }
308    }
309}
310
311impl Mutation for MatchToIfLetMutation {
312    fn describe(&self) -> String {
313        "Convert match to if let".to_string()
314    }
315
316    fn mutation_type(&self) -> &'static str {
317        "MatchToIfLet"
318    }
319
320    fn box_clone(&self) -> Box<dyn Mutation> {
321        Box::new(self.clone())
322    }
323}
324
325#[cfg(test)]
326mod tests {
327    use super::*;
328
329    #[test]
330    fn test_is_some_pattern() {
331        let pattern = PurePattern::Struct {
332            path: "Some".to_string(),
333            fields: vec![(
334                "0".to_string(),
335                PurePattern::Ident {
336                    name: "x".to_string(),
337                    is_mut: false,
338                    by_ref: false,
339                },
340            )],
341            rest: false,
342        };
343        assert_eq!(
344            MatchToIfLetMutation::is_some_pattern(&pattern),
345            Some("x".to_string())
346        );
347    }
348
349    #[test]
350    fn test_is_none_pattern_ident() {
351        let pattern = PurePattern::Ident {
352            name: "None".to_string(),
353            is_mut: false,
354            by_ref: false,
355        };
356        assert!(MatchToIfLetMutation::is_none_pattern(&pattern));
357    }
358
359    #[test]
360    fn test_is_empty_body() {
361        let empty_block = PureExpr::Block {
362            label: None,
363            block: PureBlock { stmts: vec![] },
364        };
365        assert!(MatchToIfLetMutation::is_empty_body(&empty_block));
366    }
367}