Skip to main content

ryo_executor/engine/impls/
collapsible_if.rs

1//! V2 ASTRegApply implementation for CollapsibleIfMutation
2//!
3//! Merges nested if statements into single if with &&:
4//! - `if a { if b { body } }` → `if a && b { body }`
5
6use ryo_analysis::SymbolKind;
7use ryo_mutations::idiom::CollapsibleIfMutation;
8use ryo_mutations::{Mutation, MutationResult};
9use ryo_source::pure::{PureImplItem, PureItem};
10
11use crate::engine::{ASTMutationContext, ASTRegApply, ModificationType};
12
13impl ASTRegApply for CollapsibleIfMutation {
14    fn apply_to_registry(&self, ctx: &mut ASTMutationContext) -> MutationResult {
15        let mut total_changes = 0;
16
17        if let Some(target_id) = self.target_fn {
18            // Target function specified: direct lookup
19            if let Some(PureItem::Fn(f)) = ctx.ast_registry.get_mut(target_id) {
20                let changes = self.transform_block(&mut f.body);
21                if changes > 0 {
22                    ctx.emit_modified(target_id, ModificationType::BodyModified);
23                    total_changes += changes;
24                }
25            }
26        } else {
27            // No target: process all functions
28            let fn_ids: Vec<_> = ctx
29                .symbol_registry
30                .iter()
31                .filter(|(id, _)| {
32                    matches!(ctx.symbol_registry.kind(*id), Some(SymbolKind::Function))
33                })
34                .map(|(id, _)| id)
35                .collect();
36
37            for id in fn_ids {
38                if let Some(PureItem::Fn(f)) = ctx.ast_registry.get_mut(id) {
39                    let changes = self.transform_block(&mut f.body);
40                    if changes > 0 {
41                        ctx.emit_modified(id, ModificationType::BodyModified);
42                        total_changes += changes;
43                    }
44                }
45            }
46
47            // Process impl blocks (methods)
48            let impl_ids: Vec<_> = ctx
49                .symbol_registry
50                .iter()
51                .filter(|(id, _)| matches!(ctx.symbol_registry.kind(*id), Some(SymbolKind::Impl)))
52                .map(|(id, _)| id)
53                .collect();
54
55            for id in impl_ids {
56                let mut impl_changes = 0;
57                if let Some(PureItem::Impl(imp)) = ctx.ast_registry.get_mut(id) {
58                    for impl_item in &mut imp.items {
59                        if let PureImplItem::Fn(f) = impl_item {
60                            impl_changes += self.transform_block(&mut f.body);
61                        }
62                    }
63                }
64                if impl_changes > 0 {
65                    ctx.emit_modified(id, ModificationType::BodyModified);
66                    total_changes += impl_changes;
67                }
68            }
69        }
70
71        MutationResult {
72            mutation_type: self.mutation_type().to_string(),
73            changes: total_changes,
74            description: if total_changes > 0 {
75                format!("Collapsed {} nested if statement(s)", total_changes)
76            } else {
77                "No nested if statements collapsed".to_string()
78            },
79        }
80    }
81}
82
83#[cfg(test)]
84mod tests {
85    use super::*;
86    use crate::engine::ASTMutationEngine;
87    use ryo_analysis::testing::ContextBuilder;
88
89    #[test]
90    fn test_v2_collapsible_if_basic() {
91        let mut ctx = ContextBuilder::new()
92            .with_file(
93                "src/lib.rs",
94                r#"
95fn check(a: bool, b: bool) {
96    if a {
97        if b {
98            println!("both");
99        }
100    }
101}
102"#,
103            )
104            .build();
105
106        let mutation = CollapsibleIfMutation::new();
107        let result = ASTMutationEngine::execute_ast_reg(&mutation, &mut ctx);
108
109        assert_eq!(result.result.changes, 1);
110    }
111
112    #[test]
113    fn test_v2_collapsible_if_with_else() {
114        let mut ctx = ContextBuilder::new()
115            .with_file(
116                "src/lib.rs",
117                r#"
118fn check(a: bool, b: bool) {
119    if a {
120        if b {
121            println!("both");
122        } else {
123            println!("only a");
124        }
125    }
126}
127"#,
128            )
129            .build();
130
131        let mutation = CollapsibleIfMutation::new();
132        let result = ASTMutationEngine::execute_ast_reg(&mutation, &mut ctx);
133
134        // Should not collapse because inner if has else
135        assert_eq!(result.result.changes, 0);
136    }
137
138    #[test]
139    fn test_v2_collapsible_if_no_changes() {
140        let mut ctx = ContextBuilder::new()
141            .with_file(
142                "src/lib.rs",
143                r#"
144fn check(a: bool, b: bool) {
145    if a && b {
146        println!("both");
147    }
148}
149"#,
150            )
151            .build();
152
153        let mutation = CollapsibleIfMutation::new();
154        let result = ASTMutationEngine::execute_ast_reg(&mutation, &mut ctx);
155
156        assert_eq!(result.result.changes, 0);
157    }
158}