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 }
955 };
956 fields.push((name, pat));
957 } else {
958 let name = field_part.to_string();
960 fields.push((
961 name.clone(),
962 PurePattern::Ident {
963 name,
964 is_mut: false,
965 },
966 ));
967 }
968 }
969
970 return PurePattern::Struct { path, fields, rest };
971 }
972 }
973
974 if let Some(paren_start) = pattern.find('(') {
977 if paren_start > 0 && pattern.ends_with(')') {
978 let path = pattern[..paren_start].trim().to_string();
979 let inner = &pattern[paren_start + 1..pattern.len() - 1];
980 let fields: Vec<(String, PurePattern)> = inner
981 .split(',')
982 .enumerate()
983 .filter_map(|(i, s)| {
984 let s = s.trim();
985 if s.is_empty() {
986 return None;
987 }
988 let pat = if s == "_" {
989 PurePattern::Wild
990 } else {
991 PurePattern::Ident {
992 name: s.to_string(),
993 is_mut: false,
994 }
995 };
996 Some((i.to_string(), pat))
997 })
998 .collect();
999 return PurePattern::Struct {
1000 path,
1001 fields,
1002 rest: false,
1003 };
1004 }
1005 }
1006
1007 if pattern.starts_with('(') && pattern.ends_with(')') {
1009 let inner = &pattern[1..pattern.len() - 1];
1010 let parts: Vec<_> = inner.split(',').map(|s| s.trim()).collect();
1011 if parts.len() > 1 || !inner.is_empty() {
1012 let patterns: Vec<_> = parts
1013 .iter()
1014 .map(|&s| {
1015 if s == "_" {
1016 PurePattern::Wild
1017 } else {
1018 PurePattern::Ident {
1019 name: s.to_string(),
1020 is_mut: false,
1021 }
1022 }
1023 })
1024 .collect();
1025 return PurePattern::Tuple(patterns);
1026 }
1027 }
1028
1029 if pattern == "_" {
1031 return PurePattern::Wild;
1032 }
1033
1034 PurePattern::Path(pattern.to_string())
1036}
1037
1038fn parse_body(body: &str) -> PureExpr {
1040 let body_str = body.trim();
1041
1042 if let Some(bang_pos) = body_str.find('!') {
1044 let name = body_str[..bang_pos].trim();
1045 let rest = body_str[bang_pos + 1..].trim();
1046
1047 let is_valid_ident = !name.is_empty()
1049 && name.chars().all(|c| c.is_alphanumeric() || c == '_')
1050 && name
1051 .chars()
1052 .next()
1053 .map(|c| c.is_alphabetic() || c == '_')
1054 .unwrap_or(false);
1055 let has_delimiter = rest.starts_with('(') || rest.starts_with('{') || rest.starts_with('[');
1056
1057 if is_valid_ident && has_delimiter {
1058 let (delimiter, tokens) = if rest.starts_with('(') && rest.ends_with(')') {
1059 (MacroDelimiter::Paren, rest[1..rest.len() - 1].to_string())
1060 } else if rest.starts_with('{') && rest.ends_with('}') {
1061 (MacroDelimiter::Brace, rest[1..rest.len() - 1].to_string())
1062 } else if rest.starts_with('[') && rest.ends_with(']') {
1063 (MacroDelimiter::Bracket, rest[1..rest.len() - 1].to_string())
1064 } else {
1065 (MacroDelimiter::Paren, String::new())
1066 };
1067 return PureExpr::Macro {
1068 name: name.to_string(),
1069 delimiter,
1070 tokens,
1071 };
1072 }
1073 }
1074
1075 PureExpr::Other(body.to_string())
1077}
1078
1079fn walk_expr_children_and_replace_arm(
1080 expr: &mut PureExpr,
1081 enum_name: &str,
1082 old_pattern: &str,
1083 new_pattern: &str,
1084 new_body: &str,
1085) -> bool {
1086 match expr {
1087 PureExpr::Block { block, .. } => {
1088 walk_and_replace_arm(block, enum_name, old_pattern, new_pattern, new_body)
1089 }
1090 PureExpr::If {
1091 cond,
1092 then_branch,
1093 else_branch,
1094 } => {
1095 if walk_expr_and_replace_arm(cond, enum_name, old_pattern, new_pattern, new_body) {
1096 return true;
1097 }
1098 if walk_and_replace_arm(then_branch, enum_name, old_pattern, new_pattern, new_body) {
1099 return true;
1100 }
1101 if let Some(else_expr) = else_branch {
1102 if walk_expr_and_replace_arm(
1103 else_expr,
1104 enum_name,
1105 old_pattern,
1106 new_pattern,
1107 new_body,
1108 ) {
1109 return true;
1110 }
1111 }
1112 false
1113 }
1114 PureExpr::Match { expr: e, arms } => {
1115 if walk_expr_and_replace_arm(e, enum_name, old_pattern, new_pattern, new_body) {
1116 return true;
1117 }
1118 for arm in arms {
1119 if walk_expr_and_replace_arm(
1120 &mut arm.body,
1121 enum_name,
1122 old_pattern,
1123 new_pattern,
1124 new_body,
1125 ) {
1126 return true;
1127 }
1128 }
1129 false
1130 }
1131 PureExpr::Loop { body: block, .. } | PureExpr::Unsafe(block) => {
1132 walk_and_replace_arm(block, enum_name, old_pattern, new_pattern, new_body)
1133 }
1134 PureExpr::While { cond, body, .. } => {
1135 walk_expr_and_replace_arm(cond, enum_name, old_pattern, new_pattern, new_body)
1136 || walk_and_replace_arm(body, enum_name, old_pattern, new_pattern, new_body)
1137 }
1138 PureExpr::For { expr: e, body, .. } => {
1139 walk_expr_and_replace_arm(e, 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::Async { body, .. } => {
1143 walk_and_replace_arm(body, enum_name, old_pattern, new_pattern, new_body)
1144 }
1145 PureExpr::Closure { body, .. } => {
1146 walk_expr_and_replace_arm(body, enum_name, old_pattern, new_pattern, new_body)
1147 }
1148 PureExpr::Call { func, args } => {
1149 if walk_expr_and_replace_arm(func, enum_name, old_pattern, new_pattern, new_body) {
1150 return true;
1151 }
1152 for arg in args {
1153 if walk_expr_and_replace_arm(arg, enum_name, old_pattern, new_pattern, new_body) {
1154 return true;
1155 }
1156 }
1157 false
1158 }
1159 PureExpr::MethodCall { receiver, args, .. } => {
1160 if walk_expr_and_replace_arm(receiver, enum_name, old_pattern, new_pattern, new_body) {
1161 return true;
1162 }
1163 for arg in args {
1164 if walk_expr_and_replace_arm(arg, enum_name, old_pattern, new_pattern, new_body) {
1165 return true;
1166 }
1167 }
1168 false
1169 }
1170 PureExpr::Binary { left, right, .. } => {
1171 walk_expr_and_replace_arm(left, enum_name, old_pattern, new_pattern, new_body)
1172 || walk_expr_and_replace_arm(right, enum_name, old_pattern, new_pattern, new_body)
1173 }
1174 PureExpr::Unary { expr: e, .. }
1175 | PureExpr::Field { expr: e, .. }
1176 | PureExpr::Await(e)
1177 | PureExpr::Try(e) => {
1178 walk_expr_and_replace_arm(e, enum_name, old_pattern, new_pattern, new_body)
1179 }
1180 PureExpr::Index { expr: e, index } => {
1181 walk_expr_and_replace_arm(e, enum_name, old_pattern, new_pattern, new_body)
1182 || walk_expr_and_replace_arm(index, enum_name, old_pattern, new_pattern, new_body)
1183 }
1184 PureExpr::Tuple(exprs) | PureExpr::Array(exprs) => {
1185 for e in exprs {
1186 if walk_expr_and_replace_arm(e, enum_name, old_pattern, new_pattern, new_body) {
1187 return true;
1188 }
1189 }
1190 false
1191 }
1192 PureExpr::Return(Some(e)) | PureExpr::Break { expr: Some(e), .. } => {
1193 walk_expr_and_replace_arm(e, enum_name, old_pattern, new_pattern, new_body)
1194 }
1195 PureExpr::Let { expr: e, .. }
1196 | PureExpr::Cast { expr: e, .. }
1197 | PureExpr::Ref { expr: e, .. } => {
1198 walk_expr_and_replace_arm(e, enum_name, old_pattern, new_pattern, new_body)
1199 }
1200 PureExpr::Range { start, end, .. } => {
1201 if let Some(s) = start {
1202 if walk_expr_and_replace_arm(s, enum_name, old_pattern, new_pattern, new_body) {
1203 return true;
1204 }
1205 }
1206 if let Some(e) = end {
1207 if walk_expr_and_replace_arm(e, enum_name, old_pattern, new_pattern, new_body) {
1208 return true;
1209 }
1210 }
1211 false
1212 }
1213 PureExpr::Struct { fields, .. } => {
1214 for (_, e) in fields {
1215 if walk_expr_and_replace_arm(e, enum_name, old_pattern, new_pattern, new_body) {
1216 return true;
1217 }
1218 }
1219 false
1220 }
1221 PureExpr::Repeat { expr: e, len } => {
1222 walk_expr_and_replace_arm(e, enum_name, old_pattern, new_pattern, new_body)
1223 || walk_expr_and_replace_arm(len, enum_name, old_pattern, new_pattern, new_body)
1224 }
1225 _ => false,
1226 }
1227}
1228
1229#[cfg(test)]
1230mod tests {
1231 use super::*;
1232
1233 #[test]
1234 fn normalize_simple_expr_unchanged() {
1235 assert_eq!(normalize_body_as_expr("Ok(42)"), "Ok(42)");
1236 }
1237
1238 #[test]
1239 fn normalize_block_expr_unchanged() {
1240 assert_eq!(
1241 normalize_body_as_expr("{ let v = 1; v + 1 }"),
1242 "{ let v = 1; v + 1 }"
1243 );
1244 }
1245
1246 #[test]
1247 fn normalize_stmt_sequence_wrapped() {
1248 assert_eq!(
1249 normalize_body_as_expr("let v = x; if v { a } else { b }"),
1250 "{ let v = x; if v { a } else { b } }"
1251 );
1252 }
1253
1254 #[test]
1255 fn normalize_let_match_wrapped() {
1256 assert_eq!(
1257 normalize_body_as_expr("let v = get(); match v { A => 1, _ => 2 }"),
1258 "{ let v = get(); match v { A => 1, _ => 2 } }"
1259 );
1260 }
1261
1262 #[test]
1263 fn normalize_whitespace_trimmed() {
1264 assert_eq!(normalize_body_as_expr(" Ok(1) "), "Ok(1)");
1265 }
1266
1267 #[test]
1272 fn pattern_matches_path() {
1273 let pat = PurePattern::Path("Status::Active".to_string());
1274 assert!(pattern_matches(&pat, "Status::Active"));
1275 assert!(!pattern_matches(&pat, "Status::Inactive"));
1276 }
1277
1278 #[test]
1279 fn pattern_matches_tuple_struct() {
1280 let pat = PurePattern::Struct {
1282 path: "Message::Text".to_string(),
1283 fields: vec![(
1284 "0".to_string(),
1285 PurePattern::Ident {
1286 name: "s".to_string(),
1287 is_mut: false,
1288 },
1289 )],
1290 rest: false,
1291 };
1292 assert!(pattern_matches(&pat, "Message::Text(s)"));
1293 assert!(!pattern_matches(&pat, "Message::Text { s }"));
1294 assert!(!pattern_matches(&pat, "Message::Number(n)"));
1295 }
1296
1297 #[test]
1298 fn pattern_matches_tuple_struct_multi() {
1299 let pat = PurePattern::Struct {
1300 path: "Foo::Bar".to_string(),
1301 fields: vec![
1302 (
1303 "0".to_string(),
1304 PurePattern::Ident {
1305 name: "a".to_string(),
1306 is_mut: false,
1307 },
1308 ),
1309 (
1310 "1".to_string(),
1311 PurePattern::Ident {
1312 name: "b".to_string(),
1313 is_mut: false,
1314 },
1315 ),
1316 ],
1317 rest: false,
1318 };
1319 assert!(pattern_matches(&pat, "Foo::Bar(a, b)"));
1320 }
1321
1322 #[test]
1323 fn pattern_matches_tuple_struct_wildcard() {
1324 let pat = PurePattern::Struct {
1325 path: "Some".to_string(),
1326 fields: vec![("0".to_string(), PurePattern::Wild)],
1327 rest: false,
1328 };
1329 assert!(pattern_matches(&pat, "Some(_)"));
1330 }
1331
1332 #[test]
1333 fn pattern_matches_named_struct() {
1334 let pat = PurePattern::Struct {
1335 path: "Point".to_string(),
1336 fields: vec![
1337 (
1338 "x".to_string(),
1339 PurePattern::Ident {
1340 name: "x".to_string(),
1341 is_mut: false,
1342 },
1343 ),
1344 (
1345 "y".to_string(),
1346 PurePattern::Ident {
1347 name: "y".to_string(),
1348 is_mut: false,
1349 },
1350 ),
1351 ],
1352 rest: false,
1353 };
1354 assert!(pattern_matches(&pat, "Point { x, y }"));
1355 assert!(
1357 pattern_matches(&pat, "Point { x: x, y: y }"),
1358 "Explicit field binding should match shorthand equivalent"
1359 );
1360 }
1361
1362 #[test]
1363 fn pattern_matches_wild() {
1364 assert!(pattern_matches(&PurePattern::Wild, "_"));
1365 assert!(!pattern_matches(&PurePattern::Wild, "x"));
1366 }
1367
1368 #[test]
1373 fn parse_pattern_path() {
1374 let pat = parse_pattern("Status::Active");
1375 assert!(matches!(pat, PurePattern::Path(p) if p == "Status::Active"));
1376 }
1377
1378 #[test]
1379 fn parse_pattern_tuple_struct() {
1380 let pat = parse_pattern("Message::Text(s)");
1381 if let PurePattern::Struct { path, fields, rest } = &pat {
1382 assert_eq!(path, "Message::Text");
1383 assert_eq!(fields.len(), 1);
1384 assert_eq!(fields[0].0, "0");
1385 assert!(matches!(&fields[0].1, PurePattern::Ident { name, .. } if name == "s"));
1386 assert!(!rest);
1387 } else {
1388 panic!("Expected Struct (TupleStruct), got {:?}", pat);
1389 }
1390 }
1391
1392 #[test]
1393 fn parse_pattern_tuple_struct_wild() {
1394 let pat = parse_pattern("Some(_)");
1395 if let PurePattern::Struct { path, fields, rest } = &pat {
1396 assert_eq!(path, "Some");
1397 assert_eq!(fields.len(), 1);
1398 assert_eq!(fields[0].0, "0");
1399 assert!(matches!(&fields[0].1, PurePattern::Wild));
1400 assert!(!rest);
1401 } else {
1402 panic!("Expected Struct (TupleStruct), got {:?}", pat);
1403 }
1404 }
1405
1406 #[test]
1407 fn parse_pattern_wildcard() {
1408 assert!(matches!(parse_pattern("_"), PurePattern::Wild));
1409 }
1410
1411 #[test]
1412 fn parse_pattern_struct() {
1413 let pat = parse_pattern("Point { x, y }");
1414 if let PurePattern::Struct { path, fields, rest } = &pat {
1415 assert_eq!(path, "Point");
1416 assert_eq!(fields.len(), 2);
1417 assert!(!rest);
1418 } else {
1419 panic!("Expected Struct, got {:?}", pat);
1420 }
1421 }
1422
1423 #[test]
1428 fn pattern_matches_other_variant() {
1429 let pat = PurePattern::Other("Filter::Map(_)".to_string());
1430 assert!(pattern_matches(&pat, "Filter::Map(_)"));
1431 assert!(!pattern_matches(&pat, "Filter::Exclude(_)"));
1432 }
1433
1434 #[test]
1439 fn path_segment_exact_match() {
1440 assert!(path_has_enum_segment("Filter::Recurse", "Filter"));
1441 assert!(path_has_enum_segment("Filter", "Filter"));
1442 }
1443
1444 #[test]
1445 fn path_segment_no_substring_match() {
1446 assert!(!path_has_enum_segment("FilterKind::Inclusive", "Filter"));
1448 assert!(!path_has_enum_segment("MyFilter::Recurse", "Filter"));
1449 }
1450
1451 #[test]
1452 fn path_segment_middle_match() {
1453 assert!(path_has_enum_segment("module::Filter::Recurse", "Filter"));
1454 }
1455
1456 #[test]
1461 fn pattern_contains_enum_path() {
1462 let pat = PurePattern::Path("Filter::Recurse".to_string());
1463 assert!(pattern_contains_enum(&pat, "Filter"));
1464 assert!(!pattern_contains_enum(&pat, "FilterKind"));
1465 }
1466
1467 #[test]
1468 fn pattern_contains_enum_struct() {
1469 let pat = PurePattern::Struct {
1470 path: "FilterKind::Inclusive".to_string(),
1471 fields: vec![],
1472 rest: false,
1473 };
1474 assert!(pattern_contains_enum(&pat, "FilterKind"));
1475 assert!(!pattern_contains_enum(&pat, "Filter"));
1477 }
1478
1479 #[test]
1480 fn pattern_contains_enum_other() {
1481 let pat = PurePattern::Other("Filter::Map(_)".to_string());
1482 assert!(pattern_contains_enum(&pat, "Filter"));
1483 assert!(!pattern_contains_enum(&pat, "FilterKind"));
1484 }
1485
1486 #[test]
1491 fn normalize_shorthand_explicit_to_shorthand() {
1492 assert_eq!(
1493 normalize_pattern("Filter::Slice { start: start, end: end }"),
1494 "Filter::Slice { start, end }"
1495 );
1496 }
1497
1498 #[test]
1499 fn normalize_shorthand_already_short() {
1500 assert_eq!(
1501 normalize_pattern("Filter::Slice { start, end }"),
1502 "Filter::Slice { start, end }"
1503 );
1504 }
1505
1506 #[test]
1507 fn normalize_shorthand_different_binding_unchanged() {
1508 assert_eq!(
1509 normalize_pattern("Filter::Slice { start: s, end: e }"),
1510 "Filter::Slice { start: s, end: e }"
1511 );
1512 }
1513
1514 #[test]
1515 fn normalize_shorthand_mixed() {
1516 assert_eq!(
1517 normalize_pattern("Foo { a: a, b: other, c }"),
1518 "Foo { a, b: other, c }"
1519 );
1520 }
1521
1522 #[test]
1523 fn normalize_shorthand_with_rest() {
1524 assert_eq!(normalize_pattern("Foo { x: x, .. }"), "Foo { x, .. }");
1525 }
1526
1527 #[test]
1528 fn normalize_shorthand_no_braces() {
1529 assert_eq!(normalize_pattern("Some(x)"), "Some(x)");
1530 }
1531
1532 #[test]
1537 fn replace_match_arm_struct_pattern() {
1538 use crate::engine::ASTMutationEngine;
1539 use ryo_analysis::testing::ContextBuilder;
1540 use ryo_mutations::basic::ReplaceMatchArmMutation;
1541
1542 let mut ctx = ContextBuilder::new()
1543 .with_file(
1544 "src/lib.rs",
1545 r#"
1546enum Shape {
1547 Circle { radius: f64 },
1548 Rect { width: f64, height: f64 },
1549}
1550
1551fn area(s: &Shape) -> f64 {
1552 match s {
1553 Shape::Circle { radius } => std::f64::consts::PI * radius * radius,
1554 Shape::Rect { width, height } => width * height,
1555 }
1556}
1557"#,
1558 )
1559 .build();
1560
1561 let area_id = ctx
1562 .registry
1563 .iter()
1564 .find(|(id, path)| {
1565 matches!(ctx.registry.kind(*id), Some(SymbolKind::Function))
1566 && path.name() == "area"
1567 })
1568 .map(|(id, _)| id)
1569 .expect("area function not found");
1570
1571 let mutation = ReplaceMatchArmMutation {
1573 function_id: area_id,
1574 enum_name: "Shape".to_string(),
1575 old_pattern: "Shape::Rect { width: width, height: height }".to_string(),
1576 new_pattern: "Shape::Rect { width, height }".to_string(),
1577 new_body: "width * height * 2.0".to_string(),
1578 };
1579
1580 let result = ASTMutationEngine::execute_ast_reg(&mutation, &mut ctx);
1581 assert_eq!(
1582 result.result.changes, 1,
1583 "Struct pattern with explicit binding should match: {}",
1584 result.result.description
1585 );
1586 }
1587}