ryo_executor/engine/impls/
unwrap_to_question.rs1use ryo_analysis::SymbolKind;
10use ryo_mutations::idiom::UnwrapToQuestionMutation;
11use ryo_mutations::{Mutation, MutationResult};
12use ryo_source::pure::{PureImplItem, PureItem, PureType};
13
14use crate::engine::{ASTMutationContext, ASTRegApply, ModificationType};
15
16fn returns_option_or_result(ret: &Option<PureType>) -> bool {
18 match ret {
19 Some(PureType::Path(path)) => {
20 path.starts_with("Option<")
21 || path.starts_with("Result<")
22 || path == "Option"
23 || path == "Result"
24 }
25 _ => false,
26 }
27}
28
29impl ASTRegApply for UnwrapToQuestionMutation {
30 fn apply_to_registry(&self, ctx: &mut ASTMutationContext) -> MutationResult {
31 let mut total_changes = 0;
32
33 if let Some(target_id) = self.target_fn {
34 if let Some(PureItem::Fn(f)) = ctx.ast_registry.get_mut(target_id) {
45 if returns_option_or_result(&f.ret) {
46 let changes = self.transform_block(&mut f.body);
47 if changes > 0 {
48 ctx.emit_modified(target_id, ModificationType::BodyModified);
49 total_changes += changes;
50 }
51 }
52 }
53 } else {
54 let fn_ids: Vec<_> = ctx
56 .symbol_registry
57 .iter()
58 .filter(|(id, _)| {
59 matches!(ctx.symbol_registry.kind(*id), Some(SymbolKind::Function))
60 })
61 .map(|(id, _)| id)
62 .collect();
63
64 for id in fn_ids {
65 if let Some(PureItem::Fn(f)) = ctx.ast_registry.get_mut(id) {
66 if returns_option_or_result(&f.ret) {
68 let changes = self.transform_block(&mut f.body);
69 if changes > 0 {
70 ctx.emit_modified(id, ModificationType::BodyModified);
71 total_changes += changes;
72 }
73 }
74 }
75 }
76
77 let impl_ids: Vec<_> = ctx
79 .symbol_registry
80 .iter()
81 .filter(|(id, _)| matches!(ctx.symbol_registry.kind(*id), Some(SymbolKind::Impl)))
82 .map(|(id, _)| id)
83 .collect();
84
85 for id in impl_ids {
86 let mut impl_changes = 0;
87 if let Some(PureItem::Impl(imp)) = ctx.ast_registry.get_mut(id) {
88 for impl_item in &mut imp.items {
89 if let PureImplItem::Fn(f) = impl_item {
90 if returns_option_or_result(&f.ret) {
92 impl_changes += self.transform_block(&mut f.body);
93 }
94 }
95 }
96 }
97 if impl_changes > 0 {
98 ctx.emit_modified(id, ModificationType::BodyModified);
99 total_changes += impl_changes;
100 }
101 }
102 }
103
104 MutationResult {
105 mutation_type: self.mutation_type().to_string(),
106 changes: total_changes,
107 description: if total_changes > 0 {
108 format!("Converted {} .unwrap()/.expect() to ?", total_changes)
109 } else {
110 "No unwrap calls converted".to_string()
111 },
112 }
113 }
114}
115
116#[cfg(test)]
117mod tests {
118 use super::*;
119 use crate::engine::ASTMutationEngine;
120 use ryo_analysis::testing::ContextBuilder;
121
122 #[test]
123 fn test_v2_unwrap_to_question_basic() {
124 let mut ctx = ContextBuilder::new()
125 .with_file(
126 "src/lib.rs",
127 r#"
128fn process(opt: Option<i32>) -> Option<i32> {
129 let x = opt.unwrap();
130 Some(x + 1)
131}
132"#,
133 )
134 .build();
135
136 let mutation = UnwrapToQuestionMutation::new();
137 let result = ASTMutationEngine::execute_ast_reg(&mutation, &mut ctx);
138
139 assert_eq!(result.result.changes, 1);
140 }
141
142 #[test]
143 fn test_v2_unwrap_to_question_non_option_return() {
144 let mut ctx = ContextBuilder::new()
145 .with_file(
146 "src/lib.rs",
147 r#"
148fn process(opt: Option<i32>) -> i32 {
149 opt.unwrap()
150}
151"#,
152 )
153 .build();
154
155 let mutation = UnwrapToQuestionMutation::new();
156 let result = ASTMutationEngine::execute_ast_reg(&mutation, &mut ctx);
157
158 assert_eq!(result.result.changes, 0);
160 }
161
162 #[test]
163 fn test_v2_unwrap_to_question_expect() {
164 let mut ctx = ContextBuilder::new()
165 .with_file(
166 "src/lib.rs",
167 r#"
168fn process(opt: Option<i32>) -> Option<i32> {
169 let x = opt.expect("should not be none");
170 Some(x + 1)
171}
172"#,
173 )
174 .build();
175
176 let mutation = UnwrapToQuestionMutation::new();
177 let result = ASTMutationEngine::execute_ast_reg(&mutation, &mut ctx);
178
179 assert_eq!(result.result.changes, 1);
180 }
181}