ryo_mutations/clippy/
runner.rs1use super::{ClippyDiagnostic, LintCategory};
4use std::path::Path;
5use std::process::Command;
6
7pub type DiagnosticMutationPair = (ClippyDiagnostic, Box<dyn crate::Mutation>);
9
10#[derive(Debug, Clone)]
12pub struct ClippyConfig {
13 pub machine_applicable_only: bool,
15 pub categories: Vec<LintCategory>,
17 pub include_lints: Vec<String>,
19 pub exclude_lints: Vec<String>,
21 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 pub fn new() -> Self {
40 Self::default()
41 }
42
43 pub fn machine_applicable_only(mut self) -> Self {
45 self.machine_applicable_only = true;
46 self
47 }
48
49 pub fn all_applicabilities(mut self) -> Self {
51 self.machine_applicable_only = false;
52 self
53 }
54
55 pub fn with_category(mut self, category: LintCategory) -> Self {
57 self.categories.push(category);
58 self
59 }
60
61 pub fn with_lint(mut self, lint: impl Into<String>) -> Self {
63 self.include_lints.push(lint.into());
64 self
65 }
66
67 pub fn without_lint(mut self, lint: impl Into<String>) -> Self {
69 self.exclude_lints.push(lint.into());
70 self
71 }
72
73 pub fn with_arg(mut self, arg: impl Into<String>) -> Self {
75 self.extra_args.push(arg.into());
76 self
77 }
78
79 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 for category in &self.categories {
89 args.push(format!("-W clippy::{}", category.as_str()));
90 }
91
92 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#[derive(Debug)]
118pub struct ClippyRunner {
119 config: ClippyConfig,
120}
121
122impl ClippyRunner {
123 pub fn new(config: ClippyConfig) -> Self {
125 Self { config }
126 }
127
128 pub fn default_runner() -> Self {
130 Self::new(ClippyConfig::default())
131 }
132
133 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 let _stderr = String::from_utf8_lossy(&output.stderr);
156 let stdout = String::from_utf8_lossy(&output.stdout);
157
158 let mut diagnostics = super::diagnostic::parse_clippy_output(&stdout)
160 .map_err(|e| ClippyError::ParseError(e.to_string()))?;
161
162 if self.config.machine_applicable_only {
164 diagnostics.retain(|d| d.has_auto_fix());
165 }
166
167 Ok(diagnostics)
168 }
169
170 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#[derive(Debug, Clone)]
193pub enum ClippyError {
194 IoError(String),
196 ParseError(String),
198 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}