use codespan_reporting::term::termcolor::{ColorChoice, StandardStream, WriteColor};
#[cfg(feature = "serde")]
use serde::Serialize;
#[cfg_attr(feature = "serde", derive(Serialize))]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct Span {
pub start: usize,
pub end: usize,
pub line: u32,
pub col: u32,
}
impl std::fmt::Display for Span {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}:{}", self.line, self.col)
}
}
impl Span {
#[must_use]
pub fn new(start: usize, end: usize, line: u32, col: u32) -> Self {
Self {
start,
end,
line,
col,
}
}
#[must_use]
pub fn dummy() -> Self {
Self {
start: 0,
end: 0,
line: 0,
col: 0,
}
}
#[must_use]
pub fn merge(self, other: Self) -> Self {
let first = if self.start <= other.start {
self
} else {
other
};
Self {
start: self.start.min(other.start),
end: self.end.max(other.end),
line: self.line.min(other.line),
col: first.col,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Severity {
Error,
Warning,
Note,
}
#[derive(Debug, Clone)]
pub struct Diagnostic {
pub severity: Severity,
pub message: String,
pub span: Span,
pub code: Option<&'static str>,
pub suggestion: Option<String>,
pub note: Option<String>,
pub secondary_labels: Vec<(Span, String)>,
}
impl Diagnostic {
#[must_use]
pub fn error(message: impl Into<String>, span: Span) -> Self {
Self {
severity: Severity::Error,
message: message.into(),
span,
code: None,
suggestion: None,
note: None,
secondary_labels: Vec::new(),
}
}
#[must_use]
pub fn warning(message: impl Into<String>, span: Span) -> Self {
Self {
severity: Severity::Warning,
message: message.into(),
span,
code: None,
suggestion: None,
note: None,
secondary_labels: Vec::new(),
}
}
#[must_use]
pub fn note(message: impl Into<String>, span: Span) -> Self {
Self {
severity: Severity::Note,
message: message.into(),
span,
code: None,
suggestion: None,
note: None,
secondary_labels: Vec::new(),
}
}
#[must_use]
pub fn with_code(mut self, code: &'static str) -> Self {
self.code = Some(code);
self
}
#[must_use]
pub fn with_suggestion(mut self, suggestion: impl Into<String>) -> Self {
self.suggestion = Some(suggestion.into());
self
}
#[must_use]
pub fn with_note(mut self, note: impl Into<String>) -> Self {
self.note = Some(note.into());
self
}
#[must_use]
pub fn with_secondary_label(mut self, span: Span, message: impl Into<String>) -> Self {
self.secondary_labels.push((span, message.into()));
self
}
}
pub trait Reporter {
fn report(&mut self, diagnostic: &Diagnostic, source: &str, filename: &str);
fn has_errors(&self) -> bool;
}
fn render_to_writer(
writer: &mut dyn WriteColor,
diagnostic: &Diagnostic,
source: &str,
filename: &str,
) {
use codespan_reporting::diagnostic::{Diagnostic as CsDiag, Label, Severity as CsSeverity};
use codespan_reporting::files::SimpleFiles;
use codespan_reporting::term;
let mut files: SimpleFiles<&str, &str> = SimpleFiles::new();
let file_id = files.add(filename, source);
let cs_severity = match diagnostic.severity {
Severity::Error => CsSeverity::Error,
Severity::Warning => CsSeverity::Warning,
Severity::Note => CsSeverity::Note,
};
let mut labels = vec![
Label::primary(file_id, diagnostic.span.start..diagnostic.span.end)
.with_message(&diagnostic.message),
];
for (span, msg) in &diagnostic.secondary_labels {
labels.push(Label::secondary(file_id, span.start..span.end).with_message(msg));
}
let mut cs_diag = CsDiag::new(cs_severity)
.with_message(&diagnostic.message)
.with_labels(labels);
if let Some(code) = diagnostic.code {
cs_diag = cs_diag.with_code(code);
}
let mut notes: Vec<String> = Vec::new();
if let Some(ref note) = diagnostic.note {
notes.push(format!("note: {note}"));
}
if let Some(ref suggestion) = diagnostic.suggestion {
notes.push(format!("help: {suggestion}"));
}
if !notes.is_empty() {
cs_diag = cs_diag.with_notes(notes);
}
let config = term::Config::default();
if let Err(e) = term::emit(writer, &config, &files, &cs_diag) {
eprintln!("valua: failed to render diagnostic: {e}");
}
}
#[must_use]
pub fn render_diagnostic_to_string(
diagnostic: &Diagnostic,
source: &str,
filename: &str,
) -> String {
use codespan_reporting::term::termcolor::Buffer;
let mut buf = Buffer::no_color();
render_to_writer(&mut buf, diagnostic, source, filename);
String::from_utf8_lossy(buf.as_slice()).into_owned()
}
pub struct ConsoleReporter {
error_count: usize,
color: ColorChoice,
}
impl ConsoleReporter {
#[must_use]
pub fn new(color: ColorChoice) -> Self {
Self {
error_count: 0,
color,
}
}
#[must_use]
pub fn stderr() -> Self {
Self::new(ColorChoice::Auto)
}
}
impl Reporter for ConsoleReporter {
fn report(&mut self, diagnostic: &Diagnostic, source: &str, filename: &str) {
if diagnostic.severity == Severity::Error {
self.error_count += 1;
}
let writer = StandardStream::stderr(self.color);
let mut lock = writer.lock();
render_to_writer(&mut lock, diagnostic, source, filename);
}
fn has_errors(&self) -> bool {
self.error_count > 0
}
}
#[derive(Debug, Default)]
pub struct CollectingReporter {
pub diagnostics: Vec<Diagnostic>,
}
impl Reporter for CollectingReporter {
fn report(&mut self, diagnostic: &Diagnostic, _source: &str, _filename: &str) {
self.diagnostics.push(diagnostic.clone());
}
fn has_errors(&self) -> bool {
self.diagnostics
.iter()
.any(|d| d.severity == Severity::Error)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn span(start: usize, end: usize, line: u32, col: u32) -> Span {
Span::new(start, end, line, col)
}
#[test]
fn test_span_merge_covers_both_endpoints() {
let a = span(0, 5, 1, 1);
let b = span(3, 10, 2, 3);
let m = a.merge(b);
assert_eq!(m.start, 0);
assert_eq!(m.end, 10);
}
#[test]
fn test_span_merge_col_from_earlier_span() {
let earlier = span(0, 5, 1, 7);
let later = span(6, 10, 1, 15);
let m = later.merge(earlier);
assert_eq!(m.col, 7, "col should be from whichever span starts first");
}
#[test]
fn test_span_display() {
let s = span(0, 5, 3, 7);
assert_eq!(format!("{s}"), "3:7");
}
#[test]
fn test_diagnostic_builder() {
let s = span(0, 1, 1, 1);
let d = Diagnostic::error("bad token", s)
.with_code("E0001")
.with_suggestion("remove it");
assert_eq!(d.severity, Severity::Error);
assert_eq!(d.code, Some("E0001"));
assert!(d.suggestion.is_some());
}
#[test]
fn test_diagnostic_secondary_label() {
let s1 = span(0, 5, 1, 1);
let s2 = span(10, 15, 3, 1);
let d = Diagnostic::error("mutation", s2).with_secondary_label(s1, "declared here");
assert_eq!(d.secondary_labels.len(), 1);
assert_eq!(d.secondary_labels[0].1, "declared here");
}
#[test]
fn test_collecting_reporter_tracks_errors() {
let mut r = CollectingReporter::default();
let s = span(0, 1, 1, 1);
assert!(!r.has_errors());
r.report(&Diagnostic::warning("w", s), "", "f");
assert!(!r.has_errors());
r.report(&Diagnostic::error("e", s), "", "f");
assert!(r.has_errors());
assert_eq!(r.diagnostics.len(), 2);
}
}