Skip to main content

ryo_executor/engine/impls/
noop_arm.rs

1//! V2 ASTRegApply implementation for NoOpArmToTodoMutation
2//!
3//! Replaces empty/noop match arms with todo!/unimplemented!/unreachable!:
4//! - `_ => {}` → `_ => todo!()`
5//! - `_ => ()` → `_ => todo!()`
6
7use ryo_analysis::SymbolKind;
8use ryo_mutations::idiom::NoOpArmToTodoMutation;
9use ryo_mutations::{Mutation, MutationResult};
10use ryo_source::pure::{PureImplItem, PureItem};
11
12use crate::engine::{ASTMutationContext, ASTRegApply, ModificationType};
13
14impl ASTRegApply for NoOpArmToTodoMutation {
15    fn apply_to_registry(&self, ctx: &mut ASTMutationContext) -> MutationResult {
16        let mut total_changes = 0;
17
18        if let Some(target_id) = self.target_fn {
19            // Target function specified: direct lookup
20            if let Some(PureItem::Fn(f)) = ctx.ast_registry.get_mut(target_id) {
21                let changes = self.transform_block(&mut f.body);
22                if changes > 0 {
23                    ctx.emit_modified(target_id, ModificationType::BodyModified);
24                    total_changes += changes;
25                }
26            }
27        } else {
28            // No target: process all functions
29            let fn_ids: Vec<_> = ctx
30                .symbol_registry
31                .iter()
32                .filter(|(id, _)| {
33                    matches!(ctx.symbol_registry.kind(*id), Some(SymbolKind::Function))
34                })
35                .map(|(id, _)| id)
36                .collect();
37
38            for id in fn_ids {
39                if let Some(PureItem::Fn(f)) = ctx.ast_registry.get_mut(id) {
40                    let changes = self.transform_block(&mut f.body);
41                    if changes > 0 {
42                        ctx.emit_modified(id, ModificationType::BodyModified);
43                        total_changes += changes;
44                    }
45                }
46            }
47
48            // Process impl blocks (methods)
49            let impl_ids: Vec<_> = ctx
50                .symbol_registry
51                .iter()
52                .filter(|(id, _)| matches!(ctx.symbol_registry.kind(*id), Some(SymbolKind::Impl)))
53                .map(|(id, _)| id)
54                .collect();
55
56            for id in impl_ids {
57                let mut impl_changes = 0;
58                if let Some(PureItem::Impl(imp)) = ctx.ast_registry.get_mut(id) {
59                    for impl_item in &mut imp.items {
60                        if let PureImplItem::Fn(f) = impl_item {
61                            impl_changes += self.transform_block(&mut f.body);
62                        }
63                    }
64                }
65                if impl_changes > 0 {
66                    ctx.emit_modified(id, ModificationType::BodyModified);
67                    total_changes += impl_changes;
68                }
69            }
70        }
71
72        MutationResult {
73            mutation_type: self.mutation_type().to_string(),
74            changes: total_changes,
75            description: if total_changes > 0 {
76                format!(
77                    "Replaced {} empty match arm(s) with {}!()",
78                    total_changes, self.replacement
79                )
80            } else {
81                "No empty match arms found".to_string()
82            },
83        }
84    }
85}
86
87#[cfg(test)]
88mod tests {
89    use super::*;
90    use crate::engine::ASTMutationEngine;
91    use ryo_analysis::testing::ContextBuilder;
92
93    #[test]
94    fn test_v2_noop_arm_empty_block() {
95        let mut ctx = ContextBuilder::new()
96            .with_file(
97                "src/lib.rs",
98                r#"
99fn process(x: Option<i32>) {
100    match x {
101        Some(v) => println!("{}", v),
102        _ => {}
103    }
104}
105"#,
106            )
107            .build();
108
109        let mutation = NoOpArmToTodoMutation::new();
110        let result = ASTMutationEngine::execute_ast_reg(&mutation, &mut ctx);
111
112        assert_eq!(result.result.changes, 1);
113    }
114
115    #[test]
116    fn test_v2_noop_arm_unit_tuple() {
117        let mut ctx = ContextBuilder::new()
118            .with_file(
119                "src/lib.rs",
120                r#"
121fn process(x: Option<i32>) {
122    match x {
123        Some(v) => println!("{}", v),
124        None => ()
125    }
126}
127"#,
128            )
129            .build();
130
131        let mutation = NoOpArmToTodoMutation::new();
132        let result = ASTMutationEngine::execute_ast_reg(&mutation, &mut ctx);
133
134        assert_eq!(result.result.changes, 1);
135    }
136
137    #[test]
138    fn test_v2_noop_arm_with_replacement() {
139        let mut ctx = ContextBuilder::new()
140            .with_file(
141                "src/lib.rs",
142                r#"
143fn process(x: Option<i32>) {
144    match x {
145        Some(v) => println!("{}", v),
146        _ => {}
147    }
148}
149"#,
150            )
151            .build();
152
153        let mutation = NoOpArmToTodoMutation::new().with_replacement("unreachable");
154        let result = ASTMutationEngine::execute_ast_reg(&mutation, &mut ctx);
155
156        assert_eq!(result.result.changes, 1);
157        assert!(result.result.description.contains("unreachable!()"));
158    }
159
160    #[test]
161    fn test_v2_noop_arm_no_changes() {
162        let mut ctx = ContextBuilder::new()
163            .with_file(
164                "src/lib.rs",
165                r#"
166fn process(x: Option<i32>) {
167    match x {
168        Some(v) => println!("{}", v),
169        None => todo!()
170    }
171}
172"#,
173            )
174            .build();
175
176        let mutation = NoOpArmToTodoMutation::new();
177        let result = ASTMutationEngine::execute_ast_reg(&mutation, &mut ctx);
178
179        assert_eq!(result.result.changes, 0);
180    }
181
182    #[test]
183    fn test_v2_noop_arm_emits_event_on_positive_changes() {
184        // Issue d223c4e4 regression pin: idiom mutations must emit
185        // MutationEvent via ctx.emit_modified(...) whenever
186        // transform_block returns changes > 0. Without the event,
187        // blueprint_executor's sync_files_and_rebuild aggregates an
188        // empty event set and the dry_run path reports 0 modified
189        // files even though the AST changed in place.
190        //
191        // db8f9a8c added emit_modified to all 13 idiom impls
192        // (noop_arm / collapsible_if / bool_simplify / manual_map /
193        //  match_to_if_let / map_unwrap_or / filter_next /
194        //  redundant_closure / unwrap_to_question /
195        //  comparison_to_method / loop_to_iter / clone_on_copy /
196        //  assign_op). This spec pins the contract on the
197        // representative noop_arm path (the other 12 share the
198        // same structural pattern).
199        let mut ctx = ContextBuilder::new()
200            .with_file(
201                "src/lib.rs",
202                r#"
203fn process(x: Option<i32>) {
204    match x {
205        Some(v) => println!("{}", v),
206        _ => {}
207    }
208}
209"#,
210            )
211            .build();
212
213        let mutation = NoOpArmToTodoMutation::new();
214        let result = ASTMutationEngine::execute_ast_reg(&mutation, &mut ctx);
215
216        assert_eq!(result.result.changes, 1);
217        assert!(
218            !result.events.is_empty(),
219            "events must be emitted when changes > 0 (issue d223c4e4 regression pin)"
220        );
221    }
222}