ryo-mutations 0.1.0

[experimental] Code transformation primitives for Rust source code
Documentation
//! Trait mutations: ExtractTraitMutation, InlineTraitMutation, RemoveTraitMutation, EnumToTraitMutation

use crate::Mutation;
use ryo_symbol::SymbolId;
use serde::{Deserialize, Serialize};

/// Strategy for enum-to-trait conversion
///
/// Determines how the converted type should be used:
/// - `Dynamic`: `Box<dyn Trait>` - heap allocation, runtime polymorphism
/// - `Static`: `impl Trait` or generics with `T: Trait` - monomorphization, no heap
/// - `MarkerOnly`: Only creates trait + structs, no type replacement
#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[serde(rename_all = "snake_case")]
pub enum EnumToTraitStrategy {
    /// Use `Box<dyn Trait>` for dynamic dispatch (default)
    ///
    /// Parameters: `fn process(filter: Status)` → `fn process(filter: Box<dyn Status>)`
    /// Return types: `fn create() -> Status` → `fn create() -> Box<dyn Status>`
    /// Fields: `filter: Status` → `filter: Box<dyn Status>`
    #[default]
    Dynamic,

    /// Use `impl Trait` for static dispatch (functions only)
    ///
    /// Parameters: `fn process(filter: Status)` → `fn process(filter: impl Status)`
    /// Return types: `fn create() -> Status` → `fn create() -> impl Status`
    /// Fields: Falls back to `Box<dyn Trait>` (impl Trait not allowed in fields)
    Static,

    /// Use generics `<T: Trait>` for full static dispatch
    ///
    /// Structs become generic: `struct Config { filter: Status }` → `struct Config<T: Status> { filter: T }`
    /// Functions become generic: `fn process(filter: Status)` → `fn process<T: Status>(filter: T)`
    /// Preserves monomorphization for all cases including struct fields
    Generic,

    /// Only create trait and struct definitions, no type replacement
    ///
    /// Useful when manual control over type replacement is needed
    MarkerOnly,
}

/// How to handle match expressions on the converted enum
///
/// Match expressions cannot be automatically converted because:
/// - Enum variants become separate types (different concrete types)
/// - Pattern matching on Box<dyn Trait> requires downcast
#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[serde(rename_all = "snake_case")]
pub enum MatchHandling {
    /// Emit warnings about match expressions that need manual migration
    ///
    /// The match expression is preserved as-is with a TODO comment
    #[default]
    WarnOnly,

    /// Attempt to convert match to downcast-based dispatch
    ///
    /// ```ignore
    /// // Before:
    /// match status {
    ///     Status::Running => { ... }
    ///     Status::Stopped => { ... }
    /// }
    ///
    /// // After (with appropriate Any impl):
    /// if let Some(running) = status.as_any().downcast_ref::<Running>() {
    ///     ...
    /// } else if let Some(stopped) = status.as_any().downcast_ref::<Stopped>() {
    ///     ...
    /// }
    /// ```
    ///
    /// Requires adding `Any` bound and `as_any()` method to trait
    Downcast,

    /// Block conversion if any match expression exists
    ///
    /// Returns an error with locations of all match expressions
    BlockOnMatch,
}

/// Extract a trait from an impl block
///
/// Converts:
/// ```ignore
/// impl Foo {
///     fn method(&self) -> i32 { 42 }
/// }
/// ```
/// Into:
/// ```ignore
/// trait FooTrait {
///     fn method(&self) -> i32;
/// }
/// impl FooTrait for Foo {
///     fn method(&self) -> i32 { 42 }
/// }
/// ```
#[derive(Debug, Clone)]
pub struct ExtractTraitMutation {
    /// SymbolId of the inherent impl block (required for O(1) lookup)
    pub symbol_id: SymbolId,
    /// Name for the new trait
    pub trait_name: String,
    /// Specific methods to extract (None = all methods)
    pub methods: Option<Vec<String>>,
}

impl ExtractTraitMutation {
    pub fn new(symbol_id: SymbolId, trait_name: impl Into<String>) -> Self {
        Self {
            symbol_id,
            trait_name: trait_name.into(),
            methods: None,
        }
    }

    pub fn with_methods(mut self, methods: Vec<String>) -> Self {
        self.methods = Some(methods);
        self
    }
}

impl Mutation for ExtractTraitMutation {
    fn describe(&self) -> String {
        format!(
            "Extract trait '{}' from impl (SymbolId: {:?})",
            self.trait_name, self.symbol_id
        )
    }

    fn mutation_type(&self) -> &'static str {
        "ExtractTrait"
    }

    fn box_clone(&self) -> Box<dyn Mutation> {
        Box::new(self.clone())
    }
}

/// Inline a trait back into a struct (remove trait, make inherent impl)
///
/// Converts:
/// ```ignore
/// trait FooTrait {
///     fn method(&self) -> i32;
/// }
/// impl FooTrait for Foo {
///     fn method(&self) -> i32 { 42 }
/// }
/// ```
/// Into:
/// ```ignore
/// impl Foo {
///     fn method(&self) -> i32 { 42 }
/// }
/// ```
#[derive(Debug, Clone)]
pub struct InlineTraitMutation {
    /// SymbolId of the trait definition (required for O(1) lookup)
    pub symbol_id: SymbolId,
    pub struct_name: String,
    pub remove_trait: bool, // Whether to remove the trait definition
}

