Skip to main content

ryo_mutations/idiom/
assign_op.rs

1//! AssignOpMutation: Convert assignments to compound operators
2//!
3//! Transforms:
4//! - `a = a + b` → `a += b`
5//! - `a = a - b` → `a -= b`
6//! - `a = a * b` → `a *= b`
7//! - `a = a / b` → `a /= b`
8//! - `a = a % b` → `a %= b`
9//! - `a = a & b` → `a &= b`
10//! - `a = a | b` → `a |= b`
11//! - `a = a ^ b` → `a ^= b`
12//! - `a = a << b` → `a <<= b`
13//! - `a = a >> b` → `a >>= b`
14//!
15//! Corresponds to Clippy lint: `clippy::assign_op_pattern`
16
17use ryo_source::pure::{PureBlock, PureExpr, PureStmt};
18use ryo_symbol::SymbolId;
19
20use crate::Mutation;
21
22/// Convert assignments to compound assignment operators
23///
24/// # Example
25///
26/// ```rust,ignore
27/// use ryo_mutations::idiom::AssignOpMutation;
28///
29/// let mutation = AssignOpMutation::new();
30/// // Transforms: a = a + 1;
31/// // Into:       a += 1;
32/// ```
33#[derive(Debug, Clone, Default)]
34pub struct AssignOpMutation {
35    /// Target function SymbolId. If None, applies to all functions.
36    pub target_fn: Option<SymbolId>,
37}
38
39impl AssignOpMutation {
40    pub fn new() -> Self {
41        Self::default()
42    }
43
44    /// Only apply in a specific function
45    pub fn in_function(mut self, id: SymbolId) -> Self {
46        self.target_fn = Some(id);
47        self
48    }
49
50    /// Get compound operator for binary operator
51    fn compound_op(op: &str) -> Option<&'static str> {
52        match op {
53            "+" => Some("+="),
54            "-" => Some("-="),
55            "*" => Some("*="),
56            "/" => Some("/="),
57            "%" => Some("%="),
58            "&" => Some("&="),
59            "|" => Some("|="),
60            "^" => Some("^="),
61            "<<" => Some("<<="),
62            ">>" => Some(">>="),
63            _ => None,
64        }
65    }
66
67    /// Check if two expressions are structurally equivalent
68    fn expr_eq(a: &PureExpr, b: &PureExpr) -> bool {
69        match (a, b) {
70            (PureExpr::Path(pa), PureExpr::Path(pb)) => pa == pb,
71            (
72                PureExpr::Field {
73                    expr: ea,
74                    field: fa,
75                },
76                PureExpr::Field {
77                    expr: eb,
78                    field: fb,
79                },
80            ) => fa == fb && Self::expr_eq(ea, eb),
81            (
82                PureExpr::Index {
83                    expr: ea,
84                    index: ia,
85                },
86                PureExpr::Index {
87                    expr: eb,
88                    index: ib,
89                },
90            ) => Self::expr_eq(ea, eb) && Self::expr_eq(ia, ib),
91            // Support dereference: *x == *x
92            (PureExpr::Unary { op: opa, expr: ea }, PureExpr::Unary { op: opb, expr: eb }) => {
93                opa == opb && Self::expr_eq(ea, eb)
94            }
95            // Support reference: &x == &x, &mut x == &mut x
96            (
97                PureExpr::Ref {
98                    is_mut: ma,
99                    expr: ea,
100                },
101                PureExpr::Ref {
102                    is_mut: mb,
103                    expr: eb,
104                },
105            ) => ma == mb && Self::expr_eq(ea, eb),
106            _ => false,
107        }
108    }
109
110    /// Transform statements in a block
111    pub fn transform_block(&self, block: &mut PureBlock) -> usize {
112        let mut changes = 0;
113
114        for stmt in &mut block.stmts {
115            changes += self.transform_stmt(stmt);
116        }
117
118        changes
119    }
120
121    /// Transform a single statement
122    fn transform_stmt(&self, stmt: &mut PureStmt) -> usize {
123        match stmt {
124            PureStmt::Semi(expr) | PureStmt::Expr(expr) => {
125                // Check for assignment pattern: target = target op rhs
126                if let PureExpr::Binary { op, left, right } = expr {
127                    if op == "=" {
128                        // Check if right side is: left op something
129                        if let PureExpr::Binary {
130                            op: bin_op,
131                            left: bin_left,
132                            right: bin_right,
133                        } = right.as_ref()
134                        {
135                            if Self::expr_eq(left, bin_left) {
136                                if let Some(compound) = Self::compound_op(bin_op) {
137                                    // Transform: a = a + b => a += b
138                                    let target = std::mem::replace(
139                                        left.as_mut(),
140                                        PureExpr::Path("__placeholder".to_string()),
141                                    );
142                                    let rhs = bin_right.as_ref().clone();
143
144                                    *expr = PureExpr::Binary {
145                                        op: compound.to_string(),
146                                        left: Box::new(target),
147                                        right: Box::new(rhs),
148                                    };
149
150                                    return 1;
151                                }
152                            }
153                        }
154                    }
155                }
156
157                // Recursively transform expressions
158                self.transform_expr(expr)
159            }
160            PureStmt::Local { init: Some(e), .. } => self.transform_expr(e),
161            _ => 0,
162        }
163    }
164
165    /// Transform expressions (for nested blocks/closures)
166    fn transform_expr(&self, expr: &mut PureExpr) -> usize {
167        let mut changes = 0;
168
169        match expr {
170            PureExpr::Block { block, .. } => {
171                changes += self.transform_block(block);
172            }
173            PureExpr::If {
174                cond,
175                then_branch,
176                else_branch,
177            } => {
178                changes += self.transform_expr(cond);
179                changes += self.transform_block(then_branch);
180                if let Some(else_expr) = else_branch {
181                    changes += self.transform_expr(else_expr);
182                }
183            }
184            PureExpr::Match { expr: e, arms } => {
185                changes += self.transform_expr(e);
186                for arm in arms {
187                    changes += self.transform_expr(&mut arm.body);
188                }
189            }
190            PureExpr::Loop { body: block, .. } | PureExpr::While { body: block, .. } => {
191                changes += self.transform_block(block);
192            }
193            PureExpr::For { body, .. } => {
194                changes += self.transform_block(body);
195            }
196            PureExpr::Closure { body, .. } => {
197                changes += self.transform_expr(body);
198            }
199            _ => {}
200        }
201
202        changes
203    }
204}
205
206impl Mutation for AssignOpMutation {
207    fn describe(&self) -> String {
208        "Convert assignments to compound operators (a = a + b → a += b)".to_string()
209    }
210
211    fn mutation_type(&self) -> &'static str {
212        "AssignOp"
213    }
214
215    fn box_clone(&self) -> Box<dyn Mutation> {
216        Box::new(self.clone())
217    }
218}
219
220#[cfg(test)]
221mod tests {
222    use super::*;
223
224    #[test]
225    fn test_compound_op() {
226        assert_eq!(AssignOpMutation::compound_op("+"), Some("+="));
227        assert_eq!(AssignOpMutation::compound_op("-"), Some("-="));
228        assert_eq!(AssignOpMutation::compound_op("*"), Some("*="));
229        assert_eq!(AssignOpMutation::compound_op("/"), Some("/="));
230        assert_eq!(AssignOpMutation::compound_op("&&"), None);
231    }
232
233    #[test]
234    fn test_expr_eq() {
235        let a = PureExpr::Path("x".to_string());
236        let b = PureExpr::Path("x".to_string());
237        let c = PureExpr::Path("y".to_string());
238
239        assert!(AssignOpMutation::expr_eq(&a, &b));
240        assert!(!AssignOpMutation::expr_eq(&a, &c));
241    }
242
243    #[test]
244    fn test_expr_eq_unary() {
245        // *x == *x
246        let a = PureExpr::Unary {
247            op: "*".to_string(),
248            expr: Box::new(PureExpr::Path("x".to_string())),
249        };
250        let b = PureExpr::Unary {
251            op: "*".to_string(),
252            expr: Box::new(PureExpr::Path("x".to_string())),
253        };
254        let c = PureExpr::Unary {
255            op: "*".to_string(),
256            expr: Box::new(PureExpr::Path("y".to_string())),
257        };
258
259        assert!(AssignOpMutation::expr_eq(&a, &b));
260        assert!(!AssignOpMutation::expr_eq(&a, &c));
261    }
262
263    #[test]
264    fn test_expr_eq_field() {
265        let a = PureExpr::Field {
266            expr: Box::new(PureExpr::Path("self".to_string())),
267            field: "count".to_string(),
268        };
269        let b = PureExpr::Field {
270            expr: Box::new(PureExpr::Path("self".to_string())),
271            field: "count".to_string(),
272        };
273        let c = PureExpr::Field {
274            expr: Box::new(PureExpr::Path("self".to_string())),
275            field: "other".to_string(),
276        };
277
278        assert!(AssignOpMutation::expr_eq(&a, &b));
279        assert!(!AssignOpMutation::expr_eq(&a, &c));
280    }
281}