1use ryo_mutations::basic::{AddMatchArmMutation, RemoveMatchArmMutation, ReplaceMatchArmMutation};
31use ryo_mutations::MutationResult;
32use ryo_source::pure::{
33 MacroDelimiter, PureBlock, PureExpr, PureItem, PureMatchArm, PurePattern, PureStmt,
34};
35use ryo_symbol::SymbolKind;
36
37use crate::engine::{ASTMutationContext, ASTRegApply, ModificationType};
38
39impl ASTRegApply for AddMatchArmMutation {
44 fn apply_to_registry(&self, ctx: &mut ASTMutationContext) -> MutationResult {
45 let fn_id = self.function_id;
47
48 let kind = ctx.symbol_registry.kind(fn_id);
50 if !matches!(kind, Some(SymbolKind::Function) | Some(SymbolKind::Method)) {
51 return MutationResult {
52 mutation_type: "AddMatchArm".to_string(),
53 changes: 0,
54 description: format!("Symbol {} is not a function or method", fn_id),
55 };
56 }
57
58 let ast = match ctx.get_ast_mut(fn_id) {
60 Some(ast) => ast,
61 None => {
62 return MutationResult {
63 mutation_type: "AddMatchArm".to_string(),
64 changes: 0,
65 description: format!("AST not found for function {}", fn_id),
66 };
67 }
68 };
69
70 if let PureItem::Fn(f) = ast {
72 if walk_and_add_arm(&mut f.body, &self.enum_name, &self.pattern, &self.body) {
73 ctx.emit_modified(fn_id, ModificationType::Other("MatchArmAdded".into()));
74 return MutationResult {
75 mutation_type: "AddMatchArm".to_string(),
76 changes: 1,
77 description: format!(
78 "Added match arm '{}' in function {}",
79 self.pattern, fn_id
80 ),
81 };
82 }
83 }
84
85 MutationResult {
86 mutation_type: "AddMatchArm".to_string(),
87 changes: 0,
88 description: format!(
89 "No match expression for '{}' found in function {}",
90 self.enum_name, fn_id
91 ),
92 }
93 }
94}
95
96impl ASTRegApply for RemoveMatchArmMutation {
101 fn apply_to_registry(&self, ctx: &mut ASTMutationContext) -> MutationResult {
102 let fn_id = self.function_id;
104
105 let kind = ctx.symbol_registry.kind(fn_id);
107 if !matches!(kind, Some(SymbolKind::Function) | Some(SymbolKind::Method)) {
108 return MutationResult {
109 mutation_type: "RemoveMatchArm".to_string(),
110 changes: 0,
111 description: format!("Symbol {} is not a function or method", fn_id),
112 };
113 }
114
115 let ast = match ctx.get_ast_mut(fn_id) {
117 Some(ast) => ast,
118 None => {
119 return MutationResult {
120 mutation_type: "RemoveMatchArm".to_string(),
121 changes: 0,
122 description: format!("AST not found for function {}", fn_id),
123 };
124 }
125 };
126
127 if let PureItem::Fn(f) = ast {
129 if walk_and_remove_arm(&mut f.body, &self.enum_name, &self.pattern) {
130 ctx.emit_modified(fn_id, ModificationType::Other("MatchArmRemoved".into()));
131 return MutationResult {
132 mutation_type: "RemoveMatchArm".to_string(),
133 changes: 1,
134 description: format!(
135 "Removed match arm '{}' from function {}",
136 self.pattern, fn_id
137 ),
138 };
139 }
140 }
141
142 MutationResult {
143 mutation_type: "RemoveMatchArm".to_string(),
144 changes: 0,
145 description: format!(
146 "No match arm '{}' found in function {}",
147 self.pattern, fn_id
148 ),
149 }
150 }
151}
152
153fn walk_and_add_arm(block: &mut PureBlock, enum_name: &str, pattern: &str, body: &str) -> bool {
159 for stmt in &mut block.stmts {
160 if walk_stmt_and_add_arm(stmt, enum_name, pattern, body) {
161 return true;
162 }
163 }
164 false
165}
166
167fn walk_and_remove_arm(block: &mut PureBlock, enum_name: &str, pattern: &str) -> bool {
169 for stmt in &mut block.stmts {
170 if walk_stmt_and_remove_arm(stmt, enum_name, pattern) {
171 return true;
172 }
173 }
174 false
175}
176
177fn walk_stmt_and_add_arm(stmt: &mut PureStmt, enum_name: &str, pattern: &str, body: &str) -> bool {
178 match stmt {
179 PureStmt::Local {
180 init: Some(expr), ..
181 } => {
182 return walk_expr_and_add_arm(expr, enum_name, pattern, body);
183 }
184 PureStmt::Expr(expr) | PureStmt::Semi(expr) => {
185 return walk_expr_and_add_arm(expr, enum_name, pattern, body);
186 }
187 _ => {}
188 }
189 false
190}
191
192fn walk_stmt_and_remove_arm(stmt: &mut PureStmt, enum_name: &str, pattern: &str) -> bool {
193 match stmt {
194 PureStmt::Local {
195 init: Some(expr), ..
196 } => {
197 return walk_expr_and_remove_arm(expr, enum_name, pattern);
198 }
199 PureStmt::Expr(expr) | PureStmt::Semi(expr) => {
200 return walk_expr_and_remove_arm(expr, enum_name, pattern);
201 }
202 _ => {}
203 }
204 false
205}
206
207fn walk_expr_and_add_arm(expr: &mut PureExpr, enum_name: &str, pattern: &str, body: &str) -> bool {
208 if let PureExpr::Match {
210 expr: match_expr,
211 arms,
212 } = expr
213 {
214 if is_target_match(match_expr, arms, enum_name) {
215 if arms.iter().any(|a| pattern_matches(&a.pattern, pattern)) {
217 return false;
218 }
219
220 let insert_pos = arms
222 .iter()
223 .position(|a| matches!(a.pattern, PurePattern::Wild | PurePattern::Rest))
224 .unwrap_or(arms.len());
225
226 arms.insert(insert_pos, create_arm(pattern, body));
228 return true;
229 }
230 }
231
232 walk_expr_children_and_add_arm(expr, enum_name, pattern, body)
234}
235
236fn walk_expr_and_remove_arm(expr: &mut PureExpr, enum_name: &str, pattern: &str) -> bool {
237 if let PureExpr::Match {
239 expr: match_expr,
240 arms,
241 } = expr
242 {
243 if is_target_match(match_expr, arms, enum_name) {
244 let original_len = arms.len();
245 arms.retain(|a| !pattern_matches(&a.pattern, pattern));
246 if arms.len() < original_len {
247 return true;
248 }
249 }
250 }
251
252 walk_expr_children_and_remove_arm(expr, enum_name, pattern)
254}
255
256fn is_target_match(match_expr: &PureExpr, arms: &[PureMatchArm], enum_name: &str) -> bool {
258 for arm in arms {
260 if pattern_contains_enum(&arm.pattern, enum_name) {
261 return true;
262 }
263 }
264
265 if let PureExpr::Path(path) = match_expr {
267 if path.contains(enum_name) {
268 return true;
269 }
270 }
271
272 false
273}
274
275fn pattern_contains_enum(pattern: &PurePattern, enum_name: &str) -> bool {
276 match pattern {
277 PurePattern::Path(path) => path_has_enum_segment(path, enum_name),
278 PurePattern::Struct { path, .. } => path_has_enum_segment(path, enum_name),
279 PurePattern::Or(patterns) => patterns.iter().any(|p| pattern_contains_enum(p, enum_name)),
280 PurePattern::Other(s) => {
281 let path_part = s.split(&['(', '{', ' '][..]).next().unwrap_or(s);
283 path_has_enum_segment(path_part, enum_name)
284 }
285 _ => false,
286 }
287}
288
289fn path_has_enum_segment(path: &str, enum_name: &str) -> bool {
294 path.split("::").any(|segment| segment == enum_name)
295}
296
297fn create_arm(pattern: &str, body: &str) -> PureMatchArm {
299 let pat = if pattern.contains('(') || pattern.contains('{') || pattern.contains("..") {
303 PurePattern::Other(pattern.to_string())
305 } else {
306 PurePattern::Path(pattern.to_string())
307 };
308
309 let body_expr = if let Some(bang_pos) = body.find('!') {
311 let body_str = body.trim();
312 let name = body_str[..bang_pos].trim();
313 let rest = body_str[bang_pos + 1..].trim();
314
315 let is_valid_ident = !name.is_empty()
317 && name.chars().all(|c| c.is_alphanumeric() || c == '_')
318 && name
319 .chars()
320 .next()
321 .map(|c| c.is_alphabetic() || c == '_')
322 .unwrap_or(false);
323 let has_delimiter = rest.starts_with('(') || rest.starts_with('{') || rest.starts_with('[');
324
325 if is_valid_ident && has_delimiter {
326 let (delimiter, tokens) = if rest.starts_with('(') && rest.ends_with(')') {
327 (MacroDelimiter::Paren, rest[1..rest.len() - 1].to_string())
328 } else if rest.starts_with('{') && rest.ends_with('}') {
329 (MacroDelimiter::Brace, rest[1..rest.len() - 1].to_string())
330 } else if rest.starts_with('[') && rest.ends_with(']') {
331 (MacroDelimiter::Bracket, rest[1..rest.len() - 1].to_string())
332 } else {
333 (MacroDelimiter::Paren, String::new())
334 };
335 PureExpr::Macro {
336 name: name.to_string(),
337 delimiter,
338 tokens,
339 }
340 } else {
341 PureExpr::Other(normalize_body_as_expr(body))
342 }
343 } else {
344 PureExpr::Other(normalize_body_as_expr(body))
345 };
346
347 PureMatchArm {
348 pattern: pat,
349 guard: None,
350 body: body_expr,
351 }
352}
353
354fn normalize_body_as_expr(body: &str) -> String {
361 let trimmed = body.trim();
362 if trimmed.contains(';') && !trimmed.starts_with('{') {
363 format!("{{ {} }}", trimmed)
364 } else {
365 trimmed.to_string()
366 }
367}
368
369fn walk_expr_children_and_add_arm(
374 expr: &mut PureExpr,
375 enum_name: &str,
376 pattern: &str,
377 body: &str,
378) -> bool {
379 match expr {
380 PureExpr::Block { block, .. } => walk_and_add_arm(block, enum_name, pattern, body),
381 PureExpr::If {
382 cond,
383 then_branch,
384 else_branch,
385 } => {
386 if walk_expr_and_add_arm(cond, enum_name, pattern, body) {
387 return true;
388 }
389 if walk_and_add_arm(then_branch, enum_name, pattern, body) {
390 return true;
391 }
392 if let Some(else_expr) = else_branch {
393 if walk_expr_and_add_arm(else_expr, enum_name, pattern, body) {
394 return true;
395 }
396 }
397 false
398 }
399 PureExpr::Match { expr: e, arms } => {
400 if walk_expr_and_add_arm(e, enum_name, pattern, body) {
401 return true;
402 }
403 for arm in arms {
404 if walk_expr_and_add_arm(&mut arm.body, enum_name, pattern, body) {
405 return true;
406 }
407 }
408 false
409 }
410 PureExpr::Loop { body: block, .. } | PureExpr::Unsafe(block) => {
411 walk_and_add_arm(block, enum_name, pattern, body)
412 }
413 PureExpr::While { cond, body: b, .. } => {
414 walk_expr_and_add_arm(cond, enum_name, pattern, body)
415 || walk_and_add_arm(b, enum_name, pattern, body)
416 }
417 PureExpr::For {
418 expr: e, body: b, ..
419 } => {
420 walk_expr_and_add_arm(e, enum_name, pattern, body)
421 || walk_and_add_arm(b, enum_name, pattern, body)
422 }
423 PureExpr::Async { body: b, .. } => walk_and_add_arm(b, enum_name, pattern, body),
424 PureExpr::Closure { body: b, .. } => walk_expr_and_add_arm(b, enum_name, pattern, body),
425 PureExpr::Call { func, args } => {
426 if walk_expr_and_add_arm(func, enum_name, pattern, body) {
427 return true;
428 }
429 for arg in args {
430 if walk_expr_and_add_arm(arg, enum_name, pattern, body) {
431 return true;
432 }
433 }
434 false
435 }
436 PureExpr::MethodCall { receiver, args, .. } => {
437 if walk_expr_and_add_arm(receiver, enum_name, pattern, body) {
438 return true;
439 }
440 for arg in args {
441 if walk_expr_and_add_arm(arg, enum_name, pattern, body) {
442 return true;
443 }
444 }
445 false
446 }
447 PureExpr::Binary { left, right, .. } => {
448 walk_expr_and_add_arm(left, enum_name, pattern, body)
449 || walk_expr_and_add_arm(right, enum_name, pattern, body)
450 }
451 PureExpr::Unary { expr: e, .. }
452 | PureExpr::Field { expr: e, .. }
453 | PureExpr::Await(e)
454 | PureExpr::Try(e) => walk_expr_and_add_arm(e, enum_name, pattern, body),
455 PureExpr::Index { expr: e, index } => {
456 walk_expr_and_add_arm(e, enum_name, pattern, body)
457 || walk_expr_and_add_arm(index, enum_name, pattern, body)
458 }
459 PureExpr::Tuple(exprs) | PureExpr::Array(exprs) => {
460 for e in exprs {
461 if walk_expr_and_add_arm(e, enum_name, pattern, body) {
462 return true;
463 }
464 }
465 false
466 }
467 PureExpr::Return(Some(e)) | PureExpr::Break { expr: Some(e), .. } => {
468 walk_expr_and_add_arm(e, enum_name, pattern, body)
469 }
470 PureExpr::Let { expr: e, .. }
471 | PureExpr::Cast { expr: e, .. }
472 | PureExpr::Ref { expr: e, .. } => walk_expr_and_add_arm(e, enum_name, pattern, body),
473 PureExpr::Range { start, end, .. } => {
474 if let Some(s) = start {
475 if walk_expr_and_add_arm(s, enum_name, pattern, body) {
476 return true;
477 }
478 }
479 if let Some(e) = end {
480 if walk_expr_and_add_arm(e, enum_name, pattern, body) {
481 return true;
482 }
483 }
484 false
485 }
486 PureExpr::Struct { fields, .. } => {
487 for (_, e) in fields {
488 if walk_expr_and_add_arm(e, enum_name, pattern, body) {
489 return true;
490 }
491 }
492 false
493 }
494 PureExpr::Repeat { expr: e, len } => {
495 walk_expr_and_add_arm(e, enum_name, pattern, body)
496 || walk_expr_and_add_arm(len, enum_name, pattern, body)
497 }
498 _ => false,
499 }
500}
501
502fn walk_expr_children_and_remove_arm(expr: &mut PureExpr, enum_name: &str, pattern: &str) -> bool {
503 match expr {
504 PureExpr::Block { block, .. } => walk_and_remove_arm(block, enum_name, pattern),
505 PureExpr::If {
506 cond,
507 then_branch,
508 else_branch,
509 } => {
510 if walk_expr_and_remove_arm(cond, enum_name, pattern) {
511 return true;
512 }
513 if walk_and_remove_arm(then_branch, enum_name, pattern) {
514 return true;
515 }
516 if let Some(else_expr) = else_branch {
517 if walk_expr_and_remove_arm(else_expr, enum_name, pattern) {
518 return true;
519 }
520 }
521 false
522 }
523 PureExpr::Match { expr: e, arms } => {
524 if walk_expr_and_remove_arm(e, enum_name, pattern) {
525 return true;
526 }
527 for arm in arms {
528 if walk_expr_and_remove_arm(&mut arm.body, enum_name, pattern) {
529 return true;
530 }
531 }
532 false
533 }
534 PureExpr::Loop { body: block, .. } | PureExpr::Unsafe(block) => {
535 walk_and_remove_arm(block, enum_name, pattern)
536 }
537 PureExpr::While { cond, body, .. } => {
538 walk_expr_and_remove_arm(cond, enum_name, pattern)
539 || walk_and_remove_arm(body, enum_name, pattern)
540 }
541 PureExpr::For { expr: e, body, .. } => {
542 walk_expr_and_remove_arm(e, enum_name, pattern)
543 || walk_and_remove_arm(body, enum_name, pattern)
544 }
545 PureExpr::Async { body, .. } => walk_and_remove_arm(body, enum_name, pattern),
546 PureExpr::Closure { body, .. } => walk_expr_and_remove_arm(body, enum_name, pattern),
547 PureExpr::Call { func, args } => {
548 if walk_expr_and_remove_arm(func, enum_name, pattern) {
549 return true;
550 }
551 for arg in args {
552 if walk_expr_and_remove_arm(arg, enum_name, pattern) {
553 return true;
554 }
555 }
556 false
557 }
558 PureExpr::MethodCall { receiver, args, .. } => {
559 if walk_expr_and_remove_arm(receiver, enum_name, pattern) {
560 return true;
561 }
562 for arg in args {
563 if walk_expr_and_remove_arm(arg, enum_name, pattern) {
564 return true;
565 }
566 }
567 false
568 }
569 PureExpr::Binary { left, right, .. } => {
570 walk_expr_and_remove_arm(left, enum_name, pattern)
571 || walk_expr_and_remove_arm(right, enum_name, pattern)
572 }
573 PureExpr::Unary { expr: e, .. }
574 | PureExpr::Field { expr: e, .. }
575 | PureExpr::Await(e)
576 | PureExpr::Try(e) => walk_expr_and_remove_arm(e, enum_name, pattern),
577 PureExpr::Index { expr: e, index } => {
578 walk_expr_and_remove_arm(e, enum_name, pattern)
579 || walk_expr_and_remove_arm(index, enum_name, pattern)
580 }
581 PureExpr::Tuple(exprs) | PureExpr::Array(exprs) => {
582 for e in exprs {
583 if walk_expr_and_remove_arm(e, enum_name, pattern) {
584 return true;
585 }
586 }
587 false
588 }
589 PureExpr::Return(Some(e)) | PureExpr::Break { expr: Some(e), .. } => {
590 walk_expr_and_remove_arm(e, enum_name, pattern)
591 }
592 PureExpr::Let { expr: e, .. }
593 | PureExpr::Cast { expr: e, .. }
594 | PureExpr::Ref { expr: e, .. } => walk_expr_and_remove_arm(e, enum_name, pattern),
595 PureExpr::Range { start, end, .. } => {
596 if let Some(s) = start {
597 if walk_expr_and_remove_arm(s, enum_name, pattern) {
598 return true;
599 }
600 }
601 if let Some(e) = end {
602 if walk_expr_and_remove_arm(e, enum_name, pattern) {
603 return true;
604 }
605 }
606 false
607 }
608 PureExpr::Struct { fields, .. } => {
609 for (_, e) in fields {
610 if walk_expr_and_remove_arm(e, enum_name, pattern) {
611 return true;
612 }
613 }
614 false
615 }
616 PureExpr::Repeat { expr: e, len } => {
617 walk_expr_and_remove_arm(e, enum_name, pattern)
618 || walk_expr_and_remove_arm(len, enum_name, pattern)
619 }
620 _ => false,
621 }
622}
623
624impl ASTRegApply for ReplaceMatchArmMutation {
629 fn apply_to_registry(&self, ctx: &mut ASTMutationContext) -> MutationResult {
630 let fn_id = self.function_id;
632
633 let kind = ctx.symbol_registry.kind(fn_id);
635 if !matches!(kind, Some(SymbolKind::Function) | Some(SymbolKind::Method)) {
636 return MutationResult {
637 mutation_type: "ReplaceMatchArm".to_string(),
638 changes: 0,
639 description: format!("Symbol {} is not a function or method", fn_id),
640 };
641 }
642
643 let ast = match ctx.get_ast_mut(fn_id) {
645 Some(ast) => ast,
646 None => {
647 return MutationResult {
648 mutation_type: "ReplaceMatchArm".to_string(),
649 changes: 0,
650 description: format!("AST not found for function {}", fn_id),
651 };
652 }
653 };
654
655 if let PureItem::Fn(f) = ast {
657 if walk_and_replace_arm(
658 &mut f.body,
659 &self.enum_name,
660 &self.old_pattern,
661 &self.new_pattern,
662 &self.new_body,
663 ) {
664 ctx.emit_modified(fn_id, ModificationType::Other("MatchArmReplaced".into()));
665 return MutationResult {
666 mutation_type: "ReplaceMatchArm".to_string(),
667 changes: 1,
668 description: format!(
669 "Replaced match arm '{}' with '{}' in function {}",
670 self.old_pattern, self.new_pattern, fn_id
671 ),
672 };
673 }
674 }
675
676 MutationResult {
677 mutation_type: "ReplaceMatchArm".to_string(),
678 changes: 0,
679 description: format!(
680 "No match arm '{}' for '{}' found in function {}",
681 self.old_pattern, self.enum_name, fn_id
682 ),
683 }
684 }
685}
686
687fn walk_and_replace_arm(
693 block: &mut PureBlock,
694 enum_name: &str,
695 old_pattern: &str,
696 new_pattern: &str,
697 new_body: &str,
698) -> bool {
699 for stmt in &mut block.stmts {
700 if walk_stmt_and_replace_arm(stmt, enum_name, old_pattern, new_pattern, new_body) {
701 return true;
702 }
703 }
704 false
705}
706
707fn walk_stmt_and_replace_arm(
708 stmt: &mut PureStmt,
709 enum_name: &str,
710 old_pattern: &str,
711 new_pattern: &str,
712 new_body: &str,
713) -> bool {
714 match stmt {
715 PureStmt::Local {
716 init: Some(expr), ..
717 } => {
718 return walk_expr_and_replace_arm(expr, enum_name, old_pattern, new_pattern, new_body);
719 }
720 PureStmt::Expr(expr) | PureStmt::Semi(expr) => {
721 return walk_expr_and_replace_arm(expr, enum_name, old_pattern, new_pattern, new_body);
722 }
723 _ => {}
724 }
725 false
726}
727
728fn walk_expr_and_replace_arm(
729 expr: &mut PureExpr,
730 enum_name: &str,
731 old_pattern: &str,
732 new_pattern: &str,
733 new_body: &str,
734) -> bool {
735 if let PureExpr::Match {
737 expr: match_expr,
738 arms,
739 } = expr
740 {
741 if is_target_match(match_expr, arms, enum_name) {
742 for arm in arms.iter_mut() {
744 if pattern_matches(&arm.pattern, old_pattern) {
745 arm.pattern = parse_pattern(new_pattern);
747 arm.body = parse_body(new_body);
748 return true;
749 }
750 }
751 }
752 }
753
754 walk_expr_children_and_replace_arm(expr, enum_name, old_pattern, new_pattern, new_body)
756}
757
758fn pattern_matches(pattern: &PurePattern, target: &str) -> bool {
760 match pattern {
761 PurePattern::Path(path) => path == target,
762 PurePattern::Struct { path, fields, rest } => {
763 let is_tuple_struct = !*rest
766 && !fields.is_empty()
767 && fields.iter().all(|(name, _)| name.parse::<u32>().is_ok());
768
769 let pattern_str = if is_tuple_struct {
770 let field_strs: Vec<_> = fields
772 .iter()
773 .map(|(_, pat)| format_pattern_for_display(pat))
774 .collect();
775 format!("{}({})", path, field_strs.join(", "))
776 } else {
777 let mut s = path.clone();
779 s.push_str(" { ");
780 let field_strs: Vec<_> = fields
781 .iter()
782 .map(|(name, pat)| {
783 if matches!(pat, PurePattern::Ident { name: ident, .. } if ident == name) {
784 name.clone()
785 } else if matches!(pat, PurePattern::Wild) {
786 format!("{}: _", name)
787 } else {
788 format!("{}: {}", name, format_pattern_for_display(pat))
789 }
790 })
791 .collect();
792 s.push_str(&field_strs.join(", "));
793 if *rest {
794 if !fields.is_empty() {
795 s.push_str(", ");
796 }
797 s.push_str("..");
798 }
799 s.push_str(" }");
800 s
801 };
802
803 normalize_pattern(&pattern_str) == normalize_pattern(target)
804 }
805 PurePattern::Tuple(patterns) => {
806 let patterns_str: Vec<_> = patterns.iter().map(format_pattern_for_display).collect();
807 let pattern_str = format!("({})", patterns_str.join(", "));
808 normalize_pattern(&pattern_str) == normalize_pattern(target)
809 }
810 PurePattern::Wild => target == "_",
811 PurePattern::Ident { name, .. } => name == target,
812 PurePattern::Other(s) => normalize_pattern(s) == normalize_pattern(target),
813 _ => false,
814 }
815}
816
817fn format_pattern_for_display(pattern: &PurePattern) -> String {
819 match pattern {
820 PurePattern::Ident { name, .. } => name.clone(),
821 PurePattern::Wild => "_".to_string(),
822 PurePattern::Path(path) => path.clone(),
823 PurePattern::Rest => "..".to_string(),
824 PurePattern::Tuple(pats) => {
825 let inner: Vec<_> = pats.iter().map(format_pattern_for_display).collect();
826 format!("({})", inner.join(", "))
827 }
828 PurePattern::Or(pats) => {
829 let inner: Vec<_> = pats.iter().map(format_pattern_for_display).collect();
830 inner.join(" | ")
831 }
832 PurePattern::Struct { path, fields, rest } => {
833 let is_tuple_struct = !*rest
834 && !fields.is_empty()
835 && fields.iter().all(|(name, _)| name.parse::<u32>().is_ok());
836 if is_tuple_struct {
837 let inner: Vec<_> = fields
838 .iter()
839 .map(|(_, pat)| format_pattern_for_display(pat))
840 .collect();
841 format!("{}({})", path, inner.join(", "))
842 } else {
843 let inner: Vec<_> = fields
844 .iter()
845 .map(|(name, pat)| {
846 if matches!(pat, PurePattern::Ident { name: ident, .. } if ident == name) {
847 name.clone()
848 } else {
849 format!("{}: {}", name, format_pattern_for_display(pat))
850 }
851 })
852 .collect();
853 let rest_str = if *rest {
854 if inner.is_empty() {
855 ".."
856 } else {
857 ", .."
858 }
859 } else {
860 ""
861 };
862 format!("{} {{ {}{} }}", path, inner.join(", "), rest_str)
863 }
864 }
865 other => format!("{:?}", other),
866 }
867}
868
869fn normalize_pattern(s: &str) -> String {
871 let s = s.split_whitespace().collect::<Vec<_>>().join(" ");
872 normalize_field_shorthand(&s)
875}
876
877fn normalize_field_shorthand(s: &str) -> String {
882 let Some(brace_start) = s.find('{') else {
883 return s.to_string();
884 };
885 let Some(brace_end) = s.rfind('}') else {
886 return s.to_string();
887 };
888
889 let before = &s[..=brace_start];
890 let fields_str = &s[brace_start + 1..brace_end];
891 let after = &s[brace_end..];
892
893 let normalized_fields: Vec<String> = fields_str
894 .split(',')
895 .map(|field| {
896 let field = field.trim();
897 if field.is_empty() || field == ".." {
898 return field.to_string();
899 }
900 if let Some(colon_pos) = field.find(':') {
901 let name = field[..colon_pos].trim();
902 let value = field[colon_pos + 1..].trim();
903 if name == value {
904 name.to_string()
905 } else {
906 field.to_string()
907 }
908 } else {
909 field.to_string()
910 }
911 })
912 .collect();
913
914 format!(
915 "{} {} {}",
916 before.trim(),
917 normalized_fields.join(", "),
918 after.trim()
919 )
920}
921
922fn parse_pattern(pattern: &str) -> PurePattern {
924 let pattern = pattern.trim();
925
926 if let Some(brace_start) = pattern.find('{') {
928 if let Some(brace_end) = pattern.rfind('}') {
929 let path = pattern[..brace_start].trim().to_string();
930 let fields_str = &pattern[brace_start + 1..brace_end];
931
932 let mut fields = Vec::new();
933 let mut rest = false;
934
935 for field_part in fields_str.split(',') {
936 let field_part = field_part.trim();
937 if field_part.is_empty() {
938 continue;
939 }
940 if field_part == ".." {
941 rest = true;
942 continue;
943 }
944
945 if let Some(colon_pos) = field_part.find(':') {
946 let name = field_part[..colon_pos].trim().to_string();
947 let pat_str = field_part[colon_pos + 1..].trim();
948 let pat = if pat_str == "_" {
949 PurePattern::Wild
950 } else {
951 PurePattern::Ident {
952 name: pat_str.to_string(),
953 is_mut: false,
954 by_ref: false,
955 }
956 };
957 fields.push((name, pat));
958 } else {
959 let name = field_part.to_string();
961 fields.push((
962 name.clone(),
963 PurePattern::Ident {
964 name,
965 is_mut: false,
966 by_ref: false,
967 },
968 ));
969 }
970 }
971
972 return PurePattern::Struct { path, fields, rest };
973 }
974 }
975
976 if let Some(paren_start) = pattern.find('(') {
979 if paren_start > 0 && pattern.ends_with(')') {
980 let path = pattern[..paren_start].trim().to_string();
981 let inner = &pattern[paren_start + 1..pattern.len() - 1];
982 let fields: Vec<(String, PurePattern)> = inner
983 .split(',')
984 .enumerate()
985 .filter_map(|(i, s)| {
986 let s = s.trim();
987 if s.is_empty() {
988 return None;
989 }
990 let pat = if s == "_" {
991 PurePattern::Wild
992 } else {
993 PurePattern::Ident {
994 name: s.to_string(),
995 is_mut: false,
996 by_ref: false,
997 }
998 };
999 Some((i.to_string(), pat))
1000 })
1001 .collect();
1002 return PurePattern::Struct {
1003 path,
1004 fields,
1005 rest: false,
1006 };
1007 }
1008 }
1009
1010 if pattern.starts_with('(') && pattern.ends_with(')') {
1012 let inner = &pattern[1..pattern.len() - 1];
1013 let parts: Vec<_> = inner.split(',').map(|s| s.trim()).collect();
1014 if parts.len() > 1 || !inner.is_empty() {
1015 let patterns: Vec<_> = parts
1016 .iter()
1017 .map(|&s| {
1018 if s == "_" {
1019 PurePattern::Wild
1020 } else {
1021 PurePattern::Ident {
1022 name: s.to_string(),
1023 is_mut: false,
1024 by_ref: false,
1025 }
1026 }
1027 })
1028 .collect();
1029 return PurePattern::Tuple(patterns);
1030 }
1031 }
1032
1033 if pattern == "_" {
1035 return PurePattern::Wild;
1036 }
1037
1038 PurePattern::Path(pattern.to_string())
1040}
1041
1042fn parse_body(body: &str) -> PureExpr {
1044 let body_str = body.trim();
1045
1046 if let Some(bang_pos) = body_str.find('!') {
1048 let name = body_str[..bang_pos].trim();
1049 let rest = body_str[bang_pos + 1..].trim();
1050
1051 let is_valid_ident = !name.is_empty()
1053 && name.chars().all(|c| c.is_alphanumeric() || c == '_')
1054 && name
1055 .chars()
1056 .next()
1057 .map(|c| c.is_alphabetic() || c == '_')
1058 .unwrap_or(false);
1059 let has_delimiter = rest.starts_with('(') || rest.starts_with('{') || rest.starts_with('[');
1060
1061 if is_valid_ident && has_delimiter {
1062 let (delimiter, tokens) = if rest.starts_with('(') && rest.ends_with(')') {
1063 (MacroDelimiter::Paren, rest[1..rest.len() - 1].to_string())
1064 } else if rest.starts_with('{') && rest.ends_with('}') {
1065 (MacroDelimiter::Brace, rest[1..rest.len() - 1].to_string())
1066 } else if rest.starts_with('[') && rest.ends_with(']') {
1067 (MacroDelimiter::Bracket, rest[1..rest.len() - 1].to_string())
1068 } else {
1069 (MacroDelimiter::Paren, String::new())
1070 };
1071 return PureExpr::Macro {
1072 name: name.to_string(),
1073 delimiter,
1074 tokens,
1075 };
1076 }
1077 }
1078
1079 PureExpr::Other(body.to_string())
1081}
1082
1083fn walk_expr_children_and_replace_arm(
1084 expr: &mut PureExpr,
1085 enum_name: &str,
1086 old_pattern: &str,
1087 new_pattern: &str,
1088 new_body: &str,
1089) -> bool {
1090 match expr {
1091 PureExpr::Block { block, .. } => {
1092 walk_and_replace_arm(block, enum_name, old_pattern, new_pattern, new_body)
1093 }
1094 PureExpr::If {
1095 cond,
1096 then_branch,
1097 else_branch,
1098 } => {
1099 if walk_expr_and_replace_arm(cond, enum_name, old_pattern, new_pattern, new_body) {
1100 return true;
1101 }
1102 if walk_and_replace_arm(then_branch, enum_name, old_pattern, new_pattern, new_body) {
1103 return true;
1104 }
1105 if let Some(else_expr) = else_branch {
1106 if walk_expr_and_replace_arm(
1107 else_expr,
1108 enum_name,
1109 old_pattern,
1110 new_pattern,
1111 new_body,
1112 ) {
1113 return true;
1114 }
1115 }
1116 false
1117 }
1118 PureExpr::Match { expr: e, arms } => {
1119 if walk_expr_and_replace_arm(e, enum_name, old_pattern, new_pattern, new_body) {
1120 return true;
1121 }
1122 for arm in arms {
1123 if walk_expr_and_replace_arm(
1124 &mut arm.body,
1125 enum_name,
1126 old_pattern,
1127 new_pattern,
1128 new_body,
1129 ) {
1130 return true;
1131 }
1132 }
1133 false
1134 }
1135 PureExpr::Loop { body: block, .. } | PureExpr::Unsafe(block) => {
1136 walk_and_replace_arm(block, enum_name, old_pattern, new_pattern, new_body)
1137 }
1138 PureExpr::While { cond, body, .. } => {
1139 walk_expr_and_replace_arm(cond, enum_name, old_pattern, new_pattern, new_body)
1140 || walk_and_replace_arm(body, enum_name, old_pattern, new_pattern, new_body)
1141 }
1142 PureExpr::For { expr: e, body, .. } => {
1143 walk_expr_and_replace_arm(e, enum_name, old_pattern, new_pattern, new_body)
1144 || walk_and_replace_arm(body, enum_name, old_pattern, new_pattern, new_body)
1145 }
1146 PureExpr::Async { body, .. } => {
1147 walk_and_replace_arm(body, enum_name, old_pattern, new_pattern, new_body)
1148 }
1149 PureExpr::Closure { body, .. } => {
1150 walk_expr_and_replace_arm(body, enum_name, old_pattern, new_pattern, new_body)
1151 }
1152 PureExpr::Call { func, args } => {
1153 if walk_expr_and_replace_arm(func, enum_name, old_pattern, new_pattern, new_body) {
1154 return true;
1155 }
1156 for arg in args {
1157 if walk_expr_and_replace_arm(arg, enum_name, old_pattern, new_pattern, new_body) {
1158 return true;
1159 }
1160 }
1161 false
1162 }
1163 PureExpr::MethodCall { receiver, args, .. } => {
1164 if walk_expr_and_replace_arm(receiver, enum_name, old_pattern, new_pattern, new_body) {
1165 return true;
1166 }
1167 for arg in args {
1168 if walk_expr_and_replace_arm(arg, enum_name, old_pattern, new_pattern, new_body) {
1169 return true;
1170 }
1171 }
1172 false
1173 }
1174 PureExpr::Binary { left, right, .. } => {
1175 walk_expr_and_replace_arm(left, enum_name, old_pattern, new_pattern, new_body)
1176 || walk_expr_and_replace_arm(right, enum_name, old_pattern, new_pattern, new_body)
1177 }
1178 PureExpr::Unary { expr: e, .. }
1179 | PureExpr::Field { expr: e, .. }
1180 | PureExpr::Await(e)
1181 | PureExpr::Try(e) => {
1182 walk_expr_and_replace_arm(e, enum_name, old_pattern, new_pattern, new_body)
1183 }
1184 PureExpr::Index { expr: e, index } => {
1185 walk_expr_and_replace_arm(e, enum_name, old_pattern, new_pattern, new_body)
1186 || walk_expr_and_replace_arm(index, enum_name, old_pattern, new_pattern, new_body)
1187 }
1188 PureExpr::Tuple(exprs) | PureExpr::Array(exprs) => {
1189 for e in exprs {
1190 if walk_expr_and_replace_arm(e, enum_name, old_pattern, new_pattern, new_body) {
1191 return true;
1192 }
1193 }
1194 false
1195 }
1196 PureExpr::Return(Some(e)) | PureExpr::Break { expr: Some(e), .. } => {
1197 walk_expr_and_replace_arm(e, enum_name, old_pattern, new_pattern, new_body)
1198 }
1199 PureExpr::Let { expr: e, .. }
1200 | PureExpr::Cast { expr: e, .. }
1201 | PureExpr::Ref { expr: e, .. } => {
1202 walk_expr_and_replace_arm(e, enum_name, old_pattern, new_pattern, new_body)
1203 }
1204 PureExpr::Range { start, end, .. } => {
1205 if let Some(s) = start {
1206 if walk_expr_and_replace_arm(s, enum_name, old_pattern, new_pattern, new_body) {
1207 return true;
1208 }
1209 }
1210 if let Some(e) = end {
1211 if walk_expr_and_replace_arm(e, enum_name, old_pattern, new_pattern, new_body) {
1212 return true;
1213 }
1214 }
1215 false
1216 }
1217 PureExpr::Struct { fields, .. } => {
1218 for (_, e) in fields {
1219 if walk_expr_and_replace_arm(e, enum_name, old_pattern, new_pattern, new_body) {
1220 return true;
1221 }
1222 }
1223 false
1224 }
1225 PureExpr::Repeat { expr: e, len } => {
1226 walk_expr_and_replace_arm(e, enum_name, old_pattern, new_pattern, new_body)
1227 || walk_expr_and_replace_arm(len, enum_name, old_pattern, new_pattern, new_body)
1228 }
1229 _ => false,
1230 }
1231}
1232
1233#[cfg(test)]
1234mod tests {
1235 use super::*;
1236
1237 #[test]
1238 fn normalize_simple_expr_unchanged() {
1239 assert_eq!(normalize_body_as_expr("Ok(42)"), "Ok(42)");
1240 }
1241
1242 #[test]
1243 fn normalize_block_expr_unchanged() {
1244 assert_eq!(
1245 normalize_body_as_expr("{ let v = 1; v + 1 }"),
1246 "{ let v = 1; v + 1 }"
1247 );
1248 }
1249
1250 #[test]
1251 fn normalize_stmt_sequence_wrapped() {
1252 assert_eq!(
1253 normalize_body_as_expr("let v = x; if v { a } else { b }"),
1254 "{ let v = x; if v { a } else { b } }"
1255 );
1256 }
1257
1258 #[test]
1259 fn normalize_let_match_wrapped() {
1260 assert_eq!(
1261 normalize_body_as_expr("let v = get(); match v { A => 1, _ => 2 }"),
1262 "{ let v = get(); match v { A => 1, _ => 2 } }"
1263 );
1264 }
1265
1266 #[test]
1267 fn normalize_whitespace_trimmed() {
1268 assert_eq!(normalize_body_as_expr(" Ok(1) "), "Ok(1)");
1269 }
1270
1271 #[test]
1276 fn pattern_matches_path() {
1277 let pat = PurePattern::Path("Status::Active".to_string());
1278 assert!(pattern_matches(&pat, "Status::Active"));
1279 assert!(!pattern_matches(&pat, "Status::Inactive"));
1280 }
1281
1282 #[test]
1283 fn pattern_matches_tuple_struct() {
1284 let pat = PurePattern::Struct {
1286 path: "Message::Text".to_string(),
1287 fields: vec![(
1288 "0".to_string(),
1289 PurePattern::Ident {
1290 name: "s".to_string(),
1291 is_mut: false,
1292 by_ref: false,
1293 },
1294 )],
1295 rest: false,
1296 };
1297 assert!(pattern_matches(&pat, "Message::Text(s)"));
1298 assert!(!pattern_matches(&pat, "Message::Text { s }"));
1299 assert!(!pattern_matches(&pat, "Message::Number(n)"));
1300 }
1301
1302 #[test]
1303 fn pattern_matches_tuple_struct_multi() {
1304 let pat = PurePattern::Struct {
1305 path: "Foo::Bar".to_string(),
1306 fields: vec![
1307 (
1308 "0".to_string(),
1309 PurePattern::Ident {
1310 name: "a".to_string(),
1311 is_mut: false,
1312 by_ref: false,
1313 },
1314 ),
1315 (
1316 "1".to_string(),
1317 PurePattern::Ident {
1318 name: "b".to_string(),
1319 is_mut: false,
1320 by_ref: false,
1321 },
1322 ),
1323 ],
1324 rest: false,
1325 };
1326 assert!(pattern_matches(&pat, "Foo::Bar(a, b)"));
1327 }
1328
1329 #[test]
1330 fn pattern_matches_tuple_struct_wildcard() {
1331 let pat = PurePattern::Struct {
1332 path: "Some".to_string(),
1333 fields: vec![("0".to_string(), PurePattern::Wild)],
1334 rest: false,
1335 };
1336 assert!(pattern_matches(&pat, "Some(_)"));
1337 }
1338
1339 #[test]
1340 fn pattern_matches_named_struct() {
1341 let pat = PurePattern::Struct {
1342 path: "Point".to_string(),
1343 fields: vec![
1344 (
1345 "x".to_string(),
1346 PurePattern::Ident {
1347 name: "x".to_string(),
1348 is_mut: false,
1349 by_ref: false,
1350 },
1351 ),
1352 (
1353 "y".to_string(),
1354 PurePattern::Ident {
1355 name: "y".to_string(),
1356 is_mut: false,
1357 by_ref: false,
1358 },
1359 ),
1360 ],
1361 rest: false,
1362 };
1363 assert!(pattern_matches(&pat, "Point { x, y }"));
1364 assert!(
1366 pattern_matches(&pat, "Point { x: x, y: y }"),
1367 "Explicit field binding should match shorthand equivalent"
1368 );
1369 }
1370
1371 #[test]
1372 fn pattern_matches_wild() {
1373 assert!(pattern_matches(&PurePattern::Wild, "_"));
1374 assert!(!pattern_matches(&PurePattern::Wild, "x"));
1375 }
1376
1377 #[test]
1382 fn parse_pattern_path() {
1383 let pat = parse_pattern("Status::Active");
1384 assert!(matches!(pat, PurePattern::Path(p) if p == "Status::Active"));
1385 }
1386
1387 #[test]
1388 fn parse_pattern_tuple_struct() {
1389 let pat = parse_pattern("Message::Text(s)");
1390 if let PurePattern::Struct { path, fields, rest } = &pat {
1391 assert_eq!(path, "Message::Text");
1392 assert_eq!(fields.len(), 1);
1393 assert_eq!(fields[0].0, "0");
1394 assert!(matches!(&fields[0].1, PurePattern::Ident { name, .. } if name == "s"));
1395 assert!(!rest);
1396 } else {
1397 panic!("Expected Struct (TupleStruct), got {:?}", pat);
1398 }
1399 }
1400
1401 #[test]
1402 fn parse_pattern_tuple_struct_wild() {
1403 let pat = parse_pattern("Some(_)");
1404 if let PurePattern::Struct { path, fields, rest } = &pat {
1405 assert_eq!(path, "Some");
1406 assert_eq!(fields.len(), 1);
1407 assert_eq!(fields[0].0, "0");
1408 assert!(matches!(&fields[0].1, PurePattern::Wild));
1409 assert!(!rest);
1410 } else {
1411 panic!("Expected Struct (TupleStruct), got {:?}", pat);
1412 }
1413 }
1414
1415 #[test]
1416 fn parse_pattern_wildcard() {
1417 assert!(matches!(parse_pattern("_"), PurePattern::Wild));
1418 }
1419
1420 #[test]
1421 fn parse_pattern_struct() {
1422 let pat = parse_pattern("Point { x, y }");
1423 if let PurePattern::Struct { path, fields, rest } = &pat {
1424 assert_eq!(path, "Point");
1425 assert_eq!(fields.len(), 2);
1426 assert!(!rest);
1427 } else {
1428 panic!("Expected Struct, got {:?}", pat);
1429 }
1430 }
1431
1432 #[test]
1437 fn pattern_matches_other_variant() {
1438 let pat = PurePattern::Other("Filter::Map(_)".to_string());
1439 assert!(pattern_matches(&pat, "Filter::Map(_)"));
1440 assert!(!pattern_matches(&pat, "Filter::Exclude(_)"));
1441 }
1442
1443 #[test]
1448 fn path_segment_exact_match() {
1449 assert!(path_has_enum_segment("Filter::Recurse", "Filter"));
1450 assert!(path_has_enum_segment("Filter", "Filter"));
1451 }
1452
1453 #[test]
1454 fn path_segment_no_substring_match() {
1455 assert!(!path_has_enum_segment("FilterKind::Inclusive", "Filter"));
1457 assert!(!path_has_enum_segment("MyFilter::Recurse", "Filter"));
1458 }
1459
1460 #[test]
1461 fn path_segment_middle_match() {
1462 assert!(path_has_enum_segment("module::Filter::Recurse", "Filter"));
1463 }
1464
1465 #[test]
1470 fn pattern_contains_enum_path() {
1471 let pat = PurePattern::Path("Filter::Recurse".to_string());
1472 assert!(pattern_contains_enum(&pat, "Filter"));
1473 assert!(!pattern_contains_enum(&pat, "FilterKind"));
1474 }
1475
1476 #[test]
1477 fn pattern_contains_enum_struct() {
1478 let pat = PurePattern::Struct {
1479 path: "FilterKind::Inclusive".to_string(),
1480 fields: vec![],
1481 rest: false,
1482 };
1483 assert!(pattern_contains_enum(&pat, "FilterKind"));
1484 assert!(!pattern_contains_enum(&pat, "Filter"));
1486 }
1487
1488 #[test]
1489 fn pattern_contains_enum_other() {
1490 let pat = PurePattern::Other("Filter::Map(_)".to_string());
1491 assert!(pattern_contains_enum(&pat, "Filter"));
1492 assert!(!pattern_contains_enum(&pat, "FilterKind"));
1493 }
1494
1495 #[test]
1500 fn normalize_shorthand_explicit_to_shorthand() {
1501 assert_eq!(
1502 normalize_pattern("Filter::Slice { start: start, end: end }"),
1503 "Filter::Slice { start, end }"
1504 );
1505 }
1506
1507 #[test]
1508 fn normalize_shorthand_already_short() {
1509 assert_eq!(
1510 normalize_pattern("Filter::Slice { start, end }"),
1511 "Filter::Slice { start, end }"
1512 );
1513 }
1514
1515 #[test]
1516 fn normalize_shorthand_different_binding_unchanged() {
1517 assert_eq!(
1518 normalize_pattern("Filter::Slice { start: s, end: e }"),
1519 "Filter::Slice { start: s, end: e }"
1520 );
1521 }
1522
1523 #[test]
1524 fn normalize_shorthand_mixed() {
1525 assert_eq!(
1526 normalize_pattern("Foo { a: a, b: other, c }"),
1527 "Foo { a, b: other, c }"
1528 );
1529 }
1530
1531 #[test]
1532 fn normalize_shorthand_with_rest() {
1533 assert_eq!(normalize_pattern("Foo { x: x, .. }"), "Foo { x, .. }");
1534 }
1535
1536 #[test]
1537 fn normalize_shorthand_no_braces() {
1538 assert_eq!(normalize_pattern("Some(x)"), "Some(x)");
1539 }
1540
1541 #[test]
1546 fn replace_match_arm_struct_pattern() {
1547 use crate::engine::ASTMutationEngine;
1548 use ryo_analysis::testing::ContextBuilder;
1549 use ryo_mutations::basic::ReplaceMatchArmMutation;
1550
1551 let mut ctx = ContextBuilder::new()
1552 .with_file(
1553 "src/lib.rs",
1554 r#"
1555enum Shape {
1556 Circle { radius: f64 },
1557 Rect { width: f64, height: f64 },
1558}
1559
1560fn area(s: &Shape) -> f64 {
1561 match s {
1562 Shape::Circle { radius } => std::f64::consts::PI * radius * radius,
1563 Shape::Rect { width, height } => width * height,
1564 }
1565}
1566"#,
1567 )
1568 .build();
1569
1570 let area_id = ctx
1571 .registry
1572 .iter()
1573 .find(|(id, path)| {
1574 matches!(ctx.registry.kind(*id), Some(SymbolKind::Function))
1575 && path.name() == "area"
1576 })
1577 .map(|(id, _)| id)
1578 .expect("area function not found");
1579
1580 let mutation = ReplaceMatchArmMutation {
1582 function_id: area_id,
1583 enum_name: "Shape".to_string(),
1584 old_pattern: "Shape::Rect { width: width, height: height }".to_string(),
1585 new_pattern: "Shape::Rect { width, height }".to_string(),
1586 new_body: "width * height * 2.0".to_string(),
1587 };
1588
1589 let result = ASTMutationEngine::execute_ast_reg(&mutation, &mut ctx);
1590 assert_eq!(
1591 result.result.changes, 1,
1592 "Struct pattern with explicit binding should match: {}",
1593 result.result.description
1594 );
1595 }
1596}