ryo-mutations 0.1.0

[experimental] Code transformation primitives for Rust source code
Documentation
//! Clippy runner for collecting diagnostics

use super::{ClippyDiagnostic, LintCategory};
use std::path::Path;
use std::process::Command;

/// Paired diagnostic with its corresponding mutation
pub type DiagnosticMutationPair = (ClippyDiagnostic, Box<dyn crate::Mutation>);

/// Configuration for running Clippy
#[derive(Debug, Clone)]
pub struct ClippyConfig {
    /// Only include MachineApplicable suggestions
    pub machine_applicable_only: bool,
    /// Filter by lint categories
    pub categories: Vec<LintCategory>,
    /// Specific lints to include (empty = all)
    pub include_lints: Vec<String>,
    /// Specific lints to exclude
    pub exclude_lints: Vec<String>,
    /// Additional clippy arguments
    pub extra_args: Vec<String>,
}

impl Default for ClippyConfig {
    fn default() -> Self {
        Self {
            machine_applicable_only: true,
            categories: Vec::new(),
            include_lints: Vec::new(),
            exclude_lints: Vec::new(),
            extra_args: Vec::new(),
        }
    }
}

impl ClippyConfig {
    /// Create a new config with default settings
    pub fn new() -> Self {
        Self::default()
    }

    /// Only include MachineApplicable suggestions
    pub fn machine_applicable_only(mut self) -> Self {
        self.machine_applicable_only = true;
        self
    }

    /// Include all suggestions regardless of applicability
    pub fn all_applicabilities(mut self) -> Self {
        self.machine_applicable_only = false;
        self
    }

    /// Filter by category
    pub fn with_category(mut self, category: LintCategory) -> Self {
        self.categories.push(category);
        self
    }

    /// Include specific lint
    pub fn with_lint(mut self, lint: impl Into<String>) -> Self {
        self.include_lints.push(lint.into());
        self
    }

    /// Exclude specific lint
    pub fn without_lint(mut self, lint: impl Into<String>) -> Self {
        self.exclude_lints.push(lint.into());
        self
    }

    /// Add extra clippy arguments
    pub fn with_arg(mut self, arg: impl Into<String>) -> Self {
        self.extra_args.push(arg.into());
        self
    }

    /// Build clippy command arguments
    fn build_args(&self) -> Vec<String> {
        let mut args = vec![
            "clippy".to_string(),
            "--message-format=json".to_string(),
            "--".to_string(),
        ];

        // Add category warnings
        for category in &self.categories {
            args.push(format!("-W clippy::{}", category.as_str()));
        }

        // Add specific lint allows/warns
        for lint in &self.include_lints {
            let lint_name = if lint.starts_with("clippy::") {
                lint.clone()
            } else {
                format!("clippy::{}", lint)
            };
            args.push(format!("-W {}", lint_name));
        }

        for lint in &self.exclude_lints {
            let lint_name = if lint.starts_with("clippy::") {
                lint.clone()
            } else {
                format!("clippy::{}", lint)
            };
            args.push(format!("-A {}", lint_name));
        }

        args.extend(self.extra_args.clone());
        args
    }
}

/// Runner for executing Clippy and collecting diagnostics
#[derive(Debug)]
pub struct ClippyRunner {
    config: ClippyConfig,
}

impl ClippyRunner {
    /// Create a new runner with the given config
    pub fn new(config: ClippyConfig) -> Self {
        Self { config }
    }

    /// Create a runner with default config
    pub fn default_runner() -> Self {
        Self::new(ClippyConfig::default())
    }

    /// Run Clippy on the given directory and collect diagnostics
    ///
    /// # Example
    ///
    /// ```rust,ignore
    /// let runner = ClippyRunner::default_runner();
    /// let diagnostics = runner.run(".")?;
    ///
    /// for diag in &diagnostics {
    ///     println!("{}: {}", diag.lint_name, diag.message);
    /// }
    /// ```
    pub fn run(&self, path: impl AsRef<Path>) -> Result<Vec<ClippyDiagnostic>, ClippyError> {
        let path = path.as_ref();

        let output = Command::new("cargo")
            .args(self.config.build_args())
            .current_dir(path)
            .output()
            .map_err(|e| ClippyError::IoError(e.to_string()))?;

        // Clippy outputs JSON to stdout, diagnostic messages to stderr
        let _stderr = String::from_utf8_lossy(&output.stderr);
        let stdout = String::from_utf8_lossy(&output.stdout);

        // Parse JSON messages from stdout
        let mut diagnostics = super::diagnostic::parse_clippy_output(&stdout)
            .map_err(|e| ClippyError::ParseError(e.to_string()))?;

        // Filter by applicability if configured
        if self.config.machine_applicable_only {
            diagnostics.retain(|d| d.has_auto_fix());
        }

        Ok(diagnostics)
    }

    /// Run Clippy and convert diagnostics to Mutations
    ///
    /// Returns only diagnostics that have corresponding Mutation implementations.
    pub fn run_to_mutations(
        &self,
        path: impl AsRef<Path>,
    ) -> Result<Vec<DiagnosticMutationPair>, ClippyError> {
        let diagnostics = self.run(path)?;

        let mutations: Vec<_> = diagnostics
            .into_iter()
            .filter_map(|d| {
                let mutation = d.to_mutation()?;
                Some((d, mutation))
            })
            .collect();

        Ok(mutations)
    }
}

/// Errors from Clippy operations
#[derive(Debug, Clone)]
pub enum ClippyError {
    /// IO error (e.g., cargo not found)
    IoError(String),
    /// Failed to parse Clippy output
    ParseError(String),
    /// Clippy exited with error
    ClippyFailed(String),
}

impl std::fmt::Display for ClippyError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            ClippyError::IoError(msg) => write!(f, "IO error: {}", msg),
            ClippyError::ParseError(msg) => write!(f, "Parse error: {}", msg),
            ClippyError::ClippyFailed(msg) => write!(f, "Clippy failed: {}", msg),
        }
    }
}

impl std::error::Error for ClippyError {}

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

    #[test]
    fn test_config_build_args() {
        let config = ClippyConfig::new()
            .with_category(LintCategory::Style)
            .with_lint("bool_comparison")
            .without_lint("clippy::too_many_arguments");

        let args = config.build_args();
        assert!(args.contains(&"clippy".to_string()));
        assert!(args.contains(&"--message-format=json".to_string()));
        assert!(args.iter().any(|a| a.contains("style")));
        assert!(args.iter().any(|a| a.contains("bool_comparison")));
    }

    #[test]
    fn test_config_machine_applicable() {
        let config = ClippyConfig::new().machine_applicable_only();
        assert!(config.machine_applicable_only);

        let config2 = config.all_applicabilities();
        assert!(!config2.machine_applicable_only);
    }
}