1use ryo_source::pure::{PureBlock, PureExpr, PureFn, PureParam, PureStmt, PureType};
14use ryo_symbol::SymbolId;
15use std::collections::HashSet;
16
17use crate::Mutation;
18
19#[derive(Debug, Clone, Default)]
40pub struct CloneOnCopyMutation {
41 pub target_fn: Option<SymbolId>,
43 pub aggressive: bool,
45 pub copy_vars: Vec<String>,
47}
48
49impl CloneOnCopyMutation {
50 pub fn new() -> Self {
51 Self::default()
52 }
53
54 pub fn in_function(mut self, id: SymbolId) -> Self {
56 self.target_fn = Some(id);
57 self
58 }
59
60 pub fn aggressive(mut self) -> Self {
65 self.aggressive = true;
66 self
67 }
68
69 pub fn with_copy_var(mut self, var: impl Into<String>) -> Self {
71 self.copy_vars.push(var.into());
72 self
73 }
74
75 fn is_copy_literal(expr: &PureExpr) -> bool {
77 match expr {
78 PureExpr::Lit(lit) => {
79 lit.chars().all(|c| c.is_ascii_digit() || c == '_')
81 || lit.contains('.') && lit.chars().all(|c| c.is_ascii_digit() || c == '.' || c == '_')
83 || lit == "true" || lit == "false"
85 || (lit.starts_with('\'') && lit.ends_with('\''))
87 }
88 PureExpr::Path(path) => {
89 path == "true" || path == "false"
91 }
92 _ => false,
93 }
94 }
95
96 fn is_known_copy_var(&self, path: &str, fn_copy_vars: &HashSet<String>) -> bool {
98 self.copy_vars.iter().any(|v| v == path) || fn_copy_vars.contains(path)
99 }
100
101 const COPY_TYPES: &'static [&'static str] = &[
103 "i8",
105 "i16",
106 "i32",
107 "i64",
108 "i128",
109 "isize",
110 "u8",
112 "u16",
113 "u32",
114 "u64",
115 "u128",
116 "usize",
117 "f32",
119 "f64",
120 "bool",
122 "char",
123 "NonZeroI8",
125 "NonZeroI16",
126 "NonZeroI32",
127 "NonZeroI64",
128 "NonZeroI128",
129 "NonZeroIsize",
130 "NonZeroU8",
131 "NonZeroU16",
132 "NonZeroU32",
133 "NonZeroU64",
134 "NonZeroU128",
135 "NonZeroUsize",
136 ];
137
138 fn is_copy_type(ty: &PureType) -> bool {
140 match ty {
141 PureType::Path(path) => {
142 Self::COPY_TYPES
144 .iter()
145 .any(|&t| path == t || path.ends_with(&format!("::{}", t)))
146 }
147 PureType::Ref { .. } => {
148 true
150 }
151 PureType::Tuple(types) => {
152 types.iter().all(Self::is_copy_type)
154 }
155 PureType::Array { ty, .. } => {
156 Self::is_copy_type(ty)
158 }
159 _ => false,
160 }
161 }
162
163 fn collect_copy_vars_from_params(params: &[PureParam]) -> HashSet<String> {
165 let mut copy_vars = HashSet::new();
166 for param in params {
167 if let PureParam::Typed { name, ty } = param {
168 if Self::is_copy_type(ty) {
169 copy_vars.insert(name.clone());
170 }
171 }
172 }
173 copy_vars
174 }
175
176 fn transform_expr(&self, expr: &mut PureExpr, fn_copy_vars: &HashSet<String>) -> usize {
178 let mut changes = 0;
179
180 if let PureExpr::MethodCall {
182 receiver,
183 method,
184 args,
185 ..
186 } = expr
187 {
188 if method == "clone" && args.is_empty() {
189 let should_remove = if self.aggressive {
190 true
191 } else {
192 Self::is_copy_literal(receiver)
194 || matches!(receiver.as_ref(), PureExpr::Path(p) if self.is_known_copy_var(p, fn_copy_vars))
195 };
196
197 if should_remove {
198 let inner = std::mem::replace(
200 receiver.as_mut(),
201 PureExpr::Path("__placeholder".to_string()),
202 );
203 *expr = inner;
204 return 1;
205 }
206 }
207 }
208
209 match expr {
211 PureExpr::Binary { left, right, .. } => {
212 changes += self.transform_expr(left, fn_copy_vars);
213 changes += self.transform_expr(right, fn_copy_vars);
214 }
215 PureExpr::Unary { expr: inner, .. } => {
216 changes += self.transform_expr(inner, fn_copy_vars);
217 }
218 PureExpr::Call { func, args } => {
219 changes += self.transform_expr(func, fn_copy_vars);
220 for arg in args {
221 changes += self.transform_expr(arg, fn_copy_vars);
222 }
223 }
224 PureExpr::MethodCall { receiver, args, .. } => {
225 changes += self.transform_expr(receiver, fn_copy_vars);
226 for arg in args {
227 changes += self.transform_expr(arg, fn_copy_vars);
228 }
229 }
230 PureExpr::Field { expr: inner, .. } => {
231 changes += self.transform_expr(inner, fn_copy_vars);
232 }
233 PureExpr::Index { expr: inner, index } => {
234 changes += self.transform_expr(inner, fn_copy_vars);
235 changes += self.transform_expr(index, fn_copy_vars);
236 }
237 PureExpr::Block { block, .. } => {
238 changes += self.transform_block(block, fn_copy_vars);
239 }
240 PureExpr::If {
241 cond,
242 then_branch,
243 else_branch,
244 } => {
245 changes += self.transform_expr(cond, fn_copy_vars);
246 changes += self.transform_block(then_branch, fn_copy_vars);
247 if let Some(else_expr) = else_branch {
248 changes += self.transform_expr(else_expr, fn_copy_vars);
249 }
250 }
251 PureExpr::Match { expr: e, arms } => {
252 changes += self.transform_expr(e, fn_copy_vars);
253 for arm in arms {
254 changes += self.transform_expr(&mut arm.body, fn_copy_vars);
255 }
256 }
257 PureExpr::Loop { body: block, .. } | PureExpr::While { body: block, .. } => {
258 changes += self.transform_block(block, fn_copy_vars);
259 }
260 PureExpr::For {
261 expr: iter_expr,
262 body,
263 ..
264 } => {
265 changes += self.transform_expr(iter_expr, fn_copy_vars);
266 changes += self.transform_block(body, fn_copy_vars);
267 }
268 PureExpr::Closure { body, .. } => {
269 changes += self.transform_expr(body, fn_copy_vars);
270 }
271 PureExpr::Tuple(exprs) | PureExpr::Array(exprs) => {
272 for e in exprs {
273 changes += self.transform_expr(e, fn_copy_vars);
274 }
275 }
276 PureExpr::Struct { fields, .. } => {
277 for (_, e) in fields {
278 changes += self.transform_expr(e, fn_copy_vars);
279 }
280 }
281 PureExpr::Ref { expr: inner, .. } => {
282 changes += self.transform_expr(inner, fn_copy_vars);
283 }
284 PureExpr::Return(Some(inner)) => {
285 changes += self.transform_expr(inner, fn_copy_vars);
286 }
287 PureExpr::Try(inner) | PureExpr::Await(inner) => {
288 changes += self.transform_expr(inner, fn_copy_vars);
289 }
290 _ => {}
291 }
292
293 changes
294 }
295
296 fn transform_block(&self, block: &mut PureBlock, fn_copy_vars: &HashSet<String>) -> usize {
297 let mut changes = 0;
298 for stmt in &mut block.stmts {
299 changes += self.transform_stmt(stmt, fn_copy_vars);
300 }
301 changes
302 }
303
304 fn transform_stmt(&self, stmt: &mut PureStmt, fn_copy_vars: &HashSet<String>) -> usize {
305 match stmt {
306 PureStmt::Local { init: Some(e), .. } => self.transform_expr(e, fn_copy_vars),
307 PureStmt::Semi(e) | PureStmt::Expr(e) => self.transform_expr(e, fn_copy_vars),
308 _ => 0,
309 }
310 }
311
312 pub fn transform_fn(&self, func: &mut PureFn) -> usize {
313 let fn_copy_vars = Self::collect_copy_vars_from_params(&func.params);
317 self.transform_block(&mut func.body, &fn_copy_vars)
318 }
319}
320
321impl Mutation for CloneOnCopyMutation {
322 fn describe(&self) -> String {
323 "Remove unnecessary .clone() on Copy types".to_string()
324 }
325
326 fn mutation_type(&self) -> &'static str {
327 "CloneOnCopy"
328 }
329
330 fn box_clone(&self) -> Box<dyn Mutation> {
331 Box::new(self.clone())
332 }
333}
334
335#[cfg(test)]
336mod tests {
337 use super::*;
338
339 #[test]
340 fn test_is_copy_literal_integer() {
341 assert!(CloneOnCopyMutation::is_copy_literal(&PureExpr::Lit(
342 "42".to_string()
343 )));
344 assert!(CloneOnCopyMutation::is_copy_literal(&PureExpr::Lit(
345 "1_000".to_string()
346 )));
347 }
348
349 #[test]
350 fn test_is_copy_literal_bool() {
351 assert!(CloneOnCopyMutation::is_copy_literal(&PureExpr::Lit(
352 "true".to_string()
353 )));
354 assert!(CloneOnCopyMutation::is_copy_literal(&PureExpr::Lit(
355 "false".to_string()
356 )));
357 assert!(CloneOnCopyMutation::is_copy_literal(&PureExpr::Path(
358 "true".to_string()
359 )));
360 }
361
362 #[test]
363 fn test_is_copy_literal_char() {
364 assert!(CloneOnCopyMutation::is_copy_literal(&PureExpr::Lit(
365 "'a'".to_string()
366 )));
367 assert!(CloneOnCopyMutation::is_copy_literal(&PureExpr::Lit(
368 "'\\n'".to_string()
369 )));
370 }
371
372 #[test]
373 fn test_is_not_copy_literal() {
374 assert!(!CloneOnCopyMutation::is_copy_literal(&PureExpr::Lit(
375 "\"string\"".to_string()
376 )));
377 assert!(!CloneOnCopyMutation::is_copy_literal(&PureExpr::Path(
378 "variable".to_string()
379 )));
380 }
381
382 #[test]
383 fn test_known_copy_var() {
384 let mutation = CloneOnCopyMutation::new()
385 .with_copy_var("x")
386 .with_copy_var("count");
387
388 let empty_set = std::collections::HashSet::new();
389 assert!(mutation.is_known_copy_var("x", &empty_set));
390 assert!(mutation.is_known_copy_var("count", &empty_set));
391 assert!(!mutation.is_known_copy_var("other", &empty_set));
392 }
393}