use std::fmt::{self, Write};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum DiagnosticSeverity {
Info,
Warning,
Error,
}
impl fmt::Display for DiagnosticSeverity {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
DiagnosticSeverity::Info => write!(f, "INFO"),
DiagnosticSeverity::Warning => write!(f, "WARN"),
DiagnosticSeverity::Error => write!(f, "ERROR"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum DiagnosticCategory {
Heap,
Table,
CustomAttribute,
Signature,
Type,
Method,
Field,
Validation,
General,
}
impl fmt::Display for DiagnosticCategory {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
DiagnosticCategory::Heap => write!(f, "Heap"),
DiagnosticCategory::Table => write!(f, "Table"),
DiagnosticCategory::CustomAttribute => write!(f, "CustomAttribute"),
DiagnosticCategory::Signature => write!(f, "Signature"),
DiagnosticCategory::Type => write!(f, "Type"),
DiagnosticCategory::Method => write!(f, "Method"),
DiagnosticCategory::Field => write!(f, "Field"),
DiagnosticCategory::Validation => write!(f, "Validation"),
DiagnosticCategory::General => write!(f, "General"),
}
}
}
#[derive(Debug, Clone)]
pub struct Diagnostic {
pub severity: DiagnosticSeverity,
pub category: DiagnosticCategory,
pub message: String,
pub offset: Option<u64>,
pub token: Option<u32>,
pub table_row: Option<(u8, u32)>,
}
impl Diagnostic {
pub fn new(
severity: DiagnosticSeverity,
category: DiagnosticCategory,
message: impl Into<String>,
) -> Self {
Self {
severity,
category,
message: message.into(),
offset: None,
token: None,
table_row: None,
}
}
#[must_use]
pub fn with_offset(mut self, offset: u64) -> Self {
self.offset = Some(offset);
self
}
#[must_use]
pub fn with_token(mut self, token: u32) -> Self {
self.token = Some(token);
self
}
#[must_use]
pub fn with_table_row(mut self, table_id: u8, row_index: u32) -> Self {
self.table_row = Some((table_id, row_index));
self
}
}
impl fmt::Display for Diagnostic {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "[{}] {}: {}", self.severity, self.category, self.message)?;
if let Some(offset) = self.offset {
write!(f, " (offset: 0x{offset:08x})")?;
}
if let Some(token) = self.token {
write!(f, " (token: 0x{token:08x})")?;
}
if let Some((table_id, row)) = self.table_row {
write!(f, " (table: 0x{table_id:02x}, row: {row})")?;
}
Ok(())
}
}
#[derive(Debug)]
pub struct Diagnostics {
entries: boxcar::Vec<Diagnostic>,
}
impl Default for Diagnostics {
fn default() -> Self {
Self::new()
}
}
impl Diagnostics {
#[must_use]
pub fn new() -> Self {
Self {
entries: boxcar::Vec::new(),
}
}
pub fn info(&self, category: DiagnosticCategory, message: impl Into<String>) {
self.push(Diagnostic::new(DiagnosticSeverity::Info, category, message));
}
pub fn warning(&self, category: DiagnosticCategory, message: impl Into<String>) {
self.push(Diagnostic::new(
DiagnosticSeverity::Warning,
category,
message,
));
}
pub fn error(&self, category: DiagnosticCategory, message: impl Into<String>) {
self.push(Diagnostic::new(
DiagnosticSeverity::Error,
category,
message,
));
}
pub fn push(&self, diagnostic: Diagnostic) {
self.entries.push(diagnostic);
}
pub fn has_any(&self) -> bool {
self.entries.count() > 0
}
pub fn has_errors(&self) -> bool {
self.entries
.iter()
.any(|(_, d)| d.severity == DiagnosticSeverity::Error)
}
pub fn has_warnings(&self) -> bool {
self.entries
.iter()
.any(|(_, d)| d.severity == DiagnosticSeverity::Warning)
}
pub fn count(&self) -> usize {
self.entries.count()
}
pub fn error_count(&self) -> usize {
self.entries
.iter()
.filter(|(_, d)| d.severity == DiagnosticSeverity::Error)
.count()
}
pub fn warning_count(&self) -> usize {
self.entries
.iter()
.filter(|(_, d)| d.severity == DiagnosticSeverity::Warning)
.count()
}
pub fn info_count(&self) -> usize {
self.entries
.iter()
.filter(|(_, d)| d.severity == DiagnosticSeverity::Info)
.count()
}
pub fn iter(&self) -> impl Iterator<Item = &Diagnostic> {
self.entries.iter().map(|(_, d)| d)
}
pub fn errors(&self) -> Vec<&Diagnostic> {
self.entries
.iter()
.filter(|(_, d)| d.severity == DiagnosticSeverity::Error)
.map(|(_, d)| d)
.collect()
}
pub fn warnings(&self) -> Vec<&Diagnostic> {
self.entries
.iter()
.filter(|(_, d)| d.severity == DiagnosticSeverity::Warning)
.map(|(_, d)| d)
.collect()
}
pub fn by_category(&self, category: DiagnosticCategory) -> Vec<&Diagnostic> {
self.entries
.iter()
.filter(|(_, d)| d.category == category)
.map(|(_, d)| d)
.collect()
}
pub fn summary(&self) -> String {
let mut output = String::new();
let _ = writeln!(
output,
"Diagnostics: {} error(s), {} warning(s), {} info(s)",
self.error_count(),
self.warning_count(),
self.info_count()
);
if self.error_count() > 0 {
output.push_str("\nErrors:\n");
for diag in self.errors() {
let _ = writeln!(output, " {diag}");
}
}
if self.warning_count() > 0 {
output.push_str("\nWarnings:\n");
for diag in self.warnings() {
let _ = writeln!(output, " {diag}");
}
}
output
}
}
impl fmt::Display for Diagnostics {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.summary())
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::Arc;
use std::thread;
#[test]
fn test_diagnostic_creation() {
let diag = Diagnostic::new(
DiagnosticSeverity::Warning,
DiagnosticCategory::Heap,
"Test message",
);
assert_eq!(diag.severity, DiagnosticSeverity::Warning);
assert_eq!(diag.category, DiagnosticCategory::Heap);
assert_eq!(diag.message, "Test message");
assert!(diag.offset.is_none());
assert!(diag.token.is_none());
assert!(diag.table_row.is_none());
}
#[test]
fn test_diagnostic_with_context() {
let diag = Diagnostic::new(
DiagnosticSeverity::Error,
DiagnosticCategory::Table,
"Invalid row",
)
.with_offset(0x1000)
.with_token(0x06000001)
.with_table_row(0x06, 1);
assert_eq!(diag.offset, Some(0x1000));
assert_eq!(diag.token, Some(0x06000001));
assert_eq!(diag.table_row, Some((0x06, 1)));
}
#[test]
fn test_diagnostics_container() {
let diagnostics = Diagnostics::new();
diagnostics.info(DiagnosticCategory::General, "Info message");
diagnostics.warning(DiagnosticCategory::Heap, "Warning message");
diagnostics.error(DiagnosticCategory::Table, "Error message");
assert_eq!(diagnostics.count(), 3);
assert_eq!(diagnostics.error_count(), 1);
assert_eq!(diagnostics.warning_count(), 1);
assert_eq!(diagnostics.info_count(), 1);
assert!(diagnostics.has_errors());
assert!(diagnostics.has_warnings());
assert!(diagnostics.has_any());
}
#[test]
fn test_diagnostics_thread_safety() {
let diagnostics = Arc::new(Diagnostics::new());
let mut handles = vec![];
for i in 0..10 {
let diag_clone = Arc::clone(&diagnostics);
handles.push(thread::spawn(move || {
diag_clone.warning(DiagnosticCategory::General, format!("Thread {} warning", i));
}));
}
for handle in handles {
handle.join().unwrap();
}
assert_eq!(diagnostics.count(), 10);
}
#[test]
fn test_diagnostics_by_category() {
let diagnostics = Diagnostics::new();
diagnostics.error(DiagnosticCategory::Heap, "Heap error 1");
diagnostics.error(DiagnosticCategory::Heap, "Heap error 2");
diagnostics.error(DiagnosticCategory::Table, "Table error");
diagnostics.warning(DiagnosticCategory::Heap, "Heap warning");
let heap_diags = diagnostics.by_category(DiagnosticCategory::Heap);
assert_eq!(heap_diags.len(), 3);
let table_diags = diagnostics.by_category(DiagnosticCategory::Table);
assert_eq!(table_diags.len(), 1);
}
#[test]
fn test_diagnostic_display() {
let diag = Diagnostic::new(
DiagnosticSeverity::Warning,
DiagnosticCategory::CustomAttribute,
"Parse failed",
)
.with_offset(0x1234)
.with_token(0x0A000005);
let display = format!("{}", diag);
assert!(display.contains("WARN"));
assert!(display.contains("CustomAttribute"));
assert!(display.contains("Parse failed"));
assert!(display.contains("0x00001234"));
assert!(display.contains("0x0a000005"));
}
}