Skip to main content

ryo_mutations/idiom/
comparison_to_method.rs

1//! ComparisonToMethodMutation: Convert comparisons to idiomatic method calls
2//!
3//! Transforms:
4//! - `s == ""` → `s.is_empty()`
5//! - `s != ""` → `!s.is_empty()`
6//! - `v.len() == 0` → `v.is_empty()`
7//! - `v.len() != 0` → `!v.is_empty()`
8//! - `v.len() > 0` → `!v.is_empty()`
9//! - `ptr == std::ptr::null()` → `ptr.is_null()` (planned)
10//!
11//! Corresponds to Clippy lints: `clippy::comparison_to_empty`, `clippy::len_zero`
12
13use ryo_source::pure::{PureBlock, PureExpr, PureStmt};
14use ryo_symbol::SymbolId;
15
16use crate::Mutation;
17
18/// Convert comparisons to idiomatic method calls
19///
20/// # Example
21///
22/// ```rust,ignore
23/// use ryo_mutations::idiom::ComparisonToMethodMutation;
24///
25/// let mutation = ComparisonToMethodMutation::new();
26/// // Transforms: if s == "" { ... }
27/// // Into:       if s.is_empty() { ... }
28/// ```
29#[derive(Debug, Clone, Default)]
30pub struct ComparisonToMethodMutation {
31    /// Target function SymbolId. If None, applies to all functions.
32    pub target_fn: Option<SymbolId>,
33}
34
35impl ComparisonToMethodMutation {
36    pub fn new() -> Self {
37        Self::default()
38    }
39
40    /// Only apply in a specific function
41    pub fn in_function(mut self, id: SymbolId) -> Self {
42        self.target_fn = Some(id);
43        self
44    }
45
46    /// Check if expression is an empty string literal
47    fn is_empty_string(expr: &PureExpr) -> bool {
48        match expr {
49            PureExpr::Lit(lit) => lit == "\"\"",
50            _ => false,
51        }
52    }
53
54    /// Check if expression is a zero literal
55    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    /// Check if expression is a .len() call
63    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    /// Transform an expression, returns changes count
76    fn transform_expr(&self, expr: &mut PureExpr) -> usize {
77        let mut changes = 0;
78
79        // Pattern: x == "" or "" == x
80        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                // Check for empty string comparison
88                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                // Check for len() == 0 or len() != 0
118                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                // Check for 0 == len() or 0 != len()
141                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            // Check for len() > 0 (not empty)
165            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            // Check for 0 < len() (not empty)
186            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        // Recursively transform sub-expressions
208        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(&not_len).is_none());
343    }
344}