ryo_mutations/idiom/
collapsible_if.rs1use ryo_source::pure::{PureBlock, PureExpr, PureStmt};
9use ryo_symbol::SymbolId;
10
11use crate::Mutation;
12
13#[derive(Debug, Clone, Default)]
33pub struct CollapsibleIfMutation {
34 pub target_fn: Option<SymbolId>,
36}
37
38impl CollapsibleIfMutation {
39 pub fn new() -> Self {
40 Self::default()
41 }
42
43 pub fn in_function(mut self, id: SymbolId) -> Self {
45 self.target_fn = Some(id);
46 self
47 }
48
49 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 fn transform_expr(&self, expr: &mut PureExpr) -> usize {
72 let mut changes = 0;
73
74 if let PureExpr::If {
76 cond,
77 then_branch,
78 else_branch: None,
79 } = expr
80 {
81 changes += self.transform_expr(cond);
83 changes += self.transform_block(then_branch);
84
85 if let Some((inner_cond, inner_body)) = Self::is_single_if_block(then_branch) {
87 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 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 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 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 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}