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 should_report(&self, code: DiagCode) -> bool {
let default_sev = code.severity();
let effective_sev = self.overrides.get(&code).copied().unwrap_or(default_sev);
if effective_sev <= Severity::Fatal {
return true;
}
let code_str = code.as_code();
if self
.ignore
.iter()
.any(|pattern| match_glob(pattern, code_str))
{
return false;
}
match self.max_reported_severity() {
Some(max) => effective_sev <= max,
None => false,
}
}
pub fn should_fail(&self, sev: Severity) -> bool {
sev <= 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(DiagCode::ParseError));
assert!(config.should_report(DiagCode::MacroNotImported));
assert!(!config.should_report(DiagCode::IdentifierUnderscore));
}
#[test]
fn should_report_silent() {
let config = DiagnosticConfig::silent();
assert!(!config.should_report(DiagCode::ImportNotFound));
assert!(!config.should_report(DiagCode::IdentifierUnderscore));
}
#[test]
fn should_report_verbose() {
let config = DiagnosticConfig::verbose();
assert!(config.should_report(DiagCode::ParseError));
assert!(config.should_report(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));
}
}