Skip to main content

ryo_mutations/idiom/
collapsible_if.rs

1//! CollapsibleIfMutation: Merge nested if statements
2//!
3//! Transforms:
4//! - `if a { if b { body } }` → `if a && b { body }`
5//!
6//! Corresponds to Clippy lint: `clippy::collapsible_if`
7
8use ryo_source::pure::{PureBlock, PureExpr, PureStmt};
9use ryo_symbol::SymbolId;
10
11use crate::Mutation;
12
13/// Merge nested if statements using && operator
14///
15/// # Example
16///
17/// ```rust,ignore
18/// use ryo_mutations::idiom::CollapsibleIfMutation;
19///
20/// let mutation = CollapsibleIfMutation::new();
21/// // Transforms:
22/// //   if a {
23/// //       if b {
24/// //           do_something();
25/// //       }
26/// //   }
27/// // Into:
28/// //   if a && b {
29/// //       do_something();
30/// //   }
31/// ```
32#[derive(Debug, Clone, Default)]
33pub struct CollapsibleIfMutation {
34    /// Target function SymbolId. If None, applies to all functions.
35    pub target_fn: Option<SymbolId>,
36}
37
38impl CollapsibleIfMutation {
39    pub fn new() -> Self {
40        Self::default()
41    }
42
43    /// Only apply in a specific function
44    pub fn in_function(mut self, id: SymbolId) -> Self {
45        self.target_fn = Some(id);
46        self
47    }
48
49    /// Check if a block contains only a single if statement with no else
50    fn is_single_if_block(block: &PureBlock) -> Option<(&PureExpr, &PureBlock)> {
51        if block.stmts.len() != 1 {
52            return None;
53        }
54
55        let expr = match &block.stmts[0] {
56            PureStmt::Expr(e) | PureStmt::Semi(e) => e,
57            _ => return None,
58        };
59
60        match expr {
61            PureExpr::If {
62                cond,
63                then_branch,
64                else_branch: None,
65            } => Some((cond.as_ref(), then_branch)),
66            _ => None,
67        }
68    }
69
70    /// Transform an expression, returns changes count
71    fn transform_expr(&self, expr: &mut PureExpr) -> usize {
72        let mut changes = 0;
73
74        // Check for collapsible if pattern
75        if let PureExpr::If {
76            cond,
77            then_branch,
78            else_branch: None,
79        } = expr
80        {
81            // Recursively transform inner first
82            changes += self.transform_expr(cond);
83            changes += self.transform_block(then_branch);
84
85            // Check if then_branch is a single if with no else
86            if let Some((inner_cond, inner_body)) = Self::is_single_if_block(then_branch) {
87                // Collapse: if a { if b { body } } => if a && b { body }
88                let outer_cond =
89                    std::mem::replace(cond.as_mut(), PureExpr::Path("__placeholder".to_string()));
90                let inner_cond = inner_cond.clone();
91                let inner_body = inner_body.clone();
92
93                let new_cond = PureExpr::Binary {
94                    op: "&&".to_string(),
95                    left: Box::new(outer_cond),
96                    right: Box::new(inner_cond),
97                };
98
99                *expr = PureExpr::If {
100                    cond: Box::new(new_cond),
101                    then_branch: inner_body,
102                    else_branch: None,
103                };
104
105                return changes + 1;
106            }
107        }
108
109        // Recursively transform sub-expressions
110        match expr {
111            PureExpr::Binary { left, right, .. } => {
112                changes += self.transform_expr(left);
113                changes += self.transform_expr(right);
114            }
115            PureExpr::Unary { expr: inner, .. } => {
116                changes += self.transform_expr(inner);
117            }
118            PureExpr::Call { func, args } => {
119                changes += self.transform_expr(func);
120                for arg in args {
121                    changes += self.transform_expr(arg);
122                }
123            }
124            PureExpr::MethodCall { receiver, args, .. } => {
125                changes += self.transform_expr(receiver);
126                for arg in args {
127                    changes += self.transform_expr(arg);
128                }
129            }
130            PureExpr::Block { block, .. } => {
131                changes += self.transform_block(block);
132            }
133            PureExpr::If {
134                cond,
135                then_branch,
136                else_branch,
137            } => {
138                changes += self.transform_expr(cond);
139                changes += self.transform_block(then_branch);
140                if let Some(else_expr) = else_branch {
141                    changes += self.transform_expr(else_expr);
142                }
143            }
144            PureExpr::Match { expr: e, arms } => {
145                changes += self.transform_expr(e);
146                for arm in arms {
147                    changes += self.transform_expr(&mut arm.body);
148                }
149            }
150            PureExpr::Loop { body: block, .. } | PureExpr::While { body: block, .. } => {
151                changes += self.transform_block(block);
152            }
153            PureExpr::For {
154                expr: iter_expr,
155                body,
156                ..
157            } => {
158                changes += self.transform_expr(iter_expr);
159                changes += self.transform_block(body);
160            }
161            PureExpr::Closure { body, .. } => {
162                changes += self.transform_expr(body);
163            }
164            _ => {}
165        }
166
167        changes
168    }
169
170    pub fn transform_block(&self, block: &mut PureBlock) -> usize {
171        let mut changes = 0;
172        for stmt in &mut block.stmts {
173            changes += self.transform_stmt(stmt);
174        }
175        changes
176    }
177
178    fn transform_stmt(&self, stmt: &mut PureStmt) -> usize {
179        match stmt {
180            PureStmt::Local { init: Some(e), .. } => self.transform_expr(e),
181            PureStmt::Semi(e) | PureStmt::Expr(e) => self.transform_expr(e),
182            _ => 0,
183        }
184    }
185}
186
187impl Mutation for CollapsibleIfMutation {
188    fn describe(&self) -> String {
189        "Collapse nested if statements (if a { if b { } } → if a && b { })".to_string()
190    }
191
192    fn mutation_type(&self) -> &'static str {
193        "CollapsibleIf"
194    }
195
196    fn box_clone(&self) -> Box<dyn Mutation> {
197        Box::new(self.clone())
198    }
199}
200
201#[cfg(test)]
202mod tests {
203    use super::*;
204
205    #[test]
206    fn test_is_single_if_block() {
207        // Single if with no else
208        let block = PureBlock {
209            stmts: vec![PureStmt::Expr(PureExpr::If {
210                cond: Box::new(PureExpr::Path("b".to_string())),
211                then_branch: PureBlock { stmts: vec![] },
212                else_branch: None,
213            })],
214        };
215        assert!(CollapsibleIfMutation::is_single_if_block(&block).is_some());
216
217        // If with else - not collapsible
218        let block2 = PureBlock {
219            stmts: vec![PureStmt::Expr(PureExpr::If {
220                cond: Box::new(PureExpr::Path("b".to_string())),
221                then_branch: PureBlock { stmts: vec![] },
222                else_branch: Some(Box::new(PureExpr::Block {
223                    label: None,
224                    block: PureBlock { stmts: vec![] },
225                })),
226            })],
227        };
228        assert!(CollapsibleIfMutation::is_single_if_block(&block2).is_none());
229
230        // Multiple statements
231        let block3 = PureBlock {
232            stmts: vec![
233                PureStmt::Expr(PureExpr::Path("x".to_string())),
234                PureStmt::Expr(PureExpr::Path("y".to_string())),
235            ],
236        };
237        assert!(CollapsibleIfMutation::is_single_if_block(&block3).is_none());
238    }
239}