ryo_mutations/idiom/
comparison_to_method.rs1use ryo_source::pure::{PureBlock, PureExpr, PureStmt};
14use ryo_symbol::SymbolId;
15
16use crate::Mutation;
17
18#[derive(Debug, Clone, Default)]
30pub struct ComparisonToMethodMutation {
31 pub target_fn: Option<SymbolId>,
33}
34
35impl ComparisonToMethodMutation {
36 pub fn new() -> Self {
37 Self::default()
38 }
39
40 pub fn in_function(mut self, id: SymbolId) -> Self {
42 self.target_fn = Some(id);
43 self
44 }
45
46 fn is_empty_string(expr: &PureExpr) -> bool {
48 match expr {
49 PureExpr::Lit(lit) => lit == "\"\"",
50 _ => false,
51 }
52 }
53
54 fn is_zero(expr: &PureExpr) -> bool {
56 match expr {
57 PureExpr::Lit(lit) => lit == "0" || lit == "0usize" || lit == "0_usize",
58 _ => false,
59 }
60 }
61
62 fn is_len_call(expr: &PureExpr) -> Option<&PureExpr> {
64 match expr {
65 PureExpr::MethodCall {
66 receiver,
67 method,
68 args,
69 ..
70 } if method == "len" && args.is_empty() => Some(receiver.as_ref()),
71 _ => None,
72 }
73 }
74
75 fn transform_expr(&self, expr: &mut PureExpr) -> usize {
77 let mut changes = 0;
78
79 if let PureExpr::Binary { op, left, right } = expr {
81 let is_eq = op == "==";
82 let is_neq = op == "!=";
83 let is_gt = op == ">";
84 let is_lt = op == "<";
85
86 if is_eq || is_neq {
87 let (target, is_empty_check) = if Self::is_empty_string(left) {
89 (right.as_ref(), true)
90 } else if Self::is_empty_string(right) {
91 (left.as_ref(), true)
92 } else {
93 (left.as_ref(), false)
94 };
95
96 if is_empty_check {
97 let target = target.clone();
98 let is_empty_call = PureExpr::MethodCall {
99 receiver: Box::new(target),
100 method: "is_empty".to_string(),
101 turbofish: None,
102 args: vec![],
103 };
104
105 *expr = if is_eq {
106 is_empty_call
107 } else {
108 PureExpr::Unary {
109 op: "!".to_string(),
110 expr: Box::new(is_empty_call),
111 }
112 };
113
114 return 1;
115 }
116
117 if let Some(receiver) = Self::is_len_call(left) {
119 if Self::is_zero(right) {
120 let is_empty_call = PureExpr::MethodCall {
121 receiver: Box::new(receiver.clone()),
122 method: "is_empty".to_string(),
123 turbofish: None,
124 args: vec![],
125 };
126
127 *expr = if is_eq {
128 is_empty_call
129 } else {
130 PureExpr::Unary {
131 op: "!".to_string(),
132 expr: Box::new(is_empty_call),
133 }
134 };
135
136 return 1;
137 }
138 }
139
140 if let Some(receiver) = Self::is_len_call(right) {
142 if Self::is_zero(left) {
143 let is_empty_call = PureExpr::MethodCall {
144 receiver: Box::new(receiver.clone()),
145 method: "is_empty".to_string(),
146 turbofish: None,
147 args: vec![],
148 };
149
150 *expr = if is_eq {
151 is_empty_call
152 } else {
153 PureExpr::Unary {
154 op: "!".to_string(),
155 expr: Box::new(is_empty_call),
156 }
157 };
158
159 return 1;
160 }
161 }
162 }
163
164 if is_gt {
166 if let Some(receiver) = Self::is_len_call(left) {
167 if Self::is_zero(right) {
168 let is_empty_call = PureExpr::MethodCall {
169 receiver: Box::new(receiver.clone()),
170 method: "is_empty".to_string(),
171 turbofish: None,
172 args: vec![],
173 };
174
175 *expr = PureExpr::Unary {
176 op: "!".to_string(),
177 expr: Box::new(is_empty_call),
178 };
179
180 return 1;
181 }
182 }
183 }
184
185 if is_lt {
187 if let Some(receiver) = Self::is_len_call(right) {
188 if Self::is_zero(left) {
189 let is_empty_call = PureExpr::MethodCall {
190 receiver: Box::new(receiver.clone()),
191 method: "is_empty".to_string(),
192 turbofish: None,
193 args: vec![],
194 };
195
196 *expr = PureExpr::Unary {
197 op: "!".to_string(),
198 expr: Box::new(is_empty_call),
199 };
200
201 return 1;
202 }
203 }
204 }
205 }
206
207 match expr {
209 PureExpr::Binary { left, right, .. } => {
210 changes += self.transform_expr(left);
211 changes += self.transform_expr(right);
212 }
213 PureExpr::Unary { expr: inner, .. } => {
214 changes += self.transform_expr(inner);
215 }
216 PureExpr::Call { func, args } => {
217 changes += self.transform_expr(func);
218 for arg in args {
219 changes += self.transform_expr(arg);
220 }
221 }
222 PureExpr::MethodCall { receiver, args, .. } => {
223 changes += self.transform_expr(receiver);
224 for arg in args {
225 changes += self.transform_expr(arg);
226 }
227 }
228 PureExpr::Block { block, .. } => {
229 changes += self.transform_block(block);
230 }
231 PureExpr::If {
232 cond,
233 then_branch,
234 else_branch,
235 } => {
236 changes += self.transform_expr(cond);
237 changes += self.transform_block(then_branch);
238 if let Some(else_expr) = else_branch {
239 changes += self.transform_expr(else_expr);
240 }
241 }
242 PureExpr::Match { expr: e, arms } => {
243 changes += self.transform_expr(e);
244 for arm in arms {
245 changes += self.transform_expr(&mut arm.body);
246 }
247 }
248 PureExpr::Loop { body: block, .. } | PureExpr::While { body: block, .. } => {
249 changes += self.transform_block(block);
250 }
251 PureExpr::For {
252 expr: iter_expr,
253 body,
254 ..
255 } => {
256 changes += self.transform_expr(iter_expr);
257 changes += self.transform_block(body);
258 }
259 PureExpr::Closure { body, .. } => {
260 changes += self.transform_expr(body);
261 }
262 _ => {}
263 }
264
265 changes
266 }
267
268 pub fn transform_block(&self, block: &mut PureBlock) -> usize {
269 let mut changes = 0;
270 for stmt in &mut block.stmts {
271 changes += self.transform_stmt(stmt);
272 }
273 changes
274 }
275
276 fn transform_stmt(&self, stmt: &mut PureStmt) -> usize {
277 match stmt {
278 PureStmt::Local { init: Some(e), .. } => self.transform_expr(e),
279 PureStmt::Semi(e) | PureStmt::Expr(e) => self.transform_expr(e),
280 _ => 0,
281 }
282 }
283}
284
285impl Mutation for ComparisonToMethodMutation {
286 fn describe(&self) -> String {
287 "Convert comparisons to method calls (s == \"\" → s.is_empty())".to_string()
288 }
289
290 fn mutation_type(&self) -> &'static str {
291 "ComparisonToMethod"
292 }
293
294 fn box_clone(&self) -> Box<dyn Mutation> {
295 Box::new(self.clone())
296 }
297}
298
299#[cfg(test)]
300mod tests {
301 use super::*;
302
303 #[test]
304 fn test_is_empty_string() {
305 assert!(ComparisonToMethodMutation::is_empty_string(&PureExpr::Lit(
306 "\"\"".to_string()
307 )));
308 assert!(!ComparisonToMethodMutation::is_empty_string(
309 &PureExpr::Lit("\"hello\"".to_string())
310 ));
311 }
312
313 #[test]
314 fn test_is_zero() {
315 assert!(ComparisonToMethodMutation::is_zero(&PureExpr::Lit(
316 "0".to_string()
317 )));
318 assert!(ComparisonToMethodMutation::is_zero(&PureExpr::Lit(
319 "0usize".to_string()
320 )));
321 assert!(!ComparisonToMethodMutation::is_zero(&PureExpr::Lit(
322 "1".to_string()
323 )));
324 }
325
326 #[test]
327 fn test_is_len_call() {
328 let len_call = PureExpr::MethodCall {
329 receiver: Box::new(PureExpr::Path("v".to_string())),
330 method: "len".to_string(),
331 turbofish: None,
332 args: vec![],
333 };
334 assert!(ComparisonToMethodMutation::is_len_call(&len_call).is_some());
335
336 let not_len = PureExpr::MethodCall {
337 receiver: Box::new(PureExpr::Path("v".to_string())),
338 method: "size".to_string(),
339 turbofish: None,
340 args: vec![],
341 };
342 assert!(ComparisonToMethodMutation::is_len_call(¬_len).is_none());
343 }
344}