use crate::checkers::Checker;
use crate::utils::types::{LintIssue, Severity};
use crate::{Language, Result};
use std::path::Path;
use std::process::Command;
pub struct JavaChecker;
impl JavaChecker {
pub fn new() -> Self {
Self
}
fn find_checkstyle_config(path: &Path) -> Option<std::path::PathBuf> {
let mut current = if path.is_file() {
path.parent()?.to_path_buf()
} else {
path.to_path_buf()
};
let config_names = [
".linthis/configs/java/checkstyle.xml", "checkstyle.xml",
".checkstyle.xml",
"config/checkstyle/checkstyle.xml",
"checkstyle-config.xml",
];
loop {
for config_name in &config_names {
let config_path = current.join(config_name);
if config_path.exists() {
return Some(config_path);
}
}
if !current.pop() {
break;
}
}
None
}
fn parse_checkstyle_output(&self, output: &str, file_path: &Path) -> Vec<LintIssue> {
let mut issues = Vec::new();
for line in output.lines() {
if let Some(issue) = self.parse_checkstyle_line(line, file_path) {
issues.push(issue);
}
}
issues
}
fn parse_checkstyle_line(&self, line: &str, default_path: &Path) -> Option<LintIssue> {
let line = line.trim();
let (severity, rest) = if line.starts_with("[ERROR]") {
(Severity::Error, line.strip_prefix("[ERROR]")?.trim())
} else if line.starts_with("[WARN]") {
(Severity::Warning, line.strip_prefix("[WARN]")?.trim())
} else if line.starts_with("[INFO]") {
(Severity::Info, line.strip_prefix("[INFO]")?.trim())
} else {
return None;
};
let parts: Vec<&str> = rest.splitn(4, ':').collect();
if parts.len() < 3 {
return None;
}
let file_path_parsed = std::path::PathBuf::from(parts[0]);
let line_num = parts[1].trim().parse::<usize>().ok()?;
let (col, message) = if parts.len() >= 4 {
if let Ok(c) = parts[2].trim().parse::<usize>() {
(Some(c), parts[3].trim().to_string())
} else {
(None, format!("{}: {}", parts[2].trim(), parts[3].trim()))
}
} else {
(None, parts[2].trim().to_string())
};
let mut issue = LintIssue::new(
if file_path_parsed.exists() {
file_path_parsed
} else {
default_path.to_path_buf()
},
line_num,
message,
severity,
)
.with_source("checkstyle".to_string());
if let Some(c) = col {
issue = issue.with_column(c);
}
Some(issue)
}
}
impl Default for JavaChecker {
fn default() -> Self {
Self::new()
}
}
impl Checker for JavaChecker {
fn name(&self) -> &str {
"checkstyle"
}
fn supported_languages(&self) -> &[Language] {
&[Language::Java]
}
fn check(&self, path: &Path) -> Result<Vec<LintIssue>> {
let config_arg = if let Some(config_path) = Self::find_checkstyle_config(path) {
vec!["-c".to_string(), config_path.to_string_lossy().to_string()]
} else {
vec!["-c".to_string(), "/google_checks.xml".to_string()]
};
let output = Command::new("checkstyle")
.args(&config_arg)
.arg(path)
.output()
.map_err(|e| {
crate::LintisError::checker("checkstyle", path, format!("Failed to run: {}", e))
})?;
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
let combined = format!("{}{}", stdout, stderr);
let issues = self.parse_checkstyle_output(&combined, path);
Ok(issues)
}
fn is_available(&self) -> bool {
Command::new("checkstyle")
.arg("--version")
.output()
.map(|o| o.status.success())
.unwrap_or(false)
}
}