use crate::checkers::Checker;
use crate::utils::types::{LintIssue, Severity};
use crate::{Language, Result};
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::process::Command;
use std::sync::Mutex;
static CLIPPY_CACHE: Mutex<Option<HashMap<PathBuf, Vec<LintIssue>>>> = Mutex::new(None);
pub struct RustChecker;
impl RustChecker {
pub fn new() -> Self {
Self
}
fn find_cargo_root(path: &Path) -> Option<PathBuf> {
let mut current = if path.is_file() {
path.parent()?.to_path_buf()
} else {
path.to_path_buf()
};
loop {
let cargo_toml = current.join("Cargo.toml");
if cargo_toml.exists() {
return Some(if current.as_os_str().is_empty() {
PathBuf::from(".")
} else {
current
});
}
if !current.pop() {
break;
}
}
None
}
fn run_cargo_clippy(project_root: &Path) -> Result<Vec<LintIssue>> {
let output = Command::new("cargo")
.args(["clippy", "--message-format=short", "--", "-D", "warnings"])
.current_dir(project_root)
.output()
.map_err(|e| {
crate::LintisError::checker("clippy", project_root, format!("Failed to run: {}", e))
})?;
let stderr = String::from_utf8_lossy(&output.stderr);
let issues = Self::parse_clippy_output(&stderr, project_root);
Ok(issues)
}
fn parse_clippy_output(output: &str, project_root: &Path) -> Vec<LintIssue> {
let mut issues = Vec::new();
for line in output.lines() {
if let Some(issue) = Self::parse_clippy_line(line, project_root) {
issues.push(issue);
}
}
issues
}
fn parse_clippy_line(line: &str, project_root: &Path) -> Option<LintIssue> {
if !line.contains(": warning:") && !line.contains(": error:") {
return None;
}
let parts: Vec<&str> = line.splitn(4, ':').collect();
if parts.len() < 4 {
return None;
}
let relative_path = PathBuf::from(parts[0]);
let file_path = project_root.join(relative_path);
let line_num = parts[1].trim().parse::<usize>().ok()?;
let col = parts[2].trim().parse::<usize>().ok();
let rest = parts[3];
let (severity, message) = if rest.contains("warning:") {
let msg = rest.trim_start_matches(" warning:").trim();
(Severity::Warning, msg.to_string())
} else if rest.contains("error:") {
let msg = rest.trim_start_matches(" error:").trim();
(Severity::Error, msg.to_string())
} else {
return None;
};
let mut issue = LintIssue::new(file_path, line_num, message, severity)
.with_source("clippy".to_string());
if let Some(c) = col {
issue = issue.with_column(c);
}
Some(issue)
}
fn get_cached_issues(project_root: &Path) -> Result<Vec<LintIssue>> {
let mut cache = CLIPPY_CACHE.lock().unwrap();
if cache.is_none() {
*cache = Some(HashMap::new());
}
let cache_map = cache.as_mut().unwrap();
if let Some(issues) = cache_map.get(project_root) {
return Ok(issues.clone());
}
let issues = Self::run_cargo_clippy(project_root)?;
cache_map.insert(project_root.to_path_buf(), issues.clone());
Ok(issues)
}
}
impl Default for RustChecker {
fn default() -> Self {
Self::new()
}
}
impl Checker for RustChecker {
fn name(&self) -> &str {
"clippy"
}
fn supported_languages(&self) -> &[Language] {
&[Language::Rust]
}
fn check(&self, path: &Path) -> Result<Vec<LintIssue>> {
let project_root = match Self::find_cargo_root(path) {
Some(root) => root,
None => {
return Ok(Vec::new());
}
};
let all_issues = Self::get_cached_issues(&project_root)?;
let canonical_path = path.canonicalize().unwrap_or_else(|_| path.to_path_buf());
let file_issues: Vec<LintIssue> = all_issues
.into_iter()
.filter(|issue| {
let issue_canonical = issue
.file_path
.canonicalize()
.unwrap_or_else(|_| issue.file_path.clone());
issue_canonical == canonical_path
})
.collect();
Ok(file_issues)
}
fn is_available(&self) -> bool {
Command::new("cargo")
.args(["clippy", "--version"])
.output()
.map(|o| o.status.success())
.unwrap_or(false)
}
}
pub fn clear_clippy_cache() {
let mut cache = CLIPPY_CACHE.lock().unwrap();
*cache = None;
}