use serde::Serialize;
use std::{borrow::Cow, panic::RefUnwindSafe};
use crate::Span;
macro_rules! label {
($span:expr $(,)?) => {
($span.to_owned().into(), None)
};
($span:expr, $message:expr $(,)?) => {
($span.to_owned().into(), Some($message.into()))
};
($span:expr, $fmt:literal, $($arg:expr),+) => {
label!($span, format!($fmt, $($arg),+))
}
}
pub(crate) use label;
pub type CowStr = Cow<'static, str>;
pub type Label = (Span, Option<CowStr>);
#[derive(Debug, Clone, Serialize)]
#[non_exhaustive]
pub struct SourceDiag {
pub severity: Severity,
pub stage: Stage,
pub message: CowStr,
#[serde(skip_serializing)]
source: Option<std::sync::Arc<dyn std::error::Error + Send + Sync + RefUnwindSafe + 'static>>,
pub labels: Vec<Label>,
pub hints: Vec<CowStr>,
}
impl std::fmt::Display for SourceDiag {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.message.fmt(f)
}
}
impl RichError for SourceDiag {
fn labels(&self) -> Cow<'_, [Label]> {
self.labels.as_slice().into()
}
fn hints(&self) -> Cow<'_, [CowStr]> {
self.hints.as_slice().into()
}
fn severity(&self) -> Severity {
self.severity
}
}
impl std::error::Error for SourceDiag {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match &self.source {
Some(err) => Some(err),
None => None,
}
}
}
impl PartialEq for SourceDiag {
fn eq(&self, other: &Self) -> bool {
self.severity == other.severity && self.message == other.message
}
}
impl SourceDiag {
pub(crate) fn error(message: impl Into<CowStr>, label: Label, stage: Stage) -> Self {
Self {
severity: Severity::Error,
message: message.into(),
labels: vec![label],
hints: vec![],
source: None,
stage,
}
}
pub(crate) fn warning(message: impl Into<CowStr>, label: Label, stage: Stage) -> Self {
Self {
severity: Severity::Warning,
message: message.into(),
labels: vec![label],
hints: vec![],
source: None,
stage,
}
}
pub(crate) fn unlabeled(message: impl Into<CowStr>, severity: Severity, stage: Stage) -> Self {
Self {
severity,
stage,
message: message.into(),
source: None,
labels: vec![],
hints: vec![],
}
}
pub fn is_error(&self) -> bool {
self.severity == Severity::Error
}
pub fn is_warning(&self) -> bool {
self.severity == Severity::Warning
}
pub(crate) fn label(mut self, label: Label) -> Self {
self.add_label(label);
self
}
pub(crate) fn add_label(&mut self, label: Label) -> &mut Self {
self.labels.push(label);
self
}
pub(crate) fn hint(mut self, hint: impl Into<CowStr>) -> Self {
self.add_hint(hint);
self
}
pub(crate) fn add_hint(&mut self, hint: impl Into<CowStr>) -> &mut Self {
self.hints.push(hint.into());
self
}
pub(crate) fn set_source(
mut self,
source: impl std::error::Error + Send + Sync + RefUnwindSafe + 'static,
) -> Self {
self.source = Some(std::sync::Arc::new(source));
self
}
}
#[derive(Debug, Clone, Copy, Serialize, PartialEq, Eq)]
pub enum Severity {
Error,
Warning,
}
#[derive(Debug, Clone, Copy, Serialize, PartialEq, Eq)]
pub enum Stage {
Parse,
Analysis,
}
#[derive(Debug, Clone, Serialize)]
pub struct SourceReport {
buf: Vec<SourceDiag>,
severity: Option<Severity>,
}
impl SourceReport {
pub(crate) fn empty() -> Self {
Self {
buf: Vec::new(),
severity: None,
}
}
pub(crate) fn push(&mut self, err: SourceDiag) {
debug_assert!(self.severity.is_none() || self.severity.is_some_and(|s| err.severity == s));
self.buf.push(err);
}
pub(crate) fn error(&mut self, w: SourceDiag) {
debug_assert_eq!(w.severity, Severity::Error);
self.push(w);
}
pub(crate) fn warn(&mut self, w: SourceDiag) {
debug_assert_eq!(w.severity, Severity::Warning);
self.push(w);
}
pub(crate) fn retain(&mut self, f: impl Fn(&SourceDiag) -> bool) {
self.buf.retain(f)
}
pub(crate) fn set_severity(&mut self, severity: Option<Severity>) {
debug_assert!(
severity.is_none()
|| severity.is_some_and(|s| self.buf.iter().all(|e| e.severity == s))
);
self.severity = severity;
}
pub fn severity(&self) -> Option<&Severity> {
self.severity.as_ref()
}
pub fn iter(&self) -> impl Iterator<Item = &SourceDiag> {
self.buf.iter()
}
pub fn errors(&self) -> impl Iterator<Item = &SourceDiag> {
self.iter().filter(|e| e.severity == Severity::Error)
}
pub fn warnings(&self) -> impl Iterator<Item = &SourceDiag> {
self.iter().filter(|e| e.severity == Severity::Warning)
}
pub fn has_errors(&self) -> bool {
match self.severity {
Some(Severity::Warning) => false,
Some(Severity::Error) => !self.buf.is_empty(),
None => self.errors().next().is_some(),
}
}
pub fn has_warnings(&self) -> bool {
match self.severity {
Some(Severity::Warning) => !self.buf.is_empty(),
Some(Severity::Error) => false,
None => self.warnings().next().is_some(),
}
}
pub fn is_empty(&self) -> bool {
self.buf.is_empty()
}
pub fn unzip(self) -> (SourceReport, SourceReport) {
let (errors, warnings) = self.buf.into_iter().partition(SourceDiag::is_error);
(
Self {
buf: errors,
severity: Some(Severity::Error),
},
Self {
buf: warnings,
severity: Some(Severity::Warning),
},
)
}
pub fn remove_warnings(&mut self) {
self.buf.retain(SourceDiag::is_error)
}
pub fn into_vec(self) -> Vec<SourceDiag> {
self.buf
}
pub fn write(
&self,
file_name: &str,
source_code: &str,
color: bool,
w: &mut impl std::io::Write,
) -> std::io::Result<()> {
let lidx = codesnake::LineIndex::new(source_code);
for err in self.warnings() {
write_report(&mut *w, err, &lidx, file_name, color)?;
}
for err in self.errors() {
write_report(&mut *w, err, &lidx, file_name, color)?;
}
Ok(())
}
pub fn print(&self, file_name: &str, source_code: &str, color: bool) -> std::io::Result<()> {
self.write(file_name, source_code, color, &mut std::io::stdout().lock())
}
pub fn eprint(&self, file_name: &str, source_code: &str, color: bool) -> std::io::Result<()> {
self.write(file_name, source_code, color, &mut std::io::stderr().lock())
}
}
impl std::fmt::Display for SourceReport {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
for err in self.iter() {
err.fmt(f)?;
}
Ok(())
}
}
impl std::error::Error for SourceReport {}
#[derive(Debug, Clone, Serialize)]
pub struct PassResult<T> {
output: Option<T>,
report: SourceReport,
}
impl<T> PassResult<T> {
pub(crate) fn new(output: Option<T>, report: SourceReport) -> Self {
Self { output, report }
}
pub fn has_output(&self) -> bool {
self.output.is_some()
}
pub fn report(&self) -> &SourceReport {
&self.report
}
pub fn is_valid(&self) -> bool {
self.has_output() && !self.report.has_errors()
}
pub fn output(&self) -> Option<&T> {
self.output.as_ref()
}
pub fn valid_output(&self) -> Option<&T> {
if self.is_valid() {
self.output()
} else {
None
}
}
pub fn into_result(mut self) -> Result<(T, SourceReport), SourceReport> {
if !self.is_valid() {
return Err(self.report);
}
self.report.set_severity(Some(Severity::Warning));
Ok((self.output.unwrap(), self.report))
}
pub fn into_report(self) -> SourceReport {
self.report
}
pub fn into_output(self) -> Option<T> {
self.output
}
pub fn unwrap_output(self) -> T {
self.output.unwrap()
}
pub fn into_tuple(self) -> (Option<T>, SourceReport) {
(self.output, self.report)
}
pub fn map<F, O>(self, f: F) -> PassResult<O>
where
F: FnOnce(T) -> O,
{
PassResult {
output: self.output.map(f),
report: self.report,
}
}
}
pub trait RichError: std::error::Error {
fn labels(&self) -> Cow<'_, [Label]> {
Cow::Borrowed(&[])
}
fn hints(&self) -> Cow<'_, [CowStr]> {
Cow::Borrowed(&[])
}
fn severity(&self) -> Severity {
Severity::Error
}
}
pub fn write_rich_error(
error: &dyn RichError,
file_name: &str,
source_code: &str,
color: bool,
w: impl std::io::Write,
) -> std::io::Result<()> {
let lidx = codesnake::LineIndex::new(source_code);
write_report(w, error, &lidx, file_name, color)
}
#[derive(Default)]
struct ColorGenerator(usize);
impl ColorGenerator {
const COLORS: &'static [yansi::Color] = &[
yansi::Color::BrightMagenta,
yansi::Color::BrightGreen,
yansi::Color::BrightCyan,
yansi::Color::BrightBlue,
yansi::Color::BrightGreen,
yansi::Color::BrightYellow,
yansi::Color::BrightRed,
];
fn next(&mut self) -> yansi::Color {
let c = Self::COLORS[self.0];
if self.0 == Self::COLORS.len() - 1 {
self.0 = 0;
} else {
self.0 += 1;
}
c
}
}
fn write_report(
mut w: impl std::io::Write,
err: &dyn RichError,
lidx: &codesnake::LineIndex,
file_name: &str,
color: bool,
) -> std::io::Result<()> {
use yansi::Paint;
let cond = yansi::Condition::cached(color);
let sev_color = match err.severity() {
Severity::Error => yansi::Color::Red,
Severity::Warning => yansi::Color::Yellow,
};
match err.severity() {
Severity::Error => writeln!(w, "{} {err}", "Error:".paint(sev_color).whenever(cond))?,
Severity::Warning => writeln!(w, "{} {err}", "Warning:".paint(sev_color).whenever(cond))?,
}
if let Some(source) = err.source() {
writeln!(w, " {} {source}", "╰▶ ".paint(sev_color).whenever(cond))?;
}
let mut cg = ColorGenerator::default();
let mut labels = err.labels();
if !labels.is_empty() {
labels.to_mut().sort_unstable_by_key(|l| l.0);
let mut colored_labels = Vec::with_capacity(labels.len());
for (s, t) in labels.iter() {
let c = cg.next();
let mut l = codesnake::Label::new(s.range())
.with_style(move |s| s.paint(c).whenever(cond).to_string());
if let Some(text) = t {
l = l.with_text(text)
}
colored_labels.push(l);
}
let Some(block) = codesnake::Block::new(lidx, colored_labels) else {
tracing::error!("Failed to format code span, this is a bug.");
return Ok(());
};
let mut prev_empty = false;
let block = block.map_code(|s| {
let sub = usize::from(core::mem::replace(&mut prev_empty, s.is_empty()));
let s = s.replace('\t', " ");
let w = unicode_width::UnicodeWidthStr::width(&*s);
codesnake::CodeWidth::new(s, core::cmp::max(w, 1) - sub)
});
writeln!(
w,
"{}{}{}{}",
block.prologue(),
"[".dim().whenever(cond),
file_name,
"]".dim().whenever(cond)
)?;
write!(w, "{block}")?;
writeln!(w, "{}", block.epilogue())?;
}
let hints = err.hints();
let mut hints = hints.iter();
if let Some(help) = hints.next() {
writeln!(w, "{} {}", "Help:".green().whenever(cond), help)?;
}
if let Some(note) = hints.next() {
writeln!(w, "{} {}", "Note:".green().whenever(cond), note)?;
}
#[cfg(debug_assertions)]
if hints.next().is_some() {
tracing::warn!(
hints = ?err.hints(),
"the report builder only supports 2 hints, more will be ignored",
);
}
Ok(())
}
pub trait Recover {
fn recover() -> Self;
}
impl<T> Recover for T
where
T: Default,
{
fn recover() -> Self {
Self::default()
}
}