use std::sync::LazyLock;
use regex::{Captures, Regex};
use crate::analysis::findings::{Finding, Severity};
use crate::languages::spec::ToolSpec;
static POSITION: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(r"^(?:vet:\s*)?(?P<file>\S.*?):(?P<line>\d+):(?P<col>\d+):\s*(?P<message>.+)$")
.expect("POSITION regex compiles")
});
static TSC: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(
r"^(?P<file>.+?)\((?P<line>\d+),(?P<col>\d+)\):\s*(?P<severity>error|warning)\s+(?P<code>TS\d+):\s*(?P<message>.+)$",
)
.expect("TSC regex compiles")
});
static MSBUILD: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(
r"^(?P<file>.+?)\((?P<line>\d+),(?P<col>\d+)\):\s*(?P<severity>error|warning|info)\s+(?P<code>[A-Za-z0-9_]+):\s*(?P<message>.*?)(?:\s+\[[^\]]*(?i)\.(?:csproj|vbproj|fsproj|sln|sqlproj|wixproj|shproj)\])?$",
)
.expect("MSBUILD regex compiles")
});
pub(super) fn parse_positions(spec: &ToolSpec, output: &str) -> Vec<Finding> {
let mut findings = Vec::new();
for line in output.lines() {
let Some(caps) = POSITION.captures(line.trim()) else {
continue;
};
let file = caps.name("file").map(|m| m.as_str()).unwrap_or("");
let line_num = caps
.name("line")
.and_then(|m| m.as_str().parse::<u32>().ok())
.unwrap_or(1);
let col = caps
.name("col")
.and_then(|m| m.as_str().parse::<u32>().ok())
.unwrap_or(1);
let message = caps
.name("message")
.map(|m| m.as_str().trim())
.unwrap_or("");
findings.push(Finding::deterministic(
spec.name.to_owned(),
Severity::Error,
file.strip_prefix("./").unwrap_or(file).to_owned(),
line_num,
Some(col),
message.to_owned(),
None,
));
}
findings
}
fn parse_captured(
output: &str,
re: &Regex,
severity: impl Fn(&Captures<'_>) -> Severity,
) -> Vec<Finding> {
let mut findings = Vec::new();
for line in output.lines() {
let Some(caps) = re.captures(line.trim()) else {
continue;
};
let line_num = caps["line"].parse::<u32>().ok().unwrap_or(1);
let col = caps["col"].parse::<u32>().ok().unwrap_or(1);
findings.push(Finding::deterministic(
caps["code"].to_owned(),
severity(&caps),
caps["file"].to_owned(),
line_num,
Some(col),
caps["message"].trim().to_owned(),
None,
));
}
findings
}
pub(super) fn parse_tsc(output: &str) -> Vec<Finding> {
parse_captured(output, &TSC, |caps| {
if &caps["severity"] == "warning" {
Severity::Warning
} else {
Severity::Error
}
})
}
pub(super) fn parse_msbuild(output: &str) -> Vec<Finding> {
parse_captured(output, &MSBUILD, |caps| msbuild_severity(&caps["severity"]))
}
fn msbuild_severity(word: &str) -> Severity {
match word {
"error" => Severity::Error,
"warning" => Severity::Warning,
_ => Severity::Info,
}
}