Skip to main content

ryo_executor/engine/impls/
clone_on_copy.rs

1//! V2 ASTRegApply implementation for CloneOnCopyMutation
2//!
3//! Removes unnecessary .clone() calls on Copy types:
4//! - `x.clone()` → `x` (when x is a Copy type)
5
6use ryo_analysis::SymbolKind;
7use ryo_mutations::idiom::CloneOnCopyMutation;
8use ryo_mutations::{Mutation, MutationResult};
9use ryo_source::pure::{PureImplItem, PureItem};
10
11use crate::engine::{ASTMutationContext, ASTRegApply, ModificationType};
12
13impl ASTRegApply for CloneOnCopyMutation {
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, transform only that fn.
19            // Matches convention of every other scoped idiom mutation: standalone
20            // Function only. Methods inside Impl blocks are not handled in scoped
21            // mode (pre-existing limitation across all idioms).
22            if let Some(PureItem::Fn(f)) = ctx.ast_registry.get_mut(target_id) {
23                let changes = self.transform_fn(f);
24                if changes > 0 {
25                    ctx.emit_modified(target_id, ModificationType::BodyModified);
26                    total_changes += changes;
27                }
28            }
29        } else {
30            // Process standalone functions
31            let fn_ids: Vec<_> = ctx
32                .symbol_registry
33                .iter()
34                .filter(|(id, _)| {
35                    matches!(ctx.symbol_registry.kind(*id), Some(SymbolKind::Function))
36                })
37                .map(|(id, _)| id)
38                .collect();
39
40            for id in fn_ids {
41                if let Some(PureItem::Fn(f)) = ctx.ast_registry.get_mut(id) {
42                    let changes = self.transform_fn(f);
43                    if changes > 0 {
44                        ctx.emit_modified(id, ModificationType::BodyModified);
45                        total_changes += changes;
46                    }
47                }
48            }
49
50            // Process impl blocks (methods)
51            let impl_ids: Vec<_> = ctx
52                .symbol_registry
53                .iter()
54                .filter(|(id, _)| matches!(ctx.symbol_registry.kind(*id), Some(SymbolKind::Impl)))
55                .map(|(id, _)| id)
56                .collect();
57
58            for id in impl_ids {
59                let mut impl_changes = 0;
60                if let Some(PureItem::Impl(imp)) = ctx.ast_registry.get_mut(id) {
61                    for impl_item in &mut imp.items {
62                        if let PureImplItem::Fn(f) = impl_item {
63                            impl_changes += self.transform_fn(f);
64                        }
65                    }
66                }
67                if impl_changes > 0 {
68                    ctx.emit_modified(id, ModificationType::BodyModified);
69                    total_changes += impl_changes;
70                }
71            }
72        }
73
74        MutationResult {
75            mutation_type: self.mutation_type().to_string(),
76            changes: total_changes,
77            description: if total_changes > 0 {
78                format!("Removed {} unnecessary .clone() call(s)", total_changes)
79            } else {
80                "No .clone() calls removed".to_string()
81            },
82        }
83    }
84}
85
86#[cfg(test)]
87mod tests {
88    use super::*;
89    use crate::engine::ASTMutationEngine;
90    use ryo_analysis::testing::ContextBuilder;
91
92    #[test]
93    fn test_v2_clone_on_copy_target_fn_scopes_to_target_only() {
94        // Boundary regression: target_fn must restrict transform to ONE fn.
95        // Pre-fix the impl ignored target_fn entirely → both foo and bar would
96        // be transformed (2 changes). Post-fix: only foo (1 change).
97        let mut ctx = ContextBuilder::new()
98            .with_file(
99                "src/lib.rs",
100                r#"
101fn foo(x: i32) -> i32 {
102    x.clone()
103}
104
105fn bar(y: i32) -> i32 {
106    y.clone()
107}
108"#,
109            )
110            .build();
111
112        let foo_id = ctx
113            .registry
114            .iter()
115            .find(|(id, path)| {
116                matches!(ctx.registry.kind(*id), Some(SymbolKind::Function)) && path.name() == "foo"
117            })
118            .map(|(id, _)| id)
119            .expect("foo function not found");
120
121        let mutation = CloneOnCopyMutation::new().in_function(foo_id);
122        let result = ASTMutationEngine::execute_ast_reg(&mutation, &mut ctx);
123
124        assert_eq!(result.result.changes, 1, "target_fn must scope to foo only");
125    }
126
127    #[test]
128    fn test_v2_clone_on_copy_literal() {
129        let mut ctx = ContextBuilder::new()
130            .with_file(
131                "src/lib.rs",
132                r#"
133fn process() -> i32 {
134    let x = 42.clone();
135    x
136}
137"#,
138            )
139            .build();
140
141        let mutation = CloneOnCopyMutation::new();
142        let result = ASTMutationEngine::execute_ast_reg(&mutation, &mut ctx);
143
144        assert_eq!(result.result.changes, 1);
145    }
146
147    #[test]
148    fn test_v2_clone_on_copy_param() {
149        let mut ctx = ContextBuilder::new()
150            .with_file(
151                "src/lib.rs",
152                r#"
153fn process(x: i32) -> i32 {
154    x.clone()
155}
156"#,
157            )
158            .build();
159
160        let mutation = CloneOnCopyMutation::new();
161        let result = ASTMutationEngine::execute_ast_reg(&mutation, &mut ctx);
162
163        // Should remove because x is declared as i32 (Copy type)
164        assert_eq!(result.result.changes, 1);
165    }
166
167    #[test]
168    fn test_v2_clone_on_copy_no_changes() {
169        let mut ctx = ContextBuilder::new()
170            .with_file(
171                "src/lib.rs",
172                r#"
173fn process(s: String) -> String {
174    s.clone()
175}
176"#,
177            )
178            .build();
179
180        let mutation = CloneOnCopyMutation::new();
181        let result = ASTMutationEngine::execute_ast_reg(&mutation, &mut ctx);
182
183        // String is not Copy, should not remove
184        assert_eq!(result.result.changes, 0);
185    }
186
187    #[test]
188    fn test_v2_clone_on_copy_aggressive() {
189        let mut ctx = ContextBuilder::new()
190            .with_file(
191                "src/lib.rs",
192                r#"
193fn process(s: String) -> String {
194    s.clone()
195}
196"#,
197            )
198            .build();
199
200        let mutation = CloneOnCopyMutation::new().aggressive();
201        let result = ASTMutationEngine::execute_ast_reg(&mutation, &mut ctx);
202
203        // With aggressive mode, removes all .clone() calls
204        assert_eq!(result.result.changes, 1);
205    }
206}