Skip to main content

ryo_executor/engine/impls/
unwrap_to_question.rs

1//! V2 ASTRegApply implementation for UnwrapToQuestionMutation
2//!
3//! Converts .unwrap()/.expect() to ? operator:
4//! - `x.unwrap()` → `x?`
5//! - `x.expect("msg")` → `x?`
6//!
7//! Only applies in functions that return Result/Option.
8
9use 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
16/// Check if a return type is Option or Result (compatible with ? operator)
17fn 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            // Target function specified: direct lookup, transform only that fn.
35            //
36            // Note: this matches the convention of every other idiom mutation
37            // (NoOpArm / CollapsibleIf / BoolSimplify / FilterNext /
38            // MapUnwrapOr / AssignOp / MatchToIfLet etc.): standalone Function
39            // only. Methods inside an Impl block are not handled in scoped
40            // mode — when the lint targets a method the spec falls through
41            // to "no change". This is a known pre-existing limitation
42            // shared across all mutations, not introduced here. Tracked as
43            // a separate ergonomic improvement.
44            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            // No target: process all functions
55            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                    // Only transform if function returns Option or Result
67                    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            // Process impl blocks (methods)
78            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                            // Only transform if method returns Option or Result
91                            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        // Should not convert because function doesn't return Option/Result
159        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}