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 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
38/// Build a `use <path>;` PureItem from a `::`-separated path string.
39///
40/// `path` is expected to be the fully-qualified path of an importable
41/// item (e.g. `"ryo_app::api::Api"`). The trailing segment becomes the
42/// name binding; preceding segments become a chain of `Path` nodes.
43fn 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
52/// Rewrite a fully-qualified `<crate>::<module>::<Item>` path to its
53/// same-crate `crate::<module>::<Item>` form (used by the R4 caller-side
54/// `use` injection so the import resolves regardless of which file in
55/// the crate hosts the caller).
56fn 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
78/// Conservative check: does this `PureType::Path` string mention `Self`
79/// **by value** (rather than as `&Self` / `&mut Self`)? Used by the
80/// R7 supertrait inference to decide whether `Sized` needs to be added
81/// to the extracted trait. We only have the type as an opaque string
82/// here, so the heuristic looks for a bare `Self` segment that isn't
83/// preceded by `&` (any number of spaces) — covering `Self`, `Self<T>`,
84/// `Result<Self, _>`, etc., while still rejecting `&Self` and `&mut Self`.
85fn 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            // Boundary check: previous byte (if any) must not be alnum/_.
92            let prev_ok =
93                i == 0 || !matches!(bytes[i - 1], b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'_');
94            // Boundary check: next byte (if any) must not be alnum/_.
95            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                // Is this a by-reference Self (e.g. `&Self`, `&mut Self`,
102                // `&'a Self`)? Look back for an unmatched `&` token that
103                // is not closed by another type segment.
104                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                            // Consume identifier (likely `mut`); keep walking.
111                            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, // `&Self` / `&mut Self`
122                        _ => break,
123                    }
124                }
125                return true;
126            }
127        }
128        i += 1;
129    }
130    false
131}
132
133/// Walk up the symbol's path until we land on a `Mod` symbol; returns
134/// that module's `SymbolId`. Used to identify the containing file of a
135/// caller (we inject the `use` statement at module scope).
136fn 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
150/// Returns `true` when `tree` already imports `target_path` (either as a
151/// direct path, as part of a group, or via a glob over an enclosing
152/// module). Used to suppress duplicate `use` injection.
153fn 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        // Step 1: O(1) lookup for the inherent impl using symbol_id
177        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        // Verify it's an impl and get the AST
190        let inherent_impl = match ctx.ast_registry.get(impl_id) {
191            Some(PureItem::Impl(imp)) => {
192                // Verify it's an inherent impl (not a trait impl)
193                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        // Get struct name from the impl's self_ty
222        let struct_name = inherent_impl.self_ty.clone();
223
224        // Step 2: Partition methods into extracted vs remaining
225        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, // Extract all methods
231                    }
232                } else {
233                    false // Don't extract non-method items
234                }
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        // Step 3: Create trait definition with method signatures
248        let trait_items: Vec<PureTraitItem> = extracted_items
249            .iter()
250            .filter_map(|item| {
251                if let PureImplItem::Fn(f) = item {
252                    // R5(b) edge-case fix: strip the `mut` qualifier off
253                    // `mut self` receivers and off `mut <name>` named
254                    // parameters when emitting the trait signature.
255                    // Rust's grammar rejects either form inside a
256                    // body-less fn declaration (it parses the `mut`
257                    // binding as a pattern, hence cargo's
258                    // `patterns aren't allowed in functions without
259                    // bodies`). The body-bearing impl method below
260                    // keeps its `mut` exactly as before.
261                    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                    // Create trait method signature (empty body for trait definition).
278                    //
279                    // R14: drop the `const` qualifier when lifting the
280                    // signature into the trait. Rust's grammar rejects
281                    // `const fn` inside `trait` declarations
282                    // ("functions in traits cannot be declared const"),
283                    // and the concrete impl below keeps its `const fn`
284                    // exactly as before, so the constness still applies
285                    // to the call site that goes through the impl. Same
286                    // shape as the R5(b) `mut` strip on signature params
287                    // — only the trait-side signature gets the
288                    // qualifier removed; impl-side behaviour is unchanged.
289                    let trait_fn = PureFn {
290                        attrs: f.attrs.clone(),
291                        vis: PureVis::Private, // Trait methods use default visibility
292                        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(), // Empty body for trait signature
302                    };
303                    Some(PureTraitItem::Fn(trait_fn))
304                } else {
305                    None
306                }
307            })
308            .collect();
309
310        // R7 (Sized bound): when any extracted method's signature mentions
311        // `Self` by-value — either as an owned `self` receiver (not `&self`
312        // / `&mut self`) or as a `-> Self` return type — the trait
313        // implicitly requires `Self: Sized`. Without it cargo rejects the
314        // signature with `the size for values of type Self cannot be known
315        // at compilation time` because trait `Self` is `?Sized` by default
316        // and owned access needs a known size.
317        //
318        // Add `Sized` as a supertrait whenever the heuristic fires. This
319        // is a no-op for any concrete `impl Trait for ConcreteType` whose
320        // self type is already `Sized` (i.e. every non-`dyn` newtype the
321        // ExtractTrait suggester ever targets).
322        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, // Default to public trait
342            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        // Register the new trait
351        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            // R6: When the parent of `trait_path` is an inline `mod foo { ... }`
371            // (as opposed to a file-level module / crate root), the registry
372            // generator skips this trait from the top-level symbol iter pass
373            // because every "child of an inline module" path is expected to
374            // live inside the parent's `PureMod.items` list. `register_with_ast`
375            // only updates the `id → PureItem` map without touching the
376            // parent's items list, so without this push the new trait
377            // declaration never surfaces in the regenerated source —
378            // downstream Step 6 then injects `use crate::<inline_mod>::<TraitName>;`
379            // into every caller and cargo check fails with
380            // `unresolved import 'crate::<inline_mod>::<TraitName>'` (R6
381            // fixture Case32 verify diag).
382            //
383            // For top-level traits (parent = crate root, NOT inline) the
384            // symbol iter pass already emits the trait, so we must NOT push
385            // it to crate root's PureMod.items — that would create a
386            // duplicate `trait <Name>` declaration. Guarded by
387            // `is_inline_module(parent_id)` to scope this strictly to the
388            // inline-mod case.
389            //
390            // The trait IMPL (Step 4 below) does an unconditional
391            // `get_module_items_mut().push(...)` because impls are always
392            // routed through `iter_module_items + file_level_items` filter
393            // (line 463-467 of registry_generator.rs takes only Use/Impl),
394            // never via the symbol iter pass — so no duplication risk for
395            // that side. Traits are handled by the symbol iter pass
396            // exclusively, hence the inline-mod guard here.
397            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        // Step 4: Create trait impl with method bodies
411        // Trait impl methods must NOT have visibility qualifiers (pub is inherited from trait)
412        // R8: strip `mut` qualifier from impl-side signature params to match
413        // the trait-side signature built in Step 3 (R5(b)). Without this the
414        // two sides desynchronize and cargo reports `method ... has an
415        // incompatible type for trait`. The body is left untouched — only the
416        // signature `mut` markers are removed (mirrors Step 3 exactly).
417        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; // Remove pub for trait impl methods
422                    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        // Snapshot the extracted method names before `trait_impl_items` is
446        // moved into the trait impl below. Step 6 below uses these names
447        // to look up the methods' SymbolIds via `<struct>::<method>` and
448        // walk the call graph for caller-side `use` injection.
449        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        // Register the trait impl
470        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            // Also add to module_items
498            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        // Step 5: Update the inherent impl to remove extracted methods
509        if remaining_items.is_empty() {
510            // If no methods remain, remove the inherent impl
511            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        // Step 7 (R6 visibility): when the impl's own module is declared
527        // private to its parent (e.g. `mod multi;` in
528        // `generator/mod.rs`), the trait's fully-qualified path
529        // `crate::<parent>::<impl_module>::<TraitName>` is not reachable
530        // from other modules — cargo reports
531        // `module <impl_module> is private` on every caller's `use`.
532        //
533        // Re-export the trait through the parent module so the
534        // public-facing path
535        // `crate::<parent>::<TraitName>` resolves, mirroring the
536        // pattern that production code uses when a private
537        // implementation module exposes types via the parent
538        // (`pub use multi::GeneratedFiles;` etc.). Skip when the impl
539        // module is already public, when the lookup chain bottoms out
540        // (impl at crate root, parent missing), or when a matching
541        // re-export already lives in the parent's items (idempotent
542        // ingestion).
543        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        // Step 6 (R4 + P3): Inject a trait import into every module that
583        // calls one of the extracted methods, so existing `Type::method()`
584        // / `instance.method()` sites continue to resolve through the new
585        // trait rather than the (now replaced or removed) inherent impl.
586        // Same-crate caller modules get `use crate::<module>::<Trait>;`,
587        // cross-crate caller modules get the full
588        // `use <crate_module>::<module>::<Trait>;` form (P3,
589        // CrossCrateExtractTrait).
590        //
591        // The cross-crate form is sound without any Cargo.toml edit
592        // because a caller found via `callers_of` invokes the impl's
593        // methods — its crate already depends on the trait's crate. The
594        // residual unsound case (a facade re-export consumer without the
595        // direct dependency edge) is wholesale-skipped upstream by the
596        // suggester's direct-dep guard (pattern_suggest RL061 arm,
597        // WorkspaceResolver::depends_on).
598        //
599        // Requires the optional `code_graph` handle on ASTMutationContext;
600        // when it's absent (legacy callers that construct the context
601        // without a graph) the step is silently skipped.
602        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                    // Build the same-crate `use` form: `crate::<module..>::<Trait>`.
609                    // `trait_path` is `<crate>::<module>::<Trait>`; we replace
610                    // the leading crate segment with `crate` so the import
611                    // resolves regardless of which file inside the crate
612                    // sees it.
613                    //
614                    // R6 commit 2: when Step 7 above injected a parent
615                    // `pub use <impl_module>::<TraitName>;` re-export
616                    // (i.e. the impl module is private and trait
617                    // visibility had to be lifted to the parent), use
618                    // the parent's `crate::<parent>::<TraitName>` path
619                    // instead of `crate::<parent>::<impl_module>::<TraitName>` —
620                    // the latter is unreachable through the private
621                    // `mod <impl_module>;` and would surface as
622                    // `module <impl_module> is private` at cargo check.
623                    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                    // Cross-crate `use` form (P3, CrossCrateExtractTrait):
639                    // the full `<crate_module>::<module..>::<Trait>` path,
640                    // R6-adjusted the same way as the local form when the
641                    // trait was re-exported through a parent module. The
642                    // suggester's direct-dep guard has already vetted that
643                    // every cross-crate caller's crate declares a
644                    // `[dependencies]` edge on the trait's crate, so this
645                    // path resolves there.
646                    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                    // (module set, use path, use item) per injection form:
684                    // same-crate modules get the `crate::…` form, cross-
685                    // crate modules the full `<crate_module>::…` form.
686                    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                                // Trait is declared in the same module — already in scope.
694                                continue;
695                            }
696                            let existing_items = ctx
697                                .ast_registry
698                                .get_module_items(caller_mod_id)
699                                .cloned()
700                                .unwrap_or_default();
701                            // Treat both the injection form and the original
702                            // `<crate>::`-prefixed form as "already imported"
703                            // so an existing direct path import doesn't get a
704                            // duplicate sibling.
705                            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        // Get trait name from symbol_id (O(1) lookup)
741        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        // Phase 2d: Cross-crate caller scan (informational only — Phase
753        // 2e will turn the result into actual caller-side rewrites).
754        // Surfaces the per-caller pattern set so downstream tooling
755        // (suggester gate, executor migration engine, human review) can
756        // see exactly what shape the cross-crate references take before
757        // any rewrite path is wired up. Scanner is read-only on both
758        // registries; the subsequent mutation steps proceed unchanged.
759        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        // Step 1: Find the trait impl (impl TraitName for Foo)
766        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        // Step 2: Find or identify the inherent impl
805        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        // Step 3: Move methods to inherent impl
817        if let Some((inherent_impl_id, _)) = inherent_impl_entry {
818            // Existing inherent impl - add methods to it (both ASTRegistry and module_items)
819            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                // Also update in module_items
826                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            // No inherent impl exists - create one with the trait methods
848            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                // Also add to module_items
884                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        // Step 4: Remove the trait impl
897        ctx.remove_symbol(trait_impl_id);
898        changes += 1;
899
900        // Step 5: Optionally remove the trait definition (O(1) using symbol_id)
901        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        // Phase 2d/2e caller-side migration footnote: when cross-crate
918        // callers reference the inlined trait, count the distribution
919        // and (for the Ufcs pattern, which Phase 2e implements) rewrite
920        // the caller body in-place from `Trait::method` → `Struct::m`.
921        // Other patterns (GenericBound / DynDispatch) still require
922        // caller-side rewrites that ship in later sub-phases; their
923        // count is reported but not yet rewritten.
924        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                // Phase 2e/2f/2g: rewrite in-place for every pattern the
950                // executor can mechanically migrate. Dot is no-op
951                // (post-inline dispatch unchanged). GenericBound is the
952                // most invasive — it strips `T: Trait` generic params
953                // and substitutes `T` with the concrete struct type
954                // throughout the signature, which changes the caller's
955                // external type contract. The transitive caller scanner
956                // (Phase 4) must audit downstream call sites before
957                // GenericBound rewrites should be considered safe in
958                // production workflows.
959                // Phase 4-A + 4-B: decide whether each pub
960                // GenericBound caller's transitive cascade can leave
961                // the current workspace. When `code_graph` is
962                // attached (the normal `execute_ast_reg` path), run
963                // `walk_transitive_callers` to compute the verdict;
964                // an `Escapes` verdict skips the GenericBound rewrite
965                // for that caller and records its name. Phase 4-A's
966                // audit footnote (caller name list) is kept; Phase
967                // 4-B adds the `aborted` list when any verdict came
968                // back `Escapes`.
969                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        // Use the provided SymbolId for O(1) access
1047        let trait_id = self.trait_id;
1048
1049        // Verify the trait exists and is a trait
1050        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        // Verify the symbol exists and is an enum
1073        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        // Get enum name from path for trait name and usage replacement
1093        let enum_name = enum_path.name().to_string();
1094
1095        // Determine trait name:
1096        // - If user specified trait_name, use it
1097        // - For MarkerOnly without specified name, use {EnumName}Trait to avoid collision
1098        // - Otherwise, use enum_name
1099        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                        // MarkerOnly keeps enum, so trait needs different name
1106                        default_trait_name = format!("{}Trait", enum_name);
1107                        &default_trait_name
1108                    }
1109                    _ => &enum_name,
1110                }
1111            }
1112        };
1113
1114        // Get the enum AST
1115        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        // Collect variant names for usage site replacement
1139        let variant_names: Vec<String> = enum_def.variants.iter().map(|v| v.name.clone()).collect();
1140
1141        // Step 1.5: Find enum's inherent impl block and extract methods
1142        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                    // Inherent impl: no trait, matches enum name
1151                    imp.trait_.is_none() && imp.self_ty == enum_name
1152                } else {
1153                    false
1154                }
1155            })
1156            .map(|(id, _)| id); // Extract just the SymbolId to end the borrow
1157
1158        // Extract methods from enum's impl block
1159        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                            // Only extract instance methods (with &self or &mut self)
1166                            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        // Step 1.6: Remove enum FIRST if trait name equals enum name (to free the path)
1188        // This must happen before trait registration to avoid path collision
1189        // MarkerOnly: NEVER remove enum early (types still reference it)
1190        let enum_removed_early = match self.strategy {
1191            EnumToTraitStrategy::MarkerOnly => false, // Never remove for MarkerOnly
1192            _ if self.remove_enum && trait_name == enum_name => {
1193                ctx.remove_symbol(enum_id);
1194                changes += 1;
1195                // Also remove the enum's inherent impl block early
1196                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        // Step 2: Create trait definition with method signatures
1206        let trait_items: Vec<PureTraitItem> = enum_methods
1207            .iter()
1208            .map(|f| {
1209                // Create trait method signature (no body for trait definition)
1210                let trait_fn = PureFn {
1211                    attrs: Vec::new(),
1212                    vis: PureVis::Private, // Trait methods use default visibility
1213                    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(), // Empty body = abstract method in trait
1223                };
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        // Step 3: Create struct + impl for each variant
1262        for variant in &enum_def.variants {
1263            // Convert variant fields to struct fields
1264            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            // Create impl Trait for struct with method implementations
1305            let impl_items: Vec<PureImplItem> = enum_methods
1306                .iter()
1307                .map(|f| {
1308                    // Create method implementation with todo!() body
1309                    let impl_fn = PureFn {
1310                        attrs: Vec::new(),
1311                        vis: PureVis::Private, // Trait impl methods don't have visibility
1312                        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            // NOTE: must use the dedicated trait-impl segment constructor.
1343            // `parent_path.child("<impl X for Y>")` goes through
1344            // `Segment::new`, whose identifier validation rejects the
1345            // space / angle-bracket segment and returned `Err` — silently
1346            // skipping registration of every variant trait impl (the
1347            // converted code then fails with "the trait bound `Variant:
1348            // Trait` is not satisfied" at any coercion site).
1349            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                // Also add to module_items. File generation deliberately
1360                // skips Impl symbols on the ast_registry.iter() path
1361                // (registry_generator.rs generate_internal) and renders
1362                // impls ONLY from the parent module's module_items, so a
1363                // registered-but-not-pushed impl never reaches the output
1364                // file (same wiring as the ExtractTrait registration above).
1365                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        // Step 4: Replace usage sites (EnumName::VariantName -> VariantName)
1375        // MarkerOnly: Do NOT replace usage sites - enum remains as the concrete type
1376        let usage_changes = match self.strategy {
1377            EnumToTraitStrategy::MarkerOnly => 0, // Keep enum usages as-is
1378            _ => replace_enum_usages(ctx, &enum_name, &variant_names),
1379        };
1380        changes += usage_changes;
1381
1382        // Step 5: Replace type annotations based on strategy
1383        // Note: MarkerOnly does NOT replace types - the enum remains as the concrete type
1384        let type_changes = match self.strategy {
1385            EnumToTraitStrategy::Dynamic => {
1386                // Replace `EnumName` with `Box<dyn TraitName>`
1387                replace_type_annotations(
1388                    ctx,
1389                    &enum_name,
1390                    trait_name,
1391                    TypeReplacement::BoxDyn,
1392                    &variant_names,
1393                )
1394            }
1395            EnumToTraitStrategy::Static => {
1396                // Replace `EnumName` with `impl TraitName` (falls back to Box<dyn> for fields)
1397                replace_type_annotations(
1398                    ctx,
1399                    &enum_name,
1400                    trait_name,
1401                    TypeReplacement::ImplTrait,
1402                    &variant_names,
1403                )
1404            }
1405            EnumToTraitStrategy::Generic => {
1406                // Replace `EnumName` with generic type parameter, add generics to containers
1407                replace_type_annotations(
1408                    ctx,
1409                    &enum_name,
1410                    trait_name,
1411                    TypeReplacement::Generic,
1412                    &variant_names,
1413                )
1414            }
1415            EnumToTraitStrategy::MarkerOnly => {
1416                // No type replacement - enum remains as the concrete type
1417                0
1418            }
1419        };
1420        changes += type_changes;
1421
1422        // Step 6: Handle match expressions based on match_handling
1423        let match_changes = match self.match_handling {
1424            MatchHandling::WarnOnly => {
1425                // TODO: Emit warnings for match expressions that need manual migration
1426                // For now, just count them for reporting
1427                count_match_expressions(ctx, &enum_name)
1428            }
1429            MatchHandling::Downcast => {
1430                // TODO: Convert match to downcast-based dispatch
1431                // This requires adding Any bound to trait
1432                0
1433            }
1434            MatchHandling::BlockOnMatch => {
1435                // This should have been checked before execution
1436                // If we're here, there were no match expressions
1437                0
1438            }
1439        };
1440        // Note: match_changes is informational, not counted as changes
1441
1442        // Step 7: Optionally remove the original enum and its inherent impl
1443        // MarkerOnly strategy: NEVER remove enum (types still reference it)
1444        // Other strategies: remove if remove_enum is true (skip if already removed in step 1.5)
1445        let should_remove = match self.strategy {
1446            EnumToTraitStrategy::MarkerOnly => false, // Never remove for MarkerOnly
1447            _ => self.remove_enum && !enum_removed_early,
1448        };
1449        if should_remove {
1450            ctx.remove_symbol(enum_id);
1451            changes += 1;
1452
1453            // Also remove the enum's inherent impl block
1454            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        // Track if enum was actually removed (either early or in step 7)
1477        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
1499/// Type replacement strategy
1500enum TypeReplacement {
1501    /// Replace with `Box<dyn TraitName>`
1502    BoxDyn,
1503    /// Replace with `impl TraitName`
1504    ImplTrait,
1505    /// Replace with generic type parameter `T` (requires adding generics to container)
1506    Generic,
1507}
1508
1509/// Replace type annotations in functions, structs, and impl blocks
1510fn 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    // Collect all symbols to iterate
1520    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                // For struct fields, impl Trait is not allowed in Rust
1541                // Fall back to Box<dyn Trait> for Static strategy (but keep Generic as-is)
1542                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                    // For Generic strategy, add type parameter to struct
1554                    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
1620/// Replace types in a function signature
1621fn 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    // Replace in parameters
1631    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    // Replace in return type
1640    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    // BoxDyn return-type rewrite changes the fn contract from `-> Enum` to
1649    // `-> Box<dyn Trait>`, but `replace_enum_usages` (which runs before this
1650    // pass) has already rewritten body constructions `Enum::Variant` →
1651    // `Variant` — a bare unit-struct value that no longer typechecks against
1652    // the boxed return type. Wrap variant constructions in return position
1653    // with `Box::new(...)` so the coercion to `Box<dyn Trait>` applies.
1654    if ret_replaced && matches!(replacement, TypeReplacement::BoxDyn) {
1655        changes += box_variant_returns(&mut f.body, variant_names);
1656    }
1657
1658    // For Generic strategy, add type parameter if changes were made
1659    if changes > 0 && matches!(replacement, TypeReplacement::Generic) {
1660        add_generic_param(&mut f.generics, trait_name);
1661    }
1662
1663    changes
1664}
1665
1666/// Wrap variant-construction expressions in return position with `Box::new(...)`.
1667///
1668/// Covers two return positions:
1669/// 1. every `return <expr>` whose inner expr is a direct variant construction
1670/// 2. the body tail expression (implicit return)
1671///
1672/// A "direct variant construction" is a bare path (`Cold`), a tuple-variant
1673/// call (`Cold(x)`), or a struct-variant literal (`Cold { .. }`) whose name
1674/// matches one of the converted enum's variants (post-`replace_enum_usages`
1675/// spelling, i.e. without the `Enum::` prefix).
1676fn 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    // `return <variant>` statements anywhere in the body
1682    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    // Tail expression (implicit return). A tail `return ...` was already
1692    // handled above and no longer matches `is_variant_construction`.
1693    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
1703/// Check if `expr` is a direct construction of one of `variant_names`.
1704fn 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
1715/// Replace `expr` in place with `Box::new(<expr>)`.
1716fn 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
1724/// Replace types in struct fields
1725fn 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
1754/// Add generic type parameter `T: TraitName` to generics
1755fn add_generic_param(generics: &mut PureGenerics, trait_name: &str) {
1756    // Check if T already exists
1757    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
1770/// Replace a type if it matches the enum name
1771fn 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            // Check for exact match (e.g., "Filter")
1782            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                        // Use generic type parameter (caller adds generics to container)
1788                        PureType::Path("T".to_string())
1789                    }
1790                };
1791                return true;
1792            }
1793
1794            // Check for generic types containing the enum (e.g., "Option<Filter>", "Vec<Filter>")
1795            // Replace enum name within generic arguments
1796            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                // Simple replacement: replace "EnumName" with the replacement type
1804                // This handles cases like Option<Filter> -> Option<Box<dyn Filter>>
1805                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
1846/// Replace enum type within a generic path string
1847/// e.g., "Option<Filter>" -> "Option<Box<dyn Filter>>"
1848fn replace_type_in_generic_path(path: &str, enum_name: &str, replacement: &str) -> String {
1849    // Find positions where enum_name appears as a type argument
1850    // We need to be careful to only replace complete type names, not substrings
1851    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            // Check if current_word matches enum_name
1860            if current_word == enum_name {
1861                result.push_str(replacement);
1862            } else {
1863                result.push_str(&current_word);
1864            }
1865            current_word.clear();
1866            result.push(c);
1867        }
1868    }
1869
1870    // Handle trailing word
1871    if current_word == enum_name {
1872        result.push_str(replacement);
1873    } else {
1874        result.push_str(&current_word);
1875    }
1876
1877    result
1878}
1879
1880/// Count match expressions on the enum (for warning purposes)
1881fn 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        // Verbatim is opaque raw bytes — leaf for trait_ops (B-3-cont).
1920        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            // Check if any arm pattern matches our enum
1933            for arm in arms {
1934                if pattern_references_enum(&arm.pattern, enum_name) {
1935                    count += 1;
1936                    break; // Count this match once
1937                }
1938            }
1939
1940            // Recurse into arm bodies
1941            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
2000/// Replace all usages of EnumName::VariantName with VariantName in expressions
2001fn replace_enum_usages(
2002    ctx: &mut ASTMutationContext,
2003    enum_name: &str,
2004    variant_names: &[String],
2005) -> usize {
2006    let mut changes = 0;
2007
2008    // Collect all function symbols to iterate
2009    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        // Verbatim is opaque raw bytes — leaf for trait_ops (B-3-cont).
2050        PureStmt::Verbatim(_) => 0,
2051    }
2052}
2053
2054fn replace_in_expr(expr: &mut PureExpr, enum_name: &str, variant_names: &[String]) -> usize {
2055    match expr {
2056        // Check for path expressions like Status::Running
2057        PureExpr::Path(path) => {
2058            // Check if path matches EnumName::VariantName pattern
2059            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                        // Replace EnumName::VariantName with VariantName
2064                        *path = variant.to_string();
2065                        return 1;
2066                    }
2067                }
2068            }
2069            0
2070        }
2071
2072        // Recurse into compound expressions
2073        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                // Replace in pattern (match arms may have Status::Running patterns)
2108                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        // Literals, macros, and other expressions without sub-expressions
2168        _ => 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        // Struct pattern: `Status::Running { .. }` or `Status::Running`
2181        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            // Recurse into field patterns
2196            for (_, field_pattern) in fields {
2197                changes += replace_in_pattern(field_pattern, enum_name, variant_names);
2198            }
2199            changes
2200        }
2201        // Tuple pattern: recurse into elements
2202        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        // Path pattern: simple enum variant like `Status::Running`
2210        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        // Reference pattern: recurse
2222        PurePattern::Ref { pattern: inner, .. } => {
2223            replace_in_pattern(inner, enum_name, variant_names)
2224        }
2225        // Or pattern: multiple alternatives
2226        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        // Other patterns don't need replacement
2234        _ => 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        // Find the inherent impl's SymbolId
2270        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        // Should create trait + trait impl + update inherent impl
2292        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        // Find the trait's SymbolId
2317        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        // Should move method to inherent impl + remove trait impl + remove trait
2329        assert!(result.result.changes >= 2, "Expected at least 2 changes");
2330
2331        // Phase 2d false-positive gate: single-crate fixture has no
2332        // cross-crate callers, so the description must NOT carry the
2333        // `[cross-crate callers: ...]` footnote.
2334        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        // Find the trait's SymbolId
2363        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        // Should move method + remove trait impl, but keep trait definition
2375        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        // Find the enum symbol id
2401        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        // Should create: trait, 2 structs, 2 impls, type replacements, enum removal
2416        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        // Find the enum symbol id
2442        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        // Should create: trait, 2 structs, 2 impls, type replacements, enum removal
2457        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        // Find the enum symbol id
2483        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        // Should create: trait, 2 structs, 2 impls, enum removal
2498        // But no type replacements
2499        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        // Find the enum symbol id
2529        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        // Should create: trait, 2 structs, 2 impls, type replacements with generics, enum removal
2544        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    /// RL070 sweep Case06 regression: variant trait impls must be
2552    /// registered (and survive into the registry) when the enum has an
2553    /// inherent impl with instance methods. Origin: `parent_path.child(
2554    /// "<impl X for Y>")` was rejected by `Segment::new` validation and
2555    /// silently skipped via `Err(_) => continue`, so the converted code
2556    /// failed with "the trait bound `Variant: Trait` is not satisfied"
2557    /// at any `Box::new(variant)` coercion site.
2558    #[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        // Dump the post-mutation registry for observation
2602        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}