use std::collections::{BTreeMap, BTreeSet, btree_map::Entry};
use crate::{DiagnosticSeverity, DiagnosticStage, ErrorCategory, code_is_well_formed};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum CodeStatus {
Active,
Retired {
since: &'static str,
},
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct DiagnosticInfo {
pub code: &'static str,
pub severity: DiagnosticSeverity,
pub category: Option<ErrorCategory>,
pub summary: &'static str,
pub status: CodeStatus,
}
impl DiagnosticInfo {
#[must_use]
pub const fn new(
code: &'static str,
severity: DiagnosticSeverity,
summary: &'static str,
) -> Self {
Self {
code,
severity,
category: None,
summary,
status: CodeStatus::Active,
}
}
#[must_use]
pub const fn with_category(mut self, category: ErrorCategory) -> Self {
self.category = Some(category);
self
}
#[must_use]
pub const fn retired(mut self, since: &'static str) -> Self {
self.status = CodeStatus::Retired { since };
self
}
#[must_use]
pub fn namespace(&self) -> &'static str {
self.code.split('.').next().unwrap_or("")
}
#[must_use]
pub fn stage(&self) -> Option<DiagnosticStage> {
DiagnosticStage::from_namespace(self.namespace())
}
}
#[macro_export]
macro_rules! diagnostic_codes {
($(
$(#[$attr:meta])*
$name:ident = $code:literal, $severity:ident, $summary:literal
$(, category = $category:ident)?
$(, retired = $since:literal)? ;
)*) => {
$(
$(#[$attr])*
pub const $name: $crate::DiagnosticInfo = $crate::DiagnosticInfo::new(
$code,
$crate::DiagnosticSeverity::$severity,
$summary,
)
$(.with_category($crate::ErrorCategory::$category))?
$(.retired($since))?;
)*
pub const ALL: &[&$crate::DiagnosticInfo] = &[$(&$name),*];
};
}
pub fn check_registry<'a, I>(entries: I) -> Vec<String>
where
I: IntoIterator<Item = &'a DiagnosticInfo>,
{
let mut problems = Vec::new();
let mut seen: BTreeSet<&'static str> = BTreeSet::new();
for entry in entries {
let retired = matches!(entry.status, CodeStatus::Retired { .. });
if !retired && !code_is_well_formed(entry.code) {
problems.push(format!("{}: does not match the code grammar", entry.code));
} else if !retired && entry.stage().is_none() {
problems.push(format!(
"{}: namespace {} is not one of the ten",
entry.code,
entry.namespace()
));
}
if entry.summary.is_empty() {
problems.push(format!("{}: has no summary", entry.code));
}
if !seen.insert(entry.code) {
problems.push(format!("{}: registered twice", entry.code));
}
}
problems
}
pub fn check_scope_ownership(registries: &[(&str, &[&DiagnosticInfo])]) -> Vec<String> {
let mut owner: BTreeMap<(&str, &str), &str> = BTreeMap::new();
let mut problems = Vec::new();
for (crate_name, entries) in registries {
for entry in *entries {
if matches!(entry.status, CodeStatus::Retired { .. }) {
continue;
}
let mut segments = entry.code.split('.');
let (Some(namespace), Some(scope)) = (segments.next(), segments.next()) else {
continue;
};
match owner.entry((namespace, scope)) {
Entry::Vacant(slot) => {
slot.insert(crate_name);
}
Entry::Occupied(slot) if *slot.get() != *crate_name => problems.push(format!(
"{namespace}.{scope}: claimed by both {} and {crate_name}",
slot.get()
)),
Entry::Occupied(_) => {}
}
}
}
problems
}
#[cfg(test)]
mod tests {
use super::*;
const GOOD: DiagnosticInfo = DiagnosticInfo::new(
"EMIT.PSSE.FIELD_DROPPED",
DiagnosticSeverity::Warning,
"a field with no PSS/E record was dropped",
);
#[test]
fn a_sound_registry_reports_nothing() {
let other = DiagnosticInfo::new(
"READ.DSS.INCLUDE_REFUSED",
DiagnosticSeverity::Error,
"an include escaping the case directory was refused",
)
.with_category(ErrorCategory::Io);
assert_eq!(check_registry([&GOOD, &other]), Vec::<String>::new());
assert_eq!(GOOD.stage(), Some(DiagnosticStage::Emit));
assert_eq!(GOOD.status, CodeStatus::Active);
}
#[test]
fn the_check_names_each_way_a_registry_goes_wrong() {
let malformed = DiagnosticInfo::new("emit.psse.dropped", DiagnosticSeverity::Info, "s");
let unknown_namespace =
DiagnosticInfo::new("FIDELITY.PSSE.DROPPED", DiagnosticSeverity::Info, "s");
let no_summary = DiagnosticInfo::new("EMIT.PSSE.DEFAULTED", DiagnosticSeverity::Info, "");
let problems = check_registry([&GOOD, &GOOD, &malformed, &unknown_namespace, &no_summary]);
assert_eq!(problems.len(), 4, "{problems:?}");
assert!(problems.iter().any(|p| p.contains("registered twice")));
assert!(problems.iter().any(|p| p.contains("code grammar")));
assert!(problems.iter().any(|p| p.contains("not one of the ten")));
assert!(problems.iter().any(|p| p.contains("no summary")));
}
#[test]
fn two_crates_cannot_claim_one_scope() {
const OTHER: DiagnosticInfo = DiagnosticInfo::new(
"EMIT.PSSE.DOWNGRADED",
DiagnosticSeverity::Warning,
"a newer revision was written into an older layout",
);
const ELSEWHERE: DiagnosticInfo = DiagnosticInfo::new(
"EMIT.BMOPF.TRANSFORMER_UNSUPPORTED",
DiagnosticSeverity::Warning,
"a transformer the BMOPF schema cannot state",
);
assert!(
check_scope_ownership(&[
("powerio", &[&GOOD, &OTHER]),
("powerio-dist", &[&ELSEWHERE])
])
.is_empty()
);
let problems = check_scope_ownership(&[("powerio", &[&GOOD]), ("powerio-dist", &[&OTHER])]);
assert_eq!(problems.len(), 1);
assert!(problems[0].contains("EMIT.PSSE"), "{problems:?}");
}
#[test]
fn a_retired_entry_records_when_it_stopped_being_emitted() {
const RETIRED: DiagnosticInfo = DiagnosticInfo::new(
"READ.DIST.PARSE_WARNING",
DiagnosticSeverity::Warning,
"a distribution parse warning with no identity of its own",
)
.retired("0.9.0");
assert_eq!(RETIRED.status, CodeStatus::Retired { since: "0.9.0" });
assert!(check_registry([&RETIRED]).is_empty());
}
}