Skip to main content

ryo_mutations/idiom/
unwrap_to_question.rs

1//! UnwrapToQuestionMutation: Convert .unwrap()/.expect() to ? operator
2//!
3//! Converts:
4//! - `x.unwrap()` → `x?`
5//! - `x.expect("msg")` → `x?`
6//! - `x.unwrap_or_else(|| panic!(...))` → `x?`
7//!
8//! Note: Only applies in functions that return Result/Option
9
10use ryo_source::pure::{PureBlock, PureExpr, PureStmt};
11use ryo_symbol::SymbolId;
12
13use crate::Mutation;
14
15/// Convert .unwrap() and .expect() to ? operator
16#[derive(Debug, Clone, Default)]
17pub struct UnwrapToQuestionMutation {
18    /// Also convert .expect() calls
19    pub include_expect: bool,
20    /// Target function SymbolId. If None, applies to all functions.
21    pub target_fn: Option<SymbolId>,
22}
23
24impl UnwrapToQuestionMutation {
25    pub fn new() -> Self {
26        Self {
27            include_expect: true,
28            target_fn: None,
29        }
30    }
31
32    /// Don't convert .expect() calls (keep the error message)
33    pub fn unwrap_only(mut self) -> Self {
34        self.include_expect = false;
35        self
36    }
37
38    /// Only apply in a specific function
39    pub fn in_function(mut self, id: SymbolId) -> Self {
40        self.target_fn = Some(id);
41        self
42    }
43
44    /// Check if method call is .unwrap() or .expect()
45    fn is_unwrap_call(&self, method: &str, args: &[PureExpr]) -> bool {
46        match method {
47            "unwrap" if args.is_empty() => true,
48            "expect" if args.len() == 1 && self.include_expect => true,
49            "unwrap_or_else" if args.len() == 1 => {
50                // Check if the closure panics
51                if let PureExpr::Closure { body, .. } = &args[0] {
52                    Self::is_panic_expr(body)
53                } else {
54                    false
55                }
56            }
57            _ => false,
58        }
59    }
60
61    /// Check if expression is a panic
62    fn is_panic_expr(expr: &PureExpr) -> bool {
63        match expr {
64            PureExpr::Macro { name, .. } => {
65                name == "panic"
66                    || name == "unreachable"
67                    || name == "todo"
68                    || name == "unimplemented"
69            }
70            PureExpr::Block { block, .. } if block.stmts.len() == 1 => match &block.stmts[0] {
71                PureStmt::Expr(e) | PureStmt::Semi(e) => Self::is_panic_expr(e),
72                _ => false,
73            },
74            _ => false,
75        }
76    }
77
78    /// Transform an expression (recursively), returns (new_expr, changes_count)
79    fn transform_expr(&self, expr: &mut PureExpr) -> usize {
80        let mut changes = 0;
81
82        // First, check if this is an unwrap call we should transform
83        if let PureExpr::MethodCall {
84            receiver,
85            method,
86            args,
87            ..
88        } = expr
89        {
90            if self.is_unwrap_call(method, args) {
91                // Transform: receiver.unwrap() → receiver?
92                // First transform the receiver recursively
93                changes += self.transform_expr(receiver);
94
95                // Then wrap in Try
96                let inner = std::mem::replace(
97                    receiver.as_mut(),
98                    PureExpr::Path("__placeholder".to_string()),
99                );
100                *expr = PureExpr::Try(Box::new(inner));
101                return changes + 1;
102            }
103        }
104
105        // Recursively transform sub-expressions
106        match expr {
107            PureExpr::Binary { left, right, .. } => {
108                changes += self.transform_expr(left);
109                changes += self.transform_expr(right);
110            }
111            PureExpr::Unary { expr: inner, .. } => {
112                changes += self.transform_expr(inner);
113            }
114            PureExpr::Call { func, args } => {
115                changes += self.transform_expr(func);
116                for arg in args {
117                    changes += self.transform_expr(arg);
118                }
119            }
120            PureExpr::MethodCall { receiver, args, .. } => {
121                changes += self.transform_expr(receiver);
122                for arg in args {
123                    changes += self.transform_expr(arg);
124                }
125            }
126            PureExpr::Field { expr: inner, .. } => {
127                changes += self.transform_expr(inner);
128            }
129            PureExpr::Index { expr: inner, index } => {
130                changes += self.transform_expr(inner);
131                changes += self.transform_expr(index);
132            }
133            PureExpr::Block { block, .. } => {
134                changes += self.transform_block(block);
135            }
136            PureExpr::If {
137                cond,
138                then_branch,
139                else_branch,
140            } => {
141                changes += self.transform_expr(cond);
142                changes += self.transform_block(then_branch);
143                if let Some(else_expr) = else_branch {
144                    changes += self.transform_expr(else_expr);
145                }
146            }
147            PureExpr::Match { expr: e, arms } => {
148                changes += self.transform_expr(e);
149                for arm in arms {
150                    changes += self.transform_expr(&mut arm.body);
151                }
152            }
153            PureExpr::Loop { body: block, .. } | PureExpr::While { body: block, .. } => {
154                changes += self.transform_block(block);
155            }
156            PureExpr::For {
157                expr: iter_expr,
158                body,
159                ..
160            } => {
161                changes += self.transform_expr(iter_expr);
162                changes += self.transform_block(body);
163            }
164            PureExpr::Closure { body, .. } => {
165                changes += self.transform_expr(body);
166            }
167            PureExpr::Tuple(exprs) | PureExpr::Array(exprs) => {
168                for e in exprs {
169                    changes += self.transform_expr(e);
170                }
171            }
172            PureExpr::Struct { fields, .. } => {
173                for (_, e) in fields {
174                    changes += self.transform_expr(e);
175                }
176            }
177            PureExpr::Ref { expr: inner, .. } => {
178                changes += self.transform_expr(inner);
179            }
180            PureExpr::Return(Some(inner)) => {
181                changes += self.transform_expr(inner);
182            }
183            PureExpr::Try(inner) => {
184                changes += self.transform_expr(inner);
185            }
186            PureExpr::Await(inner) => {
187                changes += self.transform_expr(inner);
188            }
189            _ => {}
190        }
191
192        changes
193    }
194
195    /// Transform a block
196    pub fn transform_block(&self, block: &mut PureBlock) -> usize {
197        let mut changes = 0;
198        for stmt in &mut block.stmts {
199            changes += self.transform_stmt(stmt);
200        }
201        changes
202    }
203
204    /// Transform a statement
205    fn transform_stmt(&self, stmt: &mut PureStmt) -> usize {
206        match stmt {
207            PureStmt::Local { init: Some(e), .. } => self.transform_expr(e),
208            PureStmt::Semi(e) | PureStmt::Expr(e) => self.transform_expr(e),
209            _ => 0,
210        }
211    }
212}
213
214impl Mutation for UnwrapToQuestionMutation {
215    fn describe(&self) -> String {
216        "Convert .unwrap()/.expect() to ? operator".to_string()
217    }
218
219    fn mutation_type(&self) -> &'static str {
220        "UnwrapToQuestion"
221    }
222
223    fn box_clone(&self) -> Box<dyn Mutation> {
224        Box::new(self.clone())
225    }
226}
227
228#[cfg(test)]
229mod tests {
230    use super::*;
231
232    fn make_unwrap_expr() -> PureExpr {
233        // x.unwrap()
234        PureExpr::MethodCall {
235            receiver: Box::new(PureExpr::Path("x".to_string())),
236            method: "unwrap".to_string(),
237            turbofish: None,
238            args: vec![],
239        }
240    }
241
242    fn make_expect_expr() -> PureExpr {
243        // x.expect("error")
244        PureExpr::MethodCall {
245            receiver: Box::new(PureExpr::Path("x".to_string())),
246            method: "expect".to_string(),
247            turbofish: None,
248            args: vec![PureExpr::Lit("\"error\"".to_string())],
249        }
250    }
251
252    #[test]
253    fn test_is_unwrap_call() {
254        let mutation = UnwrapToQuestionMutation::new();
255        assert!(mutation.is_unwrap_call("unwrap", &[]));
256        assert!(mutation.is_unwrap_call("expect", &[PureExpr::Lit("\"msg\"".to_string())]));
257        assert!(!mutation.is_unwrap_call("map", &[]));
258    }
259
260    #[test]
261    fn test_transform_unwrap() {
262        let mutation = UnwrapToQuestionMutation::new();
263        let mut expr = make_unwrap_expr();
264        let changes = mutation.transform_expr(&mut expr);
265
266        assert_eq!(changes, 1);
267        assert!(matches!(expr, PureExpr::Try(_)));
268    }
269
270    #[test]
271    fn test_transform_expect() {
272        let mutation = UnwrapToQuestionMutation::new();
273        let mut expr = make_expect_expr();
274        let changes = mutation.transform_expr(&mut expr);
275
276        assert_eq!(changes, 1);
277        assert!(matches!(expr, PureExpr::Try(_)));
278    }
279
280    #[test]
281    fn test_skip_expect_when_unwrap_only() {
282        let mutation = UnwrapToQuestionMutation::new().unwrap_only();
283        let mut expr = make_expect_expr();
284        let changes = mutation.transform_expr(&mut expr);
285
286        assert_eq!(changes, 0);
287        assert!(matches!(expr, PureExpr::MethodCall { .. }));
288    }
289}