use std::collections::HashSet;
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
use std::sync::Mutex;
static SEEN: Mutex<Option<HashSet<u64>>> = Mutex::new(None);
pub fn reset() {
*SEEN.lock().unwrap_or_else(|e| e.into_inner()) = Some(HashSet::new());
}
fn strip_ansi(s: &str) -> String {
let mut out = String::with_capacity(s.len());
let mut chars = s.chars().peekable();
while let Some(c) = chars.next() {
if c == '\x1b' {
if chars.peek() == Some(&'[') {
chars.next();
for d in chars.by_ref() {
if d.is_ascii_alphabetic() {
break;
}
}
}
continue;
}
out.push(c);
}
out
}
fn is_header(line: &str) -> bool {
let s = strip_ansi(line);
s.starts_with("warning") || s.starts_with("error")
}
fn is_summary(block: &str) -> bool {
let s = strip_ansi(block);
let first = s.lines().next().unwrap_or("");
first.starts_with("warning:") && first.trim_end().ends_with("emitted")
}
pub fn dedupe(text: &str) -> String {
let mut blocks: Vec<String> = Vec::new();
for line in text.split_inclusive('\n') {
if is_header(line) || blocks.is_empty() {
blocks.push(line.to_string());
} else {
blocks.last_mut().unwrap().push_str(line);
}
}
let mut guard = SEEN.lock().unwrap_or_else(|e| e.into_inner());
let seen = guard.get_or_insert_with(HashSet::new);
let mut dropped = 0usize;
let mut out = String::with_capacity(text.len());
for block in blocks {
if block.contains("-->") {
let mut h = DefaultHasher::new();
strip_ansi(&block).hash(&mut h);
if !seen.insert(h.finish()) {
dropped += 1;
continue;
}
out.push_str(&block);
} else if dropped > 0 && is_summary(&block) {
let (first, rest) = block.split_once('\n').unwrap_or((block.as_str(), ""));
out.push_str(first);
out.push_str(&format!(" ({dropped} duplicates)"));
if !rest.is_empty() || block.ends_with('\n') {
out.push('\n');
out.push_str(rest);
}
} else {
out.push_str(&block);
}
}
out
}
#[cfg(test)]
mod tests {
use super::{dedupe, reset};
const WARNING: &str = "warning: unused variable: `x`\n --> src/lib.rs:1:9\n |\n1 | fn f(x: u32) {}\n | ^ help: prefix with an underscore\n |\n = note: `#[warn(unused_variables)]` on by default\n\n";
#[test]
fn a_repeated_diagnostic_is_shown_once_and_counted() {
reset();
let first = dedupe(&format!("{WARNING}warning: 1 warning emitted\n"));
assert_eq!(first.matches("unused variable").count(), 1);
assert!(first.contains("warning: 1 warning emitted\n"));
let second = dedupe(&format!("{WARNING}warning: 1 warning emitted\n"));
assert_eq!(second.matches("unused variable").count(), 0);
assert!(second.contains("warning: 1 warning emitted (1 duplicates)\n"));
let other = dedupe("error[E0425]: cannot find value `y`\n --> src/lib.rs:2:5\n |\n2 | y\n | ^\n\nerror: aborting due to 1 previous error\n");
assert!(other.contains("cannot find value") && other.contains("aborting"));
}
}