ryo-mutations 0.1.0

[experimental] Code transformation primitives for Rust source code
Documentation
//! Default Pattern Mutations
//!
//! Mutations for implementing the Default trait:
//!
//! - `DefaultMutation`: Add Default to structs (derive or generate impl)
//! - `DeriveDefaultMutation`: Convert manual impl Default to #[derive(Default)]
//!
//! # Example
//!
//! ```rust,ignore
//! // Before (no Default)
//! pub struct Config {
//!     timeout: u32,
//!     enabled: bool,
//! }
//!
//! // After (with derive)
//! #[derive(Default)]
//! pub struct Config {
//!     timeout: u32,
//!     enabled: bool,
//! }
//!
//! // Or with manual impl
//! impl Default for Config {
//!     fn default() -> Self {
//!         Self { timeout: 0, enabled: false }
//!     }
//! }
//! ```

use ryo_source::pure::{
    PureAttrMeta, PureAttribute, PureExpr, PureField, PureFields, PureFile, PureImpl, PureImplItem,
    PureItem, PureStmt,
};

use super::detect::{Detect, DetectCategory, DetectLocation, DetectOperation, DetectOpportunity};
use crate::Mutation;

// ============================================================================
// DefaultMutation: Add Default to structs
// ============================================================================

/// Add Default implementation to structs
///
/// This mutation adds `#[derive(Default)]` or generates `impl Default`
/// for structs that don't have it.
///
/// # Example
///
/// ```rust,ignore
/// use ryo_mutations::idiom::DefaultMutation;
///
/// let mutation = DefaultMutation::new();
/// let result = mutation.apply(&mut file);
/// ```
#[derive(Debug, Clone)]
pub struct DefaultMutation {
    /// Only apply to specific struct
    pub target_struct: Option<String>,
    /// Use #[derive(Default)] instead of impl (default: true)
    pub use_derive: bool,
}

impl Default for DefaultMutation {
    fn default() -> Self {
        Self {
            target_struct: None,
            use_derive: true,
        }
    }
}

impl DefaultMutation {
    pub fn new() -> Self {
        Self::default()
    }

    /// Only apply to a specific struct
    pub fn for_struct(mut self, name: impl Into<String>) -> Self {
        self.target_struct = Some(name.into());
        self
    }

    /// Use #[derive(Default)] (true) or impl Default (false)
    pub fn with_derive(mut self, use_derive: bool) -> Self {
        self.use_derive = use_derive;
        self
    }

    /// Get named fields from a struct
    fn get_named_fields(fields: &PureFields) -> Option<&Vec<PureField>> {
        match fields {
            PureFields::Named(f) => Some(f),
            _ => None,
        }
    }

    /// Check if struct already has derive(Default)
    fn has_derive_default(attrs: &[PureAttribute]) -> bool {
        attrs.iter().any(|attr| {
            attr.path == "derive"
                && matches!(&attr.meta, PureAttrMeta::List(args) if args.contains("Default"))
        })
    }

    /// Check if manual Default impl exists
    fn has_manual_default_impl(file: &PureFile, struct_name: &str) -> bool {
        file.items.iter().any(|item| {
            if let PureItem::Impl(imp) = item {
                imp.self_ty == struct_name && imp.trait_.as_deref() == Some("Default")
            } else {
                false
            }
        })
    }
}

impl Mutation for DefaultMutation {
    fn describe(&self) -> String {
        if self.use_derive {
            "Add #[derive(Default)] to structs".to_string()
        } else {
            "Generate impl Default for structs".to_string()
        }
    }

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

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

impl Detect for DefaultMutation {
    fn detect(&self, file: &PureFile) -> Vec<DetectOpportunity> {
        let mut opportunities = Vec::new();

        for item in &file.items {
            if let PureItem::Struct(s) = item {
                if let Some(ref target) = self.target_struct {
                    if &s.name != target {
                        continue;
                    }
                }

                // Skip if already has Default
                if Self::has_derive_default(&s.attrs) {
                    continue;
                }
                if Self::has_manual_default_impl(file, &s.name) {
                    continue;
                }

                // Only named-field structs
                if Self::get_named_fields(&s.fields).is_some() {
                    opportunities.push(
                        DetectOpportunity::new(
                            DetectLocation::struct_item(&s.name),
                            format!("Add Default implementation for '{}'", s.name),
                        )
                        .with_operations(vec![DetectOperation::Generate])
                        .with_confidence(0.8),
                    );
                }
            }
        }

        opportunities
    }

    fn category(&self) -> DetectCategory {
        DetectCategory::Creational
    }

    fn detect_name(&self) -> &'static str {
        "Default"
    }

    fn detect_description(&self) -> &str {
        "Add Default implementation to structs"
    }
}

// ============================================================================
// DeriveDefaultMutation: Convert manual impl to derive
// ============================================================================

