ryo-mutations 0.1.0

[experimental] Code transformation primitives for Rust source code
Documentation
//! Detection trait and types for pattern/idiom detection
//!
//! Provides a unified interface for detecting transformation opportunities
//! in Rust source code.

use ryo_source::pure::PureFile;
use serde::{Deserialize, Serialize};

/// Category for detected opportunities
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum DetectCategory {
    /// Creational patterns: Builder, Default, Factory
    Creational,
    /// Structural patterns: From/Into, Adapter
    Structural,
    /// Behavioural patterns: Strategy, Observer
    Behavioural,
    /// Performance patterns: Atomic, RwLock
    Performance,
    /// Safety patterns: LockScope, etc.
    Safety,
    /// Clippy-style lints: bool_comparison, redundant_closure
    Clippy,
    /// Refactoring: extract, inline, organize
    Refactor,
}

impl DetectCategory {
    pub fn as_str(&self) -> &'static str {
        match self {
            DetectCategory::Creational => "creational",
            DetectCategory::Structural => "structural",
            DetectCategory::Behavioural => "behavioural",
            DetectCategory::Performance => "performance",
            DetectCategory::Safety => "safety",
            DetectCategory::Clippy => "clippy",
            DetectCategory::Refactor => "refactor",
        }
    }

    /// Short code for display (C/S/B/P/Y/L/R)
    pub fn short_code(&self) -> &'static str {
        match self {
            DetectCategory::Creational => "C",
            DetectCategory::Structural => "S",
            DetectCategory::Behavioural => "B",
            DetectCategory::Performance => "P",
            DetectCategory::Safety => "Y",
            DetectCategory::Clippy => "L",
            DetectCategory::Refactor => "R",
        }
    }
}

/// Location in the AST where an opportunity was detected
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DetectLocation {
    /// Item kind (struct, impl, fn, etc.)
    pub item_kind: String,
    /// Item name
    pub item_name: String,
    /// Optional symbol path (e.g., "test_crate::config::Config")
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub symbol_path: Option<String>,
}

impl DetectLocation {
    pub fn new(kind: impl Into<String>, name: impl Into<String>) -> Self {
        Self {
            item_kind: kind.into(),
            item_name: name.into(),
            symbol_path: None,
        }
    }

    pub fn struct_item(name: impl Into<String>) -> Self {
        Self::new("struct", name)
    }

    pub fn impl_item(name: impl Into<String>) -> Self {
        Self::new("impl", name)
    }

    pub fn fn_item(name: impl Into<String>) -> Self {
        Self::new("fn", name)
    }

    pub fn with_symbol_path(mut self, path: impl Into<String>) -> Self {
        self.symbol_path = Some(path.into());
        self
    }
}

/// Available operations for a detected opportunity
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum DetectOperation {
    /// Generate new code
    Generate,
    /// Refactor existing code
    Refactor,
}

/// A detected opportunity for transformation
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DetectOpportunity {
    /// Where the opportunity was detected
    pub location: DetectLocation,
    /// What operations are available
    pub operations: Vec<DetectOperation>,
    /// Human-readable suggestion
    pub suggestion: String,
    /// Confidence score (0.0 - 1.0)
    pub confidence: f32,
    /// Additional context (mutation-specific data)
    pub context: Option<String>,
}

impl DetectOpportunity {
    pub fn new(location: DetectLocation, suggestion: impl Into<String>) -> Self {
        Self {
            location,
            operations: vec![DetectOperation::Generate],
            suggestion: suggestion.into(),
            confidence: 1.0,
            context: None,
        }
    }

    pub fn with_operations(mut self, ops: Vec<DetectOperation>) -> Self {
        self.operations = ops;
        self
    }

    pub fn with_confidence(mut self, confidence: f32) -> Self {
        self.confidence = confidence.clamp(0.0, 1.0);
        self
    }

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

    pub fn can_generate(&self) -> bool {
        self.operations.contains(&DetectOperation::Generate)
    }

    pub fn can_refactor(&self) -> bool {
        self.operations.contains(&DetectOperation::Refactor)
    }
}

/// Trait for detecting transformation opportunities
///
/// Mutations can optionally implement this trait to provide detection
/// capabilities for suggestion engines.
pub trait Detect: Send + Sync {
    /// Detect opportunities in a file
    fn detect(&self, file: &PureFile) -> Vec<DetectOpportunity>;

    /// Get the category for detected opportunities
    fn category(&self) -> DetectCategory;

