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                    },
202                )],
203                rest: false,
204            },
205            expr: Box::new(scrutinee),
206        };
207
208        // Create the if expression with the let as condition
209        PureExpr::If {
210            cond: Box::new(let_expr),
211            then_branch: then_block,
212            else_branch: None,
213        }
214    }
215
216    /// Transform expressions, returns changes count
217    fn transform_expr(&self, expr: &mut PureExpr) -> usize {
218        let mut changes = 0;
219
220        // Check for match pattern
221        if let PureExpr::Match {
222            expr: scrutinee,
223            arms,
224        } = expr
225        {
226            if let Some(if_let) = Self::try_convert_match(scrutinee, arms) {
227                *expr = if_let;
228                return 1;
229            }
230        }
231
232        // Recursively transform sub-expressions
233        match expr {
234            PureExpr::Binary { left, right, .. } => {
235                changes += self.transform_expr(left);
236                changes += self.transform_expr(right);
237            }
238            PureExpr::Unary { expr: inner, .. } => {
239                changes += self.transform_expr(inner);
240            }
241            PureExpr::Call { func, args } => {
242                changes += self.transform_expr(func);
243                for arg in args {
244                    changes += self.transform_expr(arg);
245                }
246            }
247            PureExpr::MethodCall { receiver, args, .. } => {
248                changes += self.transform_expr(receiver);
249                for arg in args {
250                    changes += self.transform_expr(arg);
251                }
252            }
253            PureExpr::Block { block, .. } => {
254                changes += self.transform_block(block);
255            }
256            PureExpr::If {
257                cond,
258                then_branch,
259                else_branch,
260            } => {
261                changes += self.transform_expr(cond);
262                changes += self.transform_block(then_branch);
263                if let Some(else_expr) = else_branch {
264                    changes += self.transform_expr(else_expr);
265                }
266            }
267            PureExpr::Match { expr: e, arms } => {
268                changes += self.transform_expr(e);
269                for arm in arms {
270                    changes += self.transform_expr(&mut arm.body);
271                }
272            }
273            PureExpr::Loop { body: block, .. } | PureExpr::While { body: block, .. } => {
274                changes += self.transform_block(block);
275            }
276            PureExpr::For {
277                expr: iter_expr,
278                body,
279                ..
280            } => {
281                changes += self.transform_expr(iter_expr);
282                changes += self.transform_block(body);
283            }
284            PureExpr::Closure { body, .. } => {
285                changes += self.transform_expr(body);
286            }
287            _ => {}
288        }
289
290        changes
291    }
292
293    pub fn transform_block(&self, block: &mut PureBlock) -> usize {
294        let mut changes = 0;
295        for stmt in &mut block.stmts {
296            changes += self.transform_stmt(stmt);
297        }
298        changes
299    }
300
301    fn transform_stmt(&self, stmt: &mut PureStmt) -> usize {
302        match stmt {
303            PureStmt::Local { init: Some(e), .. } => self.transform_expr(e),
304            PureStmt::Semi(e) | PureStmt::Expr(e) => self.transform_expr(e),
305            _ => 0,
306        }
307    }
308}
309
310impl Mutation for MatchToIfLetMutation {
311    fn describe(&self) -> String {
312        "Convert match to if let".to_string()
313    }
314
315    fn mutation_type(&self) -> &'static str {
316        "MatchToIfLet"
317    }
318
319    fn box_clone(&self) -> Box<dyn Mutation> {
320        Box::new(self.clone())
321    }
322}
323
324#[cfg(test)]
325mod tests {
326    use super::*;
327
328    #[test]
329    fn test_is_some_pattern() {
330        let pattern = PurePattern::Struct {
331            path: "Some".to_string(),
332            fields: vec![(
333                "0".to_string(),
334                PurePattern::Ident {
335                    name: "x".to_string(),
336                    is_mut: false,
337                },
338            )],
339            rest: false,
340        };
341        assert_eq!(
342            MatchToIfLetMutation::is_some_pattern(&pattern),
343            Some("x".to_string())
344        );
345    }
346
347    #[test]
348    fn test_is_none_pattern_ident() {
349        let pattern = PurePattern::Ident {
350            name: "None".to_string(),
351            is_mut: false,
352        };
353        assert!(MatchToIfLetMutation::is_none_pattern(&pattern));
354    }
355
356    #[test]
357    fn test_is_empty_body() {
358        let empty_block = PureExpr::Block {
359            label: None,
360            block: PureBlock { stmts: vec![] },
361        };
362        assert!(MatchToIfLetMutation::is_empty_body(&empty_block));
363    }
364}