Skip to main content

ryo_executor/engine/impls/
trait_ops.rs

1//! V2 ASTRegApply implementations for trait operations
2//!
3//! - ExtractTrait: Extract methods from impl block into a new trait
4//! - InlineTrait: Inline trait methods back into inherent impl
5//!
6//! # Implementation Strategy
7//!
8//! **ExtractTrait:**
9//! 1. Find the inherent impl for the struct (impl Foo { ... } where trait_ is None)
10//! 2. Extract specified methods (or all if methods is None)
11//! 3. Create a new trait definition with method signatures
12//! 4. Create a new trait impl (impl TraitName for Foo { ... }) with method bodies
13//! 5. Remove the extracted methods from the inherent impl
14//!
15//! **InlineTrait:**
16//! 1. Find the trait impl (impl TraitName for Foo { ... })
17//! 2. Find or create the inherent impl (impl Foo { ... })
18//! 3. Move methods from trait impl to inherent impl
19//! 4. Optionally remove the trait definition and trait impl
20
21use 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        // Step 1: O(1) lookup for the inherent impl using symbol_id
38        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        // Verify it's an impl and get the AST
51        let inherent_impl = match ctx.ast_registry.get(impl_id) {
52            Some(PureItem::Impl(imp)) => {
53                // Verify it's an inherent impl (not a trait impl)
54                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        // Get struct name from the impl's self_ty
83        let struct_name = inherent_impl.self_ty.clone();
84
85        // Step 2: Partition methods into extracted vs remaining
86        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, // Extract all methods
92                    }
93                } else {
94                    false // Don't extract non-method items
95                }
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        // Step 3: Create trait definition with method signatures
109        let trait_items: Vec<PureTraitItem> = extracted_items
110            .iter()
111            .filter_map(|item| {
112                if let PureImplItem::Fn(f) = item {
113                    // Create trait method signature (empty body for trait definition)
114                    let trait_fn = PureFn {
115                        attrs: f.attrs.clone(),
116                        vis: PureVis::Private, // Trait methods use default visibility
117                        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(), // Empty body for trait signature
127                    };
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, // Default to public trait
138            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        // Register the new trait
147        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        // Step 4: Create trait impl with method bodies
173        // Trait impl methods must NOT have visibility qualifiers (pub is inherited from trait)
174        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; // Remove pub for trait impl methods
179                    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        // Register the trait impl
196        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            // Also add to module_items
224            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        // Step 5: Update the inherent impl to remove extracted methods
235        if remaining_items.is_empty() {
236            // If no methods remain, remove the inherent impl
237            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        // Get trait name from symbol_id (O(1) lookup)
266        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        // Step 1: Find the trait impl (impl TraitName for Foo)
278        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        // Step 2: Find or identify the inherent impl
317        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        // Step 3: Move methods to inherent impl
329        if let Some((inherent_impl_id, _)) = inherent_impl_entry {
330            // Existing inherent impl - add methods to it (both ASTRegistry and module_items)
331            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                // Also update in module_items
338                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            // No inherent impl exists - create one with the trait methods
360            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                // Also add to module_items
396                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        // Step 4: Remove the trait impl
409        ctx.remove_symbol(trait_impl_id);
410        changes += 1;
411
412        // Step 5: Optionally remove the trait definition (O(1) using symbol_id)
413        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        // Use the provided SymbolId for O(1) access
438        let trait_id = self.trait_id;
439
440        // Verify the trait exists and is a trait
441        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        // Verify the symbol exists and is an enum
464        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        // Get enum name from path for trait name and usage replacement
484        let enum_name = enum_path.name().to_string();
485
486        // Determine trait name:
487        // - If user specified trait_name, use it
488        // - For MarkerOnly without specified name, use {EnumName}Trait to avoid collision
489        // - Otherwise, use enum_name
490        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                        // MarkerOnly keeps enum, so trait needs different name
497                        default_trait_name = format!("{}Trait", enum_name);
498                        &default_trait_name
499                    }
500                    _ => &enum_name,
501                }
502            }
503        };
504
505        // Get the enum AST
506        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        // Collect variant names for usage site replacement
530        let variant_names: Vec<String> = enum_def.variants.iter().map(|v| v.name.clone()).collect();
531
532        // Step 1.5: Find enum's inherent impl block and extract methods
533        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                    // Inherent impl: no trait, matches enum name
542                    imp.trait_.is_none() && imp.self_ty == enum_name
543                } else {
544                    false
545                }
546            })
547            .map(|(id, _)| id); // Extract just the SymbolId to end the borrow
548
549        // Extract methods from enum's impl block
550        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                            // Only extract instance methods (with &self or &mut self)
557                            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        // Step 1.6: Remove enum FIRST if trait name equals enum name (to free the path)
579        // This must happen before trait registration to avoid path collision
580        // MarkerOnly: NEVER remove enum early (types still reference it)
581        let enum_removed_early = match self.strategy {
582            EnumToTraitStrategy::MarkerOnly => false, // Never remove for MarkerOnly
583            _ if self.remove_enum && trait_name == enum_name => {
584                ctx.remove_symbol(enum_id);
585                changes += 1;
586                // Also remove the enum's inherent impl block early
587                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        // Step 2: Create trait definition with method signatures
597        let trait_items: Vec<PureTraitItem> = enum_methods
598            .iter()
599            .map(|f| {
600                // Create trait method signature (no body for trait definition)
601                let trait_fn = PureFn {
602                    attrs: Vec::new(),
603                    vis: PureVis::Private, // Trait methods use default visibility
604                    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(), // Empty body = abstract method in trait
614                };
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        // Step 3: Create struct + impl for each variant
653        for variant in &enum_def.variants {
654            // Convert variant fields to struct fields
655            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            // Create impl Trait for struct with method implementations
696            let impl_items: Vec<PureImplItem> = enum_methods
697                .iter()
698                .map(|f| {
699                    // Create method implementation with todo!() body
700                    let impl_fn = PureFn {
701                        attrs: Vec::new(),
702                        vis: PureVis::Private, // Trait impl methods don't have visibility
703                        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        // Step 4: Replace usage sites (EnumName::VariantName -> VariantName)
748        // MarkerOnly: Do NOT replace usage sites - enum remains as the concrete type
749        let usage_changes = match self.strategy {
750            EnumToTraitStrategy::MarkerOnly => 0, // Keep enum usages as-is
751            _ => replace_enum_usages(ctx, &enum_name, &variant_names),
752        };
753        changes += usage_changes;
754
755        // Step 5: Replace type annotations based on strategy
756        // Note: MarkerOnly does NOT replace types - the enum remains as the concrete type
757        let type_changes = match self.strategy {
758            EnumToTraitStrategy::Dynamic => {
759                // Replace `EnumName` with `Box<dyn TraitName>`
760                replace_type_annotations(ctx, &enum_name, trait_name, TypeReplacement::BoxDyn)
761            }
762            EnumToTraitStrategy::Static => {
763                // Replace `EnumName` with `impl TraitName` (falls back to Box<dyn> for fields)
764                replace_type_annotations(ctx, &enum_name, trait_name, TypeReplacement::ImplTrait)
765            }
766            EnumToTraitStrategy::Generic => {
767                // Replace `EnumName` with generic type parameter, add generics to containers
768                replace_type_annotations(ctx, &enum_name, trait_name, TypeReplacement::Generic)
769            }
770            EnumToTraitStrategy::MarkerOnly => {
771                // No type replacement - enum remains as the concrete type
772                0
773            }
774        };
775        changes += type_changes;
776
777        // Step 6: Handle match expressions based on match_handling
778        let match_changes = match self.match_handling {
779            MatchHandling::WarnOnly => {
780                // TODO: Emit warnings for match expressions that need manual migration
781                // For now, just count them for reporting
782                count_match_expressions(ctx, &enum_name)
783            }
784            MatchHandling::Downcast => {
785                // TODO: Convert match to downcast-based dispatch
786                // This requires adding Any bound to trait
787                0
788            }
789            MatchHandling::BlockOnMatch => {
790                // This should have been checked before execution
791                // If we're here, there were no match expressions
792                0
793            }
794        };
795        // Note: match_changes is informational, not counted as changes
796
797        // Step 7: Optionally remove the original enum and its inherent impl
798        // MarkerOnly strategy: NEVER remove enum (types still reference it)
799        // Other strategies: remove if remove_enum is true (skip if already removed in step 1.5)
800        let should_remove = match self.strategy {
801            EnumToTraitStrategy::MarkerOnly => false, // Never remove for MarkerOnly
802            _ => self.remove_enum && !enum_removed_early,
803        };
804        if should_remove {
805            ctx.remove_symbol(enum_id);
806            changes += 1;
807
808            // Also remove the enum's inherent impl block
809            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        // Track if enum was actually removed (either early or in step 7)
832        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
854/// Type replacement strategy
855enum TypeReplacement {
856    /// Replace with `Box<dyn TraitName>`
857    BoxDyn,
858    /// Replace with `impl TraitName`
859    ImplTrait,
860    /// Replace with generic type parameter `T` (requires adding generics to container)
861    Generic,
862}
863
864/// Replace type annotations in functions, structs, and impl blocks
865fn 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    // Collect all symbols to iterate
874    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                // For struct fields, impl Trait is not allowed in Rust
894                // Fall back to Box<dyn Trait> for Static strategy (but keep Generic as-is)
895                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                    // For Generic strategy, add type parameter to struct
907                    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
959/// Replace types in a function signature
960fn 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    // Replace in parameters
969    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    // Replace in return type
978    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    // For Generic strategy, add type parameter if changes were made
985    if changes > 0 && matches!(replacement, TypeReplacement::Generic) {
986        add_generic_param(&mut f.generics, trait_name);
987    }
988
989    changes
990}
991
992/// Replace types in struct fields
993fn 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
1022/// Add generic type parameter `T: TraitName` to generics
1023fn add_generic_param(generics: &mut PureGenerics, trait_name: &str) {
1024    // Check if T already exists
1025    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
1038/// Replace a type if it matches the enum name
1039fn 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            // Check for exact match (e.g., "Filter")
1050            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                        // Use generic type parameter (caller adds generics to container)
1056                        PureType::Path("T".to_string())
1057                    }
1058                };
1059                return true;
1060            }
1061
1062            // Check for generic types containing the enum (e.g., "Option<Filter>", "Vec<Filter>")
1063            // Replace enum name within generic arguments
1064            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                // Simple replacement: replace "EnumName" with the replacement type
1072                // This handles cases like Option<Filter> -> Option<Box<dyn Filter>>
1073                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
1114/// Replace enum type within a generic path string
1115/// e.g., "Option<Filter>" -> "Option<Box<dyn Filter>>"
1116fn replace_type_in_generic_path(path: &str, enum_name: &str, replacement: &str) -> String {
1117    // Find positions where enum_name appears as a type argument
1118    // We need to be careful to only replace complete type names, not substrings
1119    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            // Check if current_word matches enum_name
1128            if current_word == enum_name {
1129                result.push_str(replacement);
1130            } else {
1131                result.push_str(&current_word);
1132            }
1133            current_word.clear();
1134            result.push(c);
1135        }
1136    }
1137
1138    // Handle trailing word
1139    if current_word == enum_name {
1140        result.push_str(replacement);
1141    } else {
1142        result.push_str(&current_word);
1143    }
1144
1145    result
1146}
1147
1148/// Count match expressions on the enum (for warning purposes)
1149fn 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            // Check if any arm pattern matches our enum
1199            for arm in arms {
1200                if pattern_references_enum(&arm.pattern, enum_name) {
1201                    count += 1;
1202                    break; // Count this match once
1203                }
1204            }
1205
1206            // Recurse into arm bodies
1207            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
1266/// Replace all usages of EnumName::VariantName with VariantName in expressions
1267fn replace_enum_usages(
1268    ctx: &mut ASTMutationContext,
1269    enum_name: &str,
1270    variant_names: &[String],
1271) -> usize {
1272    let mut changes = 0;
1273
1274    // Collect all function symbols to iterate
1275    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        // Check for path expressions like Status::Running
1321        PureExpr::Path(path) => {
1322            // Check if path matches EnumName::VariantName pattern
1323            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                        // Replace EnumName::VariantName with VariantName
1328                        *path = variant.to_string();
1329                        return 1;
1330                    }
1331                }
1332            }
1333            0
1334        }
1335
1336        // Recurse into compound expressions
1337        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                // Replace in pattern (match arms may have Status::Running patterns)
1372                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        // Literals, macros, and other expressions without sub-expressions
1432        _ => 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        // Struct pattern: `Status::Running { .. }` or `Status::Running`
1445        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            // Recurse into field patterns
1460            for (_, field_pattern) in fields {
1461                changes += replace_in_pattern(field_pattern, enum_name, variant_names);
1462            }
1463            changes
1464        }
1465        // Tuple pattern: recurse into elements
1466        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        // Path pattern: simple enum variant like `Status::Running`
1474        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        // Reference pattern: recurse
1486        PurePattern::Ref { pattern: inner, .. } => {
1487            replace_in_pattern(inner, enum_name, variant_names)
1488        }
1489        // Or pattern: multiple alternatives
1490        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        // Other patterns don't need replacement
1498        _ => 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        // Find the inherent impl's SymbolId
1534        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        // Should create trait + trait impl + update inherent impl
1556        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        // Find the trait's SymbolId
1581        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        // Should move method to inherent impl + remove trait impl + remove trait
1593        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        // Find the trait's SymbolId
1618        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        // Should move method + remove trait impl, but keep trait definition
1630        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        // Find the enum symbol id
1656        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        // Should create: trait, 2 structs, 2 impls, type replacements, enum removal
1671        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        // Find the enum symbol id
1697        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        // Should create: trait, 2 structs, 2 impls, type replacements, enum removal
1712        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        // Find the enum symbol id
1738        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        // Should create: trait, 2 structs, 2 impls, enum removal
1753        // But no type replacements
1754        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        // Find the enum symbol id
1784        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        // Should create: trait, 2 structs, 2 impls, type replacements with generics, enum removal
1799        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}