1use ryo_analysis::SymbolKind;
22use ryo_mutations::basic::{
23 EnumToTraitMutation, EnumToTraitStrategy, ExtractTraitMutation, InlineTraitMutation,
24 MatchHandling, RemoveTraitMutation,
25};
26use ryo_mutations::{Mutation, MutationResult};
27use ryo_source::pure::{
28 MacroDelimiter, PureBlock, PureExpr, PureField, PureFields, PureFn, PureGenericParam,
29 PureGenerics, PureImpl, PureImplItem, PureItem, PureParam, PureStmt, PureStruct, PureTrait,
30 PureTraitItem, PureType, PureVis,
31};
32
33use crate::engine::{ASTMutationContext, ASTRegApply};
34
35impl ASTRegApply for ExtractTraitMutation {
36 fn apply_to_registry(&self, ctx: &mut ASTMutationContext) -> MutationResult {
37 let impl_id = self.symbol_id;
39 let impl_path = match ctx.symbol_registry.path(impl_id) {
40 Some(path) => path.clone(),
41 None => {
42 return MutationResult {
43 mutation_type: self.mutation_type().to_string(),
44 changes: 0,
45 description: format!("SymbolId {:?} not found in registry", impl_id),
46 };
47 }
48 };
49
50 let inherent_impl = match ctx.ast_registry.get(impl_id) {
52 Some(PureItem::Impl(imp)) => {
53 if imp.trait_.is_some() {
55 return MutationResult {
56 mutation_type: self.mutation_type().to_string(),
57 changes: 0,
58 description: format!(
59 "SymbolId {:?} is a trait impl, not an inherent impl",
60 impl_id
61 ),
62 };
63 }
64 imp.clone()
65 }
66 Some(_) => {
67 return MutationResult {
68 mutation_type: self.mutation_type().to_string(),
69 changes: 0,
70 description: format!("SymbolId {:?} is not an impl block", impl_id),
71 };
72 }
73 None => {
74 return MutationResult {
75 mutation_type: self.mutation_type().to_string(),
76 changes: 0,
77 description: format!("No AST found for SymbolId {:?}", impl_id),
78 };
79 }
80 };
81
82 let struct_name = inherent_impl.self_ty.clone();
84
85 let (extracted_items, remaining_items): (Vec<_>, Vec<_>) =
87 inherent_impl.items.into_iter().partition(|item| {
88 if let PureImplItem::Fn(f) = item {
89 match &self.methods {
90 Some(methods) => methods.contains(&f.name),
91 None => true, }
93 } else {
94 false }
96 });
97
98 if extracted_items.is_empty() {
99 return MutationResult {
100 mutation_type: self.mutation_type().to_string(),
101 changes: 0,
102 description: "No methods to extract".to_string(),
103 };
104 }
105
106 let mut changes = 0;
107
108 let trait_items: Vec<PureTraitItem> = extracted_items
110 .iter()
111 .filter_map(|item| {
112 if let PureImplItem::Fn(f) = item {
113 let trait_fn = PureFn {
115 attrs: f.attrs.clone(),
116 vis: PureVis::Private, is_async: f.is_async,
118 is_async_inferred: f.is_async_inferred,
119 is_const: f.is_const,
120 is_unsafe: f.is_unsafe,
121 abi: None,
122 name: f.name.clone(),
123 generics: f.generics.clone(),
124 params: f.params.clone(),
125 ret: f.ret.clone(),
126 body: PureBlock::default(), };
128 Some(PureTraitItem::Fn(trait_fn))
129 } else {
130 None
131 }
132 })
133 .collect();
134
135 let new_trait = PureTrait {
136 attrs: Vec::new(),
137 vis: PureVis::Public, is_unsafe: false,
139 is_auto: false,
140 name: self.trait_name.clone(),
141 generics: PureGenerics::default(),
142 supertraits: Vec::new(),
143 items: trait_items,
144 };
145
146 let trait_path = match impl_path
148 .parent()
149 .and_then(|p| p.child(&self.trait_name).ok())
150 {
151 Some(path) => path,
152 None => {
153 return MutationResult {
154 mutation_type: self.mutation_type().to_string(),
155 changes: 0,
156 description: format!("Failed to create path for trait '{}'", self.trait_name),
157 };
158 }
159 };
160
161 if ctx
162 .register_with_ast(
163 trait_path.clone(),
164 SymbolKind::Trait,
165 PureItem::Trait(new_trait),
166 )
167 .is_some()
168 {
169 changes += 1;
170 }
171
172 let trait_impl_items: Vec<PureImplItem> = extracted_items
175 .into_iter()
176 .map(|item| {
177 if let PureImplItem::Fn(mut f) = item {
178 f.vis = PureVis::Private; PureImplItem::Fn(f)
180 } else {
181 item
182 }
183 })
184 .collect();
185
186 let trait_impl = PureImpl {
187 attrs: Vec::new(),
188 generics: inherent_impl.generics.clone(),
189 is_unsafe: false,
190 trait_: Some(self.trait_name.clone()),
191 self_ty: struct_name.clone(),
192 items: trait_impl_items,
193 };
194
195 let trait_impl_name = format!(
197 "<impl {} for {}>",
198 self.trait_name,
199 struct_name
200 .replace("::", "_")
201 .replace('<', "_")
202 .replace('>', "")
203 );
204 let trait_impl_path = match impl_path
205 .parent()
206 .and_then(|p| p.child(&trait_impl_name).ok())
207 {
208 Some(path) => path,
209 None => {
210 return MutationResult {
211 mutation_type: self.mutation_type().to_string(),
212 changes,
213 description: "Failed to create path for trait impl".to_string(),
214 };
215 }
216 };
217
218 if let Some(_trait_impl_id) = ctx.register_with_ast(
219 trait_impl_path,
220 SymbolKind::Impl,
221 PureItem::Impl(trait_impl.clone()),
222 ) {
223 if let Some(parent_path) = impl_path.parent() {
225 if let Some(parent_id) = ctx.symbol_registry.lookup(&parent_path) {
226 if let Some(module_items) = ctx.ast_registry.get_module_items_mut(parent_id) {
227 module_items.push(PureItem::Impl(trait_impl));
228 }
229 }
230 }
231 changes += 1;
232 }
233
234 if remaining_items.is_empty() {
236 ctx.remove_symbol(impl_id);
238 changes += 1;
239 } else {
240 let updated_impl = PureImpl {
241 attrs: inherent_impl.attrs,
242 generics: inherent_impl.generics,
243 is_unsafe: inherent_impl.is_unsafe,
244 trait_: None,
245 self_ty: struct_name.clone(),
246 items: remaining_items,
247 };
248 ctx.set_ast(impl_id, PureItem::Impl(updated_impl));
249 changes += 1;
250 }
251
252 MutationResult {
253 mutation_type: self.mutation_type().to_string(),
254 changes,
255 description: format!(
256 "Extracted trait '{}' from '{}'",
257 self.trait_name, struct_name
258 ),
259 }
260 }
261}
262
263impl ASTRegApply for InlineTraitMutation {
264 fn apply_to_registry(&self, ctx: &mut ASTMutationContext) -> MutationResult {
265 let trait_name = match ctx.symbol_registry.path(self.symbol_id) {
267 Some(path) => path.name().to_string(),
268 None => {
269 return MutationResult {
270 mutation_type: self.mutation_type().to_string(),
271 changes: 0,
272 description: format!("Trait symbol {:?} not found in registry", self.symbol_id),
273 };
274 }
275 };
276
277 let trait_impl_entry = ctx.symbol_registry.iter().find(|(id, _path)| {
279 if !matches!(ctx.symbol_registry.kind(*id), Some(SymbolKind::Impl)) {
280 return false;
281 }
282 if let Some(PureItem::Impl(imp)) = ctx.ast_registry.get(*id) {
283 imp.trait_.as_ref() == Some(&trait_name) && imp.self_ty == self.struct_name
284 } else {
285 false
286 }
287 });
288
289 let (trait_impl_id, trait_impl_path) = match trait_impl_entry {
290 Some((id, path)) => (id, path.clone()),
291 None => {
292 return MutationResult {
293 mutation_type: self.mutation_type().to_string(),
294 changes: 0,
295 description: format!(
296 "No impl of '{}' for '{}' found",
297 trait_name, self.struct_name
298 ),
299 };
300 }
301 };
302
303 let trait_impl = match ctx.ast_registry.get(trait_impl_id) {
304 Some(PureItem::Impl(imp)) => imp.clone(),
305 _ => {
306 return MutationResult {
307 mutation_type: self.mutation_type().to_string(),
308 changes: 0,
309 description: "No AST found for trait impl".to_string(),
310 };
311 }
312 };
313
314 let mut changes = 0;
315
316 let inherent_impl_entry = ctx.symbol_registry.iter().find(|(id, _path)| {
318 if !matches!(ctx.symbol_registry.kind(*id), Some(SymbolKind::Impl)) {
319 return false;
320 }
321 if let Some(PureItem::Impl(imp)) = ctx.ast_registry.get(*id) {
322 imp.trait_.is_none() && imp.self_ty == self.struct_name
323 } else {
324 false
325 }
326 });
327
328 if let Some((inherent_impl_id, _)) = inherent_impl_entry {
330 if let Some(PureItem::Impl(mut inherent_impl)) =
332 ctx.ast_registry.get(inherent_impl_id).cloned()
333 {
334 inherent_impl.items.extend(trait_impl.items.clone());
335 ctx.set_ast(inherent_impl_id, PureItem::Impl(inherent_impl.clone()));
336
337 if let Some(parent_path) = trait_impl_path.parent() {
339 if let Some(parent_id) = ctx.symbol_registry.lookup(&parent_path) {
340 if let Some(module_items) = ctx.ast_registry.get_module_items_mut(parent_id)
341 {
342 for item in module_items.iter_mut() {
343 if let PureItem::Impl(impl_block) = item {
344 if impl_block.trait_.is_none()
345 && impl_block.self_ty == self.struct_name
346 {
347 impl_block.items.extend(trait_impl.items.clone());
348 break;
349 }
350 }
351 }
352 }
353 }
354 }
355
356 changes += 1;
357 }
358 } else {
359 let new_inherent_impl = PureImpl {
361 attrs: Vec::new(),
362 generics: trait_impl.generics.clone(),
363 is_unsafe: false,
364 trait_: None,
365 self_ty: self.struct_name.clone(),
366 items: trait_impl.items.clone(),
367 };
368
369 let impl_name = format!(
370 "<impl {}>",
371 self.struct_name
372 .replace("::", "_")
373 .replace('<', "_")
374 .replace('>', "")
375 );
376 let impl_path = match trait_impl_path
377 .parent()
378 .and_then(|p| p.child(&impl_name).ok())
379 {
380 Some(path) => path,
381 None => {
382 return MutationResult {
383 mutation_type: self.mutation_type().to_string(),
384 changes: 0,
385 description: "Failed to create path for inherent impl".to_string(),
386 };
387 }
388 };
389
390 if let Some(_new_impl_id) = ctx.register_with_ast(
391 impl_path,
392 SymbolKind::Impl,
393 PureItem::Impl(new_inherent_impl.clone()),
394 ) {
395 if let Some(parent_path) = trait_impl_path.parent() {
397 if let Some(parent_id) = ctx.symbol_registry.lookup(&parent_path) {
398 if let Some(module_items) = ctx.ast_registry.get_module_items_mut(parent_id)
399 {
400 module_items.push(PureItem::Impl(new_inherent_impl));
401 }
402 }
403 }
404 changes += 1;
405 }
406 }
407
408 ctx.remove_symbol(trait_impl_id);
410 changes += 1;
411
412 if self.remove_trait {
414 ctx.remove_symbol(self.symbol_id);
415 changes += 1;
416 }
417
418 MutationResult {
419 mutation_type: self.mutation_type().to_string(),
420 changes,
421 description: format!(
422 "Inlined trait '{}' into '{}'{}",
423 trait_name,
424 self.struct_name,
425 if self.remove_trait {
426 " (trait removed)"
427 } else {
428 ""
429 }
430 ),
431 }
432 }
433}
434
435impl ASTRegApply for RemoveTraitMutation {
436 fn apply_to_registry(&self, ctx: &mut ASTMutationContext) -> MutationResult {
437 let trait_id = self.trait_id;
439
440 if ctx.symbol_registry.kind(trait_id) != Some(SymbolKind::Trait) {
442 return MutationResult {
443 mutation_type: "RemoveTrait".to_string(),
444 changes: 0,
445 description: format!("Symbol {} is not a trait", trait_id),
446 };
447 }
448
449 ctx.ast_registry.remove(trait_id);
450
451 MutationResult {
452 mutation_type: "RemoveTrait".to_string(),
453 changes: 1,
454 description: format!("Removed trait {}", trait_id),
455 }
456 }
457}
458
459impl ASTRegApply for EnumToTraitMutation {
460 fn apply_to_registry(&self, ctx: &mut ASTMutationContext) -> MutationResult {
461 let enum_id = self.symbol_id;
462
463 if !matches!(ctx.symbol_registry.kind(enum_id), Some(SymbolKind::Enum)) {
465 return MutationResult {
466 mutation_type: self.mutation_type().to_string(),
467 changes: 0,
468 description: format!("Symbol {:?} is not an enum or not found", enum_id),
469 };
470 }
471
472 let enum_path = match ctx.symbol_registry.path(enum_id) {
473 Some(p) => p.clone(),
474 None => {
475 return MutationResult {
476 mutation_type: self.mutation_type().to_string(),
477 changes: 0,
478 description: format!("Path not found for symbol {:?}", enum_id),
479 };
480 }
481 };
482
483 let enum_name = enum_path.name().to_string();
485
486 let default_trait_name;
491 let trait_name = match &self.trait_name {
492 Some(name) => name.as_str(),
493 None => {
494 match self.strategy {
495 EnumToTraitStrategy::MarkerOnly => {
496 default_trait_name = format!("{}Trait", enum_name);
498 &default_trait_name
499 }
500 _ => &enum_name,
501 }
502 }
503 };
504
505 let enum_def = match ctx.ast_registry.get(enum_id) {
507 Some(PureItem::Enum(e)) => e.clone(),
508 _ => {
509 return MutationResult {
510 mutation_type: self.mutation_type().to_string(),
511 changes: 0,
512 description: format!("No AST found for enum '{}'", enum_name),
513 };
514 }
515 };
516
517 let mut changes = 0;
518 let parent_path = match enum_path.parent() {
519 Some(p) => p,
520 None => {
521 return MutationResult {
522 mutation_type: self.mutation_type().to_string(),
523 changes: 0,
524 description: "Cannot determine parent module".to_string(),
525 };
526 }
527 };
528
529 let variant_names: Vec<String> = enum_def.variants.iter().map(|v| v.name.clone()).collect();
531
532 let enum_impl_id: Option<_> = ctx
534 .symbol_registry
535 .iter()
536 .find(|(id, _)| {
537 if !matches!(ctx.symbol_registry.kind(*id), Some(SymbolKind::Impl)) {
538 return false;
539 }
540 if let Some(PureItem::Impl(imp)) = ctx.ast_registry.get(*id) {
541 imp.trait_.is_none() && imp.self_ty == enum_name
543 } else {
544 false
545 }
546 })
547 .map(|(id, _)| id); let enum_methods: Vec<PureFn> = if let Some(impl_id) = enum_impl_id {
551 if let Some(PureItem::Impl(imp)) = ctx.ast_registry.get(impl_id) {
552 imp.items
553 .iter()
554 .filter_map(|item| {
555 if let PureImplItem::Fn(f) = item {
556 let has_self = f
558 .params
559 .iter()
560 .any(|p| matches!(p, PureParam::SelfValue { .. }));
561 if has_self {
562 Some(f.clone())
563 } else {
564 None
565 }
566 } else {
567 None
568 }
569 })
570 .collect()
571 } else {
572 Vec::new()
573 }
574 } else {
575 Vec::new()
576 };
577
578 let enum_removed_early = match self.strategy {
582 EnumToTraitStrategy::MarkerOnly => false, _ if self.remove_enum && trait_name == enum_name => {
584 ctx.remove_symbol(enum_id);
585 changes += 1;
586 if let Some(impl_id) = enum_impl_id {
588 ctx.remove_symbol(impl_id);
589 changes += 1;
590 }
591 true
592 }
593 _ => false,
594 };
595
596 let trait_items: Vec<PureTraitItem> = enum_methods
598 .iter()
599 .map(|f| {
600 let trait_fn = PureFn {
602 attrs: Vec::new(),
603 vis: PureVis::Private, is_async: f.is_async,
605 is_async_inferred: f.is_async_inferred,
606 is_const: f.is_const,
607 is_unsafe: f.is_unsafe,
608 abi: None,
609 name: f.name.clone(),
610 generics: f.generics.clone(),
611 params: f.params.clone(),
612 ret: f.ret.clone(),
613 body: PureBlock::default(), };
615 PureTraitItem::Fn(trait_fn)
616 })
617 .collect();
618
619 let new_trait = PureTrait {
620 attrs: Vec::new(),
621 vis: PureVis::Public,
622 is_unsafe: false,
623 is_auto: false,
624 name: trait_name.to_string(),
625 generics: PureGenerics::default(),
626 supertraits: Vec::new(),
627 items: trait_items,
628 };
629
630 let trait_path = match parent_path.child(trait_name) {
631 Ok(path) => path,
632 Err(_) => {
633 return MutationResult {
634 mutation_type: self.mutation_type().to_string(),
635 changes: 0,
636 description: format!("Failed to create path for trait '{}'", trait_name),
637 };
638 }
639 };
640
641 if ctx
642 .register_with_ast(
643 trait_path.clone(),
644 SymbolKind::Trait,
645 PureItem::Trait(new_trait),
646 )
647 .is_some()
648 {
649 changes += 1;
650 }
651
652 for variant in &enum_def.variants {
654 let struct_fields = match &variant.fields {
656 PureFields::Named(fields) => PureFields::Named(
657 fields
658 .iter()
659 .map(|f| PureField {
660 attrs: Vec::new(),
661 vis: PureVis::Public,
662 name: f.name.clone(),
663 ty: f.ty.clone(),
664 })
665 .collect(),
666 ),
667 PureFields::Tuple(types) => PureFields::Tuple(types.clone()),
668 PureFields::Unit => PureFields::Unit,
669 };
670
671 let new_struct = PureStruct {
672 attrs: Vec::new(),
673 vis: PureVis::Public,
674 name: variant.name.clone(),
675 generics: PureGenerics::default(),
676 fields: struct_fields,
677 };
678
679 let struct_path = match parent_path.child(&variant.name) {
680 Ok(path) => path,
681 Err(_) => continue,
682 };
683
684 if ctx
685 .register_with_ast(
686 struct_path.clone(),
687 SymbolKind::Struct,
688 PureItem::Struct(new_struct),
689 )
690 .is_some()
691 {
692 changes += 1;
693 }
694
695 let impl_items: Vec<PureImplItem> = enum_methods
697 .iter()
698 .map(|f| {
699 let impl_fn = PureFn {
701 attrs: Vec::new(),
702 vis: PureVis::Private, is_async: f.is_async,
704 is_async_inferred: f.is_async_inferred,
705 is_const: f.is_const,
706 is_unsafe: f.is_unsafe,
707 abi: None,
708 name: f.name.clone(),
709 generics: f.generics.clone(),
710 params: f.params.clone(),
711 ret: f.ret.clone(),
712 body: PureBlock {
713 stmts: vec![PureStmt::Expr(PureExpr::Macro {
714 name: "todo".to_string(),
715 delimiter: MacroDelimiter::Paren,
716 tokens: format!("\"{}::{}::{}\"", trait_name, variant.name, f.name),
717 })],
718 },
719 };
720 PureImplItem::Fn(impl_fn)
721 })
722 .collect();
723
724 let trait_impl = PureImpl {
725 attrs: Vec::new(),
726 generics: PureGenerics::default(),
727 is_unsafe: false,
728 trait_: Some(trait_name.to_string()),
729 self_ty: variant.name.clone(),
730 items: impl_items,
731 };
732
733 let impl_name = format!("<impl {} for {}>", trait_name, variant.name);
734 let impl_path = match parent_path.child(&impl_name) {
735 Ok(path) => path,
736 Err(_) => continue,
737 };
738
739 if ctx
740 .register_with_ast(impl_path, SymbolKind::Impl, PureItem::Impl(trait_impl))
741 .is_some()
742 {
743 changes += 1;
744 }
745 }
746
747 let usage_changes = match self.strategy {
750 EnumToTraitStrategy::MarkerOnly => 0, _ => replace_enum_usages(ctx, &enum_name, &variant_names),
752 };
753 changes += usage_changes;
754
755 let type_changes = match self.strategy {
758 EnumToTraitStrategy::Dynamic => {
759 replace_type_annotations(ctx, &enum_name, trait_name, TypeReplacement::BoxDyn)
761 }
762 EnumToTraitStrategy::Static => {
763 replace_type_annotations(ctx, &enum_name, trait_name, TypeReplacement::ImplTrait)
765 }
766 EnumToTraitStrategy::Generic => {
767 replace_type_annotations(ctx, &enum_name, trait_name, TypeReplacement::Generic)
769 }
770 EnumToTraitStrategy::MarkerOnly => {
771 0
773 }
774 };
775 changes += type_changes;
776
777 let match_changes = match self.match_handling {
779 MatchHandling::WarnOnly => {
780 count_match_expressions(ctx, &enum_name)
783 }
784 MatchHandling::Downcast => {
785 0
788 }
789 MatchHandling::BlockOnMatch => {
790 0
793 }
794 };
795 let should_remove = match self.strategy {
801 EnumToTraitStrategy::MarkerOnly => false, _ => self.remove_enum && !enum_removed_early,
803 };
804 if should_remove {
805 ctx.remove_symbol(enum_id);
806 changes += 1;
807
808 if let Some(impl_id) = enum_impl_id {
810 ctx.remove_symbol(impl_id);
811 changes += 1;
812 }
813 }
814
815 let strategy_desc = match self.strategy {
816 EnumToTraitStrategy::Dynamic => " with Box<dyn>",
817 EnumToTraitStrategy::Static => " with impl Trait",
818 EnumToTraitStrategy::Generic => " with generics",
819 EnumToTraitStrategy::MarkerOnly => " (marker only)",
820 };
821
822 let match_warning = if match_changes > 0 {
823 format!(
824 " ({} match expression(s) need manual migration)",
825 match_changes
826 )
827 } else {
828 String::new()
829 };
830
831 let enum_actually_removed = enum_removed_early || should_remove;
833
834 MutationResult {
835 mutation_type: self.mutation_type().to_string(),
836 changes,
837 description: format!(
838 "Converted enum '{}' to trait '{}' with {} variants{}{}{}",
839 enum_name,
840 trait_name,
841 variant_names.len(),
842 strategy_desc,
843 if enum_actually_removed {
844 " (enum removed)"
845 } else {
846 ""
847 },
848 match_warning
849 ),
850 }
851 }
852}
853
854enum TypeReplacement {
856 BoxDyn,
858 ImplTrait,
860 Generic,
862}
863
864fn replace_type_annotations(
866 ctx: &mut ASTMutationContext,
867 enum_name: &str,
868 trait_name: &str,
869 replacement: TypeReplacement,
870) -> usize {
871 let mut changes = 0;
872
873 let symbol_ids: Vec<_> = ctx.symbol_registry.iter().map(|(id, _)| id).collect();
875
876 for symbol_id in symbol_ids {
877 let item = match ctx.ast_registry.get(symbol_id) {
878 Some(item) => item.clone(),
879 None => continue,
880 };
881
882 let updated_item = match item {
883 PureItem::Fn(mut f) => {
884 let fn_changes = replace_types_in_fn(&mut f, enum_name, trait_name, &replacement);
885 if fn_changes > 0 {
886 changes += fn_changes;
887 Some(PureItem::Fn(f))
888 } else {
889 None
890 }
891 }
892 PureItem::Struct(mut s) => {
893 let field_replacement = match replacement {
896 TypeReplacement::ImplTrait => &TypeReplacement::BoxDyn,
897 _ => &replacement,
898 };
899 let struct_changes = replace_types_in_fields(
900 &mut s.fields,
901 enum_name,
902 trait_name,
903 field_replacement,
904 );
905 if struct_changes > 0 {
906 if matches!(replacement, TypeReplacement::Generic) {
908 add_generic_param(&mut s.generics, trait_name);
909 }
910 changes += struct_changes;
911 Some(PureItem::Struct(s))
912 } else {
913 None
914 }
915 }
916 PureItem::Impl(mut imp) => {
917 let mut impl_changed = false;
918 for item in &mut imp.items {
919 if let PureImplItem::Fn(ref mut f) = item {
920 if replace_types_in_fn(f, enum_name, trait_name, &replacement) > 0 {
921 impl_changed = true;
922 }
923 }
924 }
925 if impl_changed {
926 changes += 1;
927 Some(PureItem::Impl(imp))
928 } else {
929 None
930 }
931 }
932 PureItem::Trait(mut t) => {
933 let mut trait_changed = false;
934 for item in &mut t.items {
935 if let PureTraitItem::Fn(ref mut f) = item {
936 if replace_types_in_fn(f, enum_name, trait_name, &replacement) > 0 {
937 trait_changed = true;
938 }
939 }
940 }
941 if trait_changed {
942 changes += 1;
943 Some(PureItem::Trait(t))
944 } else {
945 None
946 }
947 }
948 _ => None,
949 };
950
951 if let Some(new_item) = updated_item {
952 ctx.set_ast(symbol_id, new_item);
953 }
954 }
955
956 changes
957}
958
959fn replace_types_in_fn(
961 f: &mut PureFn,
962 enum_name: &str,
963 trait_name: &str,
964 replacement: &TypeReplacement,
965) -> usize {
966 let mut changes = 0;
967
968 for param in &mut f.params {
970 if let PureParam::Typed { ty, .. } = param {
971 if replace_type(ty, enum_name, trait_name, replacement) {
972 changes += 1;
973 }
974 }
975 }
976
977 if let Some(ref mut ret) = f.ret {
979 if replace_type(ret, enum_name, trait_name, replacement) {
980 changes += 1;
981 }
982 }
983
984 if changes > 0 && matches!(replacement, TypeReplacement::Generic) {
986 add_generic_param(&mut f.generics, trait_name);
987 }
988
989 changes
990}
991
992fn replace_types_in_fields(
994 fields: &mut PureFields,
995 enum_name: &str,
996 trait_name: &str,
997 replacement: &TypeReplacement,
998) -> usize {
999 let mut changes = 0;
1000
1001 match fields {
1002 PureFields::Named(named_fields) => {
1003 for field in named_fields {
1004 if replace_type(&mut field.ty, enum_name, trait_name, replacement) {
1005 changes += 1;
1006 }
1007 }
1008 }
1009 PureFields::Tuple(types) => {
1010 for ty in types {
1011 if replace_type(ty, enum_name, trait_name, replacement) {
1012 changes += 1;
1013 }
1014 }
1015 }
1016 PureFields::Unit => {}
1017 }
1018
1019 changes
1020}
1021
1022fn add_generic_param(generics: &mut PureGenerics, trait_name: &str) {
1024 let has_t = generics
1026 .params
1027 .iter()
1028 .any(|p| matches!(p, PureGenericParam::Type { name, .. } if name == "T"));
1029
1030 if !has_t {
1031 generics.params.push(PureGenericParam::Type {
1032 name: "T".to_string(),
1033 bounds: vec![trait_name.to_string()],
1034 });
1035 }
1036}
1037
1038fn replace_type(
1040 ty: &mut PureType,
1041 enum_name: &str,
1042 trait_name: &str,
1043 replacement: &TypeReplacement,
1044) -> bool {
1045 match ty {
1046 PureType::Path(path) => {
1047 let type_name = path.split("::").last().unwrap_or(path);
1048
1049 if type_name == enum_name || path == enum_name {
1051 *ty = match replacement {
1052 TypeReplacement::BoxDyn => PureType::Path(format!("Box<dyn {}>", trait_name)),
1053 TypeReplacement::ImplTrait => PureType::ImplTrait(vec![trait_name.to_string()]),
1054 TypeReplacement::Generic => {
1055 PureType::Path("T".to_string())
1057 }
1058 };
1059 return true;
1060 }
1061
1062 if path.contains('<') && path.contains(enum_name) {
1065 let replacement_str = match replacement {
1066 TypeReplacement::BoxDyn => format!("Box<dyn {}>", trait_name),
1067 TypeReplacement::ImplTrait => format!("impl {}", trait_name),
1068 TypeReplacement::Generic => "T".to_string(),
1069 };
1070
1071 let new_path = replace_type_in_generic_path(path, enum_name, &replacement_str);
1074 if new_path != *path {
1075 *path = new_path;
1076 return true;
1077 }
1078 }
1079
1080 false
1081 }
1082 PureType::Ref { ty: inner, .. } => replace_type(inner, enum_name, trait_name, replacement),
1083 PureType::Tuple(types) => {
1084 let mut changed = false;
1085 for t in types {
1086 if replace_type(t, enum_name, trait_name, replacement) {
1087 changed = true;
1088 }
1089 }
1090 changed
1091 }
1092 PureType::Array { ty: inner, .. } => {
1093 replace_type(inner, enum_name, trait_name, replacement)
1094 }
1095 PureType::Slice(inner) => replace_type(inner, enum_name, trait_name, replacement),
1096 PureType::Fn { params, ret } => {
1097 let mut changed = false;
1098 for p in params {
1099 if replace_type(p, enum_name, trait_name, replacement) {
1100 changed = true;
1101 }
1102 }
1103 if let Some(ref mut r) = ret {
1104 if replace_type(r, enum_name, trait_name, replacement) {
1105 changed = true;
1106 }
1107 }
1108 changed
1109 }
1110 _ => false,
1111 }
1112}
1113
1114fn replace_type_in_generic_path(path: &str, enum_name: &str, replacement: &str) -> String {
1117 let mut result = String::new();
1120 let chars = path.chars().peekable();
1121 let mut current_word = String::new();
1122
1123 for c in chars {
1124 if c.is_alphanumeric() || c == '_' {
1125 current_word.push(c);
1126 } else {
1127 if current_word == enum_name {
1129 result.push_str(replacement);
1130 } else {
1131 result.push_str(¤t_word);
1132 }
1133 current_word.clear();
1134 result.push(c);
1135 }
1136 }
1137
1138 if current_word == enum_name {
1140 result.push_str(replacement);
1141 } else {
1142 result.push_str(¤t_word);
1143 }
1144
1145 result
1146}
1147
1148fn count_match_expressions(ctx: &ASTMutationContext, enum_name: &str) -> usize {
1150 let mut count = 0;
1151
1152 let fn_ids: Vec<_> = ctx
1153 .symbol_registry
1154 .iter()
1155 .filter(|(id, _)| matches!(ctx.symbol_registry.kind(*id), Some(SymbolKind::Function)))
1156 .map(|(id, _)| id)
1157 .collect();
1158
1159 for fn_id in fn_ids {
1160 if let Some(PureItem::Fn(func)) = ctx.ast_registry.get(fn_id) {
1161 count += count_matches_in_block(&func.body, enum_name);
1162 }
1163 }
1164
1165 count
1166}
1167
1168fn count_matches_in_block(block: &PureBlock, enum_name: &str) -> usize {
1169 let mut count = 0;
1170 for stmt in &block.stmts {
1171 count += count_matches_in_stmt(stmt, enum_name);
1172 }
1173 count
1174}
1175
1176fn count_matches_in_stmt(stmt: &PureStmt, enum_name: &str) -> usize {
1177 match stmt {
1178 PureStmt::Local { init, .. } => {
1179 if let Some(expr) = init {
1180 count_matches_in_expr(expr, enum_name)
1181 } else {
1182 0
1183 }
1184 }
1185 PureStmt::Semi(expr) | PureStmt::Expr(expr) => count_matches_in_expr(expr, enum_name),
1186 PureStmt::Item(_) => 0,
1187 }
1188}
1189
1190fn count_matches_in_expr(expr: &PureExpr, enum_name: &str) -> usize {
1191 match expr {
1192 PureExpr::Match {
1193 expr: scrutinee,
1194 arms,
1195 } => {
1196 let mut count = count_matches_in_expr(scrutinee, enum_name);
1197
1198 for arm in arms {
1200 if pattern_references_enum(&arm.pattern, enum_name) {
1201 count += 1;
1202 break; }
1204 }
1205
1206 for arm in arms {
1208 count += count_matches_in_expr(&arm.body, enum_name);
1209 }
1210 count
1211 }
1212 PureExpr::If {
1213 cond,
1214 then_branch,
1215 else_branch,
1216 } => {
1217 let mut count = count_matches_in_expr(cond, enum_name);
1218 count += count_matches_in_block(then_branch, enum_name);
1219 if let Some(else_expr) = else_branch {
1220 count += count_matches_in_expr(else_expr, enum_name);
1221 }
1222 count
1223 }
1224 PureExpr::Block { block, .. } => count_matches_in_block(block, enum_name),
1225 PureExpr::Call { func, args, .. } => {
1226 let mut count = count_matches_in_expr(func, enum_name);
1227 for arg in args {
1228 count += count_matches_in_expr(arg, enum_name);
1229 }
1230 count
1231 }
1232 PureExpr::MethodCall { receiver, args, .. } => {
1233 let mut count = count_matches_in_expr(receiver, enum_name);
1234 for arg in args {
1235 count += count_matches_in_expr(arg, enum_name);
1236 }
1237 count
1238 }
1239 PureExpr::Closure { body, .. } => count_matches_in_expr(body, enum_name),
1240 PureExpr::Loop { body: block, .. } => count_matches_in_block(block, enum_name),
1241 PureExpr::While { cond, body, .. } => {
1242 count_matches_in_expr(cond, enum_name) + count_matches_in_block(body, enum_name)
1243 }
1244 PureExpr::For { expr, body, .. } => {
1245 count_matches_in_expr(expr, enum_name) + count_matches_in_block(body, enum_name)
1246 }
1247 _ => 0,
1248 }
1249}
1250
1251fn pattern_references_enum(pattern: &PurePattern, enum_name: &str) -> bool {
1252 match pattern {
1253 PurePattern::Path(path) => path.starts_with(&format!("{}::", enum_name)),
1254 PurePattern::Struct { path, .. } => path.starts_with(&format!("{}::", enum_name)),
1255 PurePattern::Tuple(elements) | PurePattern::Slice(elements) => elements
1256 .iter()
1257 .any(|p| pattern_references_enum(p, enum_name)),
1258 PurePattern::Or(patterns) => patterns
1259 .iter()
1260 .any(|p| pattern_references_enum(p, enum_name)),
1261 PurePattern::Ref { pattern: inner, .. } => pattern_references_enum(inner, enum_name),
1262 _ => false,
1263 }
1264}
1265
1266fn replace_enum_usages(
1268 ctx: &mut ASTMutationContext,
1269 enum_name: &str,
1270 variant_names: &[String],
1271) -> usize {
1272 let mut changes = 0;
1273
1274 let fn_ids: Vec<_> = ctx
1276 .symbol_registry
1277 .iter()
1278 .filter(|(id, _)| matches!(ctx.symbol_registry.kind(*id), Some(SymbolKind::Function)))
1279 .map(|(id, _)| id)
1280 .collect();
1281
1282 for fn_id in fn_ids {
1283 if let Some(PureItem::Fn(mut func)) = ctx.ast_registry.get(fn_id).cloned() {
1284 let fn_changes = replace_in_block(&mut func.body, enum_name, variant_names);
1285 if fn_changes > 0 {
1286 ctx.set_ast(fn_id, PureItem::Fn(func));
1287 changes += fn_changes;
1288 }
1289 }
1290 }
1291
1292 changes
1293}
1294
1295fn replace_in_block(block: &mut PureBlock, enum_name: &str, variant_names: &[String]) -> usize {
1296 let mut changes = 0;
1297 for stmt in &mut block.stmts {
1298 changes += replace_in_stmt(stmt, enum_name, variant_names);
1299 }
1300 changes
1301}
1302
1303fn replace_in_stmt(stmt: &mut PureStmt, enum_name: &str, variant_names: &[String]) -> usize {
1304 match stmt {
1305 PureStmt::Local { init, .. } => {
1306 if let Some(expr) = init {
1307 return replace_in_expr(expr, enum_name, variant_names);
1308 }
1309 0
1310 }
1311 PureStmt::Semi(expr) | PureStmt::Expr(expr) => {
1312 replace_in_expr(expr, enum_name, variant_names)
1313 }
1314 PureStmt::Item(_) => 0,
1315 }
1316}
1317
1318fn replace_in_expr(expr: &mut PureExpr, enum_name: &str, variant_names: &[String]) -> usize {
1319 match expr {
1320 PureExpr::Path(path) => {
1322 if path.starts_with(&format!("{}::", enum_name)) {
1324 let variant_part = path.strip_prefix(&format!("{}::", enum_name));
1325 if let Some(variant) = variant_part {
1326 if variant_names.contains(&variant.to_string()) {
1327 *path = variant.to_string();
1329 return 1;
1330 }
1331 }
1332 }
1333 0
1334 }
1335
1336 PureExpr::Call { func, args, .. } => {
1338 let mut changes = replace_in_expr(func, enum_name, variant_names);
1339 for arg in args {
1340 changes += replace_in_expr(arg, enum_name, variant_names);
1341 }
1342 changes
1343 }
1344 PureExpr::MethodCall { receiver, args, .. } => {
1345 let mut changes = replace_in_expr(receiver, enum_name, variant_names);
1346 for arg in args {
1347 changes += replace_in_expr(arg, enum_name, variant_names);
1348 }
1349 changes
1350 }
1351 PureExpr::Binary { left, right, .. } => {
1352 replace_in_expr(left, enum_name, variant_names)
1353 + replace_in_expr(right, enum_name, variant_names)
1354 }
1355 PureExpr::Unary { expr, .. } => replace_in_expr(expr, enum_name, variant_names),
1356 PureExpr::If {
1357 cond,
1358 then_branch,
1359 else_branch,
1360 } => {
1361 let mut changes = replace_in_expr(cond, enum_name, variant_names);
1362 changes += replace_in_block(then_branch, enum_name, variant_names);
1363 if let Some(else_expr) = else_branch {
1364 changes += replace_in_expr(else_expr, enum_name, variant_names);
1365 }
1366 changes
1367 }
1368 PureExpr::Match { expr, arms } => {
1369 let mut changes = replace_in_expr(expr, enum_name, variant_names);
1370 for arm in arms {
1371 changes += replace_in_pattern(&mut arm.pattern, enum_name, variant_names);
1373 changes += replace_in_expr(&mut arm.body, enum_name, variant_names);
1374 }
1375 changes
1376 }
1377 PureExpr::Block { block, .. } => replace_in_block(block, enum_name, variant_names),
1378 PureExpr::Return(Some(v)) => replace_in_expr(v, enum_name, variant_names),
1379 PureExpr::Return(None) => 0,
1380 PureExpr::Struct { fields, .. } => {
1381 let mut changes = 0;
1382 for (_, field_expr) in fields {
1383 changes += replace_in_expr(field_expr, enum_name, variant_names);
1384 }
1385 changes
1386 }
1387 PureExpr::Tuple(elements) => {
1388 let mut changes = 0;
1389 for elem in elements {
1390 changes += replace_in_expr(elem, enum_name, variant_names);
1391 }
1392 changes
1393 }
1394 PureExpr::Array(elements) => {
1395 let mut changes = 0;
1396 for elem in elements {
1397 changes += replace_in_expr(elem, enum_name, variant_names);
1398 }
1399 changes
1400 }
1401 PureExpr::Index { expr, index, .. } => {
1402 replace_in_expr(expr, enum_name, variant_names)
1403 + replace_in_expr(index, enum_name, variant_names)
1404 }
1405 PureExpr::Field { expr, .. } => replace_in_expr(expr, enum_name, variant_names),
1406 PureExpr::Ref { expr, .. } => replace_in_expr(expr, enum_name, variant_names),
1407 PureExpr::Try(inner) => replace_in_expr(inner, enum_name, variant_names),
1408 PureExpr::Await(inner) => replace_in_expr(inner, enum_name, variant_names),
1409 PureExpr::Closure { body, .. } => replace_in_expr(body, enum_name, variant_names),
1410 PureExpr::Loop { body, .. } => replace_in_block(body, enum_name, variant_names),
1411 PureExpr::While { cond, body, .. } => {
1412 replace_in_expr(cond, enum_name, variant_names)
1413 + replace_in_block(body, enum_name, variant_names)
1414 }
1415 PureExpr::For { expr, body, .. } => {
1416 replace_in_expr(expr, enum_name, variant_names)
1417 + replace_in_block(body, enum_name, variant_names)
1418 }
1419 PureExpr::Let { expr, .. } => replace_in_expr(expr, enum_name, variant_names),
1420 PureExpr::Range { start, end, .. } => {
1421 let mut changes = 0;
1422 if let Some(s) = start {
1423 changes += replace_in_expr(s, enum_name, variant_names);
1424 }
1425 if let Some(e) = end {
1426 changes += replace_in_expr(e, enum_name, variant_names);
1427 }
1428 changes
1429 }
1430 PureExpr::Cast { expr, .. } => replace_in_expr(expr, enum_name, variant_names),
1431 _ => 0,
1433 }
1434}
1435
1436use ryo_source::pure::PurePattern;
1437
1438fn replace_in_pattern(
1439 pattern: &mut PurePattern,
1440 enum_name: &str,
1441 variant_names: &[String],
1442) -> usize {
1443 match pattern {
1444 PurePattern::Struct { path, fields, .. } => {
1446 let mut changes = 0;
1447 if path.starts_with(&format!("{}::", enum_name)) {
1448 if let Some(variant) = path.strip_prefix(&format!("{}::", enum_name)) {
1449 let base_variant = variant
1450 .split(|c: char| !c.is_alphanumeric() && c != '_')
1451 .next()
1452 .unwrap_or(variant);
1453 if variant_names.contains(&base_variant.to_string()) {
1454 *path = variant.to_string();
1455 changes += 1;
1456 }
1457 }
1458 }
1459 for (_, field_pattern) in fields {
1461 changes += replace_in_pattern(field_pattern, enum_name, variant_names);
1462 }
1463 changes
1464 }
1465 PurePattern::Tuple(elements) | PurePattern::Slice(elements) => {
1467 let mut changes = 0;
1468 for elem in elements {
1469 changes += replace_in_pattern(elem, enum_name, variant_names);
1470 }
1471 changes
1472 }
1473 PurePattern::Path(path) => {
1475 if path.starts_with(&format!("{}::", enum_name)) {
1476 if let Some(variant) = path.strip_prefix(&format!("{}::", enum_name)) {
1477 if variant_names.contains(&variant.to_string()) {
1478 *path = variant.to_string();
1479 return 1;
1480 }
1481 }
1482 }
1483 0
1484 }
1485 PurePattern::Ref { pattern: inner, .. } => {
1487 replace_in_pattern(inner, enum_name, variant_names)
1488 }
1489 PurePattern::Or(patterns) => {
1491 let mut changes = 0;
1492 for p in patterns {
1493 changes += replace_in_pattern(p, enum_name, variant_names);
1494 }
1495 changes
1496 }
1497 _ => 0,
1499 }
1500}
1501
1502#[cfg(test)]
1503mod tests {
1504 use super::*;
1505 use crate::engine::ASTMutationEngine;
1506 use ryo_analysis::testing::ContextBuilder;
1507
1508 #[test]
1509 fn test_v2_extract_trait() {
1510 let mut ctx = ContextBuilder::new()
1511 .with_file(
1512 "src/lib.rs",
1513 r#"
1514struct Foo {
1515 value: i32,
1516}
1517
1518impl Foo {
1519 fn get_value(&self) -> i32 {
1520 self.value
1521 }
1522
1523 fn set_value(&mut self, v: i32) {
1524 self.value = v;
1525 }
1526
1527 fn helper(&self) {}
1528}
1529"#,
1530 )
1531 .build();
1532
1533 let impl_id = ctx
1535 .registry
1536 .iter()
1537 .find(|(id, _path)| {
1538 if !matches!(ctx.registry.kind(*id), Some(SymbolKind::Impl)) {
1539 return false;
1540 }
1541 if let Some(PureItem::Impl(imp)) = ctx.ast_registry.get(*id) {
1542 imp.trait_.is_none() && imp.self_ty == "Foo"
1543 } else {
1544 false
1545 }
1546 })
1547 .map(|(id, _)| id)
1548 .expect("Should find impl Foo");
1549
1550 let mutation = ExtractTraitMutation::new(impl_id, "ValueAccessor")
1551 .with_methods(vec!["get_value".to_string(), "set_value".to_string()]);
1552 let result = ASTMutationEngine::execute_ast_reg(&mutation, &mut ctx);
1553
1554 println!("ExtractTrait result: {:?}", result.result);
1555 assert!(result.result.changes >= 2, "Expected at least 2 changes");
1557 }
1558
1559 #[test]
1560 fn test_v2_inline_trait() {
1561 let mut ctx = ContextBuilder::new()
1562 .with_file(
1563 "src/lib.rs",
1564 r#"
1565struct Foo;
1566
1567trait Greet {
1568 fn greet(&self) -> String;
1569}
1570
1571impl Greet for Foo {
1572 fn greet(&self) -> String {
1573 "Hello".to_string()
1574 }
1575}
1576"#,
1577 )
1578 .build();
1579
1580 let trait_id = ctx
1582 .registry
1583 .iter()
1584 .find(|(id, _path)| matches!(ctx.registry.kind(*id), Some(SymbolKind::Trait)))
1585 .map(|(id, _)| id)
1586 .expect("Should find trait Greet");
1587
1588 let mutation = InlineTraitMutation::new(trait_id, "Foo");
1589 let result = ASTMutationEngine::execute_ast_reg(&mutation, &mut ctx);
1590
1591 println!("InlineTrait result: {:?}", result.result);
1592 assert!(result.result.changes >= 2, "Expected at least 2 changes");
1594 }
1595
1596 #[test]
1597 fn test_v2_inline_trait_keep_trait() {
1598 let mut ctx = ContextBuilder::new()
1599 .with_file(
1600 "src/lib.rs",
1601 r#"
1602struct Foo;
1603
1604trait Greet {
1605 fn greet(&self) -> String;
1606}
1607
1608impl Greet for Foo {
1609 fn greet(&self) -> String {
1610 "Hello".to_string()
1611 }
1612}
1613"#,
1614 )
1615 .build();
1616
1617 let trait_id = ctx
1619 .registry
1620 .iter()
1621 .find(|(id, _path)| matches!(ctx.registry.kind(*id), Some(SymbolKind::Trait)))
1622 .map(|(id, _)| id)
1623 .expect("Should find trait Greet");
1624
1625 let mutation = InlineTraitMutation::new(trait_id, "Foo").keep_trait();
1626 let result = ASTMutationEngine::execute_ast_reg(&mutation, &mut ctx);
1627
1628 println!("InlineTrait (keep_trait) result: {:?}", result.result);
1629 assert!(result.result.changes >= 2, "Expected at least 2 changes");
1631 }
1632
1633 #[test]
1634 fn test_v2_enum_to_trait_dynamic_strategy() {
1635 let mut ctx = ContextBuilder::new()
1636 .with_file(
1637 "src/lib.rs",
1638 r#"
1639enum Status {
1640 Running,
1641 Stopped,
1642}
1643
1644fn process(status: Status) -> Status {
1645 status
1646}
1647
1648struct Config {
1649 current_status: Status,
1650}
1651"#,
1652 )
1653 .build();
1654
1655 let enum_id = ctx
1657 .registry
1658 .iter()
1659 .find(|(id, path)| {
1660 path.name() == "Status" && matches!(ctx.registry.kind(*id), Some(SymbolKind::Enum))
1661 })
1662 .map(|(id, _)| id)
1663 .expect("Enum 'Status' should exist");
1664
1665 let mutation = EnumToTraitMutation::from_symbol_id(enum_id)
1666 .with_strategy(EnumToTraitStrategy::Dynamic);
1667 let result = ASTMutationEngine::execute_ast_reg(&mutation, &mut ctx);
1668
1669 println!("EnumToTrait (Dynamic) result: {:?}", result.result);
1670 assert!(result.result.changes >= 5, "Expected at least 5 changes");
1672 assert!(
1673 result.result.description.contains("Box<dyn>"),
1674 "Should mention Box<dyn> strategy"
1675 );
1676 }
1677
1678 #[test]
1679 fn test_v2_enum_to_trait_static_strategy() {
1680 let mut ctx = ContextBuilder::new()
1681 .with_file(
1682 "src/lib.rs",
1683 r#"
1684enum Filter {
1685 Active,
1686 Inactive,
1687}
1688
1689fn apply_filter(filter: Filter) {
1690 let _ = filter;
1691}
1692"#,
1693 )
1694 .build();
1695
1696 let enum_id = ctx
1698 .registry
1699 .iter()
1700 .find(|(id, path)| {
1701 path.name() == "Filter" && matches!(ctx.registry.kind(*id), Some(SymbolKind::Enum))
1702 })
1703 .map(|(id, _)| id)
1704 .expect("Enum 'Filter' should exist");
1705
1706 let mutation =
1707 EnumToTraitMutation::from_symbol_id(enum_id).with_strategy(EnumToTraitStrategy::Static);
1708 let result = ASTMutationEngine::execute_ast_reg(&mutation, &mut ctx);
1709
1710 println!("EnumToTrait (Static) result: {:?}", result.result);
1711 assert!(result.result.changes >= 5, "Expected at least 5 changes");
1713 assert!(
1714 result.result.description.contains("impl Trait"),
1715 "Should mention impl Trait strategy"
1716 );
1717 }
1718
1719 #[test]
1720 fn test_v2_enum_to_trait_marker_only_strategy() {
1721 let mut ctx = ContextBuilder::new()
1722 .with_file(
1723 "src/lib.rs",
1724 r#"
1725enum Mode {
1726 Fast,
1727 Slow,
1728}
1729
1730fn get_mode() -> Mode {
1731 Mode::Fast
1732}
1733"#,
1734 )
1735 .build();
1736
1737 let enum_id = ctx
1739 .registry
1740 .iter()
1741 .find(|(id, path)| {
1742 path.name() == "Mode" && matches!(ctx.registry.kind(*id), Some(SymbolKind::Enum))
1743 })
1744 .map(|(id, _)| id)
1745 .expect("Enum 'Mode' should exist");
1746
1747 let mutation = EnumToTraitMutation::from_symbol_id(enum_id)
1748 .with_strategy(EnumToTraitStrategy::MarkerOnly);
1749 let result = ASTMutationEngine::execute_ast_reg(&mutation, &mut ctx);
1750
1751 println!("EnumToTrait (MarkerOnly) result: {:?}", result.result);
1752 assert!(result.result.changes >= 5, "Expected at least 5 changes");
1755 assert!(
1756 result.result.description.contains("marker only"),
1757 "Should mention marker only strategy"
1758 );
1759 }
1760
1761 #[test]
1762 fn test_v2_enum_to_trait_generic_strategy() {
1763 let mut ctx = ContextBuilder::new()
1764 .with_file(
1765 "src/lib.rs",
1766 r#"
1767enum Status {
1768 Running,
1769 Stopped,
1770}
1771
1772fn process(status: Status) -> Status {
1773 status
1774}
1775
1776struct Config {
1777 current_status: Status,
1778}
1779"#,
1780 )
1781 .build();
1782
1783 let enum_id = ctx
1785 .registry
1786 .iter()
1787 .find(|(id, path)| {
1788 path.name() == "Status" && matches!(ctx.registry.kind(*id), Some(SymbolKind::Enum))
1789 })
1790 .map(|(id, _)| id)
1791 .expect("Enum 'Status' should exist");
1792
1793 let mutation = EnumToTraitMutation::from_symbol_id(enum_id)
1794 .with_strategy(EnumToTraitStrategy::Generic);
1795 let result = ASTMutationEngine::execute_ast_reg(&mutation, &mut ctx);
1796
1797 println!("EnumToTrait (Generic) result: {:?}", result.result);
1798 assert!(result.result.changes >= 5, "Expected at least 5 changes");
1800 assert!(
1801 result.result.description.contains("generics"),
1802 "Should mention generics strategy"
1803 );
1804 }
1805}