use std::collections::HashMap;
use std::fmt;
use super::{DiagCode, ReportingLevel, Severity};
#[derive(Debug, Clone)]
pub struct Diagnostic {
pub severity: Severity,
pub code: DiagCode,
pub message: String,
pub module: Option<String>,
pub line: Option<usize>,
pub column: Option<usize>,
}
impl fmt::Display for Diagnostic {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "[{}]", self.severity)?;
if let Some(module) = &self.module {
write!(f, " {module}")?;
if let Some(line) = self.line {
write!(f, ":{line}")?;
if let Some(col) = self.column {
write!(f, ":{col}")?;
}
}
write!(f, ":")?;
}
write!(f, " {}", self.message)
}
}
#[derive(Debug, Clone)]
pub struct DiagnosticConfig {
pub reporting: ReportingLevel,
pub fail_at: Severity,
pub overrides: HashMap<DiagCode, Severity>,
pub ignore: Vec<String>,
}
impl Default for DiagnosticConfig {
fn default() -> Self {
DiagnosticConfig {
reporting: ReportingLevel::Default,
fail_at: Severity::Severe,
overrides: HashMap::new(),
ignore: Vec::new(),
}
}
}
impl DiagnosticConfig {
pub fn for_reporting(level: ReportingLevel) -> Self {
match level {
ReportingLevel::Verbose => Self::verbose(),
ReportingLevel::Default => Self::default(),
ReportingLevel::Quiet => Self::quiet(),
ReportingLevel::Silent => Self::silent(),
}
}
pub fn verbose() -> Self {
DiagnosticConfig {
reporting: ReportingLevel::Verbose,
fail_at: Severity::Severe,
overrides: HashMap::new(),
ignore: Vec::new(),
}
}
pub fn quiet() -> Self {
DiagnosticConfig {
reporting: ReportingLevel::Quiet,
fail_at: Severity::Severe,
overrides: HashMap::new(),
ignore: Vec::new(),
}
}
pub fn silent() -> Self {
DiagnosticConfig {
reporting: ReportingLevel::Silent,
fail_at: Severity::Fatal,
overrides: HashMap::new(),
ignore: Vec::new(),
}
}
pub fn effective_severity(&self, code: DiagCode) -> Severity {
self.overrides
.get(&code)
.copied()
.unwrap_or_else(|| code.severity())
}
pub fn is_ignored(&self, code: DiagCode) -> bool {
let code_str = code.as_code();
self.ignore
.iter()
.any(|pattern| match_glob(pattern, code_str))
}
pub fn should_report(&self, severity: Severity) -> bool {
severity == Severity::Fatal
|| self
.max_reported_severity()
.is_some_and(|max| severity <= max)
}
pub fn should_collect(&self, code: DiagCode) -> bool {
let effective_severity = self.effective_severity(code);
if effective_severity == Severity::Fatal {
return true;
}
if self.is_ignored(code) {
return false;
}
self.should_report(code.severity()) || self.should_report(effective_severity)
}
pub fn should_fail(&self, severity: Severity) -> bool {
severity <= self.fail_at
}
fn max_reported_severity(&self) -> Option<Severity> {
match self.reporting {
ReportingLevel::Verbose => Some(Severity::Info),
ReportingLevel::Default => Some(Severity::Minor),
ReportingLevel::Quiet => Some(Severity::Error),
ReportingLevel::Silent => None,
}
}
}
fn match_glob(pattern: &str, s: &str) -> bool {
glob_match(pattern.as_bytes(), s.as_bytes())
}
fn glob_match(pattern: &[u8], s: &[u8]) -> bool {
let mut pi = 0;
let mut si = 0;
let mut star_pi: Option<usize> = None;
let mut star_si = 0;
while si < s.len() {
if pi < pattern.len() && (pattern[pi] == b'?' || pattern[pi] == s[si]) {
pi += 1;
si += 1;
} else if pi < pattern.len() && pattern[pi] == b'*' {
star_pi = Some(pi);
star_si = si;
pi += 1;
} else if let Some(sp) = star_pi {
pi = sp + 1;
star_si += 1;
si = star_si;
} else {
return false;
}
}
while pi < pattern.len() && pattern[pi] == b'*' {
pi += 1;
}
pi == pattern.len()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn diagnostic_display() {
let d = Diagnostic {
severity: Severity::Error,
code: DiagCode::ImportNotFound,
message: "symbol foo not found".to_string(),
module: Some("IF-MIB".to_string()),
line: Some(42),
column: Some(5),
};
assert_eq!(d.to_string(), "[error] IF-MIB:42:5: symbol foo not found");
}
#[test]
fn diagnostic_display_no_location() {
let d = Diagnostic {
severity: Severity::Warning,
code: DiagCode::ImportUnused,
message: "unused import".to_string(),
module: None,
line: None,
column: None,
};
assert_eq!(d.to_string(), "[warning] unused import");
}
#[test]
fn glob_matching() {
assert!(match_glob("identifier-*", "identifier-underscore"));
assert!(match_glob("identifier-*", "identifier-length-32"));
assert!(!match_glob("identifier-*", "import-not-found"));
assert!(match_glob("*", "anything"));
assert!(match_glob("exact-match", "exact-match"));
assert!(!match_glob("exact-match", "exact-mismatch"));
}
#[test]
fn should_report_respects_level() {
let config = DiagnosticConfig::default();
assert!(config.should_report(Severity::Error));
assert!(config.should_report(Severity::Minor));
assert!(!config.should_report(Severity::Style));
}
#[test]
fn should_report_silent() {
let config = DiagnosticConfig::silent();
assert!(config.should_report(Severity::Fatal));
assert!(!config.should_report(Severity::Error));
assert!(!config.should_report(Severity::Style));
}
#[test]
fn should_report_verbose() {
let config = DiagnosticConfig::verbose();
assert!(config.should_report(Severity::Error));
assert!(config.should_report(Severity::Style));
}
#[test]
fn effective_severity_applies_override() {
let mut config = DiagnosticConfig::default();
config
.overrides
.insert(DiagCode::MacroNotImported, Severity::Severe);
assert_eq!(
config.effective_severity(DiagCode::MacroNotImported),
Severity::Severe
);
assert_eq!(
config.effective_severity(DiagCode::ParseError),
Severity::Error
);
}
#[test]
fn promotion_affects_collection() {
let mut config = DiagnosticConfig::default();
config
.overrides
.insert(DiagCode::IdentifierUnderscore, Severity::Minor);
assert!(config.should_collect(DiagCode::IdentifierUnderscore));
}
#[test]
fn demotion_does_not_discard_collected_diagnostic() {
let mut config = DiagnosticConfig::quiet();
config
.overrides
.insert(DiagCode::ParseError, Severity::Info);
assert!(config.should_collect(DiagCode::ParseError));
}
#[test]
fn ignore_suppresses_nonfatal_diagnostic() {
let mut config = DiagnosticConfig::verbose();
config.ignore.push("parse-*".to_string());
assert!(config.is_ignored(DiagCode::ParseError));
assert!(!config.should_collect(DiagCode::ParseError));
}
#[test]
fn effective_fatal_is_always_collected() {
let mut config = DiagnosticConfig::silent();
config
.overrides
.insert(DiagCode::IdentifierUnderscore, Severity::Fatal);
config.ignore.push("identifier-*".to_string());
assert!(config.is_ignored(DiagCode::IdentifierUnderscore));
assert!(config.should_collect(DiagCode::IdentifierUnderscore));
}
#[test]
fn should_fail_threshold() {
let config = DiagnosticConfig::default();
assert!(config.should_fail(Severity::Fatal));
assert!(config.should_fail(Severity::Severe));
assert!(!config.should_fail(Severity::Error));
}
#[test]
fn for_reporting_presets() {
let verbose = DiagnosticConfig::for_reporting(ReportingLevel::Verbose);
assert!(matches!(verbose.reporting, ReportingLevel::Verbose));
let silent = DiagnosticConfig::for_reporting(ReportingLevel::Silent);
assert!(matches!(silent.reporting, ReportingLevel::Silent));
assert!(matches!(silent.fail_at, Severity::Fatal));
}
}