1use serde::{Deserialize, Serialize};
4use std::path::PathBuf;
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
11#[serde(rename_all = "snake_case")]
12#[derive(Default)]
13pub enum Applicability {
14 MachineApplicable,
16 MaybeIncorrect,
18 HasPlaceholders,
20 #[default]
22 Unspecified,
23}
24
25impl Applicability {
26 pub fn is_auto_applicable(&self) -> bool {
28 matches!(self, Applicability::MachineApplicable)
29 }
30}
31
32#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
36#[serde(rename_all = "lowercase")]
37pub enum LintCategory {
38 Correctness,
40 Suspicious,
42 Complexity,
44 Perf,
46 Style,
48 Pedantic,
50 Restriction,
52 Cargo,
54 Nursery,
56}
57
58impl LintCategory {
59 pub fn as_str(&self) -> &'static str {
61 match self {
62 LintCategory::Correctness => "correctness",
63 LintCategory::Suspicious => "suspicious",
64 LintCategory::Complexity => "complexity",
65 LintCategory::Perf => "perf",
66 LintCategory::Style => "style",
67 LintCategory::Pedantic => "pedantic",
68 LintCategory::Restriction => "restriction",
69 LintCategory::Cargo => "cargo",
70 LintCategory::Nursery => "nursery",
71 }
72 }
73
74 pub fn from_lint_name(_name: &str) -> Option<Self> {
76 None }
80}
81
82#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
84pub struct Span {
85 pub file_name: PathBuf,
87 pub byte_start: usize,
89 pub byte_end: usize,
91 pub line_start: usize,
93 pub line_end: usize,
95 pub column_start: usize,
97 pub column_end: usize,
99}
100
101impl Span {
102 pub fn from_bytes(file_name: impl Into<PathBuf>, start: usize, end: usize) -> Self {
104 Self {
105 file_name: file_name.into(),
106 byte_start: start,
107 byte_end: end,
108 line_start: 0,
109 line_end: 0,
110 column_start: 0,
111 column_end: 0,
112 }
113 }
114
115 pub fn byte_range(&self) -> std::ops::Range<usize> {
117 self.byte_start..self.byte_end
118 }
119}
120
121#[derive(Debug, Clone, Serialize, Deserialize)]
123pub struct Suggestion {
124 pub span: Span,
126 pub replacement: String,
128 pub applicability: Applicability,
130 pub message: String,
132}
133
134impl Suggestion {
135 pub fn new(span: Span, replacement: impl Into<String>) -> Self {
137 Self {
138 span,
139 replacement: replacement.into(),
140 applicability: Applicability::Unspecified,
141 message: String::new(),
142 }
143 }
144
145 pub fn with_applicability(mut self, applicability: Applicability) -> Self {
147 self.applicability = applicability;
148 self
149 }
150
151 pub fn with_message(mut self, message: impl Into<String>) -> Self {
153 self.message = message.into();
154 self
155 }
156}
157
158#[derive(Debug, Clone, Serialize, Deserialize)]
160pub struct ClippyDiagnostic {
161 pub lint_name: String,
163 pub level: DiagnosticLevel,
165 pub message: String,
167 pub span: Option<Span>,
169 pub suggestions: Vec<Suggestion>,
171 pub notes: Vec<String>,
173}
174
175#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
177#[serde(rename_all = "lowercase")]
178pub enum DiagnosticLevel {
179 Error,
180 Warning,
181 Note,
182 Help,
183}
184
185impl ClippyDiagnostic {
186 pub fn new(lint_name: impl Into<String>, message: impl Into<String>) -> Self {
188 Self {
189 lint_name: lint_name.into(),
190 level: DiagnosticLevel::Warning,
191 message: message.into(),
192 span: None,
193 suggestions: Vec::new(),
194 notes: Vec::new(),
195 }
196 }
197
198 pub fn short_lint_name(&self) -> &str {
200 self.lint_name
201 .strip_prefix("clippy::")
202 .unwrap_or(&self.lint_name)
203 }
204
205 pub fn has_auto_fix(&self) -> bool {
207 self.suggestions
208 .iter()
209 .any(|s| s.applicability.is_auto_applicable())
210 }
211
212 pub fn auto_fix(&self) -> Option<&Suggestion> {
214 self.suggestions
215 .iter()
216 .find(|s| s.applicability.is_auto_applicable())
217 }
218
219 pub fn to_mutation(&self) -> Option<Box<dyn crate::Mutation>> {
224 use super::lints;
225 use crate::idiom::*;
226
227 match self.lint_name.as_str() {
228 lints::BOOL_COMPARISON => Some(Box::new(BoolSimplifyMutation::new())),
229 lints::COLLAPSIBLE_IF => Some(Box::new(CollapsibleIfMutation::new())),
230 lints::COMPARISON_TO_EMPTY => Some(Box::new(ComparisonToMethodMutation::new())),
231 lints::ASSIGN_OP_PATTERN => Some(Box::new(AssignOpMutation::new())),
232 lints::CLONE_ON_COPY => Some(Box::new(CloneOnCopyMutation::new())),
233 lints::REDUNDANT_CLOSURE => Some(Box::new(RedundantClosureMutation::new())),
234 _ => None,
236 }
237 }
238}
239
240pub fn parse_clippy_output(json_lines: &str) -> Result<Vec<ClippyDiagnostic>, serde_json::Error> {
254 let mut diagnostics = Vec::new();
255
256 for line in json_lines.lines() {
257 if line.trim().is_empty() {
258 continue;
259 }
260
261 if let Ok(CargoMessage::CompilerMessage { message }) =
263 serde_json::from_str::<CargoMessage>(line)
264 {
265 if let Some(diag) = convert_compiler_message(message) {
266 diagnostics.push(diag);
267 }
268 }
269 }
270
271 Ok(diagnostics)
272}
273
274#[derive(Debug, Deserialize)]
276#[serde(tag = "reason")]
277enum CargoMessage {
278 #[serde(rename = "compiler-message")]
279 CompilerMessage { message: CompilerMessage },
280 #[serde(other)]
281 Other,
282}
283
284#[derive(Debug, Deserialize)]
286struct CompilerMessage {
287 code: Option<DiagnosticCode>,
288 level: String,
289 message: String,
290 spans: Vec<CompilerSpan>,
291 children: Vec<CompilerMessage>,
292}
293
294#[derive(Debug, Deserialize)]
295struct DiagnosticCode {
296 code: String,
297}
298
299#[derive(Debug, Deserialize)]
300struct CompilerSpan {
301 file_name: String,
302 byte_start: usize,
303 byte_end: usize,
304 line_start: usize,
305 line_end: usize,
306 column_start: usize,
307 column_end: usize,
308 is_primary: bool,
309 suggested_replacement: Option<String>,
310 suggestion_applicability: Option<String>,
311}
312
313fn convert_compiler_message(msg: CompilerMessage) -> Option<ClippyDiagnostic> {
314 let code = msg.code.as_ref()?;
316 if !code.code.starts_with("clippy::") {
317 return None;
318 }
319
320 let level = match msg.level.as_str() {
321 "error" => DiagnosticLevel::Error,
322 "warning" => DiagnosticLevel::Warning,
323 "note" => DiagnosticLevel::Note,
324 "help" => DiagnosticLevel::Help,
325 _ => DiagnosticLevel::Warning,
326 };
327
328 let primary_span = msg.spans.iter().find(|s| s.is_primary).map(|s| Span {
329 file_name: PathBuf::from(&s.file_name),
330 byte_start: s.byte_start,
331 byte_end: s.byte_end,
332 line_start: s.line_start,
333 line_end: s.line_end,
334 column_start: s.column_start,
335 column_end: s.column_end,
336 });
337
338 let mut suggestions = Vec::new();
340 for span in &msg.spans {
341 if let Some(ref replacement) = span.suggested_replacement {
342 let applicability = span
343 .suggestion_applicability
344 .as_ref()
345 .map(|s| match s.as_str() {
346 "MachineApplicable" => Applicability::MachineApplicable,
347 "MaybeIncorrect" => Applicability::MaybeIncorrect,
348 "HasPlaceholders" => Applicability::HasPlaceholders,
349 _ => Applicability::Unspecified,
350 })
351 .unwrap_or(Applicability::Unspecified);
352
353 suggestions.push(Suggestion {
354 span: Span {
355 file_name: PathBuf::from(&span.file_name),
356 byte_start: span.byte_start,
357 byte_end: span.byte_end,
358 line_start: span.line_start,
359 line_end: span.line_end,
360 column_start: span.column_start,
361 column_end: span.column_end,
362 },
363 replacement: replacement.clone(),
364 applicability,
365 message: String::new(),
366 });
367 }
368 }
369
370 let notes: Vec<String> = msg
372 .children
373 .iter()
374 .filter(|c| c.level == "note" || c.level == "help")
375 .map(|c| c.message.clone())
376 .collect();
377
378 Some(ClippyDiagnostic {
379 lint_name: code.code.clone(),
380 level,
381 message: msg.message,
382 span: primary_span,
383 suggestions,
384 notes,
385 })
386}
387
388#[cfg(test)]
389mod tests {
390 use super::*;
391
392 #[test]
393 fn test_applicability_auto_applicable() {
394 assert!(Applicability::MachineApplicable.is_auto_applicable());
395 assert!(!Applicability::MaybeIncorrect.is_auto_applicable());
396 assert!(!Applicability::HasPlaceholders.is_auto_applicable());
397 assert!(!Applicability::Unspecified.is_auto_applicable());
398 }
399
400 #[test]
401 fn test_diagnostic_short_lint_name() {
402 let diag = ClippyDiagnostic::new("clippy::bool_comparison", "test");
403 assert_eq!(diag.short_lint_name(), "bool_comparison");
404
405 let diag2 = ClippyDiagnostic::new("other_lint", "test");
406 assert_eq!(diag2.short_lint_name(), "other_lint");
407 }
408
409 #[test]
410 fn test_parse_clippy_output_empty() {
411 let result = parse_clippy_output("");
412 assert!(result.is_ok());
413 assert!(result.unwrap().is_empty());
414 }
415}