use crate::diagnostics::{Span, ErrorLabel, LabelStyle};
use std::fmt;
pub trait LightweightDiagnostic: std::error::Error {
fn code(&self) -> Option<&str> {
None
}
fn help(&self) -> Option<&str> {
None
}
fn url(&self) -> Option<&str> {
None
}
fn source_code(&self) -> Option<&str> {
None
}
fn labels(&self) -> Vec<DiagnosticLabel> {
Vec::new()
}
fn related(&self) -> Vec<&dyn LightweightDiagnostic> {
Vec::new()
}
fn severity(&self) -> DiagnosticSeverity {
DiagnosticSeverity::Error
}
}
#[derive(Debug, Clone)]
pub struct DiagnosticLabel {
pub span: Span,
pub message: Option<String>,
pub style: DiagnosticLabelStyle,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DiagnosticLabelStyle {
Primary,
Secondary,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DiagnosticSeverity {
Error,
Warning,
Info,
Note,
Help,
}
impl DiagnosticLabel {
pub fn primary(span: Span, message: impl Into<String>) -> Self {
Self {
span,
message: Some(message.into()),
style: DiagnosticLabelStyle::Primary,
}
}
pub fn secondary(span: Span, message: impl Into<String>) -> Self {
Self {
span,
message: Some(message.into()),
style: DiagnosticLabelStyle::Secondary,
}
}
pub fn span_only(span: Span) -> Self {
Self {
span,
message: None,
style: DiagnosticLabelStyle::Primary,
}
}
}
impl From<ErrorLabel> for DiagnosticLabel {
fn from(label: ErrorLabel) -> Self {
Self {
span: label.span,
message: label.message,
style: match label.style {
LabelStyle::Primary => DiagnosticLabelStyle::Primary,
LabelStyle::Secondary => DiagnosticLabelStyle::Secondary,
},
}
}
}
pub struct DiagnosticReporter {
use_colors: bool,
show_source: bool,
}
impl DiagnosticReporter {
pub fn new() -> Self {
Self {
use_colors: true,
show_source: true,
}
}
pub fn plain() -> Self {
Self {
use_colors: false,
show_source: false,
}
}
pub fn with_colors(mut self, use_colors: bool) -> Self {
self.use_colors = use_colors;
self
}
pub fn with_source(mut self, show_source: bool) -> Self {
self.show_source = show_source;
self
}
pub fn report(&self, diagnostic: &dyn LightweightDiagnostic) {
eprintln!("{}", self.format_diagnostic(diagnostic));
}
pub fn format_diagnostic(&self, diagnostic: &dyn LightweightDiagnostic) -> String {
let mut output = String::new();
let severity_str = match diagnostic.severity() {
DiagnosticSeverity::Error => if self.use_colors { "\x1b[31merror\x1b[0m" } else { "error" },
DiagnosticSeverity::Warning => if self.use_colors { "\x1b[33mwarning\x1b[0m" } else { "warning" },
DiagnosticSeverity::Info => if self.use_colors { "\x1b[36minfo\x1b[0m" } else { "info" },
DiagnosticSeverity::Note => if self.use_colors { "\x1b[37mnote\x1b[0m" } else { "note" },
DiagnosticSeverity::Help => if self.use_colors { "\x1b[32mhelp\x1b[0m" } else { "help" },
};
if let Some(code) = diagnostic.code() {
output.push_str(&format!("{severity_str}[{code}]: {diagnostic}\n"));
} else {
output.push_str(&format!("{severity_str}: {diagnostic}\n"));
}
let labels = diagnostic.labels();
if !labels.is_empty() && self.show_source {
for label in labels {
if let Some(message) = &label.message {
output.push_str(&format!(" {} {}\n",
if self.use_colors { "\x1b[36m-->\x1b[0m" } else { "-->" },
message
));
}
}
}
if let Some(help) = diagnostic.help() {
output.push_str(&format!(" {} {}\n",
if self.use_colors { "\x1b[32m=\x1b[0m" } else { "=" },
help
));
}
if let Some(url) = diagnostic.url() {
output.push_str(&format!(" {} For more information: {}\n",
if self.use_colors { "\x1b[36m=\x1b[0m" } else { "=" },
url
));
}
output
}
}
impl Default for DiagnosticReporter {
fn default() -> Self {
Self::new()
}
}
pub fn report_diagnostic(diagnostic: &dyn LightweightDiagnostic) {
DiagnosticReporter::new().report(diagnostic);
}
macro_rules! derive_diagnostic {
(
$(#[$attr:meta])*
pub enum $name:ident {
$(
$(#[diagnostic(code($code:expr))])?
$(#[diagnostic(help($help:expr))])?
$variant:ident $({
$(
$(#[label($label_msg:expr)])?
$field:ident: $field_ty:ty
),* $(,)?
})?,
)*
}
) => {
$(#[$attr])*
pub enum $name {
$(
$variant $({
$($field: $field_ty),*
})?,
)*
}
impl LightweightDiagnostic for $name {
fn code(&self) -> Option<&str> {
match self {
$(
Self::$variant { .. } => {
derive_diagnostic!(@code $($code)?)
}
)*
}
}
fn help(&self) -> Option<&str> {
match self {
$(
Self::$variant { .. } => {
derive_diagnostic!(@help $($help)?)
}
)*
}
}
fn labels(&self) -> Vec<DiagnosticLabel> {
match self {
$(
Self::$variant { $($($field),*)? } => {
derive_diagnostic!(@labels $($($field: $label_msg)?)*)
}
)*
}
}
}
};
(@code $code:expr) => { Some($code) };
(@code) => { None };
(@help $help:expr) => { Some($help) };
(@help) => { None };
(@labels $($field:ident: $msg:expr)*) => {
{
let mut labels = Vec::new();
$(
if let Some(span) = derive_diagnostic!(@maybe_span $field) {
labels.push(DiagnosticLabel::primary(span, $msg));
}
)*
labels
}
};
(@labels) => { Vec::new() };
(@maybe_span $field:ident) => {
if std::any::type_name::<_>().contains("Span") {
Some(*$field)
} else {
None
}
};
}
#[cfg(test)]
mod tests {
use super::*;
use crate::diagnostics::Span;
#[test]
fn test_diagnostic_label() {
let span = Span::new(0, 5);
let label = DiagnosticLabel::primary(span, "test error");
assert_eq!(label.style, DiagnosticLabelStyle::Primary);
assert_eq!(label.message.as_ref().unwrap(), "test error");
assert_eq!(label.span.start, 0);
assert_eq!(label.span.end(), 5);
}
#[test]
fn test_diagnostic_reporter() {
struct TestDiagnostic {
message: String,
}
impl fmt::Display for TestDiagnostic {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.message)
}
}
impl fmt::Debug for TestDiagnostic {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "TestDiagnostic {{ message: {:?} }}", self.message)
}
}
impl std::error::Error for TestDiagnostic {}
impl LightweightDiagnostic for TestDiagnostic {
fn code(&self) -> Option<&str> {
Some("TEST001")
}
fn help(&self) -> Option<&str> {
Some("This is a test diagnostic")
}
}
let diagnostic = TestDiagnostic {
message: "Test error message".to_string(),
};
let reporter = DiagnosticReporter::plain();
let output = reporter.format_diagnostic(&diagnostic);
assert!(output.contains("error[TEST001]: Test error message"));
assert!(output.contains("This is a test diagnostic"));
}
#[test]
fn test_diagnostic_severity() {
assert_eq!(DiagnosticSeverity::Error, DiagnosticSeverity::Error);
assert_ne!(DiagnosticSeverity::Error, DiagnosticSeverity::Warning);
}
}