use anyhow::Result;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::PathBuf;
use std::time::Duration;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ClippyDiagnostic {
pub code: String,
pub level: DiagnosticLevel,
pub message: String,
pub file: PathBuf,
pub line_start: usize,
pub line_end: usize,
pub column_start: usize,
pub column_end: usize,
pub suggestion: Option<String>,
}
impl ClippyDiagnostic {
pub fn from_json(json: &str) -> Result<Self> {
let value: serde_json::Value = serde_json::from_str(json)?;
Self::parse_json_value(&value)
}
fn parse_json_value(value: &serde_json::Value) -> Result<Self> {
let message = &value["message"];
let code = message["code"]["code"].as_str().unwrap_or("unknown");
let level = message["level"].as_str().unwrap_or("warning");
let spans = &message["spans"][0];
Ok(Self {
code: code.to_string(),
level: DiagnosticLevel::from_str(level),
message: message["message"].as_str().unwrap_or("").to_string(),
file: PathBuf::from(spans["file_name"].as_str().unwrap_or("")),
line_start: spans["line_start"].as_u64().unwrap_or(0) as usize,
line_end: spans["line_end"].as_u64().unwrap_or(0) as usize,
column_start: spans["column_start"].as_u64().unwrap_or(0) as usize,
column_end: spans["column_end"].as_u64().unwrap_or(0) as usize,
suggestion: None,
})
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum DiagnosticLevel {
Error,
Warning,
Note,
Help,
}
impl DiagnosticLevel {
fn from_str(s: &str) -> Self {
match s {
"error" => Self::Error,
"warning" => Self::Warning,
"note" => Self::Note,
_ => Self::Help,
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum ConfidenceLevel {
High, Medium, Low, }
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FixResult {
pub success: bool,
pub diagnostic: ClippyDiagnostic,
pub modified_source: String,
pub confidence: ConfidenceLevel,
pub duration: Duration,
pub error: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FixReport {
pub total_diagnostics: usize,
pub successful_fixes: usize,
pub failed_fixes: usize,
pub skipped_low_confidence: usize,
pub success_rate: f64,
pub total_duration: Duration,
pub fixed_files: Vec<PathBuf>,
}
pub struct ClippyFixEngine {
cache: HashMap<String, FixResult>,
confidence_rules: HashMap<String, ConfidenceLevel>,
}
impl Default for ClippyFixEngine {
fn default() -> Self {
Self::new()
}
}
impl ClippyFixEngine {
#[must_use]
pub fn new() -> Self {
Self {
cache: HashMap::new(),
confidence_rules: Self::init_confidence_rules(),
}
}
fn init_confidence_rules() -> HashMap<String, ConfidenceLevel> {
let mut rules = HashMap::new();
rules.insert("clippy::needless_return".to_string(), ConfidenceLevel::High);
rules.insert("clippy::redundant_clone".to_string(), ConfidenceLevel::High);
rules.insert(
"clippy::unnecessary_wraps".to_string(),
ConfidenceLevel::High,
);
rules.insert("clippy::manual_map".to_string(), ConfidenceLevel::Medium);
rules.insert("clippy::single_match".to_string(), ConfidenceLevel::Medium);
rules.insert(
"clippy::needless_lifetimes".to_string(),
ConfidenceLevel::Low,
);
rules.insert("clippy::complex_lifetime".to_string(), ConfidenceLevel::Low);
rules
}
#[must_use]
pub fn calculate_confidence(&self, diagnostic: &ClippyDiagnostic) -> ConfidenceLevel {
self.confidence_rules
.get(&diagnostic.code)
.cloned()
.unwrap_or_else(|| self.default_confidence(diagnostic))
}
fn default_confidence(&self, diagnostic: &ClippyDiagnostic) -> ConfidenceLevel {
if diagnostic.suggestion.is_some() {
ConfidenceLevel::Medium
} else {
ConfidenceLevel::Low
}
}
pub async fn apply_fix(
&self,
source: &str,
diagnostic: &ClippyDiagnostic,
) -> Result<FixResult> {
let start = std::time::Instant::now();
let cache_key = self.generate_cache_key(source, diagnostic);
if let Some(cached) = self.cache.get(&cache_key) {
return Ok(cached.clone());
}
let modified = self.apply_fix_internal(source, diagnostic)?;
let result = FixResult {
success: true,
diagnostic: diagnostic.clone(),
modified_source: modified,
confidence: self.calculate_confidence(diagnostic),
duration: start.elapsed(),
error: None,
};
Ok(result)
}
fn apply_fix_internal(&self, source: &str, diagnostic: &ClippyDiagnostic) -> Result<String> {
if diagnostic.code == "clippy::needless_return" {
Ok(source.replace("return ", ""))
} else if let Some(suggestion) = &diagnostic.suggestion {
if suggestion.contains("{{") || suggestion.contains("}}") {
Ok(format!("{source}{suggestion}"))
} else {
Ok(source.replace(&diagnostic.message, suggestion))
}
} else {
Ok(source.to_string())
}
}
fn generate_cache_key(&self, source: &str, diagnostic: &ClippyDiagnostic) -> String {
format!(
"{}:{}:{}",
diagnostic.code,
diagnostic.line_start,
&source[..source.len().min(100)]
)
}
pub async fn apply_fix_with_validation(
&self,
source: &str,
diagnostic: &ClippyDiagnostic,
) -> Result<FixResult> {
let result = self.apply_fix(source, diagnostic).await?;
if !self.validate_fix(&result.modified_source).await? {
return Ok(FixResult {
success: false,
error: Some("Fix breaks compilation".to_string()),
..result
});
}
Ok(result)
}
async fn validate_fix(&self, source: &str) -> Result<bool> {
if source.contains("{{") || source.contains("}}") {
return Ok(false);
}
Ok(true)
}
pub async fn apply_batch_fixes(
&self,
diagnostics: &[ClippyDiagnostic],
) -> Result<Vec<FixResult>> {
let mut results = Vec::new();
for diagnostic in diagnostics {
let result = self.apply_fix("", diagnostic).await?;
results.push(result);
}
Ok(results)
}
pub async fn apply_parallel_fixes(
&self,
diagnostics: &[ClippyDiagnostic],
) -> Result<Vec<FixResult>> {
use futures::future::join_all;
let futures = diagnostics.iter().map(|d| self.apply_fix("", d));
let results = join_all(futures).await;
results.into_iter().collect()
}
#[must_use]
pub fn filter_by_confidence(
&self,
diagnostics: Vec<(ClippyDiagnostic, ConfidenceLevel)>,
min_confidence: ConfidenceLevel,
) -> Vec<(ClippyDiagnostic, ConfidenceLevel)> {
diagnostics
.into_iter()
.filter(|(_, conf)| *conf == min_confidence)
.collect()
}
#[must_use]
pub fn generate_report(&self, results: Vec<FixResult>) -> FixReport {
let total = results.len();
let successful = results.iter().filter(|r| r.success).count();
let duration = results.iter().map(|r| r.duration).sum();
let mut files = results
.iter()
.map(|r| r.diagnostic.file.clone())
.collect::<Vec<_>>();
files.dedup();
FixReport {
total_diagnostics: total,
successful_fixes: successful,
failed_fixes: total - successful,
skipped_low_confidence: 0,
success_rate: (successful as f64 / total as f64) * 100.0,
total_duration: duration,
fixed_files: files,
}
}
}
#[cfg(test)]
mod property_tests {
use proptest::prelude::*;
proptest! {
#[test]
fn basic_property_stability(_input in ".*") {
prop_assert!(true);
}
#[test]
fn module_consistency_check(_x in 0u32..1000) {
prop_assert!(_x < 1001);
}
}
}