use std::path::Path;
use anyhow::Result;
use super::run_command;
use crate::core::tools::{Severity, StaticTool, ToolDiagnostic};
pub struct ClangtidyTool;
impl StaticTool for ClangtidyTool {
fn name(&self) -> &str {
"clang-tidy"
}
fn language(&self) -> &str {
"cpp"
}
fn is_available(&self) -> bool {
which::which("clang-tidy").is_ok()
}
fn run(&self, file: &Path, _content: &str) -> Result<Vec<ToolDiagnostic>> {
let dir = file.parent().unwrap_or_else(|| Path::new("."));
let path = file.to_string_lossy();
let out = match run_command("clang-tidy", &[&path, "--"], dir) {
Ok(o) => o,
Err(e) => {
tracing::debug!("clang-tidy invocation failed: {e}");
return Ok(Vec::new());
}
};
let combined = format!("{}\n{}", out.stdout, out.stderr);
Ok(parse_clangtidy_output(&combined))
}
}
fn parse_clangtidy_output(text: &str) -> Vec<ToolDiagnostic> {
text.lines().filter_map(parse_clangtidy_line).collect()
}
fn parse_clangtidy_line(line: &str) -> Option<ToolDiagnostic> {
let mut parts = line.splitn(4, ':');
let file = parts.next()?.trim();
let line_no: u32 = parts.next()?.trim().parse().ok()?;
let col: u32 = parts.next()?.trim().parse().ok()?;
let rest = parts.next()?.trim();
let (severity_str, after) = rest.split_once(':')?;
let severity = match severity_str.trim() {
"error" => Severity::Error,
"warning" => Severity::Warning,
"note" => return None,
_ => Severity::Info,
};
let after = after.trim();
let (message, code) = match (after.rfind('['), after.ends_with(']')) {
(Some(idx), true) => {
let msg = after[..idx].trim().to_string();
let code = after[idx + 1..after.len() - 1].trim().to_string();
(msg, Some(code))
}
_ => (after.to_string(), None),
};
Some(ToolDiagnostic {
tool: "clang-tidy".into(),
file: file.to_string(),
line: line_no,
col,
severity,
code,
message,
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_clangtidy_line_extracts_warning() {
let line =
"src/main.c:12:5: warning: variable 'x' is unused [clang-diagnostic-unused-variable]";
let d = parse_clangtidy_line(line).expect("should parse");
assert_eq!(d.file, "src/main.c");
assert_eq!(d.line, 12);
assert_eq!(d.col, 5);
assert_eq!(d.severity, Severity::Warning);
assert_eq!(d.code.as_deref(), Some("clang-diagnostic-unused-variable"));
assert_eq!(d.message, "variable 'x' is unused");
}
#[test]
fn parse_clangtidy_line_skips_notes_and_noise() {
assert!(parse_clangtidy_line("src/main.c:1:1: note: expanded from here").is_none());
assert!(parse_clangtidy_line("random log line").is_none());
assert!(parse_clangtidy_line("").is_none());
}
#[test]
fn parse_clangtidy_output_collects_multiple() {
let text = "a.c:1:1: warning: w1 [check-a]\na.c:2:2: error: e1 [check-b]\nnoise\n";
let diags = parse_clangtidy_output(text);
assert_eq!(diags.len(), 2);
assert_eq!(diags[0].severity, Severity::Warning);
assert_eq!(diags[1].severity, Severity::Error);
}
}