use pathfinder_lsp::types::LspDiagnostic;
use std::collections::HashMap;
#[derive(Debug, Default)]
pub(crate) struct DiagnosticDiff {
pub introduced: Vec<LspDiagnostic>,
pub resolved: Vec<LspDiagnostic>,
}
impl DiagnosticDiff {
pub fn has_new_errors(&self) -> bool {
self.introduced.iter().any(LspDiagnostic::is_error)
}
}
pub(crate) fn diff_diagnostics(pre: &[LspDiagnostic], post: &[LspDiagnostic]) -> DiagnosticDiff {
let pre_counts = build_counts(pre);
let post_counts = build_counts(post);
let introduced = collect_introduced(post, &pre_counts, &post_counts);
let resolved = collect_resolved(pre, &pre_counts, &post_counts);
DiagnosticDiff {
introduced,
resolved,
}
}
type DiagKey = (u8, Option<String>, String, String);
fn diag_key(d: &LspDiagnostic) -> DiagKey {
(
d.severity as u8,
d.code.clone(),
d.message.clone(),
d.file.clone(),
)
}
fn build_counts(diags: &[LspDiagnostic]) -> HashMap<DiagKey, usize> {
let mut counts: HashMap<DiagKey, usize> = HashMap::with_capacity(diags.len());
for d in diags {
*counts.entry(diag_key(d)).or_insert(0) += 1;
}
counts
}
fn collect_introduced(
post: &[LspDiagnostic],
pre_counts: &HashMap<DiagKey, usize>,
post_counts: &HashMap<DiagKey, usize>,
) -> Vec<LspDiagnostic> {
let mut result = Vec::new();
let mut emitted: HashMap<DiagKey, usize> = HashMap::new();
for d in post {
let key = diag_key(d);
let pre = *pre_counts.get(&key).unwrap_or(&0);
let post_count = *post_counts.get(&key).unwrap_or(&0);
let excess = post_count.saturating_sub(pre);
let done = *emitted.get(&key).unwrap_or(&0);
if done < excess {
result.push(d.clone());
*emitted.entry(key).or_insert(0) += 1;
}
}
result
}
fn collect_resolved(
pre: &[LspDiagnostic],
pre_counts: &HashMap<DiagKey, usize>,
post_counts: &HashMap<DiagKey, usize>,
) -> Vec<LspDiagnostic> {
let mut result = Vec::new();
let mut emitted: HashMap<DiagKey, usize> = HashMap::new();
for d in pre {
let key = diag_key(d);
let post = *post_counts.get(&key).unwrap_or(&0);
let pre_count = *pre_counts.get(&key).unwrap_or(&0);
let excess = pre_count.saturating_sub(post);
let done = *emitted.get(&key).unwrap_or(&0);
if done < excess {
result.push(d.clone());
*emitted.entry(key).or_insert(0) += 1;
}
}
result
}
#[cfg(test)]
#[allow(clippy::expect_used)]
mod tests {
use super::*;
use pathfinder_lsp::types::LspDiagnosticSeverity;
fn make_error(msg: &str) -> LspDiagnostic {
LspDiagnostic {
severity: LspDiagnosticSeverity::Error,
code: None,
message: msg.into(),
file: "src/main.rs".into(),
start_line: 1,
end_line: 1,
}
}
fn make_warning(msg: &str) -> LspDiagnostic {
LspDiagnostic {
severity: LspDiagnosticSeverity::Warning,
code: None,
message: msg.into(),
file: "src/main.rs".into(),
start_line: 2,
end_line: 2,
}
}
fn make_error_at(msg: &str, line: u32) -> LspDiagnostic {
LspDiagnostic {
severity: LspDiagnosticSeverity::Error,
code: None,
message: msg.into(),
file: "src/main.rs".into(),
start_line: line,
end_line: line,
}
}
#[test]
fn test_diff_empty_pre_and_post() {
let diff = diff_diagnostics(&[], &[]);
assert!(diff.introduced.is_empty());
assert!(diff.resolved.is_empty());
assert!(!diff.has_new_errors());
}
#[test]
fn test_diff_no_change() {
let pre = vec![make_error("type mismatch")];
let post = vec![make_error("type mismatch")];
let diff = diff_diagnostics(&pre, &post);
assert!(
diff.introduced.is_empty(),
"same error should not appear as introduced"
);
assert!(
diff.resolved.is_empty(),
"same error should not appear as resolved"
);
}
#[test]
fn test_diff_new_error_detected() {
let pre = vec![];
let post = vec![make_error("type mismatch")];
let diff = diff_diagnostics(&pre, &post);
assert_eq!(diff.introduced.len(), 1);
assert_eq!(diff.introduced[0].message, "type mismatch");
assert!(diff.has_new_errors());
assert!(diff.resolved.is_empty());
}
#[test]
fn test_diff_resolved_error_detected() {
let pre = vec![make_error("type mismatch")];
let post = vec![];
let diff = diff_diagnostics(&pre, &post);
assert!(diff.introduced.is_empty());
assert_eq!(diff.resolved.len(), 1);
assert_eq!(diff.resolved[0].message, "type mismatch");
assert!(!diff.has_new_errors());
}
#[test]
fn test_diff_excludes_line_column() {
let pre = vec![make_error_at("type mismatch", 5)];
let post = vec![make_error_at("type mismatch", 20)]; let diff = diff_diagnostics(&pre, &post);
assert!(
diff.introduced.is_empty(),
"shifted error should not appear as introduced"
);
assert!(
diff.resolved.is_empty(),
"shifted error should not appear as resolved"
);
}
#[test]
fn test_diff_multiset_counting() {
let pre = vec![make_error("duplicate"), make_error("duplicate")];
let post = vec![
make_error("duplicate"),
make_error("duplicate"),
make_error("duplicate"),
];
let diff = diff_diagnostics(&pre, &post);
assert_eq!(diff.introduced.len(), 1);
assert!(diff.resolved.is_empty());
}
#[test]
fn test_diff_warning_does_not_block() {
let pre = vec![];
let post = vec![make_warning("unused variable")];
let diff = diff_diagnostics(&pre, &post);
assert_eq!(diff.introduced.len(), 1);
assert!(!diff.has_new_errors()); }
#[test]
fn test_diff_mixed_introduced_and_resolved() {
let pre = vec![make_error("old error")];
let post = vec![make_error("new error")];
let diff = diff_diagnostics(&pre, &post);
assert_eq!(diff.introduced.len(), 1);
assert_eq!(diff.introduced[0].message, "new error");
assert_eq!(diff.resolved.len(), 1);
assert_eq!(diff.resolved[0].message, "old error");
assert!(diff.has_new_errors());
}
}