1use std::collections::HashSet;
22
23use ryo_analysis::{SymbolKind, SymbolRegistry};
24use ryo_mutations::basic::{
25 EnumToTraitMutation, EnumToTraitStrategy, ExtractTraitMutation, InlineTraitMutation,
26 MatchHandling, RemoveTraitMutation,
27};
28use ryo_mutations::{Mutation, MutationResult};
29use ryo_source::pure::{
30 MacroDelimiter, PureBlock, PureExpr, PureField, PureFields, PureFn, PureGenericParam,
31 PureGenerics, PureImpl, PureImplItem, PureItem, PureParam, PureStmt, PureStruct, PureTrait,
32 PureTraitItem, PureType, PureUse, PureUseTree, PureVis,
33};
34use ryo_symbol::SymbolId;
35
36use crate::engine::{ASTMutationContext, ASTRegApply, ModificationType};
37
38fn build_use_item_for_path(path: &str) -> PureItem {
44 let segments: Vec<&str> = path.split("::").collect();
45 let tree = build_use_tree(&segments);
46 PureItem::Use(PureUse {
47 vis: PureVis::Private,
48 tree,
49 })
50}
51
52fn rewrite_to_local_use_path(full_path: &str) -> String {
57 let mut segments = full_path.split("::");
58 let _crate_seg = segments.next();
59 let mut out = String::from("crate");
60 for seg in segments {
61 out.push_str("::");
62 out.push_str(seg);
63 }
64 out
65}
66
67fn build_use_tree(segments: &[&str]) -> PureUseTree {
68 match segments.len() {
69 0 => PureUseTree::Name(String::new()),
70 1 => PureUseTree::Name(segments[0].to_string()),
71 _ => PureUseTree::Path {
72 path: segments[0].to_string(),
73 tree: Box::new(build_use_tree(&segments[1..])),
74 },
75 }
76}
77
78fn pure_type_mentions_self_by_value(ty_str: &str) -> bool {
86 let trimmed = ty_str.trim();
87 let mut i = 0;
88 let bytes = trimmed.as_bytes();
89 while i + 4 <= bytes.len() {
90 if &bytes[i..i + 4] == b"Self" {
91 let prev_ok =
93 i == 0 || !matches!(bytes[i - 1], b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'_');
94 let next_ok = i + 4 == bytes.len()
96 || !matches!(
97 bytes[i + 4],
98 b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'_'
99 );
100 if prev_ok && next_ok {
101 let mut j = i;
105 while j > 0 {
106 j -= 1;
107 match bytes[j] {
108 b' ' | b'\t' | b'\'' => continue,
109 b'a'..=b'z' | b'A'..=b'Z' | b'_' | b'0'..=b'9' => {
110 while j > 0
112 && matches!(
113 bytes[j - 1],
114 b'a'..=b'z' | b'A'..=b'Z' | b'_' | b'0'..=b'9'
115 )
116 {
117 j -= 1;
118 }
119 continue;
120 }
121 b'&' => return false, _ => break,
123 }
124 }
125 return true;
126 }
127 }
128 i += 1;
129 }
130 false
131}
132
133fn walk_to_module(symbol_id: SymbolId, registry: &SymbolRegistry) -> Option<SymbolId> {
137 let path = registry.path(symbol_id)?;
138 let mut current = path.clone();
139 while let Some(parent) = current.parent() {
140 if let Some(parent_id) = registry.lookup(&parent) {
141 if matches!(registry.kind(parent_id), Some(SymbolKind::Mod)) {
142 return Some(parent_id);
143 }
144 }
145 current = parent;
146 }
147 None
148}
149
150fn use_tree_imports_path(tree: &PureUseTree, target_path: &str) -> bool {
154 let target_segments: Vec<&str> = target_path.split("::").collect();
155 tree_matches_segments(tree, &target_segments)
156}
157
158fn tree_matches_segments(tree: &PureUseTree, target: &[&str]) -> bool {
159 match tree {
160 PureUseTree::Name(name) => target.last().is_some_and(|t| t == name),
161 PureUseTree::Rename { name, .. } => target.last().is_some_and(|t| t == name),
162 PureUseTree::Glob => target.len() <= 1,
163 PureUseTree::Path { path, tree } => {
164 if target.first().is_none_or(|head| head != path) {
165 false
166 } else {
167 tree_matches_segments(tree, &target[1..])
168 }
169 }
170 PureUseTree::Group(children) => children.iter().any(|c| tree_matches_segments(c, target)),
171 }
172}
173
174impl ASTRegApply for ExtractTraitMutation {
175 fn apply_to_registry(&self, ctx: &mut ASTMutationContext) -> MutationResult {
176 let impl_id = self.symbol_id;
178 let impl_path = match ctx.symbol_registry.path(impl_id) {
179 Some(path) => path.clone(),
180 None => {
181 return MutationResult {
182 mutation_type: self.mutation_type().to_string(),
183 changes: 0,
184 description: format!("SymbolId {:?} not found in registry", impl_id),
185 };
186 }
187 };
188
189 let inherent_impl = match ctx.ast_registry.get(impl_id) {
191 Some(PureItem::Impl(imp)) => {
192 if imp.trait_.is_some() {
194 return MutationResult {
195 mutation_type: self.mutation_type().to_string(),
196 changes: 0,
197 description: format!(
198 "SymbolId {:?} is a trait impl, not an inherent impl",
199 impl_id
200 ),
201 };
202 }
203 imp.clone()
204 }
205 Some(_) => {
206 return MutationResult {
207 mutation_type: self.mutation_type().to_string(),
208 changes: 0,
209 description: format!("SymbolId {:?} is not an impl block", impl_id),
210 };
211 }
212 None => {
213 return MutationResult {
214 mutation_type: self.mutation_type().to_string(),
215 changes: 0,
216 description: format!("No AST found for SymbolId {:?}", impl_id),
217 };
218 }
219 };
220
221 let struct_name = inherent_impl.self_ty.clone();
223
224 let (extracted_items, remaining_items): (Vec<_>, Vec<_>) =
226 inherent_impl.items.into_iter().partition(|item| {
227 if let PureImplItem::Fn(f) = item {
228 match &self.methods {
229 Some(methods) => methods.contains(&f.name),
230 None => true, }
232 } else {
233 false }
235 });
236
237 if extracted_items.is_empty() {
238 return MutationResult {
239 mutation_type: self.mutation_type().to_string(),
240 changes: 0,
241 description: "No methods to extract".to_string(),
242 };
243 }
244
245 let mut changes = 0;
246
247 let trait_items: Vec<PureTraitItem> = extracted_items
249 .iter()
250 .filter_map(|item| {
251 if let PureImplItem::Fn(f) = item {
252 let signature_params: Vec<PureParam> = f
262 .params
263 .iter()
264 .map(|p| match p {
265 PureParam::SelfValue { is_ref, .. } => PureParam::SelfValue {
266 is_ref: *is_ref,
267 is_mut: false,
268 },
269 PureParam::Typed { name, ty, pat, .. } => PureParam::Typed {
270 name: name.clone(),
271 ty: ty.clone(),
272 is_mut: false,
273 pat: pat.clone(),
274 },
275 })
276 .collect();
277 let trait_fn = PureFn {
290 attrs: f.attrs.clone(),
291 vis: PureVis::Private, is_async: f.is_async,
293 is_async_inferred: f.is_async_inferred,
294 is_const: false,
295 is_unsafe: f.is_unsafe,
296 abi: None,
297 name: f.name.clone(),
298 generics: f.generics.clone(),
299 params: signature_params,
300 ret: f.ret.clone(),
301 body: PureBlock::default(), };
303 Some(PureTraitItem::Fn(trait_fn))
304 } else {
305 None
306 }
307 })
308 .collect();
309
310 let needs_sized = trait_items.iter().any(|item| {
323 let PureTraitItem::Fn(f) = item else {
324 return false;
325 };
326 if matches!(&f.ret, Some(PureType::Path(p)) if pure_type_mentions_self_by_value(p)) {
327 return true;
328 }
329 f.params
330 .iter()
331 .any(|p| matches!(p, PureParam::SelfValue { is_ref: false, .. }))
332 });
333 let supertraits = if needs_sized {
334 vec!["Sized".to_string()]
335 } else {
336 Vec::new()
337 };
338
339 let new_trait = PureTrait {
340 attrs: Vec::new(),
341 vis: PureVis::Public, is_unsafe: false,
343 is_auto: false,
344 name: self.trait_name.clone(),
345 generics: PureGenerics::default(),
346 supertraits,
347 items: trait_items,
348 };
349
350 let trait_path = match impl_path
352 .parent()
353 .and_then(|p| p.child(&self.trait_name).ok())
354 {
355 Some(path) => path,
356 None => {
357 return MutationResult {
358 mutation_type: self.mutation_type().to_string(),
359 changes: 0,
360 description: format!("Failed to create path for trait '{}'", self.trait_name),
361 };
362 }
363 };
364
365 let trait_item = PureItem::Trait(new_trait);
366 if ctx
367 .register_with_ast(trait_path.clone(), SymbolKind::Trait, trait_item.clone())
368 .is_some()
369 {
370 if let Some(parent_path) = trait_path.parent() {
398 if let Some(parent_id) = ctx.symbol_registry.lookup(&parent_path) {
399 if ctx.ast_registry.is_inline_module(parent_id) {
400 if let Some(parent_items) = ctx.ast_registry.get_module_items_mut(parent_id)
401 {
402 parent_items.push(trait_item);
403 }
404 }
405 }
406 }
407 changes += 1;
408 }
409
410 let trait_impl_items: Vec<PureImplItem> = extracted_items
418 .into_iter()
419 .map(|item| {
420 if let PureImplItem::Fn(mut f) = item {
421 f.vis = PureVis::Private; f.params = f
423 .params
424 .into_iter()
425 .map(|p| match p {
426 PureParam::SelfValue { is_ref, .. } => PureParam::SelfValue {
427 is_ref,
428 is_mut: false,
429 },
430 PureParam::Typed { name, ty, pat, .. } => PureParam::Typed {
431 name,
432 ty,
433 is_mut: false,
434 pat,
435 },
436 })
437 .collect();
438 PureImplItem::Fn(f)
439 } else {
440 item
441 }
442 })
443 .collect();
444
445 let trait_method_names: Vec<String> = trait_impl_items
450 .iter()
451 .filter_map(|item| {
452 if let PureImplItem::Fn(f) = item {
453 Some(f.name.clone())
454 } else {
455 None
456 }
457 })
458 .collect();
459
460 let trait_impl = PureImpl {
461 attrs: Vec::new(),
462 generics: inherent_impl.generics.clone(),
463 is_unsafe: false,
464 trait_: Some(self.trait_name.clone()),
465 self_ty: struct_name.clone(),
466 items: trait_impl_items,
467 };
468
469 let trait_impl_name = format!(
471 "<impl {} for {}>",
472 self.trait_name,
473 struct_name
474 .replace("::", "_")
475 .replace('<', "_")
476 .replace('>', "")
477 );
478 let trait_impl_path = match impl_path
479 .parent()
480 .and_then(|p| p.child(&trait_impl_name).ok())
481 {
482 Some(path) => path,
483 None => {
484 return MutationResult {
485 mutation_type: self.mutation_type().to_string(),
486 changes,
487 description: "Failed to create path for trait impl".to_string(),
488 };
489 }
490 };
491
492 if let Some(_trait_impl_id) = ctx.register_with_ast(
493 trait_impl_path,
494 SymbolKind::Impl,
495 PureItem::Impl(trait_impl.clone()),
496 ) {
497 if let Some(parent_path) = impl_path.parent() {
499 if let Some(parent_id) = ctx.symbol_registry.lookup(&parent_path) {
500 if let Some(module_items) = ctx.ast_registry.get_module_items_mut(parent_id) {
501 module_items.push(PureItem::Impl(trait_impl));
502 }
503 }
504 }
505 changes += 1;
506 }
507
508 if remaining_items.is_empty() {
510 ctx.remove_symbol(impl_id);
512 changes += 1;
513 } else {
514 let updated_impl = PureImpl {
515 attrs: inherent_impl.attrs,
516 generics: inherent_impl.generics,
517 is_unsafe: inherent_impl.is_unsafe,
518 trait_: None,
519 self_ty: struct_name.clone(),
520 items: remaining_items,
521 };
522 ctx.set_ast(impl_id, PureItem::Impl(updated_impl));
523 changes += 1;
524 }
525
526 let trait_reexport_parent_id: Option<SymbolId> = (|| {
544 let impl_module_path = impl_path.parent()?;
545 let impl_module_id = ctx.symbol_registry.lookup(&impl_module_path)?;
546 let impl_module_vis = ctx.symbol_registry.visibility(impl_module_id);
547 if !matches!(impl_module_vis, Some(ryo_symbol::Visibility::Private)) {
548 return None;
549 }
550 let impl_module_name = impl_module_path.segments().last()?.to_string();
551 let parent_path = impl_module_path.parent()?;
552 let parent_id = ctx.symbol_registry.lookup(&parent_path)?;
553 let reexport_path = format!("{}::{}", impl_module_name, self.trait_name);
554 let existing_items = ctx
555 .ast_registry
556 .get_module_items(parent_id)
557 .cloned()
558 .unwrap_or_default();
559 let already_present = existing_items.iter().any(|item| {
560 if let PureItem::Use(u) = item {
561 matches!(u.vis, PureVis::Public)
562 && use_tree_imports_path(&u.tree, &reexport_path)
563 } else {
564 false
565 }
566 });
567 if already_present {
568 return Some(parent_id);
569 }
570 let mut reexport_item = build_use_item_for_path(&reexport_path);
571 if let PureItem::Use(ref mut u) = reexport_item {
572 u.vis = PureVis::Public;
573 }
574 let mut updated = existing_items;
575 updated.insert(0, reexport_item);
576 ctx.ast_registry.set_module_items(parent_id, updated);
577 ctx.emit_modified(parent_id, ModificationType::BodyModified);
578 changes += 1;
579 Some(parent_id)
580 })();
581
582 if let Some(code_graph) = ctx.code_graph {
603 if let Some(trait_parent_path) = impl_path.parent() {
604 if let Ok(struct_path) = trait_parent_path.child(&struct_name) {
605 let trait_crate_name = trait_path.crate_name().to_string();
606 let trait_module_id = ctx.symbol_registry.lookup(&trait_parent_path);
607
608 let trait_local_path = if let Some(parent_id) = trait_reexport_parent_id {
624 if let Some(parent_path) = ctx.symbol_registry.path(parent_id) {
625 rewrite_to_local_use_path(&format!(
626 "{}::{}",
627 parent_path, self.trait_name
628 ))
629 } else {
630 rewrite_to_local_use_path(&trait_path.to_string())
631 }
632 } else {
633 rewrite_to_local_use_path(&trait_path.to_string())
634 };
635 let use_stmt = build_use_item_for_path(&trait_local_path);
636 let trait_full_path_str = trait_path.to_string();
637
638 let trait_extern_path = if let Some(parent_id) = trait_reexport_parent_id {
647 if let Some(parent_path) = ctx.symbol_registry.path(parent_id) {
648 format!("{}::{}", parent_path, self.trait_name)
649 } else {
650 trait_full_path_str.clone()
651 }
652 } else {
653 trait_full_path_str.clone()
654 };
655 let extern_use_stmt = build_use_item_for_path(&trait_extern_path);
656
657 let mut caller_modules: HashSet<SymbolId> = HashSet::new();
658 let mut cross_caller_modules: HashSet<SymbolId> = HashSet::new();
659 for method_name in trait_method_names {
660 let Ok(method_path) = struct_path.child(&method_name) else {
661 continue;
662 };
663 let Some(method_id) = ctx.symbol_registry.lookup(&method_path) else {
664 continue;
665 };
666 for caller_id in code_graph.callers_of(method_id) {
667 let Some(caller_path) = ctx.symbol_registry.path(caller_id) else {
668 continue;
669 };
670 let is_same_crate = caller_path.crate_name() == trait_crate_name;
671 if let Some(parent_mod_id) =
672 walk_to_module(caller_id, ctx.symbol_registry)
673 {
674 if is_same_crate {
675 caller_modules.insert(parent_mod_id);
676 } else {
677 cross_caller_modules.insert(parent_mod_id);
678 }
679 }
680 }
681 }
682
683 let injections = [
687 (caller_modules, &trait_local_path, &use_stmt),
688 (cross_caller_modules, &trait_extern_path, &extern_use_stmt),
689 ];
690 for (modules, use_path, stmt) in injections {
691 for caller_mod_id in modules {
692 if Some(caller_mod_id) == trait_module_id {
693 continue;
695 }
696 let existing_items = ctx
697 .ast_registry
698 .get_module_items(caller_mod_id)
699 .cloned()
700 .unwrap_or_default();
701 let already_imported = existing_items.iter().any(|i| {
706 if let PureItem::Use(u) = i {
707 use_tree_imports_path(&u.tree, use_path)
708 || use_tree_imports_path(&u.tree, &trait_full_path_str)
709 } else {
710 false
711 }
712 });
713 if already_imported {
714 continue;
715 }
716 let mut updated = existing_items;
717 updated.insert(0, stmt.clone());
718 ctx.ast_registry.set_module_items(caller_mod_id, updated);
719 ctx.emit_modified(caller_mod_id, ModificationType::BodyModified);
720 changes += 1;
721 }
722 }
723 }
724 }
725 }
726
727 MutationResult {
728 mutation_type: self.mutation_type().to_string(),
729 changes,
730 description: format!(
731 "Extracted trait '{}' from '{}'",
732 self.trait_name, struct_name
733 ),
734 }
735 }
736}
737
738impl ASTRegApply for InlineTraitMutation {
739 fn apply_to_registry(&self, ctx: &mut ASTMutationContext) -> MutationResult {
740 let trait_name = match ctx.symbol_registry.path(self.symbol_id) {
742 Some(path) => path.name().to_string(),
743 None => {
744 return MutationResult {
745 mutation_type: self.mutation_type().to_string(),
746 changes: 0,
747 description: format!("Trait symbol {:?} not found in registry", self.symbol_id),
748 };
749 }
750 };
751
752 let cross_crate_callers = super::inline_trait_caller_scanner::scan_cross_crate_callers_raw(
760 ctx.ast_registry,
761 ctx.symbol_registry,
762 self.symbol_id,
763 );
764
765 let trait_impl_entry = ctx.symbol_registry.iter().find(|(id, _path)| {
767 if !matches!(ctx.symbol_registry.kind(*id), Some(SymbolKind::Impl)) {
768 return false;
769 }
770 if let Some(PureItem::Impl(imp)) = ctx.ast_registry.get(*id) {
771 imp.trait_.as_ref() == Some(&trait_name) && imp.self_ty == self.struct_name
772 } else {
773 false
774 }
775 });
776
777 let (trait_impl_id, trait_impl_path) = match trait_impl_entry {
778 Some((id, path)) => (id, path.clone()),
779 None => {
780 return MutationResult {
781 mutation_type: self.mutation_type().to_string(),
782 changes: 0,
783 description: format!(
784 "No impl of '{}' for '{}' found",
785 trait_name, self.struct_name
786 ),
787 };
788 }
789 };
790
791 let trait_impl = match ctx.ast_registry.get(trait_impl_id) {
792 Some(PureItem::Impl(imp)) => imp.clone(),
793 _ => {
794 return MutationResult {
795 mutation_type: self.mutation_type().to_string(),
796 changes: 0,
797 description: "No AST found for trait impl".to_string(),
798 };
799 }
800 };
801
802 let mut changes = 0;
803
804 let inherent_impl_entry = ctx.symbol_registry.iter().find(|(id, _path)| {
806 if !matches!(ctx.symbol_registry.kind(*id), Some(SymbolKind::Impl)) {
807 return false;
808 }
809 if let Some(PureItem::Impl(imp)) = ctx.ast_registry.get(*id) {
810 imp.trait_.is_none() && imp.self_ty == self.struct_name
811 } else {
812 false
813 }
814 });
815
816 if let Some((inherent_impl_id, _)) = inherent_impl_entry {
818 if let Some(PureItem::Impl(mut inherent_impl)) =
820 ctx.ast_registry.get(inherent_impl_id).cloned()
821 {
822 inherent_impl.items.extend(trait_impl.items.clone());
823 ctx.set_ast(inherent_impl_id, PureItem::Impl(inherent_impl.clone()));
824
825 if let Some(parent_path) = trait_impl_path.parent() {
827 if let Some(parent_id) = ctx.symbol_registry.lookup(&parent_path) {
828 if let Some(module_items) = ctx.ast_registry.get_module_items_mut(parent_id)
829 {
830 for item in module_items.iter_mut() {
831 if let PureItem::Impl(impl_block) = item {
832 if impl_block.trait_.is_none()
833 && impl_block.self_ty == self.struct_name
834 {
835 impl_block.items.extend(trait_impl.items.clone());
836 break;
837 }
838 }
839 }
840 }
841 }
842 }
843
844 changes += 1;
845 }
846 } else {
847 let new_inherent_impl = PureImpl {
849 attrs: Vec::new(),
850 generics: trait_impl.generics.clone(),
851 is_unsafe: false,
852 trait_: None,
853 self_ty: self.struct_name.clone(),
854 items: trait_impl.items.clone(),
855 };
856
857 let impl_name = format!(
858 "<impl {}>",
859 self.struct_name
860 .replace("::", "_")
861 .replace('<', "_")
862 .replace('>', "")
863 );
864 let impl_path = match trait_impl_path
865 .parent()
866 .and_then(|p| p.child(&impl_name).ok())
867 {
868 Some(path) => path,
869 None => {
870 return MutationResult {
871 mutation_type: self.mutation_type().to_string(),
872 changes: 0,
873 description: "Failed to create path for inherent impl".to_string(),
874 };
875 }
876 };
877
878 if let Some(_new_impl_id) = ctx.register_with_ast(
879 impl_path,
880 SymbolKind::Impl,
881 PureItem::Impl(new_inherent_impl.clone()),
882 ) {
883 if let Some(parent_path) = trait_impl_path.parent() {
885 if let Some(parent_id) = ctx.symbol_registry.lookup(&parent_path) {
886 if let Some(module_items) = ctx.ast_registry.get_module_items_mut(parent_id)
887 {
888 module_items.push(PureItem::Impl(new_inherent_impl));
889 }
890 }
891 }
892 changes += 1;
893 }
894 }
895
896 ctx.remove_symbol(trait_impl_id);
898 changes += 1;
899
900 if self.remove_trait {
902 ctx.remove_symbol(self.symbol_id);
903 changes += 1;
904 }
905
906 let mut description = format!(
907 "Inlined trait '{}' into '{}'{}",
908 trait_name,
909 self.struct_name,
910 if self.remove_trait {
911 " (trait removed)"
912 } else {
913 ""
914 }
915 );
916
917 if !cross_crate_callers.is_empty() {
925 use ryo_mutations::basic::trait_ops::cross_crate_caller_pattern::{
926 rewrite_body_ufcs, rewrite_fn_dyn_dispatch, rewrite_fn_generic_bound, CallerPattern,
927 };
928 let mut dot = 0usize;
929 let mut ufcs = 0usize;
930 let mut generic = 0usize;
931 let mut dyn_dispatch = 0usize;
932 let mut other = 0usize;
933 let mut ufcs_paths_rewritten = 0usize;
934 let mut dyn_types_rewritten = 0usize;
935 let mut generic_edits = 0usize;
936 let mut pub_generic_audit_callers: Vec<String> = Vec::new();
937 let mut escapes_aborted_callers: Vec<String> = Vec::new();
938
939 for r in &cross_crate_callers {
940 for p in &r.patterns {
941 match p {
942 CallerPattern::Dot => dot += 1,
943 CallerPattern::Ufcs => ufcs += 1,
944 CallerPattern::GenericBound => generic += 1,
945 CallerPattern::DynDispatch => dyn_dispatch += 1,
946 CallerPattern::Other => other += 1,
947 }
948 }
949 let mut skip_generic_bound = false;
970 if r.patterns.contains(&CallerPattern::GenericBound) && r.is_public {
971 if let Some(p) = ctx.symbol_registry.path(r.caller_id) {
972 pub_generic_audit_callers.push(p.name().to_string());
973 }
974 if let Some(code_graph) = ctx.code_graph {
975 use super::inline_trait_caller_scanner::{
976 walk_transitive_callers, CascadeVerdict,
977 };
978 let verdict =
979 walk_transitive_callers(code_graph, ctx.ast_registry, r.caller_id);
980 if verdict == CascadeVerdict::Escapes {
981 skip_generic_bound = true;
982 if let Some(p) = ctx.symbol_registry.path(r.caller_id) {
983 escapes_aborted_callers.push(p.name().to_string());
984 }
985 }
986 }
987 }
988 if r.patterns.contains(&CallerPattern::Ufcs)
989 || r.patterns.contains(&CallerPattern::DynDispatch)
990 || r.patterns.contains(&CallerPattern::GenericBound)
991 {
992 if let Some(PureItem::Fn(f)) = ctx.ast_registry.get_mut(r.caller_id) {
993 if r.patterns.contains(&CallerPattern::Ufcs) {
994 ufcs_paths_rewritten +=
995 rewrite_body_ufcs(&mut f.body, &trait_name, &self.struct_name);
996 }
997 if r.patterns.contains(&CallerPattern::DynDispatch) {
998 dyn_types_rewritten +=
999 rewrite_fn_dyn_dispatch(f, &trait_name, &self.struct_name);
1000 }
1001 if r.patterns.contains(&CallerPattern::GenericBound) && !skip_generic_bound
1002 {
1003 generic_edits +=
1004 rewrite_fn_generic_bound(f, &trait_name, &self.struct_name);
1005 }
1006 }
1007 }
1008 }
1009 changes += ufcs_paths_rewritten + dyn_types_rewritten + generic_edits;
1010 description.push_str(&format!(
1011 " [cross-crate callers: {} (dot={} ufcs={} generic={} dyn={} other={}); Phase 2e rewrote {} Ufcs path(s); Phase 2f rewrote {} Dyn type(s); Phase 2g performed {} GenericBound edit(s)]",
1012 cross_crate_callers.len(),
1013 dot,
1014 ufcs,
1015 generic,
1016 dyn_dispatch,
1017 other,
1018 ufcs_paths_rewritten,
1019 dyn_types_rewritten,
1020 generic_edits
1021 ));
1022 if !pub_generic_audit_callers.is_empty() {
1023 description.push_str(&format!(
1024 " [Phase 4-A audit: pub GenericBound caller(s) ({}) — transitive cascade verdict pending Phase 4-B walk]",
1025 pub_generic_audit_callers.join(", ")
1026 ));
1027 }
1028 if !escapes_aborted_callers.is_empty() {
1029 description.push_str(&format!(
1030 " [Phase 4-B verdict: GenericBound rewrite ABORTED for caller(s) ({}) — transitive cascade Escapes workspace boundary]",
1031 escapes_aborted_callers.join(", ")
1032 ));
1033 }
1034 }
1035
1036 MutationResult {
1037 mutation_type: self.mutation_type().to_string(),
1038 changes,
1039 description,
1040 }
1041 }
1042}
1043
1044impl ASTRegApply for RemoveTraitMutation {
1045 fn apply_to_registry(&self, ctx: &mut ASTMutationContext) -> MutationResult {
1046 let trait_id = self.trait_id;
1048
1049 if ctx.symbol_registry.kind(trait_id) != Some(SymbolKind::Trait) {
1051 return MutationResult {
1052 mutation_type: "RemoveTrait".to_string(),
1053 changes: 0,
1054 description: format!("Symbol {} is not a trait", trait_id),
1055 };
1056 }
1057
1058 ctx.ast_registry.remove(trait_id);
1059
1060 MutationResult {
1061 mutation_type: "RemoveTrait".to_string(),
1062 changes: 1,
1063 description: format!("Removed trait {}", trait_id),
1064 }
1065 }
1066}
1067
1068impl ASTRegApply for EnumToTraitMutation {
1069 fn apply_to_registry(&self, ctx: &mut ASTMutationContext) -> MutationResult {
1070 let enum_id = self.symbol_id;
1071
1072 if !matches!(ctx.symbol_registry.kind(enum_id), Some(SymbolKind::Enum)) {
1074 return MutationResult {
1075 mutation_type: self.mutation_type().to_string(),
1076 changes: 0,
1077 description: format!("Symbol {:?} is not an enum or not found", enum_id),
1078 };
1079 }
1080
1081 let enum_path = match ctx.symbol_registry.path(enum_id) {
1082 Some(p) => p.clone(),
1083 None => {
1084 return MutationResult {
1085 mutation_type: self.mutation_type().to_string(),
1086 changes: 0,
1087 description: format!("Path not found for symbol {:?}", enum_id),
1088 };
1089 }
1090 };
1091
1092 let enum_name = enum_path.name().to_string();
1094
1095 let default_trait_name;
1100 let trait_name = match &self.trait_name {
1101 Some(name) => name.as_str(),
1102 None => {
1103 match self.strategy {
1104 EnumToTraitStrategy::MarkerOnly => {
1105 default_trait_name = format!("{}Trait", enum_name);
1107 &default_trait_name
1108 }
1109 _ => &enum_name,
1110 }
1111 }
1112 };
1113
1114 let enum_def = match ctx.ast_registry.get(enum_id) {
1116 Some(PureItem::Enum(e)) => e.clone(),
1117 _ => {
1118 return MutationResult {
1119 mutation_type: self.mutation_type().to_string(),
1120 changes: 0,
1121 description: format!("No AST found for enum '{}'", enum_name),
1122 };
1123 }
1124 };
1125
1126 let mut changes = 0;
1127 let parent_path = match enum_path.parent() {
1128 Some(p) => p,
1129 None => {
1130 return MutationResult {
1131 mutation_type: self.mutation_type().to_string(),
1132 changes: 0,
1133 description: "Cannot determine parent module".to_string(),
1134 };
1135 }
1136 };
1137
1138 let variant_names: Vec<String> = enum_def.variants.iter().map(|v| v.name.clone()).collect();
1140
1141 let enum_impl_id: Option<_> = ctx
1143 .symbol_registry
1144 .iter()
1145 .find(|(id, _)| {
1146 if !matches!(ctx.symbol_registry.kind(*id), Some(SymbolKind::Impl)) {
1147 return false;
1148 }
1149 if let Some(PureItem::Impl(imp)) = ctx.ast_registry.get(*id) {
1150 imp.trait_.is_none() && imp.self_ty == enum_name
1152 } else {
1153 false
1154 }
1155 })
1156 .map(|(id, _)| id); let enum_methods: Vec<PureFn> = if let Some(impl_id) = enum_impl_id {
1160 if let Some(PureItem::Impl(imp)) = ctx.ast_registry.get(impl_id) {
1161 imp.items
1162 .iter()
1163 .filter_map(|item| {
1164 if let PureImplItem::Fn(f) = item {
1165 let has_self = f
1167 .params
1168 .iter()
1169 .any(|p| matches!(p, PureParam::SelfValue { .. }));
1170 if has_self {
1171 Some(f.clone())
1172 } else {
1173 None
1174 }
1175 } else {
1176 None
1177 }
1178 })
1179 .collect()
1180 } else {
1181 Vec::new()
1182 }
1183 } else {
1184 Vec::new()
1185 };
1186
1187 let enum_removed_early = match self.strategy {
1191 EnumToTraitStrategy::MarkerOnly => false, _ if self.remove_enum && trait_name == enum_name => {
1193 ctx.remove_symbol(enum_id);
1194 changes += 1;
1195 if let Some(impl_id) = enum_impl_id {
1197 ctx.remove_symbol(impl_id);
1198 changes += 1;
1199 }
1200 true
1201 }
1202 _ => false,
1203 };
1204
1205 let trait_items: Vec<PureTraitItem> = enum_methods
1207 .iter()
1208 .map(|f| {
1209 let trait_fn = PureFn {
1211 attrs: Vec::new(),
1212 vis: PureVis::Private, is_async: f.is_async,
1214 is_async_inferred: f.is_async_inferred,
1215 is_const: f.is_const,
1216 is_unsafe: f.is_unsafe,
1217 abi: None,
1218 name: f.name.clone(),
1219 generics: f.generics.clone(),
1220 params: f.params.clone(),
1221 ret: f.ret.clone(),
1222 body: PureBlock::default(), };
1224 PureTraitItem::Fn(trait_fn)
1225 })
1226 .collect();
1227
1228 let new_trait = PureTrait {
1229 attrs: Vec::new(),
1230 vis: PureVis::Public,
1231 is_unsafe: false,
1232 is_auto: false,
1233 name: trait_name.to_string(),
1234 generics: PureGenerics::default(),
1235 supertraits: Vec::new(),
1236 items: trait_items,
1237 };
1238
1239 let trait_path = match parent_path.child(trait_name) {
1240 Ok(path) => path,
1241 Err(_) => {
1242 return MutationResult {
1243 mutation_type: self.mutation_type().to_string(),
1244 changes: 0,
1245 description: format!("Failed to create path for trait '{}'", trait_name),
1246 };
1247 }
1248 };
1249
1250 if ctx
1251 .register_with_ast(
1252 trait_path.clone(),
1253 SymbolKind::Trait,
1254 PureItem::Trait(new_trait),
1255 )
1256 .is_some()
1257 {
1258 changes += 1;
1259 }
1260
1261 for variant in &enum_def.variants {
1263 let struct_fields = match &variant.fields {
1265 PureFields::Named(fields) => PureFields::Named(
1266 fields
1267 .iter()
1268 .map(|f| PureField {
1269 attrs: Vec::new(),
1270 vis: PureVis::Public,
1271 name: f.name.clone(),
1272 ty: f.ty.clone(),
1273 })
1274 .collect(),
1275 ),
1276 PureFields::Tuple(types) => PureFields::Tuple(types.clone()),
1277 PureFields::Unit => PureFields::Unit,
1278 };
1279
1280 let new_struct = PureStruct {
1281 attrs: Vec::new(),
1282 vis: PureVis::Public,
1283 name: variant.name.clone(),
1284 generics: PureGenerics::default(),
1285 fields: struct_fields,
1286 };
1287
1288 let struct_path = match parent_path.child(&variant.name) {
1289 Ok(path) => path,
1290 Err(_) => continue,
1291 };
1292
1293 if ctx
1294 .register_with_ast(
1295 struct_path.clone(),
1296 SymbolKind::Struct,
1297 PureItem::Struct(new_struct),
1298 )
1299 .is_some()
1300 {
1301 changes += 1;
1302 }
1303
1304 let impl_items: Vec<PureImplItem> = enum_methods
1306 .iter()
1307 .map(|f| {
1308 let impl_fn = PureFn {
1310 attrs: Vec::new(),
1311 vis: PureVis::Private, is_async: f.is_async,
1313 is_async_inferred: f.is_async_inferred,
1314 is_const: f.is_const,
1315 is_unsafe: f.is_unsafe,
1316 abi: None,
1317 name: f.name.clone(),
1318 generics: f.generics.clone(),
1319 params: f.params.clone(),
1320 ret: f.ret.clone(),
1321 body: PureBlock {
1322 stmts: vec![PureStmt::Expr(PureExpr::Macro {
1323 name: "todo".to_string(),
1324 delimiter: MacroDelimiter::Paren,
1325 tokens: format!("\"{}::{}::{}\"", trait_name, variant.name, f.name),
1326 })],
1327 },
1328 };
1329 PureImplItem::Fn(impl_fn)
1330 })
1331 .collect();
1332
1333 let trait_impl = PureImpl {
1334 attrs: Vec::new(),
1335 generics: PureGenerics::default(),
1336 is_unsafe: false,
1337 trait_: Some(trait_name.to_string()),
1338 self_ty: variant.name.clone(),
1339 items: impl_items,
1340 };
1341
1342 let impl_path = parent_path.child_trait_impl(trait_name, &variant.name);
1350
1351 if ctx
1352 .register_with_ast(
1353 impl_path,
1354 SymbolKind::Impl,
1355 PureItem::Impl(trait_impl.clone()),
1356 )
1357 .is_some()
1358 {
1359 if let Some(parent_id) = ctx.symbol_registry.lookup(&parent_path) {
1366 if let Some(module_items) = ctx.ast_registry.get_module_items_mut(parent_id) {
1367 module_items.push(PureItem::Impl(trait_impl));
1368 }
1369 }
1370 changes += 1;
1371 }
1372 }
1373
1374 let usage_changes = match self.strategy {
1377 EnumToTraitStrategy::MarkerOnly => 0, _ => replace_enum_usages(ctx, &enum_name, &variant_names),
1379 };
1380 changes += usage_changes;
1381
1382 let type_changes = match self.strategy {
1385 EnumToTraitStrategy::Dynamic => {
1386 replace_type_annotations(
1388 ctx,
1389 &enum_name,
1390 trait_name,
1391 TypeReplacement::BoxDyn,
1392 &variant_names,
1393 )
1394 }
1395 EnumToTraitStrategy::Static => {
1396 replace_type_annotations(
1398 ctx,
1399 &enum_name,
1400 trait_name,
1401 TypeReplacement::ImplTrait,
1402 &variant_names,
1403 )
1404 }
1405 EnumToTraitStrategy::Generic => {
1406 replace_type_annotations(
1408 ctx,
1409 &enum_name,
1410 trait_name,
1411 TypeReplacement::Generic,
1412 &variant_names,
1413 )
1414 }
1415 EnumToTraitStrategy::MarkerOnly => {
1416 0
1418 }
1419 };
1420 changes += type_changes;
1421
1422 let match_changes = match self.match_handling {
1424 MatchHandling::WarnOnly => {
1425 count_match_expressions(ctx, &enum_name)
1428 }
1429 MatchHandling::Downcast => {
1430 0
1433 }
1434 MatchHandling::BlockOnMatch => {
1435 0
1438 }
1439 };
1440 let should_remove = match self.strategy {
1446 EnumToTraitStrategy::MarkerOnly => false, _ => self.remove_enum && !enum_removed_early,
1448 };
1449 if should_remove {
1450 ctx.remove_symbol(enum_id);
1451 changes += 1;
1452
1453 if let Some(impl_id) = enum_impl_id {
1455 ctx.remove_symbol(impl_id);
1456 changes += 1;
1457 }
1458 }
1459
1460 let strategy_desc = match self.strategy {
1461 EnumToTraitStrategy::Dynamic => " with Box<dyn>",
1462 EnumToTraitStrategy::Static => " with impl Trait",
1463 EnumToTraitStrategy::Generic => " with generics",
1464 EnumToTraitStrategy::MarkerOnly => " (marker only)",
1465 };
1466
1467 let match_warning = if match_changes > 0 {
1468 format!(
1469 " ({} match expression(s) need manual migration)",
1470 match_changes
1471 )
1472 } else {
1473 String::new()
1474 };
1475
1476 let enum_actually_removed = enum_removed_early || should_remove;
1478
1479 MutationResult {
1480 mutation_type: self.mutation_type().to_string(),
1481 changes,
1482 description: format!(
1483 "Converted enum '{}' to trait '{}' with {} variants{}{}{}",
1484 enum_name,
1485 trait_name,
1486 variant_names.len(),
1487 strategy_desc,
1488 if enum_actually_removed {
1489 " (enum removed)"
1490 } else {
1491 ""
1492 },
1493 match_warning
1494 ),
1495 }
1496 }
1497}
1498
1499enum TypeReplacement {
1501 BoxDyn,
1503 ImplTrait,
1505 Generic,
1507}
1508
1509fn replace_type_annotations(
1511 ctx: &mut ASTMutationContext,
1512 enum_name: &str,
1513 trait_name: &str,
1514 replacement: TypeReplacement,
1515 variant_names: &[String],
1516) -> usize {
1517 let mut changes = 0;
1518
1519 let symbol_ids: Vec<_> = ctx.symbol_registry.iter().map(|(id, _)| id).collect();
1521
1522 for symbol_id in symbol_ids {
1523 let item = match ctx.ast_registry.get(symbol_id) {
1524 Some(item) => item.clone(),
1525 None => continue,
1526 };
1527
1528 let updated_item = match item {
1529 PureItem::Fn(mut f) => {
1530 let fn_changes =
1531 replace_types_in_fn(&mut f, enum_name, trait_name, &replacement, variant_names);
1532 if fn_changes > 0 {
1533 changes += fn_changes;
1534 Some(PureItem::Fn(f))
1535 } else {
1536 None
1537 }
1538 }
1539 PureItem::Struct(mut s) => {
1540 let field_replacement = match replacement {
1543 TypeReplacement::ImplTrait => &TypeReplacement::BoxDyn,
1544 _ => &replacement,
1545 };
1546 let struct_changes = replace_types_in_fields(
1547 &mut s.fields,
1548 enum_name,
1549 trait_name,
1550 field_replacement,
1551 );
1552 if struct_changes > 0 {
1553 if matches!(replacement, TypeReplacement::Generic) {
1555 add_generic_param(&mut s.generics, trait_name);
1556 }
1557 changes += struct_changes;
1558 Some(PureItem::Struct(s))
1559 } else {
1560 None
1561 }
1562 }
1563 PureItem::Impl(mut imp) => {
1564 let mut impl_changed = false;
1565 for item in &mut imp.items {
1566 if let PureImplItem::Fn(ref mut f) = item {
1567 if replace_types_in_fn(
1568 f,
1569 enum_name,
1570 trait_name,
1571 &replacement,
1572 variant_names,
1573 ) > 0
1574 {
1575 impl_changed = true;
1576 }
1577 }
1578 }
1579 if impl_changed {
1580 changes += 1;
1581 Some(PureItem::Impl(imp))
1582 } else {
1583 None
1584 }
1585 }
1586 PureItem::Trait(mut t) => {
1587 let mut trait_changed = false;
1588 for item in &mut t.items {
1589 if let PureTraitItem::Fn(ref mut f) = item {
1590 if replace_types_in_fn(
1591 f,
1592 enum_name,
1593 trait_name,
1594 &replacement,
1595 variant_names,
1596 ) > 0
1597 {
1598 trait_changed = true;
1599 }
1600 }
1601 }
1602 if trait_changed {
1603 changes += 1;
1604 Some(PureItem::Trait(t))
1605 } else {
1606 None
1607 }
1608 }
1609 _ => None,
1610 };
1611
1612 if let Some(new_item) = updated_item {
1613 ctx.set_ast(symbol_id, new_item);
1614 }
1615 }
1616
1617 changes
1618}
1619
1620fn replace_types_in_fn(
1622 f: &mut PureFn,
1623 enum_name: &str,
1624 trait_name: &str,
1625 replacement: &TypeReplacement,
1626 variant_names: &[String],
1627) -> usize {
1628 let mut changes = 0;
1629
1630 for param in &mut f.params {
1632 if let PureParam::Typed { ty, .. } = param {
1633 if replace_type(ty, enum_name, trait_name, replacement) {
1634 changes += 1;
1635 }
1636 }
1637 }
1638
1639 let mut ret_replaced = false;
1641 if let Some(ref mut ret) = f.ret {
1642 if replace_type(ret, enum_name, trait_name, replacement) {
1643 ret_replaced = true;
1644 changes += 1;
1645 }
1646 }
1647
1648 if ret_replaced && matches!(replacement, TypeReplacement::BoxDyn) {
1655 changes += box_variant_returns(&mut f.body, variant_names);
1656 }
1657
1658 if changes > 0 && matches!(replacement, TypeReplacement::Generic) {
1660 add_generic_param(&mut f.generics, trait_name);
1661 }
1662
1663 changes
1664}
1665
1666fn box_variant_returns(body: &mut PureBlock, variant_names: &[String]) -> usize {
1677 use ryo_mutations::basic::trait_ops::cross_crate_caller_pattern::walk_block_mut;
1678
1679 let mut changes = 0;
1680
1681 walk_block_mut(body, &mut |expr| {
1683 if let PureExpr::Return(Some(inner)) = expr {
1684 if is_variant_construction(inner, variant_names) {
1685 wrap_in_box_new(inner);
1686 changes += 1;
1687 }
1688 }
1689 });
1690
1691 if let Some(PureStmt::Expr(tail)) = body.stmts.last_mut() {
1694 if is_variant_construction(tail, variant_names) {
1695 wrap_in_box_new(tail);
1696 changes += 1;
1697 }
1698 }
1699
1700 changes
1701}
1702
1703fn is_variant_construction(expr: &PureExpr, variant_names: &[String]) -> bool {
1705 match expr {
1706 PureExpr::Path(p) => variant_names.iter().any(|v| v == p),
1707 PureExpr::Call { func, .. } => {
1708 matches!(&**func, PureExpr::Path(p) if variant_names.iter().any(|v| v == p))
1709 }
1710 PureExpr::Struct { path, .. } => variant_names.iter().any(|v| v == path),
1711 _ => false,
1712 }
1713}
1714
1715fn wrap_in_box_new(expr: &mut PureExpr) {
1717 let inner = std::mem::replace(expr, PureExpr::Path(String::new()));
1718 *expr = PureExpr::Call {
1719 func: Box::new(PureExpr::Path("Box::new".to_string())),
1720 args: vec![inner],
1721 };
1722}
1723
1724fn replace_types_in_fields(
1726 fields: &mut PureFields,
1727 enum_name: &str,
1728 trait_name: &str,
1729 replacement: &TypeReplacement,
1730) -> usize {
1731 let mut changes = 0;
1732
1733 match fields {
1734 PureFields::Named(named_fields) => {
1735 for field in named_fields {
1736 if replace_type(&mut field.ty, enum_name, trait_name, replacement) {
1737 changes += 1;
1738 }
1739 }
1740 }
1741 PureFields::Tuple(tuple_fields) => {
1742 for f in tuple_fields {
1743 if replace_type(&mut f.ty, enum_name, trait_name, replacement) {
1744 changes += 1;
1745 }
1746 }
1747 }
1748 PureFields::Unit => {}
1749 }
1750
1751 changes
1752}
1753
1754fn add_generic_param(generics: &mut PureGenerics, trait_name: &str) {
1756 let has_t = generics
1758 .params
1759 .iter()
1760 .any(|p| matches!(p, PureGenericParam::Type { name, .. } if name == "T"));
1761
1762 if !has_t {
1763 generics.params.push(PureGenericParam::Type {
1764 name: "T".to_string(),
1765 bounds: vec![trait_name.to_string()],
1766 });
1767 }
1768}
1769
1770fn replace_type(
1772 ty: &mut PureType,
1773 enum_name: &str,
1774 trait_name: &str,
1775 replacement: &TypeReplacement,
1776) -> bool {
1777 match ty {
1778 PureType::Path(path) => {
1779 let type_name = path.split("::").last().unwrap_or(path);
1780
1781 if type_name == enum_name || path == enum_name {
1783 *ty = match replacement {
1784 TypeReplacement::BoxDyn => PureType::Path(format!("Box<dyn {}>", trait_name)),
1785 TypeReplacement::ImplTrait => PureType::ImplTrait(vec![trait_name.to_string()]),
1786 TypeReplacement::Generic => {
1787 PureType::Path("T".to_string())
1789 }
1790 };
1791 return true;
1792 }
1793
1794 if path.contains('<') && path.contains(enum_name) {
1797 let replacement_str = match replacement {
1798 TypeReplacement::BoxDyn => format!("Box<dyn {}>", trait_name),
1799 TypeReplacement::ImplTrait => format!("impl {}", trait_name),
1800 TypeReplacement::Generic => "T".to_string(),
1801 };
1802
1803 let new_path = replace_type_in_generic_path(path, enum_name, &replacement_str);
1806 if new_path != *path {
1807 *path = new_path;
1808 return true;
1809 }
1810 }
1811
1812 false
1813 }
1814 PureType::Ref { ty: inner, .. } => replace_type(inner, enum_name, trait_name, replacement),
1815 PureType::Tuple(types) => {
1816 let mut changed = false;
1817 for t in types {
1818 if replace_type(t, enum_name, trait_name, replacement) {
1819 changed = true;
1820 }
1821 }
1822 changed
1823 }
1824 PureType::Array { ty: inner, .. } => {
1825 replace_type(inner, enum_name, trait_name, replacement)
1826 }
1827 PureType::Slice(inner) => replace_type(inner, enum_name, trait_name, replacement),
1828 PureType::Fn { params, ret } => {
1829 let mut changed = false;
1830 for p in params {
1831 if replace_type(p, enum_name, trait_name, replacement) {
1832 changed = true;
1833 }
1834 }
1835 if let Some(ref mut r) = ret {
1836 if replace_type(r, enum_name, trait_name, replacement) {
1837 changed = true;
1838 }
1839 }
1840 changed
1841 }
1842 _ => false,
1843 }
1844}
1845
1846fn replace_type_in_generic_path(path: &str, enum_name: &str, replacement: &str) -> String {
1849 let mut result = String::new();
1852 let chars = path.chars().peekable();
1853 let mut current_word = String::new();
1854
1855 for c in chars {
1856 if c.is_alphanumeric() || c == '_' {
1857 current_word.push(c);
1858 } else {
1859 if current_word == enum_name {
1861 result.push_str(replacement);
1862 } else {
1863 result.push_str(¤t_word);
1864 }
1865 current_word.clear();
1866 result.push(c);
1867 }
1868 }
1869
1870 if current_word == enum_name {
1872 result.push_str(replacement);
1873 } else {
1874 result.push_str(¤t_word);
1875 }
1876
1877 result
1878}
1879
1880fn count_match_expressions(ctx: &ASTMutationContext, enum_name: &str) -> usize {
1882 let mut count = 0;
1883
1884 let fn_ids: Vec<_> = ctx
1885 .symbol_registry
1886 .iter()
1887 .filter(|(id, _)| matches!(ctx.symbol_registry.kind(*id), Some(SymbolKind::Function)))
1888 .map(|(id, _)| id)
1889 .collect();
1890
1891 for fn_id in fn_ids {
1892 if let Some(PureItem::Fn(func)) = ctx.ast_registry.get(fn_id) {
1893 count += count_matches_in_block(&func.body, enum_name);
1894 }
1895 }
1896
1897 count
1898}
1899
1900fn count_matches_in_block(block: &PureBlock, enum_name: &str) -> usize {
1901 let mut count = 0;
1902 for stmt in &block.stmts {
1903 count += count_matches_in_stmt(stmt, enum_name);
1904 }
1905 count
1906}
1907
1908fn count_matches_in_stmt(stmt: &PureStmt, enum_name: &str) -> usize {
1909 match stmt {
1910 PureStmt::Local { init, .. } => {
1911 if let Some(expr) = init {
1912 count_matches_in_expr(expr, enum_name)
1913 } else {
1914 0
1915 }
1916 }
1917 PureStmt::Semi(expr) | PureStmt::Expr(expr) => count_matches_in_expr(expr, enum_name),
1918 PureStmt::Item(_) => 0,
1919 PureStmt::Verbatim(_) => 0,
1921 }
1922}
1923
1924fn count_matches_in_expr(expr: &PureExpr, enum_name: &str) -> usize {
1925 match expr {
1926 PureExpr::Match {
1927 expr: scrutinee,
1928 arms,
1929 } => {
1930 let mut count = count_matches_in_expr(scrutinee, enum_name);
1931
1932 for arm in arms {
1934 if pattern_references_enum(&arm.pattern, enum_name) {
1935 count += 1;
1936 break; }
1938 }
1939
1940 for arm in arms {
1942 count += count_matches_in_expr(&arm.body, enum_name);
1943 }
1944 count
1945 }
1946 PureExpr::If {
1947 cond,
1948 then_branch,
1949 else_branch,
1950 } => {
1951 let mut count = count_matches_in_expr(cond, enum_name);
1952 count += count_matches_in_block(then_branch, enum_name);
1953 if let Some(else_expr) = else_branch {
1954 count += count_matches_in_expr(else_expr, enum_name);
1955 }
1956 count
1957 }
1958 PureExpr::Block { block, .. } => count_matches_in_block(block, enum_name),
1959 PureExpr::Call { func, args, .. } => {
1960 let mut count = count_matches_in_expr(func, enum_name);
1961 for arg in args {
1962 count += count_matches_in_expr(arg, enum_name);
1963 }
1964 count
1965 }
1966 PureExpr::MethodCall { receiver, args, .. } => {
1967 let mut count = count_matches_in_expr(receiver, enum_name);
1968 for arg in args {
1969 count += count_matches_in_expr(arg, enum_name);
1970 }
1971 count
1972 }
1973 PureExpr::Closure { body, .. } => count_matches_in_expr(body, enum_name),
1974 PureExpr::Loop { body: block, .. } => count_matches_in_block(block, enum_name),
1975 PureExpr::While { cond, body, .. } => {
1976 count_matches_in_expr(cond, enum_name) + count_matches_in_block(body, enum_name)
1977 }
1978 PureExpr::For { expr, body, .. } => {
1979 count_matches_in_expr(expr, enum_name) + count_matches_in_block(body, enum_name)
1980 }
1981 _ => 0,
1982 }
1983}
1984
1985fn pattern_references_enum(pattern: &PurePattern, enum_name: &str) -> bool {
1986 match pattern {
1987 PurePattern::Path(path) => path.starts_with(&format!("{}::", enum_name)),
1988 PurePattern::Struct { path, .. } => path.starts_with(&format!("{}::", enum_name)),
1989 PurePattern::Tuple(elements) | PurePattern::Slice(elements) => elements
1990 .iter()
1991 .any(|p| pattern_references_enum(p, enum_name)),
1992 PurePattern::Or(patterns) => patterns
1993 .iter()
1994 .any(|p| pattern_references_enum(p, enum_name)),
1995 PurePattern::Ref { pattern: inner, .. } => pattern_references_enum(inner, enum_name),
1996 _ => false,
1997 }
1998}
1999
2000fn replace_enum_usages(
2002 ctx: &mut ASTMutationContext,
2003 enum_name: &str,
2004 variant_names: &[String],
2005) -> usize {
2006 let mut changes = 0;
2007
2008 let fn_ids: Vec<_> = ctx
2010 .symbol_registry
2011 .iter()
2012 .filter(|(id, _)| matches!(ctx.symbol_registry.kind(*id), Some(SymbolKind::Function)))
2013 .map(|(id, _)| id)
2014 .collect();
2015
2016 for fn_id in fn_ids {
2017 if let Some(PureItem::Fn(mut func)) = ctx.ast_registry.get(fn_id).cloned() {
2018 let fn_changes = replace_in_block(&mut func.body, enum_name, variant_names);
2019 if fn_changes > 0 {
2020 ctx.set_ast(fn_id, PureItem::Fn(func));
2021 changes += fn_changes;
2022 }
2023 }
2024 }
2025
2026 changes
2027}
2028
2029fn replace_in_block(block: &mut PureBlock, enum_name: &str, variant_names: &[String]) -> usize {
2030 let mut changes = 0;
2031 for stmt in &mut block.stmts {
2032 changes += replace_in_stmt(stmt, enum_name, variant_names);
2033 }
2034 changes
2035}
2036
2037fn replace_in_stmt(stmt: &mut PureStmt, enum_name: &str, variant_names: &[String]) -> usize {
2038 match stmt {
2039 PureStmt::Local { init, .. } => {
2040 if let Some(expr) = init {
2041 return replace_in_expr(expr, enum_name, variant_names);
2042 }
2043 0
2044 }
2045 PureStmt::Semi(expr) | PureStmt::Expr(expr) => {
2046 replace_in_expr(expr, enum_name, variant_names)
2047 }
2048 PureStmt::Item(_) => 0,
2049 PureStmt::Verbatim(_) => 0,
2051 }
2052}
2053
2054fn replace_in_expr(expr: &mut PureExpr, enum_name: &str, variant_names: &[String]) -> usize {
2055 match expr {
2056 PureExpr::Path(path) => {
2058 if path.starts_with(&format!("{}::", enum_name)) {
2060 let variant_part = path.strip_prefix(&format!("{}::", enum_name));
2061 if let Some(variant) = variant_part {
2062 if variant_names.contains(&variant.to_string()) {
2063 *path = variant.to_string();
2065 return 1;
2066 }
2067 }
2068 }
2069 0
2070 }
2071
2072 PureExpr::Call { func, args, .. } => {
2074 let mut changes = replace_in_expr(func, enum_name, variant_names);
2075 for arg in args {
2076 changes += replace_in_expr(arg, enum_name, variant_names);
2077 }
2078 changes
2079 }
2080 PureExpr::MethodCall { receiver, args, .. } => {
2081 let mut changes = replace_in_expr(receiver, enum_name, variant_names);
2082 for arg in args {
2083 changes += replace_in_expr(arg, enum_name, variant_names);
2084 }
2085 changes
2086 }
2087 PureExpr::Binary { left, right, .. } => {
2088 replace_in_expr(left, enum_name, variant_names)
2089 + replace_in_expr(right, enum_name, variant_names)
2090 }
2091 PureExpr::Unary { expr, .. } => replace_in_expr(expr, enum_name, variant_names),
2092 PureExpr::If {
2093 cond,
2094 then_branch,
2095 else_branch,
2096 } => {
2097 let mut changes = replace_in_expr(cond, enum_name, variant_names);
2098 changes += replace_in_block(then_branch, enum_name, variant_names);
2099 if let Some(else_expr) = else_branch {
2100 changes += replace_in_expr(else_expr, enum_name, variant_names);
2101 }
2102 changes
2103 }
2104 PureExpr::Match { expr, arms } => {
2105 let mut changes = replace_in_expr(expr, enum_name, variant_names);
2106 for arm in arms {
2107 changes += replace_in_pattern(&mut arm.pattern, enum_name, variant_names);
2109 changes += replace_in_expr(&mut arm.body, enum_name, variant_names);
2110 }
2111 changes
2112 }
2113 PureExpr::Block { block, .. } => replace_in_block(block, enum_name, variant_names),
2114 PureExpr::Return(Some(v)) => replace_in_expr(v, enum_name, variant_names),
2115 PureExpr::Return(None) => 0,
2116 PureExpr::Struct { fields, .. } => {
2117 let mut changes = 0;
2118 for (_, field_expr) in fields {
2119 changes += replace_in_expr(field_expr, enum_name, variant_names);
2120 }
2121 changes
2122 }
2123 PureExpr::Tuple(elements) => {
2124 let mut changes = 0;
2125 for elem in elements {
2126 changes += replace_in_expr(elem, enum_name, variant_names);
2127 }
2128 changes
2129 }
2130 PureExpr::Array(elements) => {
2131 let mut changes = 0;
2132 for elem in elements {
2133 changes += replace_in_expr(elem, enum_name, variant_names);
2134 }
2135 changes
2136 }
2137 PureExpr::Index { expr, index, .. } => {
2138 replace_in_expr(expr, enum_name, variant_names)
2139 + replace_in_expr(index, enum_name, variant_names)
2140 }
2141 PureExpr::Field { expr, .. } => replace_in_expr(expr, enum_name, variant_names),
2142 PureExpr::Ref { expr, .. } => replace_in_expr(expr, enum_name, variant_names),
2143 PureExpr::Try(inner) => replace_in_expr(inner, enum_name, variant_names),
2144 PureExpr::Await(inner) => replace_in_expr(inner, enum_name, variant_names),
2145 PureExpr::Closure { body, .. } => replace_in_expr(body, enum_name, variant_names),
2146 PureExpr::Loop { body, .. } => replace_in_block(body, enum_name, variant_names),
2147 PureExpr::While { cond, body, .. } => {
2148 replace_in_expr(cond, enum_name, variant_names)
2149 + replace_in_block(body, enum_name, variant_names)
2150 }
2151 PureExpr::For { expr, body, .. } => {
2152 replace_in_expr(expr, enum_name, variant_names)
2153 + replace_in_block(body, enum_name, variant_names)
2154 }
2155 PureExpr::Let { expr, .. } => replace_in_expr(expr, enum_name, variant_names),
2156 PureExpr::Range { start, end, .. } => {
2157 let mut changes = 0;
2158 if let Some(s) = start {
2159 changes += replace_in_expr(s, enum_name, variant_names);
2160 }
2161 if let Some(e) = end {
2162 changes += replace_in_expr(e, enum_name, variant_names);
2163 }
2164 changes
2165 }
2166 PureExpr::Cast { expr, .. } => replace_in_expr(expr, enum_name, variant_names),
2167 _ => 0,
2169 }
2170}
2171
2172use ryo_source::pure::PurePattern;
2173
2174fn replace_in_pattern(
2175 pattern: &mut PurePattern,
2176 enum_name: &str,
2177 variant_names: &[String],
2178) -> usize {
2179 match pattern {
2180 PurePattern::Struct { path, fields, .. } => {
2182 let mut changes = 0;
2183 if path.starts_with(&format!("{}::", enum_name)) {
2184 if let Some(variant) = path.strip_prefix(&format!("{}::", enum_name)) {
2185 let base_variant = variant
2186 .split(|c: char| !c.is_alphanumeric() && c != '_')
2187 .next()
2188 .unwrap_or(variant);
2189 if variant_names.contains(&base_variant.to_string()) {
2190 *path = variant.to_string();
2191 changes += 1;
2192 }
2193 }
2194 }
2195 for (_, field_pattern) in fields {
2197 changes += replace_in_pattern(field_pattern, enum_name, variant_names);
2198 }
2199 changes
2200 }
2201 PurePattern::Tuple(elements) | PurePattern::Slice(elements) => {
2203 let mut changes = 0;
2204 for elem in elements {
2205 changes += replace_in_pattern(elem, enum_name, variant_names);
2206 }
2207 changes
2208 }
2209 PurePattern::Path(path) => {
2211 if path.starts_with(&format!("{}::", enum_name)) {
2212 if let Some(variant) = path.strip_prefix(&format!("{}::", enum_name)) {
2213 if variant_names.contains(&variant.to_string()) {
2214 *path = variant.to_string();
2215 return 1;
2216 }
2217 }
2218 }
2219 0
2220 }
2221 PurePattern::Ref { pattern: inner, .. } => {
2223 replace_in_pattern(inner, enum_name, variant_names)
2224 }
2225 PurePattern::Or(patterns) => {
2227 let mut changes = 0;
2228 for p in patterns {
2229 changes += replace_in_pattern(p, enum_name, variant_names);
2230 }
2231 changes
2232 }
2233 _ => 0,
2235 }
2236}
2237
2238#[cfg(test)]
2239mod tests {
2240 use super::*;
2241 use crate::engine::ASTMutationEngine;
2242 use ryo_analysis::testing::ContextBuilder;
2243
2244 #[test]
2245 fn test_v2_extract_trait() {
2246 let mut ctx = ContextBuilder::new()
2247 .with_file(
2248 "src/lib.rs",
2249 r#"
2250struct Foo {
2251 value: i32,
2252}
2253
2254impl Foo {
2255 fn get_value(&self) -> i32 {
2256 self.value
2257 }
2258
2259 fn set_value(&mut self, v: i32) {
2260 self.value = v;
2261 }
2262
2263 fn helper(&self) {}
2264}
2265"#,
2266 )
2267 .build();
2268
2269 let impl_id = ctx
2271 .registry
2272 .iter()
2273 .find(|(id, _path)| {
2274 if !matches!(ctx.registry.kind(*id), Some(SymbolKind::Impl)) {
2275 return false;
2276 }
2277 if let Some(PureItem::Impl(imp)) = ctx.ast_registry.get(*id) {
2278 imp.trait_.is_none() && imp.self_ty == "Foo"
2279 } else {
2280 false
2281 }
2282 })
2283 .map(|(id, _)| id)
2284 .expect("Should find impl Foo");
2285
2286 let mutation = ExtractTraitMutation::new(impl_id, "ValueAccessor")
2287 .with_methods(vec!["get_value".to_string(), "set_value".to_string()]);
2288 let result = ASTMutationEngine::execute_ast_reg(&mutation, &mut ctx);
2289
2290 println!("ExtractTrait result: {:?}", result.result);
2291 assert!(result.result.changes >= 2, "Expected at least 2 changes");
2293 }
2294
2295 #[test]
2296 fn test_v2_inline_trait() {
2297 let mut ctx = ContextBuilder::new()
2298 .with_file(
2299 "src/lib.rs",
2300 r#"
2301struct Foo;
2302
2303trait Greet {
2304 fn greet(&self) -> String;
2305}
2306
2307impl Greet for Foo {
2308 fn greet(&self) -> String {
2309 "Hello".to_string()
2310 }
2311}
2312"#,
2313 )
2314 .build();
2315
2316 let trait_id = ctx
2318 .registry
2319 .iter()
2320 .find(|(id, _path)| matches!(ctx.registry.kind(*id), Some(SymbolKind::Trait)))
2321 .map(|(id, _)| id)
2322 .expect("Should find trait Greet");
2323
2324 let mutation = InlineTraitMutation::new(trait_id, "Foo");
2325 let result = ASTMutationEngine::execute_ast_reg(&mutation, &mut ctx);
2326
2327 println!("InlineTrait result: {:?}", result.result);
2328 assert!(result.result.changes >= 2, "Expected at least 2 changes");
2330
2331 assert!(
2335 !result.result.description.contains("cross-crate callers"),
2336 "single-crate inline must not emit cross-crate footnote; got: {}",
2337 result.result.description
2338 );
2339 }
2340
2341 #[test]
2342 fn test_v2_inline_trait_keep_trait() {
2343 let mut ctx = ContextBuilder::new()
2344 .with_file(
2345 "src/lib.rs",
2346 r#"
2347struct Foo;
2348
2349trait Greet {
2350 fn greet(&self) -> String;
2351}
2352
2353impl Greet for Foo {
2354 fn greet(&self) -> String {
2355 "Hello".to_string()
2356 }
2357}
2358"#,
2359 )
2360 .build();
2361
2362 let trait_id = ctx
2364 .registry
2365 .iter()
2366 .find(|(id, _path)| matches!(ctx.registry.kind(*id), Some(SymbolKind::Trait)))
2367 .map(|(id, _)| id)
2368 .expect("Should find trait Greet");
2369
2370 let mutation = InlineTraitMutation::new(trait_id, "Foo").keep_trait();
2371 let result = ASTMutationEngine::execute_ast_reg(&mutation, &mut ctx);
2372
2373 println!("InlineTrait (keep_trait) result: {:?}", result.result);
2374 assert!(result.result.changes >= 2, "Expected at least 2 changes");
2376 }
2377
2378 #[test]
2379 fn test_v2_enum_to_trait_dynamic_strategy() {
2380 let mut ctx = ContextBuilder::new()
2381 .with_file(
2382 "src/lib.rs",
2383 r#"
2384enum Status {
2385 Running,
2386 Stopped,
2387}
2388
2389fn process(status: Status) -> Status {
2390 status
2391}
2392
2393struct Config {
2394 current_status: Status,
2395}
2396"#,
2397 )
2398 .build();
2399
2400 let enum_id = ctx
2402 .registry
2403 .iter()
2404 .find(|(id, path)| {
2405 path.name() == "Status" && matches!(ctx.registry.kind(*id), Some(SymbolKind::Enum))
2406 })
2407 .map(|(id, _)| id)
2408 .expect("Enum 'Status' should exist");
2409
2410 let mutation = EnumToTraitMutation::from_symbol_id(enum_id)
2411 .with_strategy(EnumToTraitStrategy::Dynamic);
2412 let result = ASTMutationEngine::execute_ast_reg(&mutation, &mut ctx);
2413
2414 println!("EnumToTrait (Dynamic) result: {:?}", result.result);
2415 assert!(result.result.changes >= 5, "Expected at least 5 changes");
2417 assert!(
2418 result.result.description.contains("Box<dyn>"),
2419 "Should mention Box<dyn> strategy"
2420 );
2421 }
2422
2423 #[test]
2424 fn test_v2_enum_to_trait_static_strategy() {
2425 let mut ctx = ContextBuilder::new()
2426 .with_file(
2427 "src/lib.rs",
2428 r#"
2429enum Filter {
2430 Active,
2431 Inactive,
2432}
2433
2434fn apply_filter(filter: Filter) {
2435 let _ = filter;
2436}
2437"#,
2438 )
2439 .build();
2440
2441 let enum_id = ctx
2443 .registry
2444 .iter()
2445 .find(|(id, path)| {
2446 path.name() == "Filter" && matches!(ctx.registry.kind(*id), Some(SymbolKind::Enum))
2447 })
2448 .map(|(id, _)| id)
2449 .expect("Enum 'Filter' should exist");
2450
2451 let mutation =
2452 EnumToTraitMutation::from_symbol_id(enum_id).with_strategy(EnumToTraitStrategy::Static);
2453 let result = ASTMutationEngine::execute_ast_reg(&mutation, &mut ctx);
2454
2455 println!("EnumToTrait (Static) result: {:?}", result.result);
2456 assert!(result.result.changes >= 5, "Expected at least 5 changes");
2458 assert!(
2459 result.result.description.contains("impl Trait"),
2460 "Should mention impl Trait strategy"
2461 );
2462 }
2463
2464 #[test]
2465 fn test_v2_enum_to_trait_marker_only_strategy() {
2466 let mut ctx = ContextBuilder::new()
2467 .with_file(
2468 "src/lib.rs",
2469 r#"
2470enum Mode {
2471 Fast,
2472 Slow,
2473}
2474
2475fn get_mode() -> Mode {
2476 Mode::Fast
2477}
2478"#,
2479 )
2480 .build();
2481
2482 let enum_id = ctx
2484 .registry
2485 .iter()
2486 .find(|(id, path)| {
2487 path.name() == "Mode" && matches!(ctx.registry.kind(*id), Some(SymbolKind::Enum))
2488 })
2489 .map(|(id, _)| id)
2490 .expect("Enum 'Mode' should exist");
2491
2492 let mutation = EnumToTraitMutation::from_symbol_id(enum_id)
2493 .with_strategy(EnumToTraitStrategy::MarkerOnly);
2494 let result = ASTMutationEngine::execute_ast_reg(&mutation, &mut ctx);
2495
2496 println!("EnumToTrait (MarkerOnly) result: {:?}", result.result);
2497 assert!(result.result.changes >= 5, "Expected at least 5 changes");
2500 assert!(
2501 result.result.description.contains("marker only"),
2502 "Should mention marker only strategy"
2503 );
2504 }
2505
2506 #[test]
2507 fn test_v2_enum_to_trait_generic_strategy() {
2508 let mut ctx = ContextBuilder::new()
2509 .with_file(
2510 "src/lib.rs",
2511 r#"
2512enum Status {
2513 Running,
2514 Stopped,
2515}
2516
2517fn process(status: Status) -> Status {
2518 status
2519}
2520
2521struct Config {
2522 current_status: Status,
2523}
2524"#,
2525 )
2526 .build();
2527
2528 let enum_id = ctx
2530 .registry
2531 .iter()
2532 .find(|(id, path)| {
2533 path.name() == "Status" && matches!(ctx.registry.kind(*id), Some(SymbolKind::Enum))
2534 })
2535 .map(|(id, _)| id)
2536 .expect("Enum 'Status' should exist");
2537
2538 let mutation = EnumToTraitMutation::from_symbol_id(enum_id)
2539 .with_strategy(EnumToTraitStrategy::Generic);
2540 let result = ASTMutationEngine::execute_ast_reg(&mutation, &mut ctx);
2541
2542 println!("EnumToTrait (Generic) result: {:?}", result.result);
2543 assert!(result.result.changes >= 5, "Expected at least 5 changes");
2545 assert!(
2546 result.result.description.contains("generics"),
2547 "Should mention generics strategy"
2548 );
2549 }
2550
2551 #[test]
2559 fn test_v2_enum_to_trait_registers_variant_trait_impls() {
2560 let mut ctx = ContextBuilder::new()
2561 .with_file(
2562 "src/lib.rs",
2563 r#"
2564enum Status {
2565 Idle,
2566 Running,
2567 Stopped,
2568 Failed,
2569}
2570
2571impl Status {
2572 fn code(&self) -> u8 {
2573 0
2574 }
2575}
2576
2577fn make() -> Status {
2578 Status::Idle
2579}
2580"#,
2581 )
2582 .build();
2583
2584 let enum_id = ctx
2585 .registry
2586 .iter()
2587 .find(|(id, path)| {
2588 path.name() == "Status" && matches!(ctx.registry.kind(*id), Some(SymbolKind::Enum))
2589 })
2590 .map(|(id, _)| id)
2591 .expect("Enum 'Status' should exist");
2592
2593 let mutation = EnumToTraitMutation::from_symbol_id(enum_id)
2594 .with_strategy(EnumToTraitStrategy::Dynamic);
2595 let result = ASTMutationEngine::execute_ast_reg(&mutation, &mut ctx);
2596 println!(
2597 "EnumToTrait (Dynamic, with impl) result: {:?}",
2598 result.result
2599 );
2600
2601 let mut impl_paths: Vec<String> = Vec::new();
2603 for (id, path) in ctx.registry.iter() {
2604 if matches!(ctx.registry.kind(id), Some(SymbolKind::Impl)) {
2605 impl_paths.push(path.to_string());
2606 }
2607 }
2608 println!("post-mutation Impl symbols: {:?}", impl_paths);
2609
2610 for variant in ["Idle", "Running", "Stopped", "Failed"] {
2611 let expected = format!("<impl Status for {}>", variant);
2612 assert!(
2613 impl_paths.iter().any(|p| p.contains(&expected)),
2614 "missing variant trait impl symbol: {} (got {:?})",
2615 expected,
2616 impl_paths
2617 );
2618 }
2619 }
2620}