use std::collections::HashSet;
use std::sync::{Mutex, OnceLock, PoisonError};
static SOURCE: OnceLock<Mutex<String>> = OnceLock::new();
const UNNAMED_SOURCE: &str = "<xml>";
fn source_cell() -> &'static Mutex<String> {
SOURCE.get_or_init(|| Mutex::new(String::from(UNNAMED_SOURCE)))
}
pub(crate) fn set_source_name(name: String) {
*source_cell().lock().unwrap_or_else(PoisonError::into_inner) = name;
}
pub(crate) fn source_name() -> String {
source_cell()
.lock()
.unwrap_or_else(PoisonError::into_inner)
.clone()
}
pub(crate) struct WarnState {
seen: std::cell::RefCell<HashSet<String>>,
}
impl WarnState {
pub(crate) fn new() -> Self {
Self {
seen: std::cell::RefCell::new(HashSet::new()),
}
}
}
pub(crate) fn warn_once(message: &str, node: Option<roxmltree::Node<'_, '_>>, state: &WarnState) {
let dedup_key = if let Some(n) = node {
format!("{}:{}", n.range().start, message)
} else {
message.to_string()
};
if state.seen.borrow_mut().insert(dedup_key) {
if let Some(n) = node {
let pos = n.range().start;
let text = n.document().input_text();
let line = text[..pos].matches('\n').count() + 1;
let last_nl = text[..pos].rfind('\n').map(|i| i + 1).unwrap_or(0);
let col = pos - last_nl + 1;
let line_end = text[pos..]
.find('\n')
.map(|i| pos + i)
.unwrap_or(text.len());
let snippet = text[last_nl..line_end].trim();
eprintln!(
"{}:{}:{}: {message}\n |\n | {snippet}\n |",
source_name(),
line,
col,
);
} else {
eprintln!("warning: {message}");
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn set_source_name_is_what_warnings_report() -> Result<(), Box<dyn std::error::Error>> {
let previous = source_name();
set_source_name("orderbook-schema.xml".into());
assert_eq!(
source_name(),
"orderbook-schema.xml",
"warnings must name the file that was actually parsed"
);
assert_ne!(
source_name(),
UNNAMED_SOURCE,
"the placeholder means the setter never reached the cell the getter reads"
);
set_source_name(previous);
Ok(())
}
}