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> {
let short = name.strip_prefix("clippy::").unwrap_or(name);
match short {
"absurd_extreme_comparisons"
| "almost_swapped"
| "approx_constant"
| "async_yields_async"
| "bad_bit_mask"
| "cast_slice_different_sizes"
| "char_indices_as_byte_indices"
| "derive_hash_xor_eq"
| "derive_ord_xor_partial_ord"
| "erasing_op"
| "if_let_mutex"
| "impossible_comparisons"
| "invalid_regex"
| "mistyped_literal_suffixes"
| "nonsensical_open_options"
| "uninit_assumed_init"
| "unit_cmp"
| "unit_return_expecting_ord"
| "while_immutable_condition" => Some(LintCategory::Correctness),
"almost_complete_range"
| "arc_with_non_send_sync"
| "await_holding_lock"
| "await_holding_refcell_ref"
| "blanket_clippy_restriction_lints"
| "cast_abs_to_unsigned"
| "cast_enum_constructor"
| "cast_enum_truncation"
| "cast_nan_to_int"
| "cast_slice_from_raw_parts"
| "confusing_method_to_numeric_cast"
| "debug_assert_with_mut_call" => Some(LintCategory::Suspicious),
"bind_instead_of_map"
| "bool_comparison"
| "borrow_deref_ref"
| "borrowed_box"
| "bytes_count_to_len"
| "char_lit_as_u8"
| "clone_on_copy"
| "deref_addrof"
| "manual_unwrap_or"
| "needless_bool"
| "needless_borrow"
| "option_map_unwrap_or"
| "redundant_closure"
| "redundant_slicing"
| "too_many_arguments"
| "unnecessary_cast"
| "unused_self"
| "useless_format" => Some(LintCategory::Complexity),
"box_collection"
| "boxed_local"
| "cmp_owned"
| "collapsible_str_replace"
| "iter_cloned_collect"
| "manual_filter_map"
| "manual_find_map"
| "manual_map"
| "map_clone"
| "or_fun_call"
| "redundant_allocation"
| "unnecessary_lazy_evaluations"
| "useless_conversion"
| "vec_init_then_push" => Some(LintCategory::Perf),
"assertions_on_constants"
| "assign_op_pattern"
| "blocks_in_conditions"
| "bool_assert_comparison"
| "box_default"
| "builtin_type_shadow"
| "byte_char_slices"
| "bytes_nth"
| "chars_last_cmp"
| "chars_next_cmp"
| "cmp_null"
| "collapsible_if"
| "collapsible_match"
| "comparison_to_empty"
| "const_static_lifetime"
| "declare_interior_mutable_const"
| "doc_markdown"
| "if_not_else"
| "len_without_is_empty"
| "len_zero"
| "manual_string_new"
| "match_bool"
| "match_wildcard_for_single_variants"
| "needless_return"
| "new_without_default"
| "ptr_arg"
| "redundant_pattern_matching"
| "semicolon_if_nothing_returned"
| "should_implement_trait"
| "single_match"
| "unnecessary_struct_initialization"
| "upper_case_acronyms"
| "wrong_self_convention" => Some(LintCategory::Style),
"assigning_clones"
| "bool_to_int_with_if"
| "borrow_as_ptr"
| "case_sensitive_file_extension_comparisons"
| "cast_lossless"
| "cast_possible_truncation"
| "cast_possible_wrap"
| "cast_precision_loss"
| "cast_ptr_alignment"
| "cast_sign_loss"
| "checked_conversions"
| "cloned_instead_of_copied"
| "collapsible_else_if"
| "comparison_chain"
| "copy_iterator"
| "default_constructed_unit_structs"
| "default_numeric_fallback"
| "derive_partial_eq_without_eq"
| "enum_glob_use"
| "if_same_then_else"
| "implicit_clone"
| "map_unwrap_or"
| "match_wild_err_arm"
| "missing_errors_doc"
| "missing_panics_doc"
| "module_name_repetitions"
| "must_use_candidate"
| "needless_pass_by_value"
| "option_if_let_else"
| "similar_names"
| "todo"
| "too_many_lines"
| "type_repetition_in_bounds"
| "unimplemented"
| "unnecessary_wraps"
| "unused_async"
| "use_self"
| "verbose_bit_mask"
| "wildcard_enum_match_arm" => Some(LintCategory::Pedantic),
"absolute_paths"
| "alloc_instead_of_core"
| "allow_attributes"
| "allow_attributes_without_reason"
| "arithmetic_side_effects"
| "as_conversions"
| "as_underscore"
| "clone_on_ref_ptr"
| "cognitive_complexity"
| "create_dir"
| "dbg_macro"
| "decimal_literal_representation"
| "default_union_representation"
| "expect_used"
| "implicit_return"
| "missing_docs_in_private_items"
| "panic"
| "print_stderr"
| "print_stdout"
| "str_to_string"
| "string_add"
| "string_to_string"
| "todo_in_production"
| "unwrap_used"
| "wildcard_imports" => Some(LintCategory::Restriction),
"cargo_common_metadata"
| "multiple_crate_versions"
| "negative_feature_names"
| "redundant_feature_names"
| "wildcard_dependencies" => Some(LintCategory::Cargo),
"as_ptr_cast_mut"
| "branches_sharing_code"
| "clear_with_drain"
| "collection_is_never_read"
| "coerce_container_to_any"
| "empty_line_after_doc_comments"
| "empty_line_after_outer_attr"
| "equatable_if_let"
| "fallible_impl_from"
| "future_not_send"
| "imprecise_flops"
| "iter_on_empty_collections"
| "iter_on_single_items"
| "iter_with_drain"
| "large_stack_frames"
| "missing_const_for_fn"
| "mutex_integer"
| "needless_collect"
| "non_send_fields_in_send_ty"
| "path_buf_push_overwrite"
| "redundant_pub_crate"
| "significant_drop_in_scrutinee"
| "significant_drop_tightening"
| "string_lit_as_bytes"
| "suboptimal_flops"
| "suspicious_operation_groupings"
| "trait_duplication_in_bounds"
| "useless_let_if_seq" => Some(LintCategory::Nursery),
_ => 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());
}
#[test]
fn test_from_lint_name_correctness() {
assert_eq!(
LintCategory::from_lint_name("absurd_extreme_comparisons"),
Some(LintCategory::Correctness)
);
assert_eq!(
LintCategory::from_lint_name("erasing_op"),
Some(LintCategory::Correctness)
);
}
#[test]
fn test_from_lint_name_suspicious() {
assert_eq!(
LintCategory::from_lint_name("await_holding_lock"),
Some(LintCategory::Suspicious)
);
assert_eq!(
LintCategory::from_lint_name("cast_nan_to_int"),
Some(LintCategory::Suspicious)
);
}
#[test]
fn test_from_lint_name_complexity() {
assert_eq!(
LintCategory::from_lint_name("bool_comparison"),
Some(LintCategory::Complexity)
);
assert_eq!(
LintCategory::from_lint_name("redundant_closure"),
Some(LintCategory::Complexity)
);
}
#[test]
fn test_from_lint_name_perf() {
assert_eq!(
LintCategory::from_lint_name("box_collection"),
Some(LintCategory::Perf)
);
assert_eq!(
LintCategory::from_lint_name("useless_conversion"),
Some(LintCategory::Perf)
);
}
#[test]
fn test_from_lint_name_style() {
assert_eq!(
LintCategory::from_lint_name("collapsible_if"),
Some(LintCategory::Style)
);
assert_eq!(
LintCategory::from_lint_name("needless_return"),
Some(LintCategory::Style)
);
}
#[test]
fn test_from_lint_name_pedantic() {
assert_eq!(
LintCategory::from_lint_name("cast_lossless"),
Some(LintCategory::Pedantic)
);
assert_eq!(
LintCategory::from_lint_name("missing_errors_doc"),
Some(LintCategory::Pedantic)
);
}
#[test]
fn test_from_lint_name_restriction() {
assert_eq!(
LintCategory::from_lint_name("unwrap_used"),
Some(LintCategory::Restriction)
);
assert_eq!(
LintCategory::from_lint_name("expect_used"),
Some(LintCategory::Restriction)
);
}
#[test]
fn test_from_lint_name_cargo() {
assert_eq!(
LintCategory::from_lint_name("cargo_common_metadata"),
Some(LintCategory::Cargo)
);
assert_eq!(
LintCategory::from_lint_name("multiple_crate_versions"),
Some(LintCategory::Cargo)
);
}
#[test]
fn test_from_lint_name_nursery() {
assert_eq!(
LintCategory::from_lint_name("branches_sharing_code"),
Some(LintCategory::Nursery)
);
assert_eq!(
LintCategory::from_lint_name("needless_collect"),
Some(LintCategory::Nursery)
);
}
#[test]
fn test_from_lint_name_unknown() {
assert_eq!(LintCategory::from_lint_name("unknown_lint"), None);
assert_eq!(LintCategory::from_lint_name(""), None);
assert_eq!(LintCategory::from_lint_name("rustc::unused_imports"), None);
}
#[test]
fn test_from_lint_name_with_prefix() {
assert_eq!(
LintCategory::from_lint_name("clippy::unwrap_used"),
Some(LintCategory::Restriction)
);
assert_eq!(
LintCategory::from_lint_name("clippy::bool_comparison"),
Some(LintCategory::Complexity)
);
}
#[test]
fn test_from_lint_name_without_prefix() {
assert_eq!(
LintCategory::from_lint_name("unwrap_used"),
Some(LintCategory::Restriction)
);
assert_eq!(
LintCategory::from_lint_name("bool_comparison"),
Some(LintCategory::Complexity)
);
}
}