use std::path::PathBuf;
use regex::Regex;
use crate::issue_registry::{NewIssue, Severity};
use crate::vt_parser::StyledLine;
pub struct DiagnosticsExtractor {
marker: String,
source: String,
gnu_3: Regex,
gnu_2: Regex,
todo_pat: Regex,
rustc_header: Regex,
rustc_arrow: Regex,
}
impl DiagnosticsExtractor {
pub fn new(marker: impl Into<String>, source: impl Into<String>) -> Self {
let gnu_3 = Regex::new(
r"(?i)^((?:[A-Za-z]:\\[^:\n]*|[^:\n]+)):(\d+):(\d+):\s*(error|warning|note|info|hint):\s+(.+)$",
)
.expect("gnu_3 regex is valid");
let gnu_2 = Regex::new(
r"(?i)^((?:[A-Za-z]:\\[^:\n]*|[^:\n]+)):(\d+):\s*(error|warning|note|info|hint):\s+(.+)$",
)
.expect("gnu_2 regex is valid");
let todo_pat = Regex::new(r"(?i)\b(TODO|FIXME|HACK|XXX)\b[:\s]+(.+)$")
.expect("todo_pat regex is valid");
let rustc_header = Regex::new(
r"(?i)^(error|warning|note|info|hint)(\[.*?\])?:\s+(.+)$",
)
.expect("rustc_header regex is valid");
let rustc_arrow = Regex::new(
r"^\s+-->\s+((?:[A-Za-z]:\\[^:\n]*|[^:\n]+)):(\d+):(\d+)\s*$",
)
.expect("rustc_arrow regex is valid");
Self {
marker: marker.into(),
source: source.into(),
gnu_3,
gnu_2,
todo_pat,
rustc_header,
rustc_arrow,
}
}
pub fn extract_from_line(&self, line: &StyledLine) -> Vec<NewIssue> {
self.extract_from_str(&line.text)
}
pub fn extract_from_str(&self, text: &str) -> Vec<NewIssue> {
let text = text.trim_end();
if text.is_empty() {
return Vec::new();
}
if let Some(caps) = self.gnu_3.captures(text) {
let path = PathBuf::from(&caps[1]);
let lineno: usize = caps[2].parse().unwrap_or(0);
let col: usize = caps[3].parse().unwrap_or(0);
let severity = parse_severity(&caps[4]);
let message = caps[5].trim().to_string();
return vec![self.make_issue(
severity,
message,
Some(path),
Some(lineno),
Some(col),
)];
}
if let Some(caps) = self.gnu_2.captures(text) {
let path = PathBuf::from(&caps[1]);
let lineno: usize = caps[2].parse().unwrap_or(0);
let severity = parse_severity(&caps[3]);
let message = caps[4].trim().to_string();
return vec![self.make_issue(
severity,
message,
Some(path),
Some(lineno),
None,
)];
}
if let Some(caps) = self.todo_pat.captures(text) {
let message = format!("{}: {}", &caps[1].to_uppercase(), caps[2].trim());
return vec![self.make_issue(Severity::Todo, message, None, None, None)];
}
Vec::new()
}
pub fn try_rustc_header(&self, text: &str) -> Option<(Severity, String)> {
let text = text.trim_end();
self.rustc_header.captures(text).map(|caps| {
let severity = parse_severity(&caps[1]);
let message = caps[3].trim().to_string();
(severity, message)
})
}
pub fn try_rustc_arrow(&self, text: &str) -> Option<(PathBuf, usize, usize)> {
let text = text.trim_end();
self.rustc_arrow.captures(text).map(|caps| {
let path = PathBuf::from(&caps[1]);
let line: usize = caps[2].parse().unwrap_or(0);
let col: usize = caps[3].parse().unwrap_or(0);
(path, line, col)
})
}
pub fn make_issue(
&self,
severity: Severity,
message: String,
path: Option<PathBuf>,
line: Option<usize>,
column: Option<usize>,
) -> NewIssue {
use crate::editor::position::Position;
let range = match (line, column) {
(Some(l), Some(c)) => {
let pos = Position { line: l.saturating_sub(1), column: c.saturating_sub(1) };
Some((pos, pos))
}
(Some(l), None) => {
let pos = Position { line: l.saturating_sub(1), column: 0 };
Some((pos, pos))
}
_ => None,
};
NewIssue {
marker: Some(self.marker.clone()),
source: self.source.clone(),
path,
range,
message,
severity,
}
}
}
fn parse_severity(s: &str) -> Severity {
match s.to_ascii_lowercase().as_str() {
"error" => Severity::Error,
"warning" => Severity::Warning,
_ => Severity::Info,
}
}
#[cfg(test)]
mod tests {
use super::*;
fn extractor() -> DiagnosticsExtractor {
DiagnosticsExtractor::new("task:build:crate_a", "build")
}
fn line(text: &str) -> StyledLine {
StyledLine { text: text.to_string(), spans: vec![] }
}
fn single(text: &str) -> Option<NewIssue> {
let results = extractor().extract_from_line(&line(text));
assert!(
results.len() <= 1,
"expected at most 1 result, got {}: {results:?}",
results.len()
);
results.into_iter().next()
}
#[test]
fn gnu_error_with_column() {
let issue = single("src/main.rs:42:10: error: type mismatch").unwrap();
assert_eq!(issue.severity, Severity::Error);
assert_eq!(issue.message, "type mismatch");
assert_eq!(issue.path, Some(PathBuf::from("src/main.rs")));
let (start, _) = issue.range.unwrap();
assert_eq!(start.line, 41);
assert_eq!(start.column, 9);
}
#[test]
fn gnu_warning_with_column() {
let issue = single("lib/foo.rs:10:5: warning: unused variable").unwrap();
assert_eq!(issue.severity, Severity::Warning);
assert_eq!(issue.message, "unused variable");
assert_eq!(issue.path, Some(PathBuf::from("lib/foo.rs")));
}
#[test]
fn gnu_error_without_column() {
let issue = single("build/Makefile:7: error: missing separator").unwrap();
assert_eq!(issue.severity, Severity::Error);
assert_eq!(issue.message, "missing separator");
assert_eq!(issue.path, Some(PathBuf::from("build/Makefile")));
let (start, _) = issue.range.unwrap();
assert_eq!(start.line, 6); assert_eq!(start.column, 0);
}
#[test]
fn todo_comment() {
let issue = single(" // TODO: refactor this function").unwrap();
assert_eq!(issue.severity, Severity::Todo);
assert!(issue.message.contains("refactor this function"), "msg: {}", issue.message);
assert!(issue.path.is_none());
}
#[test]
fn fixme_comment() {
let issue = single("FIXME: this is broken").unwrap();
assert_eq!(issue.severity, Severity::Todo);
}
#[test]
fn hack_comment() {
let issue = single(" HACK: workaround for upstream bug").unwrap();
assert_eq!(issue.severity, Severity::Todo);
}
#[test]
fn xxx_comment() {
let issue = single("XXX: needs review").unwrap();
assert_eq!(issue.severity, Severity::Todo);
}
#[test]
fn marker_and_source_are_set() {
let ext = DiagnosticsExtractor::new("task:lint:mylib", "lint");
let results = ext.extract_from_line(&line("src/lib.rs:1:1: error: oops"));
let issue = &results[0];
assert_eq!(issue.marker, Some("task:lint:mylib".into()));
assert_eq!(issue.source, "lint");
}
#[test]
fn plain_line_produces_no_issues() {
assert!(single(" Compiling mylib v0.1.0").is_none());
}
#[test]
fn empty_line_produces_no_issues() {
assert!(single("").is_none());
}
#[test]
fn note_keyword_maps_to_info() {
let issue = single("src/main.rs:5:3: note: consider using a semicolon").unwrap();
assert_eq!(issue.severity, Severity::Info);
}
#[test]
fn different_markers_are_independent() {
let ext_a = DiagnosticsExtractor::new("task:build:crate_a", "build");
let ext_b = DiagnosticsExtractor::new("task:build:crate_b", "build");
let results_a = ext_a.extract_from_line(&line("a.rs:1:1: error: a broke"));
let results_b = ext_b.extract_from_line(&line("b.rs:1:1: error: b broke"));
assert_eq!(results_a[0].marker, Some("task:build:crate_a".into()));
assert_eq!(results_b[0].marker, Some("task:build:crate_b".into()));
}
#[test]
fn case_insensitive_severity() {
let issue = single("src/main.rs:1:1: ERROR: uppercase error").unwrap();
assert_eq!(issue.severity, Severity::Error);
let issue2 = single("src/main.rs:1:1: Warning: mixed case").unwrap();
assert_eq!(issue2.severity, Severity::Warning);
}
#[test]
fn path_with_colon_only_no_line_number_not_matched() {
assert!(single("Running: building the project").is_none());
}
#[test]
fn extract_from_str_matches_extract_from_line() {
let ext = extractor();
let text = "src/main.rs:10:5: warning: dead code";
let from_line = ext.extract_from_line(&line(text));
let from_str = ext.extract_from_str(text);
assert_eq!(from_line.len(), from_str.len());
assert_eq!(from_line[0].severity, from_str[0].severity);
assert_eq!(from_line[0].message, from_str[0].message);
}
#[test]
fn extract_from_str_ansi_stripped() {
let ext = extractor();
let text = "src/main.rs:1:1: error: oh no";
let issue = ext.extract_from_str(text).into_iter().next().unwrap();
assert_eq!(issue.severity, Severity::Error);
assert_eq!(issue.message, "oh no");
}
}