    /// Get the mutation/pattern name
    fn detect_name(&self) -> &'static str;

    /// Get description
    fn detect_description(&self) -> &str {
        ""
    }
}

/// Registry of detectable mutations
#[derive(Default)]
pub struct DetectRegistry {
    detectors: Vec<Box<dyn Detect>>,
}

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

    /// Register a detector
    pub fn register<D: Detect + 'static>(&mut self, detector: D) {
        self.detectors.push(Box::new(detector));
    }

    /// Get all registered detectors
    pub fn all(&self) -> &[Box<dyn Detect>] {
        &self.detectors
    }

    /// Find detector by name
    pub fn find(&self, name: &str) -> Option<&dyn Detect> {
        self.detectors
            .iter()
            .find(|d| d.detect_name().eq_ignore_ascii_case(name))
            .map(|d| d.as_ref())
    }

    /// Get detectors by category
    pub fn by_category(&self, category: DetectCategory) -> Vec<&dyn Detect> {
        self.detectors
            .iter()
            .filter(|d| d.category() == category)
            .map(|d| d.as_ref())
            .collect()
    }

    /// Detect all opportunities in a file
    pub fn detect_all(&self, file: &PureFile) -> Vec<(&dyn Detect, DetectOpportunity)> {
        self.detectors
            .iter()
            .flat_map(|d| d.detect(file).into_iter().map(move |opp| (d.as_ref(), opp)))
            .collect()
    }
}

/// Create a registry with all built-in detectors
pub fn create_default_registry() -> DetectRegistry {
    use super::{
        DefaultMutation, DeriveDefaultMutation, LockScopeMutation, UseAtomicMutation,
        UseRwLockMutation,
    };

    let mut registry = DetectRegistry::new();

    // Tier 3: Code generation
    registry.register(DefaultMutation::new());
    registry.register(DeriveDefaultMutation::new());

    // Tier 4: Performance/Safety
    registry.register(UseAtomicMutation::new());
    registry.register(UseRwLockMutation::new());
    registry.register(LockScopeMutation::new());

    registry
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_detect_location() {
        let loc = DetectLocation::struct_item("Config");
        assert_eq!(loc.item_kind, "struct");
        assert_eq!(loc.item_name, "Config");
    }

    #[test]
    fn test_detect_opportunity() {
        let opp = DetectOpportunity::new(
            DetectLocation::struct_item("Config"),
            "Generate Builder pattern",
        )
        .with_operations(vec![DetectOperation::Generate])
        .with_confidence(0.9);

        assert!(opp.can_generate());
        assert!(!opp.can_refactor());
        assert_eq!(opp.confidence, 0.9);
    }

    #[test]
    fn test_detect_category() {
        assert_eq!(DetectCategory::Creational.as_str(), "creational");
        assert_eq!(DetectCategory::Clippy.as_str(), "clippy");
    }

    /// Ensures all Detect implementations are registered in create_default_registry().
    /// This test prevents the "forgot to register" bug.
    #[test]
    fn test_all_detectors_are_registered() {
        use super::super::{
            DefaultMutation, DeriveDefaultMutation, LockScopeMutation, UseAtomicMutation,
            UseRwLockMutation,
        };
        use std::collections::HashSet;

        let registry = create_default_registry();
        let registered_names: HashSet<&str> =
            registry.all().iter().map(|d| d.detect_name()).collect();

        // All Detect implementations must be registered
        let expected_detectors: Vec<(&str, Box<dyn Detect>)> = vec![
            ("DefaultMutation", Box::new(DefaultMutation::new())),
            (
                "DeriveDefaultMutation",
                Box::new(DeriveDefaultMutation::new()),
            ),
            ("UseAtomicMutation", Box::new(UseAtomicMutation::new())),
            ("UseRwLockMutation", Box::new(UseRwLockMutation::new())),
            ("LockScopeMutation", Box::new(LockScopeMutation::new())),
        ];

        for (label, detector) in expected_detectors {
            let name = detector.detect_name();
            assert!(
                registered_names.contains(name),
                "{} (detect_name='{}') is NOT registered in create_default_registry(). \
                Add: registry.register({}::new());",
                label,
                name,
                label
            );
        }

        // Verify count matches (removed BuilderMutation - now Intent-based)
        assert_eq!(
            registry.all().len(),
            5,
            "Expected 5 detectors registered, found {}",
            registry.all().len()
        );
    }
}