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}