Skip to main content

ryo_mutations/clippy/
runner.rs

1//! Clippy runner for collecting diagnostics
2
3use super::{ClippyDiagnostic, LintCategory};
4use std::path::Path;
5use std::process::Command;
6
7/// Paired diagnostic with its corresponding mutation
8pub type DiagnosticMutationPair = (ClippyDiagnostic, Box<dyn crate::Mutation>);
9
10/// Configuration for running Clippy
11#[derive(Debug, Clone)]
12pub struct ClippyConfig {
13    /// Only include MachineApplicable suggestions
14    pub machine_applicable_only: bool,
15    /// Filter by lint categories
16    pub categories: Vec<LintCategory>,
17    /// Specific lints to include (empty = all)
18    pub include_lints: Vec<String>,
19    /// Specific lints to exclude
20    pub exclude_lints: Vec<String>,
21    /// Additional clippy arguments
22    pub extra_args: Vec<String>,
23}
24
25impl Default for ClippyConfig {
26    fn default() -> Self {
27        Self {
28            machine_applicable_only: true,
29            categories: Vec::new(),
30            include_lints: Vec::new(),
31            exclude_lints: Vec::new(),
32            extra_args: Vec::new(),
33        }
34    }
35}
36
37impl ClippyConfig {
38    /// Create a new config with default settings
39    pub fn new() -> Self {
40        Self::default()
41    }
42
43    /// Only include MachineApplicable suggestions
44    pub fn machine_applicable_only(mut self) -> Self {
45        self.machine_applicable_only = true;
46        self
47    }
48
49    /// Include all suggestions regardless of applicability
50    pub fn all_applicabilities(mut self) -> Self {
51        self.machine_applicable_only = false;
52        self
53    }
54
55    /// Filter by category
56    pub fn with_category(mut self, category: LintCategory) -> Self {
57        self.categories.push(category);
58        self
59    }
60
61    /// Include specific lint
62    pub fn with_lint(mut self, lint: impl Into<String>) -> Self {
63        self.include_lints.push(lint.into());
64        self
65    }
66
67    /// Exclude specific lint
68    pub fn without_lint(mut self, lint: impl Into<String>) -> Self {
69        self.exclude_lints.push(lint.into());
70        self
71    }
72
73    /// Add extra clippy arguments
74    pub fn with_arg(mut self, arg: impl Into<String>) -> Self {
75        self.extra_args.push(arg.into());
76        self
77    }
78
79    /// Build clippy command arguments
80    fn build_args(&self) -> Vec<String> {
81        let mut args = vec![
82            "clippy".to_string(),
83            "--message-format=json".to_string(),
84            "--".to_string(),
85        ];
86
87        // Add category warnings
88        for category in &self.categories {
89            args.push(format!("-W clippy::{}", category.as_str()));
90        }
91
92        // Add specific lint allows/warns
93        for lint in &self.include_lints {
94            let lint_name = if lint.starts_with("clippy::") {
95                lint.clone()
96            } else {
97                format!("clippy::{}", lint)
98            };
99            args.push(format!("-W {}", lint_name));
100        }
101
102        for lint in &self.exclude_lints {
103            let lint_name = if lint.starts_with("clippy::") {
104                lint.clone()
105            } else {
106                format!("clippy::{}", lint)
107            };
108            args.push(format!("-A {}", lint_name));
109        }
110
111        args.extend(self.extra_args.clone());
112        args
113    }
114}
115
116/// Runner for executing Clippy and collecting diagnostics
117#[derive(Debug)]
118pub struct ClippyRunner {
119    config: ClippyConfig,
120}
121
122impl ClippyRunner {
123    /// Create a new runner with the given config
124    pub fn new(config: ClippyConfig) -> Self {
125        Self { config }
126    }
127
128    /// Create a runner with default config
129    pub fn default_runner() -> Self {
130        Self::new(ClippyConfig::default())
131    }
132
133    /// Run Clippy on the given directory and collect diagnostics
134    ///
135    /// # Example
136    ///
137    /// ```rust,ignore
138    /// let runner = ClippyRunner::default_runner();
139    /// let diagnostics = runner.run(".")?;
140    ///
141    /// for diag in &diagnostics {
142    ///     println!("{}: {}", diag.lint_name, diag.message);
143    /// }
144    /// ```
145    pub fn run(&self, path: impl AsRef<Path>) -> Result<Vec<ClippyDiagnostic>, ClippyError> {
146        let path = path.as_ref();
147
148        let output = Command::new("cargo")
149            .args(self.config.build_args())
150            .current_dir(path)
151            .output()
152            .map_err(|e| ClippyError::IoError(e.to_string()))?;
153
154        // Clippy outputs JSON to stdout, diagnostic messages to stderr
155        let _stderr = String::from_utf8_lossy(&output.stderr);
156        let stdout = String::from_utf8_lossy(&output.stdout);
157
158        // Parse JSON messages from stdout
159        let mut diagnostics = super::diagnostic::parse_clippy_output(&stdout)
160            .map_err(|e| ClippyError::ParseError(e.to_string()))?;
161
162        // Filter by applicability if configured
163        if self.config.machine_applicable_only {
164            diagnostics.retain(|d| d.has_auto_fix());
165        }
166
167        Ok(diagnostics)
168    }
169
170    /// Run Clippy and convert diagnostics to Mutations
171    ///
172    /// Returns only diagnostics that have corresponding Mutation implementations.
173    pub fn run_to_mutations(
174        &self,
175        path: impl AsRef<Path>,
176    ) -> Result<Vec<DiagnosticMutationPair>, ClippyError> {
177        let diagnostics = self.run(path)?;
178
179        let mutations: Vec<_> = diagnostics
180            .into_iter()
181            .filter_map(|d| {
182                let mutation = d.to_mutation()?;
183                Some((d, mutation))
184            })
185            .collect();
186
187        Ok(mutations)
188    }
189}
190
191/// Errors from Clippy operations
192#[derive(Debug, Clone)]
193pub enum ClippyError {
194    /// IO error (e.g., cargo not found)
195    IoError(String),
196    /// Failed to parse Clippy output
197    ParseError(String),
198    /// Clippy exited with error
199    ClippyFailed(String),
200}
201
202impl std::fmt::Display for ClippyError {
203    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
204        match self {
205            ClippyError::IoError(msg) => write!(f, "IO error: {}", msg),
206            ClippyError::ParseError(msg) => write!(f, "Parse error: {}", msg),
207            ClippyError::ClippyFailed(msg) => write!(f, "Clippy failed: {}", msg),
208        }
209    }
210}
211
212impl std::error::Error for ClippyError {}
213
214#[cfg(test)]
215mod tests {
216    use super::*;
217
218    #[test]
219    fn test_config_build_args() {
220        let config = ClippyConfig::new()
221            .with_category(LintCategory::Style)
222            .with_lint("bool_comparison")
223            .without_lint("clippy::too_many_arguments");
224
225        let args = config.build_args();
226        assert!(args.contains(&"clippy".to_string()));
227        assert!(args.contains(&"--message-format=json".to_string()));
228        assert!(args.iter().any(|a| a.contains("style")));
229        assert!(args.iter().any(|a| a.contains("bool_comparison")));
230    }
231
232    #[test]
233    fn test_config_machine_applicable() {
234        let config = ClippyConfig::new().machine_applicable_only();
235        assert!(config.machine_applicable_only);
236
237        let config2 = config.all_applicabilities();
238        assert!(!config2.machine_applicable_only);
239    }
240}