/// Convert manual Default implementations to #[derive(Default)]
///
/// This mutation:
/// 1. Finds `impl Default for T` blocks
/// 2. Checks if all field initializations use default values
/// 3. Removes the impl block
/// 4. Adds `#[derive(Default)]` to the struct
///
/// # Example
///
/// ```rust,ignore
/// use ryo_mutations::idiom::DeriveDefaultMutation;
///
/// let mutation = DeriveDefaultMutation::new();
/// let result = mutation.apply(&mut file);
/// ```
#[derive(Debug, Clone, Default)]
pub struct DeriveDefaultMutation {
    /// Only apply to specific type
    pub target_type: Option<String>,
}

impl DeriveDefaultMutation {
    pub fn new() -> Self {
        Self::default()
    }

    /// Only apply to a specific type
    pub fn for_type(mut self, type_name: impl Into<String>) -> Self {
        self.target_type = Some(type_name.into());
        self
    }

    /// Check if an impl block is a Default impl
    fn is_default_impl(imp: &PureImpl) -> bool {
        imp.trait_.as_deref() == Some("Default")
    }

    /// Check if Default impl is derivable (all fields use default values)
    fn is_derivable_default(imp: &PureImpl) -> Option<String> {
        // Find the default() method
        let default_fn = imp.items.iter().find_map(|item| {
            if let PureImplItem::Fn(f) = item {
                if f.name == "default" {
                    return Some(f);
                }
            }
            None
        })?;

        // Check the function body - should be a single expression returning Self { ... }
        if default_fn.body.stmts.len() != 1 {
            return None;
        }

        let expr = match &default_fn.body.stmts[0] {
            PureStmt::Expr(e) => e,
            _ => return None,
        };

        // Should be a struct literal Self { field: value, ... }
        let fields = match expr {
            PureExpr::Struct { path, fields } => {
                if path != "Self" && path != &imp.self_ty {
                    return None;
                }
                fields
            }
            _ => return None,
        };

        // Check all field values are defaults
        for (_, value) in fields {
            if !Self::is_default_value(value) {
                return None;
            }
        }

        Some(imp.self_ty.clone())
    }

    /// Check if an expression is a default value
    fn is_default_value(expr: &PureExpr) -> bool {
        match expr {
            PureExpr::Lit(lit) => Self::is_zero_literal(lit),
            PureExpr::Path(p) => p == "None",
            PureExpr::Call { func, args } => {
                if args.is_empty() {
                    if let PureExpr::Path(p) = func.as_ref() {
                        return matches!(
                            p.as_str(),
                            "Default::default"
                                | "Vec::new"
                                | "String::new"
                                | "HashMap::new"
                                | "HashSet::new"
                                | "BTreeMap::new"
                                | "BTreeSet::new"
                                | "PathBuf::new"
                                | "OsString::new"
                        );
                    }
                }
                false
            }
            PureExpr::MethodCall { method, args, .. } => {
                (method == "default" || method == "new") && args.is_empty()
            }
            _ => false,
        }
    }

    /// Check if a literal is "zero" or "empty"
    fn is_zero_literal(lit: &str) -> bool {
        matches!(
            lit.trim(),
            "0" | "0i8"
                | "0i16"
                | "0i32"
                | "0i64"
                | "0i128"
                | "0isize"
                | "0u8"
                | "0u16"
                | "0u32"
                | "0u64"
                | "0u128"
                | "0usize"
                | "0.0"
                | "0.0f32"
                | "0.0f64"
                | "false"
                | "'\\0'"
                | "\"\""
        )
    }
}

impl Mutation for DeriveDefaultMutation {
    fn describe(&self) -> String {
        "Convert manual Default implementations to #[derive(Default)]".to_string()
    }

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

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

impl Detect for DeriveDefaultMutation {
    fn detect(&self, file: &PureFile) -> Vec<DetectOpportunity> {
        let mut opportunities = Vec::new();

        for item in &file.items {
            if let PureItem::Impl(imp) = item {
                if Self::is_default_impl(imp) {
                    if let Some(ref target) = self.target_type {
                        if &imp.self_ty != target {
                            continue;
                        }
                    }

                    if Self::is_derivable_default(imp).is_some() {
                        opportunities.push(
                            DetectOpportunity::new(
                                DetectLocation::impl_item(&imp.self_ty),
                                format!(
                                    "Convert manual Default impl for '{}' to #[derive(Default)]",
                                    imp.self_ty
                                ),
                            )
                            .with_operations(vec![DetectOperation::Refactor])
                            .with_confidence(0.95),
                        );
                    }
                }
            }
        }

        opportunities
    }

    fn category(&self) -> DetectCategory {
        DetectCategory::Creational
    }

    fn detect_name(&self) -> &'static str {
        "DeriveDefault"
    }

    fn detect_description(&self) -> &str {
        "Convert manual Default implementations to #[derive(Default)]"
    }
}