Skip to main content

ryo_mutations/idiom/
bool_simplify.rs

1//! BoolSimplifyMutation: Simplify boolean comparisons
2//!
3//! Transforms:
4//! - `x == true` → `x`
5//! - `x == false` → `!x`
6//! - `true == x` → `x`
7//! - `false == x` → `!x`
8//! - `x != true` → `!x`
9//! - `x != false` → `x`
10//!
11//! Corresponds to Clippy lint: `clippy::bool_comparison`
12
13use ryo_source::pure::{PureBlock, PureExpr, PureStmt};
14use ryo_symbol::SymbolId;
15
16use crate::Mutation;
17
18/// Simplify boolean comparisons to idiomatic Rust
19///
20/// # Example
21///
22/// ```rust,ignore
23/// use ryo_mutations::idiom::BoolSimplifyMutation;
24///
25/// let mutation = BoolSimplifyMutation::new();
26/// // Transforms: if x == true { ... }
27/// // Into:       if x { ... }
28/// ```
29#[derive(Debug, Clone, Default)]
30pub struct BoolSimplifyMutation {
31    /// Target function SymbolId. If None, applies to all functions.
32    pub target_fn: Option<SymbolId>,
33}
34
35impl BoolSimplifyMutation {
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 a boolean literal
47    fn is_bool_literal(expr: &PureExpr) -> Option<bool> {
48        match expr {
49            PureExpr::Lit(lit) => match lit.as_str() {
50                "true" => Some(true),
51                "false" => Some(false),
52                _ => None,
53            },
54            PureExpr::Path(path) => match path.as_str() {
55                "true" => Some(true),
56                "false" => Some(false),
57                _ => None,
58            },
59            _ => None,
60        }
61    }
62
63    /// Transform an expression, returns (new_expr, changes_count)
64    fn transform_expr(&self, expr: &mut PureExpr) -> usize {
65        let mut changes = 0;
66
67        // Check for comparison patterns: x == true, x == false, etc.
68        if let PureExpr::Binary { op, left, right } = expr {
69            let is_eq = op == "==";
70            let is_neq = op == "!=";
71
72            if is_eq || is_neq {
73                // Check left side for bool literal
74                if let Some(bool_val) = Self::is_bool_literal(left) {
75                    // Transform: true == x, false == x, true != x, false != x
76                    let other = std::mem::replace(
77                        right.as_mut(),
78                        PureExpr::Path("__placeholder".to_string()),
79                    );
80                    *expr = Self::simplify(other, bool_val, is_eq);
81                    return 1;
82                }
83
84                // Check right side for bool literal
85                if let Some(bool_val) = Self::is_bool_literal(right) {
86                    // Transform: x == true, x == false, x != true, x != false
87                    let other = std::mem::replace(
88                        left.as_mut(),
89                        PureExpr::Path("__placeholder".to_string()),
90                    );
91                    *expr = Self::simplify(other, bool_val, is_eq);
92                    return 1;
93                }
94            }
95        }
96
97        // Recursively transform sub-expressions
98        match expr {
99            PureExpr::Binary { left, right, .. } => {
100                changes += self.transform_expr(left);
101                changes += self.transform_expr(right);
102            }
103            PureExpr::Unary { expr: inner, .. } => {
104                changes += self.transform_expr(inner);
105            }
106            PureExpr::Call { func, args } => {
107                changes += self.transform_expr(func);
108                for arg in args {
109                    changes += self.transform_expr(arg);
110                }
111            }
112            PureExpr::MethodCall { receiver, args, .. } => {
113                changes += self.transform_expr(receiver);
114                for arg in args {
115                    changes += self.transform_expr(arg);
116                }
117            }
118            PureExpr::Field { expr: inner, .. } => {
119                changes += self.transform_expr(inner);
120            }
121            PureExpr::Index { expr: inner, index } => {
122                changes += self.transform_expr(inner);
123                changes += self.transform_expr(index);
124            }
125            PureExpr::Block { block, .. } => {
126                changes += self.transform_block(block);
127            }
128            PureExpr::If {
129                cond,
130                then_branch,
131                else_branch,
132            } => {
133                changes += self.transform_expr(cond);
134                changes += self.transform_block(then_branch);
135                if let Some(else_expr) = else_branch {
136                    changes += self.transform_expr(else_expr);
137                }
138            }
139            PureExpr::Match { expr: e, arms } => {
140                changes += self.transform_expr(e);
141                for arm in arms {
142                    changes += self.transform_expr(&mut arm.body);
143                }
144            }
145            PureExpr::Loop { body: block, .. } | PureExpr::While { body: block, .. } => {
146                changes += self.transform_block(block);
147            }
148            PureExpr::For {
149                expr: iter_expr,
150                body,
151                ..
152            } => {
153                changes += self.transform_expr(iter_expr);
154                changes += self.transform_block(body);
155            }
156            PureExpr::Closure { body, .. } => {
157                changes += self.transform_expr(body);
158            }
159            PureExpr::Tuple(exprs) | PureExpr::Array(exprs) => {
160                for e in exprs {
161                    changes += self.transform_expr(e);
162                }
163            }
164            PureExpr::Struct { fields, .. } => {
165                for (_, e) in fields {
166                    changes += self.transform_expr(e);
167                }
168            }
169            PureExpr::Ref { expr: inner, .. } => {
170                changes += self.transform_expr(inner);
171            }
172            PureExpr::Return(Some(inner)) => {
173                changes += self.transform_expr(inner);
174            }
175            PureExpr::Try(inner) | PureExpr::Await(inner) => {
176                changes += self.transform_expr(inner);
177            }
178            _ => {}
179        }
180
181        changes
182    }
183
184    /// Simplify a boolean comparison
185    fn simplify(expr: PureExpr, bool_val: bool, is_eq: bool) -> PureExpr {
186        // x == true => x
187        // x == false => !x
188        // x != true => !x
189        // x != false => x
190        let need_negate = (is_eq && !bool_val) || (!is_eq && bool_val);
191
192        if need_negate {
193            PureExpr::Unary {
194                op: "!".to_string(),
195                expr: Box::new(expr),
196            }
197        } else {
198            expr
199        }
200    }
201
202    pub fn transform_block(&self, block: &mut PureBlock) -> usize {
203        let mut changes = 0;
204        for stmt in &mut block.stmts {
205            changes += self.transform_stmt(stmt);
206        }
207        changes
208    }
209
210    fn transform_stmt(&self, stmt: &mut PureStmt) -> usize {
211        match stmt {
212            PureStmt::Local { init: Some(e), .. } => self.transform_expr(e),
213            PureStmt::Semi(e) | PureStmt::Expr(e) => self.transform_expr(e),
214            _ => 0,
215        }
216    }
217}
218
219impl Mutation for BoolSimplifyMutation {
220    fn describe(&self) -> String {
221        "Simplify boolean comparisons (x == true → x)".to_string()
222    }
223
224    fn mutation_type(&self) -> &'static str {
225        "BoolSimplify"
226    }
227
228    fn box_clone(&self) -> Box<dyn Mutation> {
229        Box::new(self.clone())
230    }
231}
232
233#[cfg(test)]
234mod tests {
235    use super::*;
236
237    #[test]
238    fn test_is_bool_literal() {
239        assert_eq!(
240            BoolSimplifyMutation::is_bool_literal(&PureExpr::Lit("true".to_string())),
241            Some(true)
242        );
243        assert_eq!(
244            BoolSimplifyMutation::is_bool_literal(&PureExpr::Lit("false".to_string())),
245            Some(false)
246        );
247        assert_eq!(
248            BoolSimplifyMutation::is_bool_literal(&PureExpr::Path("true".to_string())),
249            Some(true)
250        );
251        assert_eq!(
252            BoolSimplifyMutation::is_bool_literal(&PureExpr::Lit("42".to_string())),
253            None
254        );
255    }
256
257    #[test]
258    fn test_simplify_eq_true() {
259        // x == true => x
260        let expr = PureExpr::Path("x".to_string());
261        let result = BoolSimplifyMutation::simplify(expr, true, true);
262        assert!(matches!(result, PureExpr::Path(s) if s == "x"));
263    }
264
265    #[test]
266    fn test_simplify_eq_false() {
267        // x == false => !x
268        let expr = PureExpr::Path("x".to_string());
269        let result = BoolSimplifyMutation::simplify(expr, false, true);
270        assert!(matches!(result, PureExpr::Unary { op, .. } if op == "!"));
271    }
272
273    #[test]
274    fn test_simplify_neq_true() {
275        // x != true => !x
276        let expr = PureExpr::Path("x".to_string());
277        let result = BoolSimplifyMutation::simplify(expr, true, false);
278        assert!(matches!(result, PureExpr::Unary { op, .. } if op == "!"));
279    }
280
281    #[test]
282    fn test_simplify_neq_false() {
283        // x != false => x
284        let expr = PureExpr::Path("x".to_string());
285        let result = BoolSimplifyMutation::simplify(expr, false, false);
286        assert!(matches!(result, PureExpr::Path(s) if s == "x"));
287    }
288}