ryo-suggest 0.1.0

[experimental] Pattern-based suggestion engine for RYO
Documentation
//! Lint rules implemented as Suggest patterns
//!
//! This module provides code quality checks that integrate with the Suggest framework.
//! Lint rules detect violations and report them as SuggestOpportunity with `SuggestCategory::Lint`.
//!
//! # Architecture
//!
//! ```text
//! ┌─────────────────────────────────────────────────────────────┐
//! │  Lint as Suggest                                            │
//! │  ─────────────────                                          │
//! │  LintSuggest trait extends Suggest                          │
//! │    ├─ code() → Rule code (e.g., "RL001")                    │
//! │    ├─ default_severity() → LintSeverity                     │
//! │    └─ detect() → SuggestOpportunity with Lint context       │
//! └─────────────────────────────────────────────────────────────┘
//! ```
//!
//! # Available Rules
//!
//! - [`RequireTestForMutation`] - Ensures mutation implementations have tests

mod require_test_for_mutation;

pub use require_test_for_mutation::RequireTestForMutation;

use crate::{
    LintSeverity, OpportunityContext, OpportunityId, Suggest, SuggestCategory, SuggestLocation,
    SuggestOpportunity,
};
use ryo_analysis::SymbolId;

/// Lint diagnostic details: suggestion, expected value, and actual value.
pub struct LintDetails {
    /// Fix suggestion text shown to the user
    pub suggestion: Option<String>,
    /// Expected code pattern
    pub expected: Option<String>,
    /// Actual code found
    pub actual: Option<String>,
}

/// Extension trait for lint-specific functionality
pub trait LintSuggest: Suggest {
    /// Returns the rule code (e.g., "RL001")
    fn code(&self) -> &'static str;

    /// Returns the default severity for this lint rule
    fn default_severity(&self) -> LintSeverity;

    /// Helper to create a lint opportunity
    fn create_lint_opportunity(
        &self,
        id: OpportunityId,
        targets: Vec<SymbolId>,
        location: SuggestLocation,
        message: impl Into<String>,
        details: LintDetails,
    ) -> SuggestOpportunity {
        SuggestOpportunity::new(
            id,
            targets,
            location,
            message,
            1.0, // Lint violations have 100% confidence
            OpportunityContext::Lint {
                code: self.code().to_string(),
                rule: self.name().to_string(),
                severity: self.default_severity(),
                suggestion: details.suggestion,
                expected: details.expected,
                actual: details.actual,
            },
        )
    }
}

/// Helper to check if this Suggest is a lint rule
pub fn is_lint_suggest(suggest: &dyn Suggest) -> bool {
    suggest.category() == SuggestCategory::Lint
}

// ========== Output Formatters ==========

/// Format a lint opportunity as Clippy-compatible output
///
/// Format: `file (symbol_path): SEVERITY [CODE] message`
pub fn format_clippy(opp: &SuggestOpportunity) -> String {
    match &opp.context {
        OpportunityContext::Lint {
            code,
            severity,
            suggestion,
            ..
        } => {
            let mut output = format!(
                "{} ({}): {} [{}] {}\n",
                opp.location.file, opp.location.symbol_path, severity, code, opp.message
            );

            if let Some(sugg) = suggestion {
                output.push_str(&format!("  = help: {}\n", sugg));
            }

            output
        }
        _ => format!("{}: {}\n", opp.location, opp.message),
    }
}

/// Format multiple lint opportunities as Clippy-compatible output
pub fn format_clippy_all(opportunities: &[SuggestOpportunity]) -> String {
    let mut output = String::new();

    for opp in opportunities {
        output.push_str(&format_clippy(opp));
    }

    // Summary
    let (errors, warnings, infos) = count_by_severity(opportunities);
    output.push_str(&format!(
        "\nFound {} error(s), {} warning(s), {} info(s)\n",
        errors, warnings, infos
    ));

    output
}

/// Format a lint opportunity as JSON
pub fn format_json(opp: &SuggestOpportunity) -> String {
    serde_json::to_string(opp).unwrap_or_else(|_| "{}".to_string())
}

/// Format multiple lint opportunities as JSON array
pub fn format_json_all(opportunities: &[SuggestOpportunity]) -> String {
    serde_json::to_string_pretty(opportunities).unwrap_or_else(|_| "[]".to_string())
}

/// Count opportunities by severity
pub fn count_by_severity(opportunities: &[SuggestOpportunity]) -> (usize, usize, usize) {
    let mut errors = 0;
    let mut warnings = 0;
    let mut infos = 0;

    for opp in opportunities {
        if let OpportunityContext::Lint { severity, .. } = &opp.context {
            match severity {
                LintSeverity::Error => errors += 1,
                LintSeverity::Warning => warnings += 1,
                LintSeverity::Info => infos += 1,
            }
        }
    }

    (errors, warnings, infos)
}

/// Check if any opportunity is an error
pub fn has_errors(opportunities: &[SuggestOpportunity]) -> bool {
    opportunities.iter().any(|opp| {
        matches!(
            &opp.context,
            OpportunityContext::Lint {
                severity: LintSeverity::Error,
                ..
            }
        )
    })
}

/// Lint result summary
#[derive(Debug, Clone, Default)]
pub struct LintResult {
    /// All violations found
    pub violations: Vec<SuggestOpportunity>,
    /// Number of files checked (if tracked)
    pub files_checked: usize,
}

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

    /// Check if there are any errors
    pub fn has_errors(&self) -> bool {
        has_errors(&self.violations)
    }

    /// Count by severity
    pub fn count_by_severity(&self) -> (usize, usize, usize) {
        count_by_severity(&self.violations)
    }

    /// Format as Clippy output
    pub fn format_clippy(&self) -> String {
        format_clippy_all(&self.violations)
    }

    /// Format as JSON
    pub fn format_json(&self) -> String {
        format_json_all(&self.violations)
    }
}