impl InlineTraitMutation {
    pub fn new(symbol_id: SymbolId, struct_name: impl Into<String>) -> Self {
        Self {
            symbol_id,
            struct_name: struct_name.into(),
            remove_trait: true,
        }
    }

    pub fn keep_trait(mut self) -> Self {
        self.remove_trait = false;
        self
    }
}

impl Mutation for InlineTraitMutation {
    fn describe(&self) -> String {
        format!(
            "Inline trait {:?} into impl {}",
            self.symbol_id, self.struct_name
        )
    }

    fn mutation_type(&self) -> &'static str {
        "InlineTrait"
    }

    fn box_clone(&self) -> Box<dyn Mutation> {
        Box::new(self.clone())
    }
}

/// Remove a trait from the file
#[derive(Debug, Clone)]
pub struct RemoveTraitMutation {
    /// SymbolId of the trait to remove (required, O(1) access)
    pub trait_id: SymbolId,
}

impl RemoveTraitMutation {
    pub fn new(trait_id: SymbolId) -> Self {
        Self { trait_id }
    }
}

impl Mutation for RemoveTraitMutation {
    fn describe(&self) -> String {
        format!("Remove trait {}", self.trait_id)
    }

    fn mutation_type(&self) -> &'static str {
        "RemoveTrait"
    }

    fn box_clone(&self) -> Box<dyn Mutation> {
        Box::new(self.clone())
    }
}

/// Convert an enum to a trait with struct implementations
///
/// Transforms:
/// ```ignore
/// enum Status {
///     Running,
///     Stopped,
/// }
/// ```
/// Into:
/// ```ignore
/// pub trait Status {}
/// pub struct Running;
/// pub struct Stopped;
/// impl Status for Running {}
/// impl Status for Stopped {}
/// ```
///
/// Also replaces all usage sites: `Status::Running` → `Running`
///
/// ## Type Replacement Strategy
///
/// With `strategy: Dynamic` (default):
/// ```ignore
/// fn process(status: Status) → fn process(status: Box<dyn Status>)
/// ```
///
/// With `strategy: Static`:
/// ```ignore
/// fn process(status: Status) → fn process(status: impl Status)
/// ```
///
/// With `strategy: MarkerOnly`: No type replacement (manual migration)
///
/// **Note**: Uses `symbol_id` for reliable O(1) enum identification.
#[derive(Debug, Clone)]
pub struct EnumToTraitMutation {
    /// SymbolId of the target enum (required for reliable identification)
    pub symbol_id: ryo_symbol::SymbolId,
    /// Custom trait name (None = same as enum name)
    pub trait_name: Option<String>,
    /// Whether to remove the original enum
    pub remove_enum: bool,
    /// Variant info (populated during apply from AST)
    pub variants: Vec<VariantInfo>,
    /// Strategy for type replacement (default: Dynamic)
    pub strategy: EnumToTraitStrategy,
    /// How to handle match expressions (default: WarnOnly)
    pub match_handling: MatchHandling,
}

/// Information about an enum variant for conversion
#[derive(Debug, Clone)]
pub struct VariantInfo {
    pub name: String,
    pub fields: Vec<FieldInfo>,
}

/// Field information for struct variants
#[derive(Debug, Clone)]
pub struct FieldInfo {
    pub name: String,
    pub ty: String,
}

impl EnumToTraitMutation {
    /// Create from SymbolId (required for reliable enum identification)
    pub fn from_symbol_id(symbol_id: ryo_symbol::SymbolId) -> Self {
        Self {
            symbol_id,
            trait_name: None,
            remove_enum: true,
            variants: Vec::new(),
            strategy: EnumToTraitStrategy::default(),
            match_handling: MatchHandling::default(),
        }
    }

    pub fn with_trait_name(mut self, name: impl Into<String>) -> Self {
        self.trait_name = Some(name.into());
        self
    }

    pub fn keep_enum(mut self) -> Self {
        self.remove_enum = false;
        self
    }

    pub fn with_variants(mut self, variants: Vec<VariantInfo>) -> Self {
        self.variants = variants;
        self
    }

    /// Set the type replacement strategy
    pub fn with_strategy(mut self, strategy: EnumToTraitStrategy) -> Self {
        self.strategy = strategy;
        self
    }

    /// Set how match expressions should be handled
    pub fn with_match_handling(mut self, handling: MatchHandling) -> Self {
        self.match_handling = handling;
        self
    }
}

impl Mutation for EnumToTraitMutation {
    fn describe(&self) -> String {
        let trait_name = self.trait_name.as_deref().unwrap_or("<from_enum>");
        format!(
            "Convert enum (symbol:{:?}) to trait '{}' with {} struct implementations",
            self.symbol_id,
            trait_name,
            self.variants.len()
        )
    }

    fn mutation_type(&self) -> &'static str {
        "EnumToTrait"
    }

    fn box_clone(&self) -> Box<dyn Mutation> {
        Box::new(self.clone())
    }
}