ryo_mutations/idiom/
unwrap_to_question.rs1use ryo_source::pure::{PureBlock, PureExpr, PureStmt};
11use ryo_symbol::SymbolId;
12
13use crate::Mutation;
14
15#[derive(Debug, Clone, Default)]
17pub struct UnwrapToQuestionMutation {
18 pub include_expect: bool,
20 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 pub fn unwrap_only(mut self) -> Self {
34 self.include_expect = false;
35 self
36 }
37
38 pub fn in_function(mut self, id: SymbolId) -> Self {
40 self.target_fn = Some(id);
41 self
42 }
43
44 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 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 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 fn transform_expr(&self, expr: &mut PureExpr) -> usize {
80 let mut changes = 0;
81
82 if let PureExpr::MethodCall {
84 receiver,
85 method,
86 args,
87 ..
88 } = expr
89 {
90 if self.is_unwrap_call(method, args) {
91 changes += self.transform_expr(receiver);
94
95 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 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 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 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 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 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}