use owo_colors::{OwoColorize, Stream, Style};
use std::fmt;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Severity {
Warning,
Info,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum DiagnosticKind {
MalformedTable,
UnclosedCodeBlock,
Other,
}
#[derive(Debug, Clone)]
pub struct Diagnostic {
pub severity: Severity,
pub kind: DiagnosticKind,
pub line: usize,
pub message: String,
pub snippet: Option<String>,
}
impl Diagnostic {
pub fn new(
severity: Severity,
kind: DiagnosticKind,
line: usize,
message: impl Into<String>,
) -> Self {
Self {
severity,
kind,
line,
message: message.into(),
snippet: None,
}
}
#[must_use]
pub fn with_snippet(mut self, snippet: impl Into<String>) -> Self {
self.snippet = Some(snippet.into());
self
}
}
impl fmt::Display for Diagnostic {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let severity_icon = match self.severity {
Severity::Warning => "⚠️",
Severity::Info => "ℹ️",
};
write!(f, "{severity_icon} Line {}: {}", self.line, self.message)?;
if let Some(snippet) = &self.snippet {
write!(f, "\n │ {snippet}")?;
}
Ok(())
}
}
#[derive(Debug, Default, Clone)]
pub struct Diagnostics {
messages: Vec<Diagnostic>,
}
impl Diagnostics {
#[must_use]
pub const fn new() -> Self {
Self {
messages: Vec::new(),
}
}
pub fn add(&mut self, diagnostic: Diagnostic) {
self.messages.push(diagnostic);
}
pub fn warn(&mut self, kind: DiagnosticKind, line: usize, message: impl Into<String>) {
self.add(Diagnostic::new(Severity::Warning, kind, line, message));
}
pub fn info(&mut self, kind: DiagnosticKind, line: usize, message: impl Into<String>) {
self.add(Diagnostic::new(Severity::Info, kind, line, message));
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.messages.is_empty()
}
#[must_use]
pub fn len(&self) -> usize {
self.messages.len()
}
#[must_use]
pub fn messages(&self) -> &[Diagnostic] {
&self.messages
}
#[must_use]
pub fn by_severity(&self, severity: Severity) -> Vec<&Diagnostic> {
self.messages
.iter()
.filter(|d| d.severity == severity)
.collect()
}
pub fn print_to_stderr(&self) {
self.print_to_stderr_impl(false);
}
pub fn print_to_stderr_colored(&self) {
self.print_to_stderr_impl(true);
}
fn print_to_stderr_impl(&self, use_colors: bool) {
if self.is_empty() {
return;
}
let len = self.len();
if use_colors {
let header = format!("{len} issues found:");
let header_style = Style::new().yellow().bold();
eprintln!(
"\n{}",
header.if_supports_color(Stream::Stderr, |text| text.style(header_style))
);
for diagnostic in &self.messages {
Self::print_diagnostic_colored(diagnostic);
}
} else {
eprintln!("\n{len} issues found:");
for diagnostic in &self.messages {
eprintln!("{diagnostic}");
}
}
eprintln!();
}
fn print_diagnostic_colored(diagnostic: &Diagnostic) {
let severity_icon = match diagnostic.severity {
Severity::Warning => "⚠️",
Severity::Info => "ℹ️",
};
let line_text = format!("Line {}", diagnostic.line);
eprint!(
"{} {}: ",
severity_icon,
line_text.if_supports_color(Stream::Stderr, |text| text.dimmed())
);
match diagnostic.severity {
Severity::Warning => eprintln!(
"{}",
diagnostic
.message
.if_supports_color(Stream::Stderr, |text| text.yellow())
),
Severity::Info => eprintln!(
"{}",
diagnostic
.message
.if_supports_color(Stream::Stderr, |text| text.cyan())
),
}
if let Some(snippet) = &diagnostic.snippet {
eprintln!(
" {} {}",
"│".if_supports_color(Stream::Stderr, |text| text.dimmed()),
snippet.if_supports_color(Stream::Stderr, |text| text.dimmed())
);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_diagnostic_creation() {
let diag = Diagnostic::new(
Severity::Warning,
DiagnosticKind::MalformedTable,
42,
"Test message",
);
assert_eq!(diag.line, 42);
assert_eq!(diag.message, "Test message");
}
#[test]
fn test_diagnostics_collection() {
let mut diags = Diagnostics::new();
assert!(diags.is_empty());
diags.warn(DiagnosticKind::MalformedTable, 10, "Test");
assert_eq!(diags.len(), 1);
let warnings = diags.by_severity(Severity::Warning);
assert_eq!(warnings.len(), 1);
}
}