use std::collections::HashMap;
use std::sync::Mutex;
use std::sync::atomic::{AtomicBool, Ordering};
static QUIET: AtomicBool = AtomicBool::new(false);
static SUPPRESSED: Mutex<Option<HashMap<String, usize>>> = Mutex::new(None);
pub fn init(quiet: bool) {
QUIET.store(quiet, Ordering::Relaxed);
if let Ok(mut guard) = SUPPRESSED.lock()
&& guard.is_none()
{
*guard = Some(HashMap::new());
}
}
pub fn warn(msg: impl AsRef<str>) {
if QUIET.load(Ordering::Relaxed) {
return;
}
let msg = msg.as_ref();
if let Ok(mut guard) = SUPPRESSED.lock()
&& let Some(ref mut map) = *guard
{
if let Some(count) = map.get_mut(msg) {
*count += 1;
return;
}
map.insert(msg.to_owned(), 1);
}
eprintln!("warning: {msg}");
}
#[cfg(test)]
pub fn reset_for_test() {
QUIET.store(false, Ordering::Relaxed);
if let Ok(mut guard) = SUPPRESSED.lock() {
*guard = None;
}
}
#[cfg(test)]
pub fn suppressed_count_for(msg: &str) -> usize {
SUPPRESSED
.lock()
.ok()
.and_then(|g| g.as_ref().and_then(|m| m.get(msg).copied()))
.map_or(0, |seen| seen.saturating_sub(1))
}
#[cfg(test)]
pub fn was_emitted(msg: &str) -> bool {
SUPPRESSED
.lock()
.ok()
.and_then(|g| g.as_ref().map(|m| m.contains_key(msg)))
.unwrap_or(false)
}
#[cfg(test)]
pub fn total_suppressed() -> usize {
SUPPRESSED
.lock()
.ok()
.and_then(|g| {
g.as_ref().map(|m| {
m.values()
.map(|&seen| seen.saturating_sub(1))
.sum::<usize>()
})
})
.unwrap_or(0)
}
pub fn warn_glob_dir_overlap(dir: &std::path::Path, globs: &[String], matched_count: usize) {
if matched_count > 0 || globs.is_empty() {
return;
}
let dir_str = dir.to_string_lossy().replace('\\', "/");
let dir_str = dir_str
.strip_prefix("./")
.unwrap_or(&dir_str)
.trim_end_matches('/');
if dir_str == "." || dir_str.is_empty() {
return;
}
for glob in globs {
if glob.starts_with('!') {
continue;
}
let glob_norm = glob.replace('\\', "/");
let full_prefix = format!("{dir_str}/");
if let Some(rest) = glob_norm.strip_prefix(full_prefix.as_str())
&& !rest.is_empty()
{
warn(format!(
"glob '{glob}' matched 0 files. Globs are relative to --dir '{dir_str}'. \
Did you mean '{rest}'?"
));
return;
}
if let Some(last_component) = dir.file_name().and_then(|n| n.to_str()) {
let component_prefix = format!("{last_component}/");
if let Some(rest) = glob_norm.strip_prefix(component_prefix.as_str())
&& !rest.is_empty()
{
warn(format!(
"glob '{glob}' matched 0 files. Globs are relative to --dir '{dir_str}'. \
Did you mean '{rest}'?"
));
return;
}
}
}
}
pub fn flush_summary() {
let total_suppressed: usize = match SUPPRESSED.lock() {
Ok(guard) => guard.as_ref().map_or(0, |map| {
map.values()
.map(|&seen| seen.saturating_sub(1))
.sum()
}),
Err(_) => return,
};
if !QUIET.load(Ordering::Relaxed) && total_suppressed > 0 {
eprintln!("warning: {total_suppressed} additional identical warning(s) suppressed");
}
}
#[cfg(test)]
pub(crate) static WARN_TEST_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn dedup_first_occurrence_not_suppressed() {
let _guard = super::WARN_TEST_LOCK.lock().unwrap();
reset_for_test();
init(false);
warn("msg-dedup-first");
assert_eq!(suppressed_count_for("msg-dedup-first"), 0);
}
#[test]
fn dedup_second_occurrence_counted() {
let _guard = super::WARN_TEST_LOCK.lock().unwrap();
reset_for_test();
init(false);
warn("msg-dedup-second");
warn("msg-dedup-second");
assert_eq!(suppressed_count_for("msg-dedup-second"), 1);
}
#[test]
fn dedup_many_occurrences_all_counted() {
let _guard = super::WARN_TEST_LOCK.lock().unwrap();
reset_for_test();
init(false);
for _ in 0..5 {
warn("msg-dedup-many");
}
assert_eq!(suppressed_count_for("msg-dedup-many"), 4);
assert_eq!(total_suppressed(), 4);
}
#[test]
fn quiet_mode_suppresses_all() {
let _guard = super::WARN_TEST_LOCK.lock().unwrap();
reset_for_test();
init(true);
warn("msg-quiet-a");
warn("msg-quiet-a");
assert_eq!(suppressed_count_for("msg-quiet-a"), 0);
}
#[test]
fn different_messages_not_deduped() {
let _guard = super::WARN_TEST_LOCK.lock().unwrap();
reset_for_test();
init(false);
warn("msg-diff-a");
warn("msg-diff-b");
assert_eq!(suppressed_count_for("msg-diff-a"), 0);
assert_eq!(suppressed_count_for("msg-diff-b"), 0);
assert_eq!(total_suppressed(), 0);
}
#[test]
fn total_suppressed_across_multiple_messages() {
let _guard = super::WARN_TEST_LOCK.lock().unwrap();
reset_for_test();
init(false);
for _ in 0..3 {
warn("msg-total-x");
}
for _ in 0..2 {
warn("msg-total-y");
}
assert_eq!(total_suppressed(), 3);
}
#[test]
fn glob_overlap_no_warning_when_results_found() {
let _guard = super::WARN_TEST_LOCK.lock().unwrap();
reset_for_test();
init(false);
let dir = std::path::Path::new("files/en-us");
warn_glob_dir_overlap(dir, &["files/en-us/web/**".to_owned()], 5);
assert!(!was_emitted(
"glob 'files/en-us/web/**' matched 0 files. Globs are relative to --dir 'files/en-us'. Did you mean 'web/**'?"
));
}
#[test]
fn glob_overlap_no_warning_when_dir_is_dot() {
let _guard = super::WARN_TEST_LOCK.lock().unwrap();
reset_for_test();
init(false);
let dir = std::path::Path::new(".");
warn_glob_dir_overlap(dir, &["web/**".to_owned()], 0);
assert_eq!(total_suppressed(), 0);
}
#[test]
fn glob_overlap_warns_on_full_dir_prefix() {
let _guard = super::WARN_TEST_LOCK.lock().unwrap();
reset_for_test();
init(false);
let dir = std::path::Path::new("files/en-us");
warn_glob_dir_overlap(dir, &["files/en-us/web/css/**".to_owned()], 0);
assert!(was_emitted(
"glob 'files/en-us/web/css/**' matched 0 files. Globs are relative to --dir 'files/en-us'. Did you mean 'web/css/**'?"
));
}
#[test]
fn glob_overlap_warns_on_last_component() {
let _guard = super::WARN_TEST_LOCK.lock().unwrap();
reset_for_test();
init(false);
let dir = std::path::Path::new("files/en-us");
warn_glob_dir_overlap(dir, &["en-us/web/**".to_owned()], 0);
assert!(was_emitted(
"glob 'en-us/web/**' matched 0 files. Globs are relative to --dir 'files/en-us'. Did you mean 'web/**'?"
));
}
#[test]
fn glob_overlap_no_false_positive_on_partial_prefix() {
let _guard = super::WARN_TEST_LOCK.lock().unwrap();
reset_for_test();
init(false);
let dir = std::path::Path::new("vault/notes");
warn_glob_dir_overlap(dir, &["notes-archive/**".to_owned()], 0);
assert_eq!(total_suppressed(), 0);
}
#[test]
fn glob_overlap_no_false_positive_on_partial_dir_prefix() {
let _guard = super::WARN_TEST_LOCK.lock().unwrap();
reset_for_test();
init(false);
let dir = std::path::Path::new("files/en-us");
warn_glob_dir_overlap(dir, &["files/en-us-old/**".to_owned()], 0);
assert_eq!(total_suppressed(), 0);
}
#[test]
fn glob_overlap_skips_negation_patterns() {
let _guard = super::WARN_TEST_LOCK.lock().unwrap();
reset_for_test();
init(false);
let dir = std::path::Path::new("docs");
warn_glob_dir_overlap(dir, &["!docs/drafts/**".to_owned()], 0);
assert_eq!(total_suppressed(), 0);
}
#[test]
fn glob_overlap_no_warning_when_globs_empty() {
let _guard = super::WARN_TEST_LOCK.lock().unwrap();
reset_for_test();
init(false);
let dir = std::path::Path::new("docs");
warn_glob_dir_overlap(dir, &[], 0);
assert_eq!(total_suppressed(), 0);
}
}