Skip to main content

ryo_mutations/idiom/
noop_arm.rs

1//! NoOpArmToTodoMutation: Replace empty/noop match arms with todo!/unimplemented!/unreachable!
2//!
3//! Transforms:
4//! - `_ => {}` → `_ => todo!()`
5//! - `_ => ()` → `_ => todo!()`
6//! - `Variant => {}` → `Variant => todo!()`
7//!
8//! This helps identify unhandled cases that might be silently ignored.
9
10use ryo_source::pure::{MacroDelimiter, PureBlock, PureExpr, PureMatchArm, PureStmt};
11use ryo_symbol::SymbolId;
12
13use crate::Mutation;
14
15/// Replace empty/noop match arms with explicit placeholders
16///
17/// # Example
18///
19/// ```rust,ignore
20/// use ryo_mutations::idiom::NoOpArmToTodoMutation;
21///
22/// let mutation = NoOpArmToTodoMutation::new();
23/// // Transforms:
24/// //   match x {
25/// //       Some(v) => process(v),
26/// //       _ => {}
27/// //   }
28/// // Into:
29/// //   match x {
30/// //       Some(v) => process(v),
31/// //       _ => todo!()
32/// //   }
33/// ```
34#[derive(Debug, Clone)]
35pub struct NoOpArmToTodoMutation {
36    /// Target function SymbolId. If None, applies to all functions.
37    pub target_fn: Option<SymbolId>,
38    /// Replacement macro: "todo", "unimplemented", or "unreachable"
39    pub replacement: String,
40}
41
42impl Default for NoOpArmToTodoMutation {
43    fn default() -> Self {
44        Self {
45            target_fn: None,
46            replacement: "todo".to_string(),
47        }
48    }
49}
50
51impl NoOpArmToTodoMutation {
52    pub fn new() -> Self {
53        Self::default()
54    }
55
56    /// Only apply in a specific function
57    pub fn in_function(mut self, id: SymbolId) -> Self {
58        self.target_fn = Some(id);
59        self
60    }
61
62    /// Set replacement macro
63    pub fn with_replacement(mut self, replacement: impl Into<String>) -> Self {
64        self.replacement = replacement.into();
65        self
66    }
67
68    /// Check if a match arm body is a noop (empty block or unit tuple)
69    fn is_noop_body(expr: &PureExpr) -> bool {
70        match expr {
71            // Empty block: {}
72            PureExpr::Block { block, .. } => block.stmts.is_empty(),
73            // Unit tuple: ()
74            PureExpr::Tuple(items) => items.is_empty(),
75            _ => false,
76        }
77    }
78
79    /// Create a macro call expression for the replacement
80    fn create_replacement_expr(&self) -> PureExpr {
81        PureExpr::Macro {
82            name: self.replacement.clone(),
83            delimiter: MacroDelimiter::Paren,
84            tokens: "".to_string(),
85        }
86    }
87
88    /// Transform match arms, returns number of changes
89    fn transform_arms(&self, arms: &mut [PureMatchArm]) -> usize {
90        let mut changes = 0;
91
92        for arm in arms.iter_mut() {
93            // First, recursively transform the body (in case there's a nested match)
94            changes += self.transform_expr(&mut arm.body);
95
96            // Then, check if this arm's body is a noop
97            if Self::is_noop_body(&arm.body) {
98                arm.body = self.create_replacement_expr();
99                changes += 1;
100            }
101        }
102
103        changes
104    }
105
106    /// Transform an expression, returns changes count
107    fn transform_expr(&self, expr: &mut PureExpr) -> usize {
108        let mut changes = 0;
109
110        match expr {
111            PureExpr::Match { arms, expr: inner } => {
112                // Recursively transform the match scrutinee
113                changes += self.transform_expr(inner);
114                // Transform the arms
115                changes += self.transform_arms(arms);
116            }
117            PureExpr::If {
118                cond,
119                then_branch,
120                else_branch,
121            } => {
122                changes += self.transform_expr(cond);
123                changes += self.transform_block(then_branch);
124                if let Some(else_expr) = else_branch {
125                    changes += self.transform_expr(else_expr);
126                }
127            }
128            PureExpr::Block { block, .. } => {
129                changes += self.transform_block(block);
130            }
131            PureExpr::Call { func, args } => {
132                changes += self.transform_expr(func);
133                for arg in args {
134                    changes += self.transform_expr(arg);
135                }
136            }
137            PureExpr::MethodCall { receiver, args, .. } => {
138                changes += self.transform_expr(receiver);
139                for arg in args {
140                    changes += self.transform_expr(arg);
141                }
142            }
143            PureExpr::Binary { left, right, .. } => {
144                changes += self.transform_expr(left);
145                changes += self.transform_expr(right);
146            }
147            PureExpr::Unary { expr: inner, .. } => {
148                changes += self.transform_expr(inner);
149            }
150            PureExpr::Closure { body, .. } => {
151                changes += self.transform_expr(body);
152            }
153            PureExpr::While { cond, body, .. } => {
154                changes += self.transform_expr(cond);
155                changes += self.transform_block(body);
156            }
157            PureExpr::Loop { body, .. } => {
158                changes += self.transform_block(body);
159            }
160            PureExpr::For { expr, body, .. } => {
161                changes += self.transform_expr(expr);
162                changes += self.transform_block(body);
163            }
164            PureExpr::Tuple(items) | PureExpr::Array(items) => {
165                for item in items {
166                    changes += self.transform_expr(item);
167                }
168            }
169            PureExpr::Struct { fields, .. } => {
170                for (_, value) in fields {
171                    changes += self.transform_expr(value);
172                }
173            }
174            PureExpr::Index { expr, index } => {
175                changes += self.transform_expr(expr);
176                changes += self.transform_expr(index);
177            }
178            PureExpr::Field { expr, .. } => {
179                changes += self.transform_expr(expr);
180            }
181            PureExpr::Cast { expr, .. } => {
182                changes += self.transform_expr(expr);
183            }
184            PureExpr::Return(Some(inner))
185            | PureExpr::Break {
186                expr: Some(inner), ..
187            }
188            | PureExpr::Await(inner)
189            | PureExpr::Try(inner)
190            | PureExpr::Ref { expr: inner, .. }
191            | PureExpr::Let { expr: inner, .. } => {
192                changes += self.transform_expr(inner);
193            }
194            PureExpr::Range { start, end, .. } => {
195                if let Some(s) = start {
196                    changes += self.transform_expr(s);
197                }
198                if let Some(e) = end {
199                    changes += self.transform_expr(e);
200                }
201            }
202            PureExpr::Unsafe(block) | PureExpr::Async { body: block, .. } => {
203                changes += self.transform_block(block);
204            }
205            PureExpr::Repeat { expr, len } => {
206                changes += self.transform_expr(expr);
207                changes += self.transform_expr(len);
208            }
209            // Leaf nodes - no recursion needed
210            _ => {}
211        }
212
213        changes
214    }
215
216    /// Transform a block, returns changes count
217    pub fn transform_block(&self, block: &mut PureBlock) -> usize {
218        let mut changes = 0;
219
220        for stmt in &mut block.stmts {
221            match stmt {
222                PureStmt::Local {
223                    init: Some(init_expr),
224                    ..
225                } => {
226                    changes += self.transform_expr(init_expr);
227                }
228                PureStmt::Expr(expr) | PureStmt::Semi(expr) => {
229                    changes += self.transform_expr(expr);
230                }
231                _ => {}
232            }
233        }
234
235        changes
236    }
237}
238
239impl Mutation for NoOpArmToTodoMutation {
240    fn describe(&self) -> String {
241        format!(
242            "Replace empty match arms with {}!() (_ => {{}} → _ => {}!())",
243            self.replacement, self.replacement
244        )
245    }
246
247    fn mutation_type(&self) -> &'static str {
248        "NoOpArmToTodo"
249    }
250
251    fn box_clone(&self) -> Box<dyn Mutation> {
252        Box::new(self.clone())
253    }
254}
255
256#[cfg(test)]
257mod tests {
258    use super::*;
259    use ryo_source::pure::PurePattern;
260
261    fn create_empty_block() -> PureExpr {
262        PureExpr::Block {
263            label: None,
264            block: PureBlock { stmts: vec![] },
265        }
266    }
267
268    fn create_unit_tuple() -> PureExpr {
269        PureExpr::Tuple(vec![])
270    }
271
272    #[test]
273    fn test_is_noop_body_empty_block() {
274        assert!(NoOpArmToTodoMutation::is_noop_body(&create_empty_block()));
275    }
276
277    #[test]
278    fn test_is_noop_body_unit_tuple() {
279        assert!(NoOpArmToTodoMutation::is_noop_body(&create_unit_tuple()));
280    }
281
282    #[test]
283    fn test_is_noop_body_non_empty() {
284        let non_empty = PureExpr::Path("something".to_string());
285        assert!(!NoOpArmToTodoMutation::is_noop_body(&non_empty));
286    }
287
288    #[test]
289    fn test_create_replacement_expr_default() {
290        let mutation = NoOpArmToTodoMutation::new();
291        let expr = mutation.create_replacement_expr();
292        match expr {
293            PureExpr::Macro { name, tokens, .. } => {
294                assert_eq!(name, "todo");
295                assert_eq!(tokens, "");
296            }
297            _ => panic!("Expected macro expression"),
298        }
299    }
300
301    #[test]
302    fn test_create_replacement_expr_custom() {
303        let mutation = NoOpArmToTodoMutation::new().with_replacement("unreachable");
304        let expr = mutation.create_replacement_expr();
305        match expr {
306            PureExpr::Macro { name, tokens, .. } => {
307                assert_eq!(name, "unreachable");
308                assert_eq!(tokens, "");
309            }
310            _ => panic!("Expected macro expression"),
311        }
312    }
313
314    #[test]
315    fn test_transform_match_arms() {
316        let mut arms = vec![
317            PureMatchArm {
318                pattern: PurePattern::Wild,
319                guard: None,
320                body: create_empty_block(),
321            },
322            PureMatchArm {
323                pattern: PurePattern::Wild,
324                guard: None,
325                body: PureExpr::Path("existing".to_string()),
326            },
327        ];
328
329        let mutation = NoOpArmToTodoMutation::new();
330        let changes = mutation.transform_arms(&mut arms);
331
332        assert_eq!(changes, 1);
333        // First arm should be transformed
334        match &arms[0].body {
335            PureExpr::Macro { name, .. } => assert_eq!(name, "todo"),
336            _ => panic!("Expected macro"),
337        }
338        // Second arm should remain unchanged
339        match &arms[1].body {
340            PureExpr::Path(p) => assert_eq!(p, "existing"),
341            _ => panic!("Expected path"),
342        }
343    }
344}