Skip to main content

ryo_mutations/basic/
trait_ops.rs

1//! Trait mutations: ExtractTraitMutation, InlineTraitMutation, RemoveTraitMutation, EnumToTraitMutation
2
3use crate::Mutation;
4use ryo_symbol::SymbolId;
5use serde::{Deserialize, Serialize};
6
7/// Strategy for enum-to-trait conversion
8///
9/// Determines how the converted type should be used:
10/// - `Dynamic`: `Box<dyn Trait>` - heap allocation, runtime polymorphism
11/// - `Static`: `impl Trait` or generics with `T: Trait` - monomorphization, no heap
12/// - `MarkerOnly`: Only creates trait + structs, no type replacement
13#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
14#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
15#[serde(rename_all = "snake_case")]
16pub enum EnumToTraitStrategy {
17    /// Use `Box<dyn Trait>` for dynamic dispatch (default)
18    ///
19    /// Parameters: `fn process(filter: Status)` → `fn process(filter: Box<dyn Status>)`
20    /// Return types: `fn create() -> Status` → `fn create() -> Box<dyn Status>`
21    /// Fields: `filter: Status` → `filter: Box<dyn Status>`
22    #[default]
23    Dynamic,
24
25    /// Use `impl Trait` for static dispatch (functions only)
26    ///
27    /// Parameters: `fn process(filter: Status)` → `fn process(filter: impl Status)`
28    /// Return types: `fn create() -> Status` → `fn create() -> impl Status`
29    /// Fields: Falls back to `Box<dyn Trait>` (impl Trait not allowed in fields)
30    Static,
31
32    /// Use generics `<T: Trait>` for full static dispatch
33    ///
34    /// Structs become generic: `struct Config { filter: Status }` → `struct Config<T: Status> { filter: T }`
35    /// Functions become generic: `fn process(filter: Status)` → `fn process<T: Status>(filter: T)`
36    /// Preserves monomorphization for all cases including struct fields
37    Generic,
38
39    /// Only create trait and struct definitions, no type replacement
40    ///
41    /// Useful when manual control over type replacement is needed
42    MarkerOnly,
43}
44
45/// How to handle match expressions on the converted enum
46///
47/// Match expressions cannot be automatically converted because:
48/// - Enum variants become separate types (different concrete types)
49/// - Pattern matching on Box<dyn Trait> requires downcast
50#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
51#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
52#[serde(rename_all = "snake_case")]
53pub enum MatchHandling {
54    /// Emit warnings about match expressions that need manual migration
55    ///
56    /// The match expression is preserved as-is with a TODO comment
57    #[default]
58    WarnOnly,
59
60    /// Attempt to convert match to downcast-based dispatch
61    ///
62    /// ```ignore
63    /// // Before:
64    /// match status {
65    ///     Status::Running => { ... }
66    ///     Status::Stopped => { ... }
67    /// }
68    ///
69    /// // After (with appropriate Any impl):
70    /// if let Some(running) = status.as_any().downcast_ref::<Running>() {
71    ///     ...
72    /// } else if let Some(stopped) = status.as_any().downcast_ref::<Stopped>() {
73    ///     ...
74    /// }
75    /// ```
76    ///
77    /// Requires adding `Any` bound and `as_any()` method to trait
78    Downcast,
79
80    /// Block conversion if any match expression exists
81    ///
82    /// Returns an error with locations of all match expressions
83    BlockOnMatch,
84}
85
86/// Extract a trait from an impl block
87///
88/// Converts:
89/// ```ignore
90/// impl Foo {
91///     fn method(&self) -> i32 { 42 }
92/// }
93/// ```
94/// Into:
95/// ```ignore
96/// trait FooTrait {
97///     fn method(&self) -> i32;
98/// }
99/// impl FooTrait for Foo {
100///     fn method(&self) -> i32 { 42 }
101/// }
102/// ```
103#[derive(Debug, Clone)]
104pub struct ExtractTraitMutation {
105    /// SymbolId of the inherent impl block (required for O(1) lookup)
106    pub symbol_id: SymbolId,
107    /// Name for the new trait
108    pub trait_name: String,
109    /// Specific methods to extract (None = all methods)
110    pub methods: Option<Vec<String>>,
111}
112
113impl ExtractTraitMutation {
114    pub fn new(symbol_id: SymbolId, trait_name: impl Into<String>) -> Self {
115        Self {
116            symbol_id,
117            trait_name: trait_name.into(),
118            methods: None,
119        }
120    }
121
122    pub fn with_methods(mut self, methods: Vec<String>) -> Self {
123        self.methods = Some(methods);
124        self
125    }
126}
127
128impl Mutation for ExtractTraitMutation {
129    fn describe(&self) -> String {
130        format!(
131            "Extract trait '{}' from impl (SymbolId: {:?})",
132            self.trait_name, self.symbol_id
133        )
134    }
135
136    fn mutation_type(&self) -> &'static str {
137        "ExtractTrait"
138    }
139
140    fn box_clone(&self) -> Box<dyn Mutation> {
141        Box::new(self.clone())
142    }
143}
144
145/// Inline a trait back into a struct (remove trait, make inherent impl)
146///
147/// Converts:
148/// ```ignore
149/// trait FooTrait {
150///     fn method(&self) -> i32;
151/// }
152/// impl FooTrait for Foo {
153///     fn method(&self) -> i32 { 42 }
154/// }
155/// ```
156/// Into:
157/// ```ignore
158/// impl Foo {
159///     fn method(&self) -> i32 { 42 }
160/// }
161/// ```
162#[derive(Debug, Clone)]
163pub struct InlineTraitMutation {
164    /// SymbolId of the trait definition (required for O(1) lookup)
165    pub symbol_id: SymbolId,
166    pub struct_name: String,
167    pub remove_trait: bool, // Whether to remove the trait definition
168}
169
170impl InlineTraitMutation {
171    pub fn new(symbol_id: SymbolId, struct_name: impl Into<String>) -> Self {
172        Self {
173            symbol_id,
174            struct_name: struct_name.into(),
175            remove_trait: true,
176        }
177    }
178
179    pub fn keep_trait(mut self) -> Self {
180        self.remove_trait = false;
181        self
182    }
183}
184
185impl Mutation for InlineTraitMutation {
186    fn describe(&self) -> String {
187        format!(
188            "Inline trait {:?} into impl {}",
189            self.symbol_id, self.struct_name
190        )
191    }
192
193    fn mutation_type(&self) -> &'static str {
194        "InlineTrait"
195    }
196
197    fn box_clone(&self) -> Box<dyn Mutation> {
198        Box::new(self.clone())
199    }
200}
201
202/// Remove a trait from the file
203#[derive(Debug, Clone)]
204pub struct RemoveTraitMutation {
205    /// SymbolId of the trait to remove (required, O(1) access)
206    pub trait_id: SymbolId,
207}
208
209impl RemoveTraitMutation {
210    pub fn new(trait_id: SymbolId) -> Self {
211        Self { trait_id }
212    }
213}
214
215impl Mutation for RemoveTraitMutation {
216    fn describe(&self) -> String {
217        format!("Remove trait {}", self.trait_id)
218    }
219
220    fn mutation_type(&self) -> &'static str {
221        "RemoveTrait"
222    }
223
224    fn box_clone(&self) -> Box<dyn Mutation> {
225        Box::new(self.clone())
226    }
227}
228
229/// Convert an enum to a trait with struct implementations
230///
231/// Transforms:
232/// ```ignore
233/// enum Status {
234///     Running,
235///     Stopped,
236/// }
237/// ```
238/// Into:
239/// ```ignore
240/// pub trait Status {}
241/// pub struct Running;
242/// pub struct Stopped;
243/// impl Status for Running {}
244/// impl Status for Stopped {}
245/// ```
246///
247/// Also replaces all usage sites: `Status::Running` → `Running`
248///
249/// ## Type Replacement Strategy
250///
251/// With `strategy: Dynamic` (default):
252/// ```ignore
253/// fn process(status: Status) → fn process(status: Box<dyn Status>)
254/// ```
255///
256/// With `strategy: Static`:
257/// ```ignore
258/// fn process(status: Status) → fn process(status: impl Status)
259/// ```
260///
261/// With `strategy: MarkerOnly`: No type replacement (manual migration)
262///
263/// **Note**: Uses `symbol_id` for reliable O(1) enum identification.
264#[derive(Debug, Clone)]
265pub struct EnumToTraitMutation {
266    /// SymbolId of the target enum (required for reliable identification)
267    pub symbol_id: ryo_symbol::SymbolId,
268    /// Custom trait name (None = same as enum name)
269    pub trait_name: Option<String>,
270    /// Whether to remove the original enum
271    pub remove_enum: bool,
272    /// Variant info (populated during apply from AST)
273    pub variants: Vec<VariantInfo>,
274    /// Strategy for type replacement (default: Dynamic)
275    pub strategy: EnumToTraitStrategy,
276    /// How to handle match expressions (default: WarnOnly)
277    pub match_handling: MatchHandling,
278}
279
280/// Information about an enum variant for conversion
281#[derive(Debug, Clone)]
282pub struct VariantInfo {
283    pub name: String,
284    pub fields: Vec<FieldInfo>,
285}
286
287/// Field information for struct variants
288#[derive(Debug, Clone)]
289pub struct FieldInfo {
290    pub name: String,
291    pub ty: String,
292}
293
294impl EnumToTraitMutation {
295    /// Create from SymbolId (required for reliable enum identification)
296    pub fn from_symbol_id(symbol_id: ryo_symbol::SymbolId) -> Self {
297        Self {
298            symbol_id,
299            trait_name: None,
300            remove_enum: true,
301            variants: Vec::new(),
302            strategy: EnumToTraitStrategy::default(),
303            match_handling: MatchHandling::default(),
304        }
305    }
306
307    pub fn with_trait_name(mut self, name: impl Into<String>) -> Self {
308        self.trait_name = Some(name.into());
309        self
310    }
311
312    pub fn keep_enum(mut self) -> Self {
313        self.remove_enum = false;
314        self
315    }
316
317    pub fn with_variants(mut self, variants: Vec<VariantInfo>) -> Self {
318        self.variants = variants;
319        self
320    }
321
322    /// Set the type replacement strategy
323    pub fn with_strategy(mut self, strategy: EnumToTraitStrategy) -> Self {
324        self.strategy = strategy;
325        self
326    }
327
328    /// Set how match expressions should be handled
329    pub fn with_match_handling(mut self, handling: MatchHandling) -> Self {
330        self.match_handling = handling;
331        self
332    }
333}
334
335impl Mutation for EnumToTraitMutation {
336    fn describe(&self) -> String {
337        let trait_name = self.trait_name.as_deref().unwrap_or("<from_enum>");
338        format!(
339            "Convert enum (symbol:{:?}) to trait '{}' with {} struct implementations",
340            self.symbol_id,
341            trait_name,
342            self.variants.len()
343        )
344    }
345
346    fn mutation_type(&self) -> &'static str {
347        "EnumToTrait"
348    }
349
350    fn box_clone(&self) -> Box<dyn Mutation> {
351        Box::new(self.clone())
352    }
353}
354
355// ============================================================================
356// CrossCrate InlineTrait — caller-side pattern classifier (Phase 2a)
357// ============================================================================
358
359/// Caller-side pattern classifier for CrossCrate InlineTrait.
360///
361/// When InlineTrait runs across crate boundaries, the executor must
362/// detect how each external caller references the target trait so it
363/// can pick the right rewrite path. Phase 2a covers signature-level
364/// detection (generic bound + `&dyn Trait` parameter + UFCS path in
365/// a `PureExpr::Path` string). Body walking for dot calls and nested
366/// UFCS arrives in Phase 2b.
367///
368/// Patterns:
369/// - `Dot`          — `s.method()`: post-inline dispatch is unchanged. No-op.
370/// - `Ufcs`         — `<S as Trait>::method(...)`: rewrite path to `S::method`.
371/// - `GenericBound` — `fn f<T: Trait>(t: T)`: strip generic param, specialize.
372/// - `DynDispatch`  — `fn f(t: &dyn Trait)`: rewrite type to `&S`.
373/// - `Other`        — unrecognised; the executor conservatively blocks inline.
374pub mod cross_crate_caller_pattern {
375    use ryo_source::pure::{
376        PureBlock, PureExpr, PureFn, PureGenericParam, PureGenerics, PureParam, PureStmt, PureType,
377    };
378
379    /// Pattern category for a single caller-side reference.
380    #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
381    pub enum CallerPattern {
382        Dot,
383        Ufcs,
384        GenericBound,
385        DynDispatch,
386        Other,
387    }
388
389    /// Returns `Some(Ufcs)` when `path` matches either of the two
390    /// trait-qualified shapes the executor must rewrite during
391    /// caller-side migration:
392    ///
393    /// - `<TypeName as TraitName>::method` — the literal UFCS form.
394    /// - `TraitName::method` — the trait-qualified path form that the
395    ///   Pure AST conversion collapses UFCS into (see `ufcs_case01`
396    ///   fixture: `<Case01 as Case01Trait>::foo` lowers to
397    ///   `Path("Case01Trait::foo")`).
398    ///
399    /// Detection is name-based on the verbatim path string stored in
400    /// `PureExpr::Path` — confirming the receiver type lives upstream
401    /// (executor caller-resolver layer).
402    pub fn classify_path_expr(path: &str, trait_name: &str) -> Option<CallerPattern> {
403        let needle_as = format!("as {}", trait_name);
404        let needle_prefix = format!("{}::", trait_name);
405        if path.contains(&needle_as) || path.starts_with(&needle_prefix) {
406            Some(CallerPattern::Ufcs)
407        } else {
408            None
409        }
410    }
411
412    /// Returns `Some(Dot)` if `expr` is `<receiver>.<method>(..)` whose
413    /// method name appears in `trait_methods`. Receiver-type resolution
414    /// is left to the executor (this helper is name-based only).
415    pub fn classify_method_call(
416        expr: &PureExpr,
417        trait_methods: &[String],
418    ) -> Option<CallerPattern> {
419        if let PureExpr::MethodCall { method, .. } = expr {
420            if trait_methods.iter().any(|m| m == method) {
421                return Some(CallerPattern::Dot);
422            }
423        }
424        None
425    }
426
427    /// Returns `true` if any generic parameter in `generics` is bounded
428    /// by `trait_name`, either via inline bounds (`T: Trait`) or the
429    /// where-clause (`where T: Trait`).
430    pub fn classify_fn_generic_bound(generics: &PureGenerics, trait_name: &str) -> bool {
431        for param in &generics.params {
432            if let PureGenericParam::Type { bounds, .. } = param {
433                if bounds.iter().any(|b| b == trait_name) {
434                    return true;
435                }
436            }
437        }
438        for w in &generics.where_clause {
439            if w.contains(trait_name) {
440                return true;
441            }
442        }
443        false
444    }
445
446    /// Returns `true` if any function parameter type is `&dyn TraitName`
447    /// or `&mut dyn TraitName`. Walks through `Ref` to find the
448    /// underlying `TraitObject`.
449    pub fn classify_fn_dyn_param(params: &[PureParam], trait_name: &str) -> bool {
450        params.iter().any(|p| match p {
451            PureParam::Typed { ty, .. } => type_contains_dyn_trait(ty, trait_name),
452            _ => false,
453        })
454    }
455
456    fn type_contains_dyn_trait(ty: &PureType, trait_name: &str) -> bool {
457        match ty {
458            PureType::TraitObject(bounds) => bounds.iter().any(|b| b == trait_name),
459            PureType::Ref { ty, .. } => type_contains_dyn_trait(ty, trait_name),
460            _ => false,
461        }
462    }
463
464    /// Detect signature-level caller patterns on a `PureFn`. Covers
465    /// `GenericBound` + `DynDispatch`. Body-level detection lives in
466    /// [`classify_fn`].
467    pub fn classify_fn_signature(f: &PureFn, trait_name: &str) -> Vec<CallerPattern> {
468        let mut out = Vec::new();
469        if classify_fn_generic_bound(&f.generics, trait_name) {
470            out.push(CallerPattern::GenericBound);
471        }
472        if classify_fn_dyn_param(&f.params, trait_name) {
473            out.push(CallerPattern::DynDispatch);
474        }
475        out
476    }
477
478    /// Walk every sub-expression of `expr`, invoking `visitor` on each
479    /// node (including `expr` itself). Recursive descent covers every
480    /// `PureExpr` variant; leaf variants without nested expressions
481    /// (`Lit`, `Path`, `Macro`, `Other`, `Verbatim`, `Continue`) bottom
482    /// out the recursion.
483    pub fn walk_expr<F: FnMut(&PureExpr)>(expr: &PureExpr, visitor: &mut F) {
484        visitor(expr);
485        match expr {
486            PureExpr::Lit(_)
487            | PureExpr::Path(_)
488            | PureExpr::Macro { .. }
489            | PureExpr::Other(_)
490            | PureExpr::Verbatim(_)
491            | PureExpr::Continue { .. } => {}
492            PureExpr::Binary { left, right, .. } => {
493                walk_expr(left, visitor);
494                walk_expr(right, visitor);
495            }
496            PureExpr::Unary { expr, .. }
497            | PureExpr::Field { expr, .. }
498            | PureExpr::Await(expr)
499            | PureExpr::Try(expr)
500            | PureExpr::Ref { expr, .. }
501            | PureExpr::Cast { expr, .. }
502            | PureExpr::Let { expr, .. } => walk_expr(expr, visitor),
503            PureExpr::Call { func, args } => {
504                walk_expr(func, visitor);
505                for a in args {
506                    walk_expr(a, visitor);
507                }
508            }
509            PureExpr::MethodCall { receiver, args, .. } => {
510                walk_expr(receiver, visitor);
511                for a in args {
512                    walk_expr(a, visitor);
513                }
514            }
515            PureExpr::Index { expr, index } => {
516                walk_expr(expr, visitor);
517                walk_expr(index, visitor);
518            }
519            PureExpr::Block { block, .. }
520            | PureExpr::Async { body: block, .. }
521            | PureExpr::Unsafe(block) => walk_block(block, visitor),
522            PureExpr::If {
523                cond,
524                then_branch,
525                else_branch,
526            } => {
527                walk_expr(cond, visitor);
528                walk_block(then_branch, visitor);
529                if let Some(e) = else_branch {
530                    walk_expr(e, visitor);
531                }
532            }
533            PureExpr::Match { expr, arms } => {
534                walk_expr(expr, visitor);
535                for arm in arms {
536                    if let Some(g) = &arm.guard {
537                        walk_expr(g, visitor);
538                    }
539                    walk_expr(&arm.body, visitor);
540                }
541            }
542            PureExpr::Loop { body, .. } => walk_block(body, visitor),
543            PureExpr::While { cond, body, .. } => {
544                walk_expr(cond, visitor);
545                walk_block(body, visitor);
546            }
547            PureExpr::For { expr, body, .. } => {
548                walk_expr(expr, visitor);
549                walk_block(body, visitor);
550            }
551            PureExpr::Return(opt) => {
552                if let Some(e) = opt {
553                    walk_expr(e, visitor);
554                }
555            }
556            PureExpr::Break { expr, .. } => {
557                if let Some(e) = expr {
558                    walk_expr(e, visitor);
559                }
560            }
561            PureExpr::Closure { body, .. } => walk_expr(body, visitor),
562            PureExpr::Struct { fields, rest, .. } => {
563                for (_, e) in fields {
564                    walk_expr(e, visitor);
565                }
566                if let Some(r) = rest {
567                    walk_expr(r, visitor);
568                }
569            }
570            PureExpr::Tuple(exprs) | PureExpr::Array(exprs) => {
571                for e in exprs {
572                    walk_expr(e, visitor);
573                }
574            }
575            PureExpr::Range { start, end, .. } => {
576                if let Some(s) = start {
577                    walk_expr(s, visitor);
578                }
579                if let Some(e) = end {
580                    walk_expr(e, visitor);
581                }
582            }
583            PureExpr::Repeat { expr, len } => {
584                walk_expr(expr, visitor);
585                walk_expr(len, visitor);
586            }
587        }
588    }
589
590    /// Walk every expression in `block` via [`walk_expr`]. Visits the
591    /// initializer and `else_branch` of `Local` statements as well as
592    /// `Semi` and `Expr` statements.
593    pub fn walk_block<F: FnMut(&PureExpr)>(block: &PureBlock, visitor: &mut F) {
594        for stmt in &block.stmts {
595            match stmt {
596                PureStmt::Local {
597                    init, else_branch, ..
598                } => {
599                    if let Some(e) = init {
600                        walk_expr(e, visitor);
601                    }
602                    if let Some(e) = else_branch {
603                        walk_expr(e, visitor);
604                    }
605                }
606                PureStmt::Semi(e) | PureStmt::Expr(e) => walk_expr(e, visitor),
607                PureStmt::Item(_) | PureStmt::Verbatim(_) => {}
608            }
609        }
610    }
611
612    /// Scan a function body for `Dot` and `Ufcs` references to
613    /// `trait_name` / `trait_methods`. Returns the set of patterns
614    /// found (each at most once).
615    pub fn scan_body_for_patterns(
616        body: &PureBlock,
617        trait_name: &str,
618        trait_methods: &[String],
619    ) -> Vec<CallerPattern> {
620        let mut found_dot = false;
621        let mut found_ufcs = false;
622        walk_block(body, &mut |e| {
623            if let PureExpr::Path(p) = e {
624                if classify_path_expr(p, trait_name).is_some() {
625                    found_ufcs = true;
626                }
627            }
628            if classify_method_call(e, trait_methods).is_some() {
629                found_dot = true;
630            }
631        });
632        let mut out = Vec::new();
633        if found_dot {
634            out.push(CallerPattern::Dot);
635        }
636        if found_ufcs {
637            out.push(CallerPattern::Ufcs);
638        }
639        out
640    }
641
642    /// Full caller-side classification (signature + body): combines
643    /// [`classify_fn_signature`] with [`scan_body_for_patterns`].
644    pub fn classify_fn(
645        f: &PureFn,
646        trait_name: &str,
647        trait_methods: &[String],
648    ) -> Vec<CallerPattern> {
649        let mut out = classify_fn_signature(f, trait_name);
650        out.extend(scan_body_for_patterns(&f.body, trait_name, trait_methods));
651        out
652    }
653
654    // ========================================================================
655    // Phase 2e — caller-side rewrite engine (Ufcs pattern)
656    // ========================================================================
657
658    /// Walk every sub-expression of `expr` mutably. Mirror of [`walk_expr`]
659    /// but with `&mut PureExpr` references so rewriters can edit the AST
660    /// in-place. Leaf variants without nested expressions bottom out.
661    pub fn walk_expr_mut<F: FnMut(&mut PureExpr)>(expr: &mut PureExpr, visitor: &mut F) {
662        visitor(expr);
663        match expr {
664            PureExpr::Lit(_)
665            | PureExpr::Path(_)
666            | PureExpr::Macro { .. }
667            | PureExpr::Other(_)
668            | PureExpr::Verbatim(_)
669            | PureExpr::Continue { .. } => {}
670            PureExpr::Binary { left, right, .. } => {
671                walk_expr_mut(left, visitor);
672                walk_expr_mut(right, visitor);
673            }
674            PureExpr::Unary { expr, .. }
675            | PureExpr::Field { expr, .. }
676            | PureExpr::Await(expr)
677            | PureExpr::Try(expr)
678            | PureExpr::Ref { expr, .. }
679            | PureExpr::Cast { expr, .. }
680            | PureExpr::Let { expr, .. } => walk_expr_mut(expr, visitor),
681            PureExpr::Call { func, args } => {
682                walk_expr_mut(func, visitor);
683                for a in args {
684                    walk_expr_mut(a, visitor);
685                }
686            }
687            PureExpr::MethodCall { receiver, args, .. } => {
688                walk_expr_mut(receiver, visitor);
689                for a in args {
690                    walk_expr_mut(a, visitor);
691                }
692            }
693            PureExpr::Index { expr, index } => {
694                walk_expr_mut(expr, visitor);
695                walk_expr_mut(index, visitor);
696            }
697            PureExpr::Block { block, .. }
698            | PureExpr::Async { body: block, .. }
699            | PureExpr::Unsafe(block) => walk_block_mut(block, visitor),
700            PureExpr::If {
701                cond,
702                then_branch,
703                else_branch,
704            } => {
705                walk_expr_mut(cond, visitor);
706                walk_block_mut(then_branch, visitor);
707                if let Some(e) = else_branch {
708                    walk_expr_mut(e, visitor);
709                }
710            }
711            PureExpr::Match { expr, arms } => {
712                walk_expr_mut(expr, visitor);
713                for arm in arms {
714                    if let Some(g) = &mut arm.guard {
715                        walk_expr_mut(g, visitor);
716                    }
717                    walk_expr_mut(&mut arm.body, visitor);
718                }
719            }
720            PureExpr::Loop { body, .. } => walk_block_mut(body, visitor),
721            PureExpr::While { cond, body, .. } => {
722                walk_expr_mut(cond, visitor);
723                walk_block_mut(body, visitor);
724            }
725            PureExpr::For { expr, body, .. } => {
726                walk_expr_mut(expr, visitor);
727                walk_block_mut(body, visitor);
728            }
729            PureExpr::Return(opt) => {
730                if let Some(e) = opt {
731                    walk_expr_mut(e, visitor);
732                }
733            }
734            PureExpr::Break { expr, .. } => {
735                if let Some(e) = expr {
736                    walk_expr_mut(e, visitor);
737                }
738            }
739            PureExpr::Closure { body, .. } => walk_expr_mut(body, visitor),
740            PureExpr::Struct { fields, rest, .. } => {
741                for (_, e) in fields {
742                    walk_expr_mut(e, visitor);
743                }
744                if let Some(r) = rest {
745                    walk_expr_mut(r, visitor);
746                }
747            }
748            PureExpr::Tuple(exprs) | PureExpr::Array(exprs) => {
749                for e in exprs {
750                    walk_expr_mut(e, visitor);
751                }
752            }
753            PureExpr::Range { start, end, .. } => {
754                if let Some(s) = start {
755                    walk_expr_mut(s, visitor);
756                }
757                if let Some(e) = end {
758                    walk_expr_mut(e, visitor);
759                }
760            }
761            PureExpr::Repeat { expr, len } => {
762                walk_expr_mut(expr, visitor);
763                walk_expr_mut(len, visitor);
764            }
765        }
766    }
767
768    /// Mutable counterpart of [`walk_block`]. Visits every statement's
769    /// expression node (`Local` init + else_branch, `Semi`, `Expr`).
770    pub fn walk_block_mut<F: FnMut(&mut PureExpr)>(block: &mut PureBlock, visitor: &mut F) {
771        for stmt in &mut block.stmts {
772            match stmt {
773                PureStmt::Local {
774                    init, else_branch, ..
775                } => {
776                    if let Some(e) = init {
777                        walk_expr_mut(e, visitor);
778                    }
779                    if let Some(e) = else_branch {
780                        walk_expr_mut(e, visitor);
781                    }
782                }
783                PureStmt::Semi(e) | PureStmt::Expr(e) => walk_expr_mut(e, visitor),
784                PureStmt::Item(_) | PureStmt::Verbatim(_) => {}
785            }
786        }
787    }
788
789    /// Rewrite a single `PureExpr::Path` string in-place by replacing the
790    /// trait-qualified UFCS prefix with the struct-qualified inherent
791    /// prefix:
792    ///
793    /// - `Trait::method` → `Struct::method`
794    /// - `<Receiver as Trait>::method` → `Struct::method` (the `<T as
795    ///   Trait>` envelope is removed; the resulting path is the
796    ///   inherent path expected after [`InlineTraitMutation`]).
797    ///
798    /// Returns `true` when the path string was modified.
799    pub fn rewrite_path_expr_ufcs(path: &mut String, trait_name: &str, struct_name: &str) -> bool {
800        // Form A: trait-qualified `Trait::method` (Pure AST collapse).
801        let prefix = format!("{}::", trait_name);
802        if let Some(rest) = path.strip_prefix(&prefix) {
803            let new_path = format!("{}::{}", struct_name, rest);
804            *path = new_path;
805            return true;
806        }
807        // Form B: literal `<S as Trait>::method` UFCS envelope.
808        let needle_as = format!("as {}>::", trait_name);
809        if let Some(idx_as) = path.find(&needle_as) {
810            // Strip everything up to and including `<…as Trait>::` and
811            // keep the trailing method path; prepend `<struct_name>::`.
812            let tail_start = idx_as + needle_as.len();
813            let tail = path[tail_start..].to_string();
814            *path = format!("{}::{}", struct_name, tail);
815            return true;
816        }
817        false
818    }
819
820    /// Rewrite every UFCS path in `body` from `Trait::*` (or
821    /// `<S as Trait>::*`) to `Struct::*`. Returns the number of paths
822    /// rewritten — useful for the executor's mutation count + footnote
823    /// reporting.
824    pub fn rewrite_body_ufcs(body: &mut PureBlock, trait_name: &str, struct_name: &str) -> usize {
825        let mut count = 0usize;
826        walk_block_mut(body, &mut |e| {
827            if let PureExpr::Path(p) = e {
828                if rewrite_path_expr_ufcs(p, trait_name, struct_name) {
829                    count += 1;
830                }
831            }
832        });
833        count
834    }
835
836    // ========================================================================
837    // Phase 2f — DynDispatch rewriter
838    // ========================================================================
839
840    /// Recursively substitute `dyn TraitName` with `StructName` inside
841    /// `ty`. Walks through compositional type shapes so callers do not
842    /// need to flatten `&dyn Trait` / `&mut dyn Trait` / `(&dyn Trait,
843    /// T)` / `[dyn Trait]` etc. by hand.
844    ///
845    /// Returns `true` when at least one node was rewritten.
846    pub fn substitute_trait_in_type(
847        ty: &mut PureType,
848        trait_name: &str,
849        struct_name: &str,
850    ) -> bool {
851        match ty {
852            PureType::TraitObject(bounds) => {
853                if bounds.iter().any(|b| b == trait_name) {
854                    *ty = PureType::Path(struct_name.to_string());
855                    true
856                } else {
857                    false
858                }
859            }
860            PureType::Ref {
861                ty: inner,
862                lifetime: _,
863                is_mut: _,
864            } => substitute_trait_in_type(inner, trait_name, struct_name),
865            PureType::Tuple(items) => {
866                let mut changed = false;
867                for t in items {
868                    if substitute_trait_in_type(t, trait_name, struct_name) {
869                        changed = true;
870                    }
871                }
872                changed
873            }
874            PureType::Slice(inner) => substitute_trait_in_type(inner, trait_name, struct_name),
875            PureType::Array { ty: inner, .. } => {
876                substitute_trait_in_type(inner, trait_name, struct_name)
877            }
878            PureType::Fn { params, ret } => {
879                let mut changed = false;
880                for t in params {
881                    if substitute_trait_in_type(t, trait_name, struct_name) {
882                        changed = true;
883                    }
884                }
885                if let Some(r) = ret {
886                    if substitute_trait_in_type(r, trait_name, struct_name) {
887                        changed = true;
888                    }
889                }
890                changed
891            }
892            PureType::Path(_)
893            | PureType::ImplTrait(_)
894            | PureType::Infer
895            | PureType::Never
896            | PureType::Other(_) => false,
897        }
898    }
899
900    /// Rewrite a function's signature to specialize every `&dyn Trait`
901    /// (and other `dyn Trait` occurrences) to the concrete `Struct`
902    /// type. Covers both parameter types and the return type. The body
903    /// is not touched here — type-annotated `let` bindings will be
904    /// handled by a body-level rewriter in a later sub-phase.
905    ///
906    /// Returns the total number of type nodes rewritten across the
907    /// signature.
908    pub fn rewrite_fn_dyn_dispatch(f: &mut PureFn, trait_name: &str, struct_name: &str) -> usize {
909        let mut count = 0usize;
910        for p in &mut f.params {
911            if let PureParam::Typed { ty, .. } = p {
912                if substitute_trait_in_type(ty, trait_name, struct_name) {
913                    count += 1;
914                }
915            }
916        }
917        if let Some(ret) = &mut f.ret {
918            if substitute_trait_in_type(ret, trait_name, struct_name) {
919                count += 1;
920            }
921        }
922        count
923    }
924
925    // ========================================================================
926    // Phase 2g — GenericBound rewriter
927    // ========================================================================
928
929    /// Recursively substitute `Path(generic_name)` with `Path(struct_name)`
930    /// in a type. Used by `rewrite_fn_generic_bound` after stripping the
931    /// generic parameter to specialize every remaining `T` reference
932    /// inside the function signature to the concrete `Struct`.
933    ///
934    /// Returns `true` when at least one `Path` was substituted. Compound
935    /// shapes (Ref / Tuple / Slice / Array / Fn) recurse into their
936    /// children.
937    pub fn substitute_generic_in_type(
938        ty: &mut PureType,
939        generic_name: &str,
940        struct_name: &str,
941    ) -> bool {
942        match ty {
943            PureType::Path(p) => {
944                if p == generic_name {
945                    *p = struct_name.to_string();
946                    true
947                } else {
948                    false
949                }
950            }
951            PureType::Ref { ty: inner, .. } => {
952                substitute_generic_in_type(inner, generic_name, struct_name)
953            }
954            PureType::Tuple(items) => {
955                let mut changed = false;
956                for t in items {
957                    if substitute_generic_in_type(t, generic_name, struct_name) {
958                        changed = true;
959                    }
960                }
961                changed
962            }
963            PureType::Slice(inner) => substitute_generic_in_type(inner, generic_name, struct_name),
964            PureType::Array { ty: inner, .. } => {
965                substitute_generic_in_type(inner, generic_name, struct_name)
966            }
967            PureType::Fn { params, ret } => {
968                let mut changed = false;
969                for t in params {
970                    if substitute_generic_in_type(t, generic_name, struct_name) {
971                        changed = true;
972                    }
973                }
974                if let Some(r) = ret {
975                    if substitute_generic_in_type(r, generic_name, struct_name) {
976                        changed = true;
977                    }
978                }
979                changed
980            }
981            PureType::TraitObject(_)
982            | PureType::ImplTrait(_)
983            | PureType::Infer
984            | PureType::Never
985            | PureType::Other(_) => false,
986        }
987    }
988
989    /// Rewrite a single `PureExpr::Path` string that references the
990    /// stripped generic parameter:
991    ///
992    /// - `T` (bare path) → `StructName`
993    /// - `T::method` / `T::Item::sub` → `StructName::method` /
994    ///   `StructName::Item::sub`
995    ///
996    /// Identifier-based: a path whose first segment equals
997    /// `generic_name` exactly is rewritten; substrings like
998    /// `Type::method` or paths whose first segment is `U` are left
999    /// untouched. Returns `true` when the path was modified.
1000    pub fn rewrite_path_expr_generic(
1001        path: &mut String,
1002        generic_name: &str,
1003        struct_name: &str,
1004    ) -> bool {
1005        if path == generic_name {
1006            *path = struct_name.to_string();
1007            return true;
1008        }
1009        let prefix = format!("{}::", generic_name);
1010        if let Some(rest) = path.strip_prefix(&prefix) {
1011            let new_path = format!("{}::{}", struct_name, rest);
1012            *path = new_path;
1013            return true;
1014        }
1015        false
1016    }
1017
1018    /// Phase 6-A body-axis rewriter for GenericBound: after the
1019    /// signature has been specialized via the existing Phase 2g logic,
1020    /// walk the body to substitute every remaining reference to the
1021    /// stripped generic parameter:
1022    ///
1023    /// - `PureExpr::Path("T::xxx")` references via [`walk_block_mut`]
1024    /// - `PureStmt::Local.ty` type annotations via
1025    ///   [`substitute_generic_in_type`]
1026    ///
1027    /// Returns the number of structural edits performed (Path
1028    /// rewrites + Local type substitutions). Other body shapes such
1029    /// as `MethodCall.turbofish` strings and `where_clause` text are
1030    /// intentionally left alone — they ship in Phase 6-B / 6-C.
1031    pub fn rewrite_body_generic_bound(
1032        body: &mut PureBlock,
1033        generic_name: &str,
1034        struct_name: &str,
1035    ) -> usize {
1036        let mut count = 0usize;
1037
1038        // (a) Walk every expression, rewrite Path strings.
1039        walk_block_mut(body, &mut |e| {
1040            if let PureExpr::Path(p) = e {
1041                if rewrite_path_expr_generic(p, generic_name, struct_name) {
1042                    count += 1;
1043                }
1044            }
1045        });
1046
1047        // (b) Stmt-level `let x: T = ...` annotations — the expr walker
1048        //     does not visit the type slot, so handle it explicitly.
1049        for stmt in &mut body.stmts {
1050            if let PureStmt::Local { ty: Some(ty), .. } = stmt {
1051                if substitute_generic_in_type(ty, generic_name, struct_name) {
1052                    count += 1;
1053                }
1054            }
1055        }
1056
1057        count
1058    }
1059
1060    /// Phase 6-B turbofish-axis helper: rewrite every standalone
1061    /// identifier token equal to `generic_name` inside a turbofish
1062    /// snippet to `struct_name`. The snippet is the raw `Option<String>`
1063    /// stored on `PureExpr::MethodCall.turbofish`, e.g. `"< T >"` or
1064    /// `"< Vec < T > , U >"`.
1065    ///
1066    /// Identifier-boundary scan: walks the string char by char,
1067    /// collects a contiguous run of `is_alphanumeric()` / `_`
1068    /// characters as one token, and rewrites only when the token
1069    /// matches `generic_name` exactly. `Type` / `Tx` / `Vec` are left
1070    /// untouched even though they begin with the generic's leading
1071    /// character. Returns the count of token rewrites performed.
1072    pub fn rewrite_turbofish_generic(
1073        s: &mut String,
1074        generic_name: &str,
1075        struct_name: &str,
1076    ) -> usize {
1077        let chars: Vec<char> = s.chars().collect();
1078        let mut out = String::with_capacity(s.len());
1079        let mut count = 0usize;
1080        let mut i = 0usize;
1081        while i < chars.len() {
1082            let c = chars[i];
1083            if c.is_alphabetic() || c == '_' {
1084                let start = i;
1085                while i < chars.len() && (chars[i].is_alphanumeric() || chars[i] == '_') {
1086                    i += 1;
1087                }
1088                let ident: String = chars[start..i].iter().collect();
1089                if ident == generic_name {
1090                    out.push_str(struct_name);
1091                    count += 1;
1092                } else {
1093                    out.push_str(&ident);
1094                }
1095            } else {
1096                out.push(c);
1097                i += 1;
1098            }
1099        }
1100        *s = out;
1101        count
1102    }
1103
1104    /// Walk every `PureExpr::MethodCall.turbofish` in `body` via
1105    /// [`walk_block_mut`] and apply [`rewrite_turbofish_generic`]. Returns
1106    /// the aggregate token-rewrite count across all turbofish
1107    /// occurrences.
1108    pub fn rewrite_body_turbofish_generic(
1109        body: &mut PureBlock,
1110        generic_name: &str,
1111        struct_name: &str,
1112    ) -> usize {
1113        let mut count = 0usize;
1114        walk_block_mut(body, &mut |e| {
1115            if let PureExpr::MethodCall {
1116                turbofish: Some(s), ..
1117            } = e
1118            {
1119                count += rewrite_turbofish_generic(s, generic_name, struct_name);
1120            }
1121        });
1122        count
1123    }
1124
1125    /// Phase 6-C where-clause rewriter: edits the function's
1126    /// `Generics.where_clause: Vec<String>` in two passes.
1127    ///
1128    /// For each predicate string (one entry in the `Vec`):
1129    /// 1. Tokenize the leading LHS identifier (the part before `:`).
1130    ///    If the LHS equals `generic_name` exactly — i.e. the
1131    ///    predicate is the `T: …` bound that the Phase 2g signature
1132    ///    work has already obviated — the entire predicate is
1133    ///    removed.
1134    /// 2. Otherwise, walk the predicate string and rewrite every
1135    ///    standalone identifier token equal to `generic_name` to
1136    ///    `struct_name`, using the same identifier-boundary scan as
1137    ///    [`rewrite_turbofish_generic`]. This handles RHS references
1138    ///    like `where U: Foo<T>` → `where U: Foo<Case01>`.
1139    ///
1140    /// Returns the total count of structural edits: each dropped
1141    /// predicate counts once, plus one for each in-place identifier
1142    /// rewrite in the surviving predicates.
1143    ///
1144    /// Out of scope (deferred): higher-ranked trait bounds such as
1145    /// `for<'a> T: Foo<'a>` whose LHS is not a bare identifier; an
1146    /// HRTB-aware splitter is needed before applying the LHS rule.
1147    pub fn rewrite_where_clause_generic(
1148        clause: &mut Vec<String>,
1149        generic_name: &str,
1150        struct_name: &str,
1151    ) -> usize {
1152        let mut count = 0usize;
1153        clause.retain_mut(|predicate| {
1154            // (1) Predicate LHS check: tokenize the identifier that
1155            //     precedes the `:` colon.
1156            let trimmed = predicate.trim_start();
1157            let lhs_end = trimmed.find(':').unwrap_or(trimmed.len());
1158            let lhs = trimmed[..lhs_end].trim();
1159            if lhs == generic_name {
1160                count += 1;
1161                return false;
1162            }
1163
1164            // (2) Identifier-boundary substitution on the surviving
1165            //     predicate, mirroring `rewrite_turbofish_generic`.
1166            let chars: Vec<char> = predicate.chars().collect();
1167            let mut out = String::with_capacity(predicate.len());
1168            let mut i = 0usize;
1169            while i < chars.len() {
1170                let c = chars[i];
1171                if c.is_alphabetic() || c == '_' {
1172                    let start = i;
1173                    while i < chars.len() && (chars[i].is_alphanumeric() || chars[i] == '_') {
1174                        i += 1;
1175                    }
1176                    let ident: String = chars[start..i].iter().collect();
1177                    if ident == generic_name {
1178                        out.push_str(struct_name);
1179                        count += 1;
1180                    } else {
1181                        out.push_str(&ident);
1182                    }
1183                } else {
1184                    out.push(c);
1185                    i += 1;
1186                }
1187            }
1188            *predicate = out;
1189            true
1190        });
1191        count
1192    }
1193
1194    /// Specialize every `T: TraitName` generic parameter on a function:
1195    ///
1196    /// - Identify each `PureGenericParam::Type` whose `bounds` contains
1197    ///   `trait_name`; collect the param's name (`"T"`).
1198    /// - Remove the param from `f.generics.params` (the function is no
1199    ///   longer generic over that parameter; it is specialized to
1200    ///   `struct_name`).
1201    /// - Substitute every `Path(T)` in the parameter types and return
1202    ///   type with `Path(struct_name)`.
1203    /// - Phase 6-A: also walk the body and substitute every
1204    ///   `PureExpr::Path("T::xxx")` reference and `let x: T = ...`
1205    ///   type annotation. Other body shapes (turbofish, where-clause
1206    ///   string) remain out of scope here.
1207    ///
1208    /// Returns the total number of structural edits performed: each
1209    /// removed generic param + each substituted type node + each
1210    /// body-level Path or Local-ty rewrite.
1211    pub fn rewrite_fn_generic_bound(f: &mut PureFn, trait_name: &str, struct_name: &str) -> usize {
1212        // (a) Collect generic params bounded by `trait_name`.
1213        let bound_names: Vec<String> = f
1214            .generics
1215            .params
1216            .iter()
1217            .filter_map(|p| match p {
1218                PureGenericParam::Type { name, bounds }
1219                    if bounds.iter().any(|b| b == trait_name) =>
1220                {
1221                    Some(name.clone())
1222                }
1223                _ => None,
1224            })
1225            .collect();
1226
1227        if bound_names.is_empty() {
1228            return 0;
1229        }
1230
1231        // (b) Strip those params from the generics list.
1232        let before = f.generics.params.len();
1233        f.generics.params.retain(|p| match p {
1234            PureGenericParam::Type { name, .. } => !bound_names.contains(name),
1235            _ => true,
1236        });
1237        let removed = before - f.generics.params.len();
1238
1239        // (c) Substitute every `Path(name)` in the signature with
1240        //     `Path(struct_name)`.
1241        let mut substitutions = 0usize;
1242        for bound_name in &bound_names {
1243            for p in &mut f.params {
1244                if let PureParam::Typed { ty, .. } = p {
1245                    if substitute_generic_in_type(ty, bound_name, struct_name) {
1246                        substitutions += 1;
1247                    }
1248                }
1249            }
1250            if let Some(ret) = &mut f.ret {
1251                if substitute_generic_in_type(ret, bound_name, struct_name) {
1252                    substitutions += 1;
1253                }
1254            }
1255        }
1256
1257        // (d) Phase 6-A + 6-B + 6-C body / where axis: substitute
1258        //     body Path expressions, Local type annotations,
1259        //     turbofish identifier tokens, and where-clause predicates
1260        //     referencing each stripped generic parameter.
1261        let mut body_edits = 0usize;
1262        for bound_name in &bound_names {
1263            body_edits += rewrite_body_generic_bound(&mut f.body, bound_name, struct_name);
1264            body_edits += rewrite_body_turbofish_generic(&mut f.body, bound_name, struct_name);
1265            body_edits +=
1266                rewrite_where_clause_generic(&mut f.generics.where_clause, bound_name, struct_name);
1267        }
1268
1269        removed + substitutions + body_edits
1270    }
1271}
1272
1273#[cfg(test)]
1274mod cross_crate_caller_pattern_tests {
1275    use super::cross_crate_caller_pattern::*;
1276    use ryo_source::pure::{
1277        PureBlock, PureExpr, PureFn, PureGenericParam, PureGenerics, PureParam, PureType, PureVis,
1278    };
1279
1280    fn mk_fn(name: &str, generics: PureGenerics, params: Vec<PureParam>) -> PureFn {
1281        PureFn {
1282            attrs: Vec::new(),
1283            vis: PureVis::Public,
1284            is_async: false,
1285            is_async_inferred: false,
1286            is_const: false,
1287            is_unsafe: false,
1288            abi: None,
1289            name: name.to_string(),
1290            generics,
1291            params,
1292            ret: None,
1293            body: PureBlock { stmts: Vec::new() },
1294        }
1295    }
1296
1297    #[test]
1298    fn ufcs_path_detected() {
1299        let p = "<Case01 as Case01Trait>::foo";
1300        assert_eq!(
1301            classify_path_expr(p, "Case01Trait"),
1302            Some(CallerPattern::Ufcs)
1303        );
1304        assert_eq!(classify_path_expr(p, "OtherTrait"), None);
1305    }
1306
1307    #[test]
1308    fn ufcs_path_negative_case() {
1309        let p = "Case01::foo";
1310        assert_eq!(classify_path_expr(p, "Case01Trait"), None);
1311    }
1312
1313    #[test]
1314    fn ufcs_path_trait_qualified_form_detected() {
1315        // Pure AST collapses `<S as Trait>::method` to `Trait::method`.
1316        let p = "Case01Trait::foo";
1317        assert_eq!(
1318            classify_path_expr(p, "Case01Trait"),
1319            Some(CallerPattern::Ufcs)
1320        );
1321    }
1322
1323    #[test]
1324    fn dot_method_call_detected() {
1325        let receiver = PureExpr::Path("s".to_string());
1326        let expr = PureExpr::MethodCall {
1327            receiver: Box::new(receiver),
1328            method: "foo".to_string(),
1329            turbofish: None,
1330            args: Vec::new(),
1331        };
1332        let methods = vec!["foo".to_string(), "bar".to_string()];
1333        assert_eq!(
1334            classify_method_call(&expr, &methods),
1335            Some(CallerPattern::Dot)
1336        );
1337    }
1338
1339    #[test]
1340    fn dot_method_call_negative_when_name_mismatch() {
1341        let expr = PureExpr::MethodCall {
1342            receiver: Box::new(PureExpr::Path("s".to_string())),
1343            method: "qux".to_string(),
1344            turbofish: None,
1345            args: Vec::new(),
1346        };
1347        let methods = vec!["foo".to_string()];
1348        assert_eq!(classify_method_call(&expr, &methods), None);
1349    }
1350
1351    #[test]
1352    fn dot_method_call_negative_when_not_method_call() {
1353        let expr = PureExpr::Path("foo".to_string());
1354        let methods = vec!["foo".to_string()];
1355        assert_eq!(classify_method_call(&expr, &methods), None);
1356    }
1357
1358    #[test]
1359    fn generic_bound_detected_inline() {
1360        let g = PureGenerics {
1361            params: vec![PureGenericParam::Type {
1362                name: "T".to_string(),
1363                bounds: vec!["Case01Trait".to_string()],
1364            }],
1365            where_clause: Vec::new(),
1366        };
1367        assert!(classify_fn_generic_bound(&g, "Case01Trait"));
1368        assert!(!classify_fn_generic_bound(&g, "OtherTrait"));
1369    }
1370
1371    #[test]
1372    fn generic_bound_detected_where_clause() {
1373        let g = PureGenerics {
1374            params: vec![PureGenericParam::Type {
1375                name: "T".to_string(),
1376                bounds: Vec::new(),
1377            }],
1378            where_clause: vec!["T: Case01Trait".to_string()],
1379        };
1380        assert!(classify_fn_generic_bound(&g, "Case01Trait"));
1381    }
1382
1383    #[test]
1384    fn dyn_param_detected() {
1385        let params = vec![PureParam::Typed {
1386            name: "t".to_string(),
1387            ty: PureType::Ref {
1388                lifetime: None,
1389                is_mut: false,
1390                ty: Box::new(PureType::TraitObject(vec!["Case01Trait".to_string()])),
1391            },
1392            is_mut: false,
1393            pat: None,
1394        }];
1395        assert!(classify_fn_dyn_param(&params, "Case01Trait"));
1396        assert!(!classify_fn_dyn_param(&params, "OtherTrait"));
1397    }
1398
1399    #[test]
1400    fn dyn_param_negative_when_no_dyn() {
1401        let params = vec![PureParam::Typed {
1402            name: "t".to_string(),
1403            ty: PureType::Path("Case01".to_string()),
1404            is_mut: false,
1405            pat: None,
1406        }];
1407        assert!(!classify_fn_dyn_param(&params, "Case01Trait"));
1408    }
1409
1410    #[test]
1411    fn classify_fn_signature_combines_axes() {
1412        let g = PureGenerics {
1413            params: vec![PureGenericParam::Type {
1414                name: "T".to_string(),
1415                bounds: vec!["Case01Trait".to_string()],
1416            }],
1417            where_clause: Vec::new(),
1418        };
1419        let params = vec![PureParam::Typed {
1420            name: "t".to_string(),
1421            ty: PureType::Ref {
1422                lifetime: None,
1423                is_mut: false,
1424                ty: Box::new(PureType::TraitObject(vec!["Case01Trait".to_string()])),
1425            },
1426            is_mut: false,
1427            pat: None,
1428        }];
1429        let f = mk_fn("use_both", g, params);
1430        let out = classify_fn_signature(&f, "Case01Trait");
1431        assert!(out.contains(&CallerPattern::GenericBound));
1432        assert!(out.contains(&CallerPattern::DynDispatch));
1433        assert_eq!(out.len(), 2);
1434    }
1435
1436    #[test]
1437    fn classify_fn_signature_empty_for_unrelated_trait() {
1438        let g = PureGenerics {
1439            params: vec![PureGenericParam::Type {
1440                name: "T".to_string(),
1441                bounds: vec!["Case01Trait".to_string()],
1442            }],
1443            where_clause: Vec::new(),
1444        };
1445        let f = mk_fn("use_generic", g, Vec::new());
1446        let out = classify_fn_signature(&f, "OtherTrait");
1447        assert!(out.is_empty());
1448    }
1449
1450    // ===== Phase 2b: body walker tests =====
1451
1452    use ryo_source::pure::PureStmt;
1453
1454    fn empty_generics() -> PureGenerics {
1455        PureGenerics {
1456            params: Vec::new(),
1457            where_clause: Vec::new(),
1458        }
1459    }
1460
1461    fn mk_method_call(receiver_name: &str, method: &str) -> PureExpr {
1462        PureExpr::MethodCall {
1463            receiver: Box::new(PureExpr::Path(receiver_name.to_string())),
1464            method: method.to_string(),
1465            turbofish: None,
1466            args: Vec::new(),
1467        }
1468    }
1469
1470    fn fn_with_body(name: &str, body: PureBlock) -> PureFn {
1471        PureFn {
1472            attrs: Vec::new(),
1473            vis: PureVis::Public,
1474            is_async: false,
1475            is_async_inferred: false,
1476            is_const: false,
1477            is_unsafe: false,
1478            abi: None,
1479            name: name.to_string(),
1480            generics: empty_generics(),
1481            params: Vec::new(),
1482            ret: None,
1483            body,
1484        }
1485    }
1486
1487    #[test]
1488    fn walk_expr_visits_method_call_arguments() {
1489        // foo(s.bar())
1490        let inner = mk_method_call("s", "bar");
1491        let outer = PureExpr::Call {
1492            func: Box::new(PureExpr::Path("foo".to_string())),
1493            args: vec![inner],
1494        };
1495        let mut visited = 0usize;
1496        walk_expr(&outer, &mut |e| {
1497            if matches!(e, PureExpr::MethodCall { .. }) {
1498                visited += 1;
1499            }
1500        });
1501        assert_eq!(visited, 1);
1502    }
1503
1504    #[test]
1505    fn scan_body_detects_dot() {
1506        // { s.foo() }
1507        let body = PureBlock {
1508            stmts: vec![PureStmt::Expr(mk_method_call("s", "foo"))],
1509        };
1510        let methods = vec!["foo".to_string()];
1511        let out = scan_body_for_patterns(&body, "Case01Trait", &methods);
1512        assert_eq!(out, vec![CallerPattern::Dot]);
1513    }
1514
1515    #[test]
1516    fn scan_body_detects_ufcs_path() {
1517        // { <Case01 as Case01Trait>::foo(s) }
1518        let body = PureBlock {
1519            stmts: vec![PureStmt::Expr(PureExpr::Call {
1520                func: Box::new(PureExpr::Path("<Case01 as Case01Trait>::foo".to_string())),
1521                args: vec![PureExpr::Path("s".to_string())],
1522            })],
1523        };
1524        let out = scan_body_for_patterns(&body, "Case01Trait", &[]);
1525        assert_eq!(out, vec![CallerPattern::Ufcs]);
1526    }
1527
1528    #[test]
1529    fn scan_body_detects_both_dot_and_ufcs() {
1530        // { s.foo(); <Case01 as Case01Trait>::bar(s) }
1531        let body = PureBlock {
1532            stmts: vec![
1533                PureStmt::Semi(mk_method_call("s", "foo")),
1534                PureStmt::Expr(PureExpr::Path("<Case01 as Case01Trait>::bar".to_string())),
1535            ],
1536        };
1537        let methods = vec!["foo".to_string()];
1538        let out = scan_body_for_patterns(&body, "Case01Trait", &methods);
1539        assert!(out.contains(&CallerPattern::Dot));
1540        assert!(out.contains(&CallerPattern::Ufcs));
1541        assert_eq!(out.len(), 2);
1542    }
1543
1544    #[test]
1545    fn scan_body_detects_dot_in_nested_block() {
1546        // { if cond { s.foo() } else { 0 } }
1547        let inner_if = PureExpr::If {
1548            cond: Box::new(PureExpr::Lit("true".to_string())),
1549            then_branch: PureBlock {
1550                stmts: vec![PureStmt::Expr(mk_method_call("s", "foo"))],
1551            },
1552            else_branch: Some(Box::new(PureExpr::Lit("0".to_string()))),
1553        };
1554        let body = PureBlock {
1555            stmts: vec![PureStmt::Expr(inner_if)],
1556        };
1557        let methods = vec!["foo".to_string()];
1558        let out = scan_body_for_patterns(&body, "Case01Trait", &methods);
1559        assert_eq!(out, vec![CallerPattern::Dot]);
1560    }
1561
1562    #[test]
1563    fn scan_body_no_pattern_when_unrelated() {
1564        // { s.other_method() }
1565        let body = PureBlock {
1566            stmts: vec![PureStmt::Expr(mk_method_call("s", "qux"))],
1567        };
1568        let methods = vec!["foo".to_string()];
1569        let out = scan_body_for_patterns(&body, "Case01Trait", &methods);
1570        assert!(out.is_empty());
1571    }
1572
1573    // ===== Phase 2e: rewriter tests =====
1574
1575    #[test]
1576    fn rewrite_path_expr_ufcs_trait_qualified() {
1577        let mut p = String::from("Case01Trait::foo");
1578        let changed = rewrite_path_expr_ufcs(&mut p, "Case01Trait", "Case01");
1579        assert!(changed);
1580        assert_eq!(p, "Case01::foo");
1581    }
1582
1583    #[test]
1584    fn rewrite_path_expr_ufcs_literal_envelope() {
1585        let mut p = String::from("<Case01 as Case01Trait>::foo");
1586        let changed = rewrite_path_expr_ufcs(&mut p, "Case01Trait", "Case01");
1587        assert!(changed);
1588        assert_eq!(p, "Case01::foo");
1589    }
1590
1591    #[test]
1592    fn rewrite_path_expr_ufcs_no_match_returns_false() {
1593        let mut p = String::from("Case01::foo");
1594        let changed = rewrite_path_expr_ufcs(&mut p, "Case01Trait", "Case01");
1595        assert!(!changed);
1596        assert_eq!(p, "Case01::foo");
1597    }
1598
1599    #[test]
1600    fn rewrite_path_expr_ufcs_unrelated_trait_no_change() {
1601        let mut p = String::from("OtherTrait::foo");
1602        let changed = rewrite_path_expr_ufcs(&mut p, "Case01Trait", "Case01");
1603        assert!(!changed);
1604        assert_eq!(p, "OtherTrait::foo");
1605    }
1606
1607    #[test]
1608    fn rewrite_body_ufcs_single_path() {
1609        let mut body = PureBlock {
1610            stmts: vec![PureStmt::Expr(PureExpr::Call {
1611                func: Box::new(PureExpr::Path("Case01Trait::foo".to_string())),
1612                args: vec![PureExpr::Path("s".to_string())],
1613            })],
1614        };
1615        let count = rewrite_body_ufcs(&mut body, "Case01Trait", "Case01");
1616        assert_eq!(count, 1);
1617        if let PureStmt::Expr(PureExpr::Call { func, .. }) = &body.stmts[0] {
1618            if let PureExpr::Path(p) = func.as_ref() {
1619                assert_eq!(p, "Case01::foo");
1620            } else {
1621                panic!("expected Path");
1622            }
1623        } else {
1624            panic!("expected Call");
1625        }
1626    }
1627
1628    #[test]
1629    fn rewrite_body_ufcs_multiple_paths_in_nested_block() {
1630        // { let x = Case01Trait::foo(s); if cond { Case01Trait::bar() } else { 0 } }
1631        let inner_if = PureExpr::If {
1632            cond: Box::new(PureExpr::Lit("true".to_string())),
1633            then_branch: PureBlock {
1634                stmts: vec![PureStmt::Expr(PureExpr::Call {
1635                    func: Box::new(PureExpr::Path("Case01Trait::bar".to_string())),
1636                    args: Vec::new(),
1637                })],
1638            },
1639            else_branch: Some(Box::new(PureExpr::Lit("0".to_string()))),
1640        };
1641        let mut body = PureBlock {
1642            stmts: vec![
1643                PureStmt::Semi(PureExpr::Call {
1644                    func: Box::new(PureExpr::Path("Case01Trait::foo".to_string())),
1645                    args: vec![PureExpr::Path("s".to_string())],
1646                }),
1647                PureStmt::Expr(inner_if),
1648            ],
1649        };
1650        let count = rewrite_body_ufcs(&mut body, "Case01Trait", "Case01");
1651        assert_eq!(count, 2);
1652    }
1653
1654    #[test]
1655    fn rewrite_body_ufcs_leaves_unrelated_paths_alone() {
1656        let mut body = PureBlock {
1657            stmts: vec![PureStmt::Expr(PureExpr::Call {
1658                func: Box::new(PureExpr::Path("OtherTrait::foo".to_string())),
1659                args: Vec::new(),
1660            })],
1661        };
1662        let count = rewrite_body_ufcs(&mut body, "Case01Trait", "Case01");
1663        assert_eq!(count, 0);
1664    }
1665
1666    // ===== Phase 2f: DynDispatch rewriter tests =====
1667
1668    #[test]
1669    fn substitute_trait_in_type_dyn_object() {
1670        let mut ty = PureType::TraitObject(vec!["Case01Trait".to_string()]);
1671        let changed = substitute_trait_in_type(&mut ty, "Case01Trait", "Case01");
1672        assert!(changed);
1673        assert_eq!(ty, PureType::Path("Case01".to_string()));
1674    }
1675
1676    #[test]
1677    fn substitute_trait_in_type_ref_dyn() {
1678        // `&dyn Case01Trait` → `&Case01`
1679        let mut ty = PureType::Ref {
1680            lifetime: None,
1681            is_mut: false,
1682            ty: Box::new(PureType::TraitObject(vec!["Case01Trait".to_string()])),
1683        };
1684        let changed = substitute_trait_in_type(&mut ty, "Case01Trait", "Case01");
1685        assert!(changed);
1686        match ty {
1687            PureType::Ref { ty: inner, .. } => {
1688                assert_eq!(*inner, PureType::Path("Case01".to_string()));
1689            }
1690            _ => panic!("expected Ref"),
1691        }
1692    }
1693
1694    #[test]
1695    fn substitute_trait_in_type_unrelated() {
1696        let mut ty = PureType::Path("u32".to_string());
1697        let changed = substitute_trait_in_type(&mut ty, "Case01Trait", "Case01");
1698        assert!(!changed);
1699        assert_eq!(ty, PureType::Path("u32".to_string()));
1700    }
1701
1702    #[test]
1703    fn substitute_trait_in_type_tuple_mixed() {
1704        // `(&dyn Case01Trait, u32)` → `(&Case01, u32)`
1705        let mut ty = PureType::Tuple(vec![
1706            PureType::Ref {
1707                lifetime: None,
1708                is_mut: false,
1709                ty: Box::new(PureType::TraitObject(vec!["Case01Trait".to_string()])),
1710            },
1711            PureType::Path("u32".to_string()),
1712        ]);
1713        let changed = substitute_trait_in_type(&mut ty, "Case01Trait", "Case01");
1714        assert!(changed);
1715    }
1716
1717    #[test]
1718    fn rewrite_fn_dyn_dispatch_signature_param_and_ret() {
1719        // fn use_both(t: &dyn Case01Trait) -> &dyn Case01Trait { ... }
1720        let dyn_ref = |bounds: &str| PureType::Ref {
1721            lifetime: None,
1722            is_mut: false,
1723            ty: Box::new(PureType::TraitObject(vec![bounds.to_string()])),
1724        };
1725        let mut f = mk_fn(
1726            "use_both",
1727            PureGenerics {
1728                params: Vec::new(),
1729                where_clause: Vec::new(),
1730            },
1731            vec![PureParam::Typed {
1732                name: "t".to_string(),
1733                ty: dyn_ref("Case01Trait"),
1734                is_mut: false,
1735                pat: None,
1736            }],
1737        );
1738        f.ret = Some(dyn_ref("Case01Trait"));
1739
1740        let count = rewrite_fn_dyn_dispatch(&mut f, "Case01Trait", "Case01");
1741        assert_eq!(count, 2);
1742
1743        // Param ty.
1744        if let PureParam::Typed { ty, .. } = &f.params[0] {
1745            match ty {
1746                PureType::Ref { ty: inner, .. } => {
1747                    assert_eq!(**inner, PureType::Path("Case01".to_string()));
1748                }
1749                _ => panic!("expected Ref"),
1750            }
1751        }
1752        // Return ty.
1753        match &f.ret {
1754            Some(PureType::Ref { ty: inner, .. }) => {
1755                assert_eq!(**inner, PureType::Path("Case01".to_string()));
1756            }
1757            _ => panic!("expected Ref ret"),
1758        }
1759    }
1760
1761    #[test]
1762    fn rewrite_fn_dyn_dispatch_no_change_when_unrelated() {
1763        let mut f = mk_fn(
1764            "use_nothing",
1765            PureGenerics {
1766                params: Vec::new(),
1767                where_clause: Vec::new(),
1768            },
1769            vec![PureParam::Typed {
1770                name: "x".to_string(),
1771                ty: PureType::Path("u32".to_string()),
1772                is_mut: false,
1773                pat: None,
1774            }],
1775        );
1776        let count = rewrite_fn_dyn_dispatch(&mut f, "Case01Trait", "Case01");
1777        assert_eq!(count, 0);
1778    }
1779
1780    // ===== Phase 2g: GenericBound rewriter tests =====
1781
1782    #[test]
1783    fn substitute_generic_in_type_direct_path() {
1784        let mut ty = PureType::Path("T".to_string());
1785        let changed = substitute_generic_in_type(&mut ty, "T", "Case01");
1786        assert!(changed);
1787        assert_eq!(ty, PureType::Path("Case01".to_string()));
1788    }
1789
1790    #[test]
1791    fn substitute_generic_in_type_ref_path() {
1792        // `&T` → `&Case01`
1793        let mut ty = PureType::Ref {
1794            lifetime: None,
1795            is_mut: false,
1796            ty: Box::new(PureType::Path("T".to_string())),
1797        };
1798        let changed = substitute_generic_in_type(&mut ty, "T", "Case01");
1799        assert!(changed);
1800    }
1801
1802    #[test]
1803    fn substitute_generic_in_type_unrelated_name_unchanged() {
1804        let mut ty = PureType::Path("u32".to_string());
1805        let changed = substitute_generic_in_type(&mut ty, "T", "Case01");
1806        assert!(!changed);
1807        assert_eq!(ty, PureType::Path("u32".to_string()));
1808    }
1809
1810    #[test]
1811    fn rewrite_fn_generic_bound_strips_param_and_substitutes_type() {
1812        // fn use_generic<T: Case01Trait>(t: T) -> u32
1813        let g = PureGenerics {
1814            params: vec![PureGenericParam::Type {
1815                name: "T".to_string(),
1816                bounds: vec!["Case01Trait".to_string()],
1817            }],
1818            where_clause: Vec::new(),
1819        };
1820        let mut f = mk_fn(
1821            "use_generic",
1822            g,
1823            vec![PureParam::Typed {
1824                name: "t".to_string(),
1825                ty: PureType::Path("T".to_string()),
1826                is_mut: false,
1827                pat: None,
1828            }],
1829        );
1830        f.ret = Some(PureType::Path("u32".to_string()));
1831
1832        let count = rewrite_fn_generic_bound(&mut f, "Case01Trait", "Case01");
1833        // 1 generic param removed + 1 type substitution = 2 edits.
1834        assert_eq!(count, 2);
1835        assert!(f.generics.params.is_empty(), "generic params must be empty");
1836        if let PureParam::Typed { ty, .. } = &f.params[0] {
1837            assert_eq!(*ty, PureType::Path("Case01".to_string()));
1838        } else {
1839            panic!("expected Typed param");
1840        }
1841    }
1842
1843    #[test]
1844    fn rewrite_fn_generic_bound_leaves_unrelated_generics_alone() {
1845        // fn use_other<U: OtherTrait, T: Case01Trait>(t: T, u: U) -> T
1846        let g = PureGenerics {
1847            params: vec![
1848                PureGenericParam::Type {
1849                    name: "U".to_string(),
1850                    bounds: vec!["OtherTrait".to_string()],
1851                },
1852                PureGenericParam::Type {
1853                    name: "T".to_string(),
1854                    bounds: vec!["Case01Trait".to_string()],
1855                },
1856            ],
1857            where_clause: Vec::new(),
1858        };
1859        let mut f = mk_fn(
1860            "use_other",
1861            g,
1862            vec![
1863                PureParam::Typed {
1864                    name: "t".to_string(),
1865                    ty: PureType::Path("T".to_string()),
1866                    is_mut: false,
1867                    pat: None,
1868                },
1869                PureParam::Typed {
1870                    name: "u".to_string(),
1871                    ty: PureType::Path("U".to_string()),
1872                    is_mut: false,
1873                    pat: None,
1874                },
1875            ],
1876        );
1877        f.ret = Some(PureType::Path("T".to_string()));
1878
1879        let count = rewrite_fn_generic_bound(&mut f, "Case01Trait", "Case01");
1880        // T removed + param ty substituted + ret ty substituted = 3.
1881        assert_eq!(count, 3);
1882        assert_eq!(f.generics.params.len(), 1, "U generic must remain");
1883        if let PureGenericParam::Type { name, .. } = &f.generics.params[0] {
1884            assert_eq!(name, "U");
1885        } else {
1886            panic!("expected Type param");
1887        }
1888        if let PureParam::Typed { ty, .. } = &f.params[1] {
1889            assert_eq!(*ty, PureType::Path("U".to_string()));
1890        }
1891    }
1892
1893    // ===== Phase 6-A: body axis tests =====
1894
1895    #[test]
1896    fn rewrite_path_expr_generic_bare() {
1897        let mut p = String::from("T");
1898        let changed = rewrite_path_expr_generic(&mut p, "T", "Case01");
1899        assert!(changed);
1900        assert_eq!(p, "Case01");
1901    }
1902
1903    #[test]
1904    fn rewrite_path_expr_generic_prefixed() {
1905        let mut p = String::from("T::default");
1906        let changed = rewrite_path_expr_generic(&mut p, "T", "Case01");
1907        assert!(changed);
1908        assert_eq!(p, "Case01::default");
1909    }
1910
1911    #[test]
1912    fn rewrite_path_expr_generic_nested_segments() {
1913        let mut p = String::from("T::Item::sub");
1914        let changed = rewrite_path_expr_generic(&mut p, "T", "Case01");
1915        assert!(changed);
1916        assert_eq!(p, "Case01::Item::sub");
1917    }
1918
1919    #[test]
1920    fn rewrite_path_expr_generic_no_match_when_unrelated() {
1921        // `Type` shares a prefix character with `T` but is a different
1922        // identifier; must NOT be rewritten.
1923        let mut p = String::from("Type::method");
1924        let changed = rewrite_path_expr_generic(&mut p, "T", "Case01");
1925        assert!(!changed);
1926        assert_eq!(p, "Type::method");
1927    }
1928
1929    #[test]
1930    fn rewrite_path_expr_generic_no_match_different_generic_name() {
1931        let mut p = String::from("U::new");
1932        let changed = rewrite_path_expr_generic(&mut p, "T", "Case01");
1933        assert!(!changed);
1934        assert_eq!(p, "U::new");
1935    }
1936
1937    #[test]
1938    fn rewrite_body_generic_bound_path_in_call() {
1939        // { T::default() }
1940        let mut body = PureBlock {
1941            stmts: vec![PureStmt::Expr(PureExpr::Call {
1942                func: Box::new(PureExpr::Path("T::default".to_string())),
1943                args: Vec::new(),
1944            })],
1945        };
1946        let count = rewrite_body_generic_bound(&mut body, "T", "Case01");
1947        assert_eq!(count, 1);
1948        if let PureStmt::Expr(PureExpr::Call { func, .. }) = &body.stmts[0] {
1949            if let PureExpr::Path(p) = func.as_ref() {
1950                assert_eq!(p, "Case01::default");
1951            } else {
1952                panic!("expected Path");
1953            }
1954        } else {
1955            panic!("expected Call");
1956        }
1957    }
1958
1959    #[test]
1960    fn rewrite_body_generic_bound_local_type_annotation() {
1961        // { let x: T = ...; }
1962        let mut body = PureBlock {
1963            stmts: vec![PureStmt::Local {
1964                pattern: ryo_source::pure::PurePattern::Ident {
1965                    name: "x".to_string(),
1966                    is_mut: false,
1967                    by_ref: false,
1968                },
1969                ty: Some(PureType::Path("T".to_string())),
1970                init: None,
1971                else_branch: None,
1972            }],
1973        };
1974        let count = rewrite_body_generic_bound(&mut body, "T", "Case01");
1975        assert_eq!(count, 1);
1976        if let PureStmt::Local { ty: Some(ty), .. } = &body.stmts[0] {
1977            assert_eq!(*ty, PureType::Path("Case01".to_string()));
1978        } else {
1979            panic!("expected Local with ty");
1980        }
1981    }
1982
1983    #[test]
1984    fn rewrite_body_generic_bound_combined_path_and_local_ty() {
1985        // { let x: T = T::default(); }
1986        let mut body = PureBlock {
1987            stmts: vec![PureStmt::Local {
1988                pattern: ryo_source::pure::PurePattern::Ident {
1989                    name: "x".to_string(),
1990                    is_mut: false,
1991                    by_ref: false,
1992                },
1993                ty: Some(PureType::Path("T".to_string())),
1994                init: Some(PureExpr::Call {
1995                    func: Box::new(PureExpr::Path("T::default".to_string())),
1996                    args: Vec::new(),
1997                }),
1998                else_branch: None,
1999            }],
2000        };
2001        let count = rewrite_body_generic_bound(&mut body, "T", "Case01");
2002        // 1 Path rewrite (in init) + 1 Local-ty substitute = 2 edits.
2003        assert_eq!(count, 2);
2004    }
2005
2006    #[test]
2007    fn rewrite_body_generic_bound_no_change_when_unrelated() {
2008        // { U::new() }
2009        let mut body = PureBlock {
2010            stmts: vec![PureStmt::Expr(PureExpr::Call {
2011                func: Box::new(PureExpr::Path("U::new".to_string())),
2012                args: Vec::new(),
2013            })],
2014        };
2015        let count = rewrite_body_generic_bound(&mut body, "T", "Case01");
2016        assert_eq!(count, 0);
2017    }
2018
2019    #[test]
2020    fn rewrite_fn_generic_bound_with_body_path() {
2021        // fn use_generic<T: Case01Trait>(t: T) -> u32 {
2022        //   let x = T::default();
2023        //   ...
2024        // }
2025        let g = PureGenerics {
2026            params: vec![PureGenericParam::Type {
2027                name: "T".to_string(),
2028                bounds: vec!["Case01Trait".to_string()],
2029            }],
2030            where_clause: Vec::new(),
2031        };
2032        let body = PureBlock {
2033            stmts: vec![PureStmt::Local {
2034                pattern: ryo_source::pure::PurePattern::Ident {
2035                    name: "x".to_string(),
2036                    is_mut: false,
2037                    by_ref: false,
2038                },
2039                ty: None,
2040                init: Some(PureExpr::Call {
2041                    func: Box::new(PureExpr::Path("T::default".to_string())),
2042                    args: Vec::new(),
2043                }),
2044                else_branch: None,
2045            }],
2046        };
2047        let mut f = fn_with_body("use_generic", body);
2048        f.generics = g;
2049        f.params = vec![PureParam::Typed {
2050            name: "t".to_string(),
2051            ty: PureType::Path("T".to_string()),
2052            is_mut: false,
2053            pat: None,
2054        }];
2055        f.ret = Some(PureType::Path("u32".to_string()));
2056
2057        let count = rewrite_fn_generic_bound(&mut f, "Case01Trait", "Case01");
2058        // 1 removed generic param + 1 param ty substitute + 1 body Path = 3.
2059        assert_eq!(count, 3);
2060        assert!(f.generics.params.is_empty());
2061        if let PureStmt::Local {
2062            init: Some(PureExpr::Call { func, .. }),
2063            ..
2064        } = &f.body.stmts[0]
2065        {
2066            if let PureExpr::Path(p) = func.as_ref() {
2067                assert_eq!(p, "Case01::default");
2068            } else {
2069                panic!("expected Path");
2070            }
2071        } else {
2072            panic!("expected Local with init");
2073        }
2074    }
2075
2076    // ===== Phase 6-B: turbofish axis tests =====
2077
2078    #[test]
2079    fn rewrite_turbofish_generic_bare_token() {
2080        let mut s = String::from("< T >");
2081        let count = rewrite_turbofish_generic(&mut s, "T", "Case01");
2082        assert_eq!(count, 1);
2083        assert_eq!(s, "< Case01 >");
2084    }
2085
2086    #[test]
2087    fn rewrite_turbofish_generic_multi_args_only_t_changes() {
2088        let mut s = String::from("< T , U >");
2089        let count = rewrite_turbofish_generic(&mut s, "T", "Case01");
2090        assert_eq!(count, 1);
2091        assert_eq!(s, "< Case01 , U >");
2092    }
2093
2094    #[test]
2095    fn rewrite_turbofish_generic_nested() {
2096        let mut s = String::from("< Vec < T > >");
2097        let count = rewrite_turbofish_generic(&mut s, "T", "Case01");
2098        assert_eq!(count, 1);
2099        assert_eq!(s, "< Vec < Case01 > >");
2100    }
2101
2102    #[test]
2103    fn rewrite_turbofish_generic_does_not_match_prefix_identifier() {
2104        let mut s = String::from("< Type >");
2105        let count = rewrite_turbofish_generic(&mut s, "T", "Case01");
2106        assert_eq!(count, 0);
2107        assert_eq!(s, "< Type >");
2108    }
2109
2110    #[test]
2111    fn rewrite_turbofish_generic_does_not_match_extended_identifier() {
2112        let mut s = String::from("< Tx >");
2113        let count = rewrite_turbofish_generic(&mut s, "T", "Case01");
2114        assert_eq!(count, 0);
2115        assert_eq!(s, "< Tx >");
2116    }
2117
2118    #[test]
2119    fn rewrite_turbofish_generic_repeated_occurrences() {
2120        let mut s = String::from("< T , T , T >");
2121        let count = rewrite_turbofish_generic(&mut s, "T", "Case01");
2122        assert_eq!(count, 3);
2123        assert_eq!(s, "< Case01 , Case01 , Case01 >");
2124    }
2125
2126    #[test]
2127    fn rewrite_body_turbofish_generic_in_method_call() {
2128        let mut body = PureBlock {
2129            stmts: vec![PureStmt::Expr(PureExpr::MethodCall {
2130                receiver: Box::new(PureExpr::Path("vec".to_string())),
2131                method: "iter".to_string(),
2132                turbofish: Some("< T >".to_string()),
2133                args: Vec::new(),
2134            })],
2135        };
2136        let count = rewrite_body_turbofish_generic(&mut body, "T", "Case01");
2137        assert_eq!(count, 1);
2138        if let PureStmt::Expr(PureExpr::MethodCall {
2139            turbofish: Some(s), ..
2140        }) = &body.stmts[0]
2141        {
2142            assert_eq!(s, "< Case01 >");
2143        } else {
2144            panic!("expected MethodCall with turbofish");
2145        }
2146    }
2147
2148    #[test]
2149    fn rewrite_body_turbofish_generic_skips_unrelated() {
2150        let mut body = PureBlock {
2151            stmts: vec![PureStmt::Expr(PureExpr::MethodCall {
2152                receiver: Box::new(PureExpr::Path("vec".to_string())),
2153                method: "iter".to_string(),
2154                turbofish: Some("< U >".to_string()),
2155                args: Vec::new(),
2156            })],
2157        };
2158        let count = rewrite_body_turbofish_generic(&mut body, "T", "Case01");
2159        assert_eq!(count, 0);
2160    }
2161
2162    #[test]
2163    fn rewrite_fn_generic_bound_with_body_turbofish() {
2164        let g = PureGenerics {
2165            params: vec![PureGenericParam::Type {
2166                name: "T".to_string(),
2167                bounds: vec!["Case01Trait".to_string()],
2168            }],
2169            where_clause: Vec::new(),
2170        };
2171        let body = PureBlock {
2172            stmts: vec![PureStmt::Expr(PureExpr::MethodCall {
2173                receiver: Box::new(PureExpr::Path("vec".to_string())),
2174                method: "iter".to_string(),
2175                turbofish: Some("< T >".to_string()),
2176                args: Vec::new(),
2177            })],
2178        };
2179        let mut f = fn_with_body("use_generic", body);
2180        f.generics = g;
2181        f.params = vec![PureParam::Typed {
2182            name: "t".to_string(),
2183            ty: PureType::Path("T".to_string()),
2184            is_mut: false,
2185            pat: None,
2186        }];
2187
2188        let count = rewrite_fn_generic_bound(&mut f, "Case01Trait", "Case01");
2189        // 1 removed generic + 1 param ty substitute + 1 turbofish = 3.
2190        assert_eq!(count, 3);
2191        assert!(f.generics.params.is_empty());
2192        if let PureStmt::Expr(PureExpr::MethodCall {
2193            turbofish: Some(s), ..
2194        }) = &f.body.stmts[0]
2195        {
2196            assert_eq!(s, "< Case01 >");
2197        } else {
2198            panic!("expected MethodCall with turbofish");
2199        }
2200    }
2201
2202    // ===== Phase 6-C: where_clause axis tests =====
2203
2204    #[test]
2205    fn rewrite_where_clause_drops_predicate_whose_lhs_is_stripped() {
2206        // where T: Clone
2207        let mut clause = vec!["T: Clone".to_string()];
2208        let count = rewrite_where_clause_generic(&mut clause, "T", "Case01");
2209        assert_eq!(count, 1);
2210        assert!(clause.is_empty(), "predicate should be dropped");
2211    }
2212
2213    #[test]
2214    fn rewrite_where_clause_keeps_predicate_with_different_lhs() {
2215        // where U: Foo (LHS != T, no T elsewhere)
2216        let mut clause = vec!["U: Foo".to_string()];
2217        let count = rewrite_where_clause_generic(&mut clause, "T", "Case01");
2218        assert_eq!(count, 0);
2219        assert_eq!(clause, vec!["U: Foo".to_string()]);
2220    }
2221
2222    #[test]
2223    fn rewrite_where_clause_substitutes_in_predicate_rhs() {
2224        // where U: Foo<T> — LHS != T but T appears in RHS bound.
2225        let mut clause = vec!["U: Foo<T>".to_string()];
2226        let count = rewrite_where_clause_generic(&mut clause, "T", "Case01");
2227        assert_eq!(count, 1);
2228        assert_eq!(clause, vec!["U: Foo<Case01>".to_string()]);
2229    }
2230
2231    #[test]
2232    fn rewrite_where_clause_substitutes_repeated_in_rhs() {
2233        // where U: Foo<T> + Bar<T>
2234        let mut clause = vec!["U: Foo<T> + Bar<T>".to_string()];
2235        let count = rewrite_where_clause_generic(&mut clause, "T", "Case01");
2236        assert_eq!(count, 2);
2237        assert_eq!(clause, vec!["U: Foo<Case01> + Bar<Case01>".to_string()]);
2238    }
2239
2240    #[test]
2241    fn rewrite_where_clause_does_not_match_prefix_identifier_in_rhs() {
2242        // where U: Foo<Type> — `Type` is not `T`.
2243        let mut clause = vec!["U: Foo<Type>".to_string()];
2244        let count = rewrite_where_clause_generic(&mut clause, "T", "Case01");
2245        assert_eq!(count, 0);
2246        assert_eq!(clause, vec!["U: Foo<Type>".to_string()]);
2247    }
2248
2249    #[test]
2250    fn rewrite_where_clause_mixed_drop_and_substitute() {
2251        // where T: Clone, U: Foo<T>, V: Bar
2252        //  → drop "T: Clone", substitute T in "U: Foo<T>", keep "V: Bar"
2253        let mut clause = vec![
2254            "T: Clone".to_string(),
2255            "U: Foo<T>".to_string(),
2256            "V: Bar".to_string(),
2257        ];
2258        let count = rewrite_where_clause_generic(&mut clause, "T", "Case01");
2259        // 1 drop + 1 RHS substitution = 2 edits.
2260        assert_eq!(count, 2);
2261        assert_eq!(
2262            clause,
2263            vec!["U: Foo<Case01>".to_string(), "V: Bar".to_string()]
2264        );
2265    }
2266
2267    #[test]
2268    fn rewrite_where_clause_leading_whitespace_lhs() {
2269        // "  T: Clone" — leading spaces should not prevent the LHS
2270        // match.
2271        let mut clause = vec!["  T: Clone".to_string()];
2272        let count = rewrite_where_clause_generic(&mut clause, "T", "Case01");
2273        assert_eq!(count, 1);
2274        assert!(clause.is_empty());
2275    }
2276
2277    #[test]
2278    fn rewrite_fn_generic_bound_with_where_clause() {
2279        // fn use_generic<T: Case01Trait>(t: T) -> u32 where T: Clone {
2280        //   ...
2281        // }
2282        let g = PureGenerics {
2283            params: vec![PureGenericParam::Type {
2284                name: "T".to_string(),
2285                bounds: vec!["Case01Trait".to_string()],
2286            }],
2287            where_clause: vec!["T: Clone".to_string()],
2288        };
2289        let mut f = mk_fn(
2290            "use_generic",
2291            g,
2292            vec![PureParam::Typed {
2293                name: "t".to_string(),
2294                ty: PureType::Path("T".to_string()),
2295                is_mut: false,
2296                pat: None,
2297            }],
2298        );
2299
2300        let count = rewrite_fn_generic_bound(&mut f, "Case01Trait", "Case01");
2301        // 1 removed generic param + 1 param ty substitute + 1 dropped
2302        // where predicate = 3 edits.
2303        assert_eq!(count, 3);
2304        assert!(f.generics.params.is_empty());
2305        assert!(f.generics.where_clause.is_empty());
2306    }
2307
2308    #[test]
2309    fn rewrite_fn_generic_bound_no_change_when_no_matching_bound() {
2310        let g = PureGenerics {
2311            params: vec![PureGenericParam::Type {
2312                name: "T".to_string(),
2313                bounds: vec!["OtherTrait".to_string()],
2314            }],
2315            where_clause: Vec::new(),
2316        };
2317        let mut f = mk_fn("use_unrelated", g, Vec::new());
2318        let count = rewrite_fn_generic_bound(&mut f, "Case01Trait", "Case01");
2319        assert_eq!(count, 0);
2320        assert_eq!(f.generics.params.len(), 1);
2321    }
2322
2323    #[test]
2324    fn classify_fn_combines_signature_and_body() {
2325        // fn use_all<T: Case01Trait>(t: &dyn Case01Trait, s: &Case01) -> u32 {
2326        //   <Case01 as Case01Trait>::foo(s);
2327        //   t.foo()
2328        // }
2329        let g = PureGenerics {
2330            params: vec![PureGenericParam::Type {
2331                name: "T".to_string(),
2332                bounds: vec!["Case01Trait".to_string()],
2333            }],
2334            where_clause: Vec::new(),
2335        };
2336        let dyn_param = PureParam::Typed {
2337            name: "t".to_string(),
2338            ty: PureType::Ref {
2339                lifetime: None,
2340                is_mut: false,
2341                ty: Box::new(PureType::TraitObject(vec!["Case01Trait".to_string()])),
2342            },
2343            is_mut: false,
2344            pat: None,
2345        };
2346        let body = PureBlock {
2347            stmts: vec![
2348                PureStmt::Semi(PureExpr::Path("<Case01 as Case01Trait>::foo".to_string())),
2349                PureStmt::Expr(mk_method_call("t", "foo")),
2350            ],
2351        };
2352        let mut f = fn_with_body("use_all", body);
2353        f.generics = g;
2354        f.params = vec![dyn_param];
2355
2356        let methods = vec!["foo".to_string()];
2357        let out = classify_fn(&f, "Case01Trait", &methods);
2358        assert!(out.contains(&CallerPattern::GenericBound));
2359        assert!(out.contains(&CallerPattern::DynDispatch));
2360        assert!(out.contains(&CallerPattern::Dot));
2361        assert!(out.contains(&CallerPattern::Ufcs));
2362        assert_eq!(out.len(), 4);
2363    }
2364}