use serde::{Deserialize, Serialize};
use std::path::PathBuf;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[derive(Default)]
pub enum Applicability {
MachineApplicable,
MaybeIncorrect,
HasPlaceholders,
#[default]
Unspecified,
}
impl Applicability {
pub fn is_auto_applicable(&self) -> bool {
matches!(self, Applicability::MachineApplicable)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum LintCategory {
Correctness,
Suspicious,
Complexity,
Perf,
Style,
Pedantic,
Restriction,
Cargo,
Nursery,
}
impl LintCategory {
pub fn as_str(&self) -> &'static str {
match self {
LintCategory::Correctness => "correctness",
LintCategory::Suspicious => "suspicious",
LintCategory::Complexity => "complexity",
LintCategory::Perf => "perf",
LintCategory::Style => "style",
LintCategory::Pedantic => "pedantic",
LintCategory::Restriction => "restriction",
LintCategory::Cargo => "cargo",
LintCategory::Nursery => "nursery",
}
}
pub fn from_lint_name(_name: &str) -> Option<Self> {
None }
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Span {
pub file_name: PathBuf,
pub byte_start: usize,
pub byte_end: usize,
pub line_start: usize,
pub line_end: usize,
pub column_start: usize,
pub column_end: usize,
}
impl Span {
pub fn from_bytes(file_name: impl Into<PathBuf>, start: usize, end: usize) -> Self {
Self {
file_name: file_name.into(),
byte_start: start,
byte_end: end,
line_start: 0,
line_end: 0,
column_start: 0,
column_end: 0,
}
}
pub fn byte_range(&self) -> std::ops::Range<usize> {
self.byte_start..self.byte_end
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Suggestion {
pub span: Span,
pub replacement: String,
pub applicability: Applicability,
pub message: String,
}
impl Suggestion {
pub fn new(span: Span, replacement: impl Into<String>) -> Self {
Self {
span,
replacement: replacement.into(),
applicability: Applicability::Unspecified,
message: String::new(),
}
}
pub fn with_applicability(mut self, applicability: Applicability) -> Self {
self.applicability = applicability;
self
}
pub fn with_message(mut self, message: impl Into<String>) -> Self {
self.message = message.into();
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ClippyDiagnostic {
pub lint_name: String,
pub level: DiagnosticLevel,
pub message: String,
pub span: Option<Span>,
pub suggestions: Vec<Suggestion>,
pub notes: Vec<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum DiagnosticLevel {
Error,
Warning,
Note,
Help,
}
impl ClippyDiagnostic {
pub fn new(lint_name: impl Into<String>, message: impl Into<String>) -> Self {
Self {
lint_name: lint_name.into(),
level: DiagnosticLevel::Warning,
message: message.into(),
span: None,
suggestions: Vec::new(),
notes: Vec::new(),
}
}
pub fn short_lint_name(&self) -> &str {
self.lint_name
.strip_prefix("clippy::")
.unwrap_or(&self.lint_name)
}
pub fn has_auto_fix(&self) -> bool {
self.suggestions
.iter()
.any(|s| s.applicability.is_auto_applicable())
}
pub fn auto_fix(&self) -> Option<&Suggestion> {
self.suggestions
.iter()
.find(|s| s.applicability.is_auto_applicable())
}
pub fn to_mutation(&self) -> Option<Box<dyn crate::Mutation>> {
use super::lints;
use crate::idiom::*;
match self.lint_name.as_str() {
lints::BOOL_COMPARISON => Some(Box::new(BoolSimplifyMutation::new())),
lints::COLLAPSIBLE_IF => Some(Box::new(CollapsibleIfMutation::new())),
lints::COMPARISON_TO_EMPTY => Some(Box::new(ComparisonToMethodMutation::new())),
lints::ASSIGN_OP_PATTERN => Some(Box::new(AssignOpMutation::new())),
lints::CLONE_ON_COPY => Some(Box::new(CloneOnCopyMutation::new())),
lints::REDUNDANT_CLOSURE => Some(Box::new(RedundantClosureMutation::new())),
_ => None,
}
}
}
pub fn parse_clippy_output(json_lines: &str) -> Result<Vec<ClippyDiagnostic>, serde_json::Error> {
let mut diagnostics = Vec::new();
for line in json_lines.lines() {
if line.trim().is_empty() {
continue;
}
if let Ok(CargoMessage::CompilerMessage { message }) =
serde_json::from_str::<CargoMessage>(line)
{
if let Some(diag) = convert_compiler_message(message) {
diagnostics.push(diag);
}
}
}
Ok(diagnostics)
}
#[derive(Debug, Deserialize)]
#[serde(tag = "reason")]
enum CargoMessage {
#[serde(rename = "compiler-message")]
CompilerMessage { message: CompilerMessage },
#[serde(other)]
Other,
}
#[derive(Debug, Deserialize)]
struct CompilerMessage {
code: Option<DiagnosticCode>,
level: String,
message: String,
spans: Vec<CompilerSpan>,
children: Vec<CompilerMessage>,
}
#[derive(Debug, Deserialize)]
struct DiagnosticCode {
code: String,
}
#[derive(Debug, Deserialize)]
struct CompilerSpan {
file_name: String,
byte_start: usize,
byte_end: usize,
line_start: usize,
line_end: usize,
column_start: usize,
column_end: usize,
is_primary: bool,
suggested_replacement: Option<String>,
suggestion_applicability: Option<String>,
}
fn convert_compiler_message(msg: CompilerMessage) -> Option<ClippyDiagnostic> {
let code = msg.code.as_ref()?;
if !code.code.starts_with("clippy::") {
return None;
}
let level = match msg.level.as_str() {
"error" => DiagnosticLevel::Error,
"warning" => DiagnosticLevel::Warning,
"note" => DiagnosticLevel::Note,
"help" => DiagnosticLevel::Help,
_ => DiagnosticLevel::Warning,
};
let primary_span = msg.spans.iter().find(|s| s.is_primary).map(|s| Span {
file_name: PathBuf::from(&s.file_name),
byte_start: s.byte_start,
byte_end: s.byte_end,
line_start: s.line_start,
line_end: s.line_end,
column_start: s.column_start,
column_end: s.column_end,
});
let mut suggestions = Vec::new();
for span in &msg.spans {
if let Some(ref replacement) = span.suggested_replacement {
let applicability = span
.suggestion_applicability
.as_ref()
.map(|s| match s.as_str() {
"MachineApplicable" => Applicability::MachineApplicable,
"MaybeIncorrect" => Applicability::MaybeIncorrect,
"HasPlaceholders" => Applicability::HasPlaceholders,
_ => Applicability::Unspecified,
})
.unwrap_or(Applicability::Unspecified);
suggestions.push(Suggestion {
span: Span {
file_name: PathBuf::from(&span.file_name),
byte_start: span.byte_start,
byte_end: span.byte_end,
line_start: span.line_start,
line_end: span.line_end,
column_start: span.column_start,
column_end: span.column_end,
},
replacement: replacement.clone(),
applicability,
message: String::new(),
});
}
}
let notes: Vec<String> = msg
.children
.iter()
.filter(|c| c.level == "note" || c.level == "help")
.map(|c| c.message.clone())
.collect();
Some(ClippyDiagnostic {
lint_name: code.code.clone(),
level,
message: msg.message,
span: primary_span,
suggestions,
notes,
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_applicability_auto_applicable() {
assert!(Applicability::MachineApplicable.is_auto_applicable());
assert!(!Applicability::MaybeIncorrect.is_auto_applicable());
assert!(!Applicability::HasPlaceholders.is_auto_applicable());
assert!(!Applicability::Unspecified.is_auto_applicable());
}
#[test]
fn test_diagnostic_short_lint_name() {
let diag = ClippyDiagnostic::new("clippy::bool_comparison", "test");
assert_eq!(diag.short_lint_name(), "bool_comparison");
let diag2 = ClippyDiagnostic::new("other_lint", "test");
assert_eq!(diag2.short_lint_name(), "other_lint");
}
#[test]
fn test_parse_clippy_output_empty() {
let result = parse_clippy_output("");
assert!(result.is_ok());
assert!(result.unwrap().is_empty());
}
}