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}