use std::borrow::Cow;
use std::ops::Deref;
use thiserror::Error;
#[derive(Debug, Clone)]
pub struct Report<E, W> {
pub(crate) errors: Vec<E>,
pub(crate) warnings: Vec<W>,
}
pub type CooklangReport = Report<CooklangError, CooklangWarning>;
impl<E, W> Report<E, W> {
pub fn new(errors: Vec<E>, warnings: Vec<W>) -> Self {
Self { errors, warnings }
}
pub fn errors(&self) -> &[E] {
&self.errors
}
pub fn warnings(&self) -> &[W] {
&self.warnings
}
pub fn has_errors(&self) -> bool {
!self.errors.is_empty()
}
pub fn has_warnings(&self) -> bool {
!self.warnings.is_empty()
}
pub fn is_empty(&self) -> bool {
self.errors.is_empty() && self.warnings.is_empty()
}
}
impl<E, W> Report<E, W>
where
E: RichError,
W: RichError,
{
pub fn write(
&self,
file_name: &str,
source_code: &str,
hide_warnings: bool,
color: bool,
w: &mut impl std::io::Write,
) -> std::io::Result<()> {
let mut cache = DummyCache::new(file_name, source_code);
if !hide_warnings {
for warn in &self.warnings {
build_report(warn, file_name, source_code, color).write(&mut cache, &mut *w)?;
}
}
for err in &self.errors {
build_report(err, file_name, source_code, color).write(&mut cache, &mut *w)?;
}
Ok(())
}
pub fn print(
&self,
file_name: &str,
source_code: &str,
hide_warnings: bool,
color: bool,
) -> std::io::Result<()> {
self.write(
file_name,
source_code,
hide_warnings,
color,
&mut std::io::stdout(),
)
}
pub fn eprint(
&self,
file_name: &str,
source_code: &str,
hide_warnings: bool,
color: bool,
) -> std::io::Result<()> {
self.write(
file_name,
source_code,
hide_warnings,
color,
&mut std::io::stderr(),
)
}
}
impl<E, W> std::fmt::Display for Report<E, W>
where
E: std::fmt::Display,
W: std::fmt::Display,
{
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let errors = &self.errors;
let warnings = &self.warnings;
if errors.len() == 1 {
errors[0].fmt(f)?;
} else if warnings.len() == 1 {
warnings[0].fmt(f)?;
} else {
match (errors.is_empty(), warnings.is_empty()) {
(true, true) => writeln!(f, "Unknown error")?,
(true, false) => writeln!(f, "Multiple warnings:")?,
(false, _) => writeln!(f, "Multiple errors:")?,
}
for warn in warnings {
warn.fmt(f)?;
}
for err in errors {
err.fmt(f)?;
}
}
Ok(())
}
}
impl<E, W> std::error::Error for Report<E, W>
where
E: std::fmt::Display + std::fmt::Debug,
W: std::fmt::Display + std::fmt::Debug,
{
}
pub struct Warnings<W>(Vec<W>);
impl<W> Warnings<W> {
pub fn new(warnings: Vec<W>) -> Self {
Self(warnings)
}
pub fn into_report<E>(self) -> Report<E, W> {
self.into()
}
}
impl<W: RichError> Warnings<W> {
pub fn write(
&self,
file_name: &str,
source_code: &str,
color: bool,
w: &mut impl std::io::Write,
) -> std::io::Result<()> {
let mut cache = DummyCache::new(file_name, source_code);
for warn in &self.0 {
build_report(warn, file_name, source_code, color).write(&mut cache, &mut *w)?;
}
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())
}
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())
}
}
impl<E, W> From<Warnings<W>> for Report<E, W> {
fn from(value: Warnings<W>) -> Self {
Self::new(vec![], value.0)
}
}
impl<W> Deref for Warnings<W> {
type Target = [W];
fn deref(&self) -> &Self::Target {
&self.0
}
}
#[derive(Debug)]
pub struct PassResult<T, E, W> {
output: Option<T>,
warnings: Vec<W>,
errors: Vec<E>,
}
impl<T, E, W> PassResult<T, E, W> {
pub(crate) fn new(output: Option<T>, warnings: Vec<W>, errors: Vec<E>) -> Self {
Self {
output,
warnings,
errors,
}
}
pub fn has_output(&self) -> bool {
self.output.is_some()
}
pub fn has_errors(&self) -> bool {
!self.errors.is_empty()
}
pub fn has_warnings(&self) -> bool {
!self.warnings.is_empty()
}
pub fn is_valid(&self) -> bool {
!self.has_errors() && self.has_output()
}
pub fn output(&self) -> Option<&T> {
self.output.as_ref()
}
pub fn warnings(&self) -> &[W] {
&self.warnings
}
pub fn errors(&self) -> &[E] {
&self.errors
}
pub fn into_result(mut self) -> Result<(T, Warnings<W>), Report<E, W>> {
if let Some(o) = self.output.take() {
if self.errors.is_empty() {
return Ok((o, Warnings::new(self.warnings)));
}
}
Err(self.into_report())
}
pub fn into_report(self) -> Report<E, W> {
Report {
errors: self.errors,
warnings: self.warnings,
}
}
pub fn take_output(&mut self) -> Option<T> {
self.output.take()
}
pub fn into_output(self) -> Option<T> {
self.output
}
pub fn into_errors(self) -> Vec<E> {
self.errors
}
pub fn into_warnings(self) -> Vec<W> {
self.warnings
}
pub fn into_tuple(self) -> (Option<T>, Vec<W>, Vec<E>) {
(self.output, self.warnings, self.errors)
}
pub fn map<F, O>(self, f: F) -> PassResult<O, E, W>
where
F: FnOnce(T) -> O,
{
PassResult {
output: self.output.map(f),
warnings: self.warnings,
errors: self.errors,
}
}
pub fn try_map<F, O, E2>(self, f: F) -> Result<PassResult<O, E, W>, E2>
where
F: FnOnce(T) -> Result<O, E2>,
{
let output = self.output.map(f).transpose()?;
Ok(PassResult {
output,
warnings: self.warnings,
errors: self.errors,
})
}
}
pub trait RichError: std::error::Error {
fn labels(&self) -> Vec<(Span, Option<Cow<'static, str>>)> {
vec![]
}
fn help(&self) -> Option<Cow<'static, str>> {
None
}
fn note(&self) -> Option<Cow<'static, str>> {
None
}
fn code(&self) -> Option<&'static str> {
None
}
fn kind(&self) -> ariadne::ReportKind {
ariadne::ReportKind::Error
}
}
macro_rules! label {
($span:expr) => {
($span.to_owned().into(), None)
};
($span:expr, $message:expr) => {
($span.to_owned().into(), Some($message.into()))
};
}
pub(crate) use label;
macro_rules! help {
() => {
None
};
($help:expr) => {
Some($help.into())
};
(opt $help:expr) => {
$help.map(|h| h.into())
};
}
pub(crate) use help;
pub(crate) use help as note;
use crate::span::Span;
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 mut cache = DummyCache::new(file_name, source_code);
let report = build_report(error, file_name, source_code, color);
report.write(&mut cache, w)
}
fn build_report<'a>(
err: &'a dyn RichError,
file_name: &str,
src_code: &str,
color: bool,
) -> ariadne::Report<'a> {
use ariadne::{Color, ColorGenerator, Fmt, Label, Report};
let labels = err
.labels()
.into_iter()
.map(|(s, t)| (s.to_chars_span(src_code, file_name).range(), t))
.collect::<Vec<_>>();
let offset = labels.iter().map(|l| l.0.start).min().unwrap_or_default();
let mut r = Report::build(err.kind(), (), offset)
.with_config(ariadne::Config::default().with_color(color));
if let Some(source) = err.source() {
let arrow_color = if color {
match err.kind() {
ariadne::ReportKind::Error => Color::Red,
ariadne::ReportKind::Warning => Color::Yellow,
ariadne::ReportKind::Advice => Color::Fixed(147),
ariadne::ReportKind::Custom(_, c) => c,
}
} else {
Color::Default
};
let message = format!("{err}\n {} {source}", "╰▶ ".fg(arrow_color));
r.set_message(message);
} else {
r.set_message(err);
}
let mut c = ColorGenerator::new();
r.add_labels(labels.into_iter().enumerate().map(|(order, (span, text))| {
let mut l = Label::new(span)
.with_order(order as i32)
.with_color(c.next());
if let Some(text) = text {
l = l.with_message(text);
}
l
}));
if let Some(help) = err.help() {
r.set_help(help);
}
if let Some(note) = err.note() {
r.set_note(note);
}
r.finish()
}
struct DummyCache(String, ariadne::Source);
impl DummyCache {
fn new(file_name: &str, src_code: &str) -> Self {
Self(file_name.into(), src_code.into())
}
}
impl ariadne::Cache<()> for DummyCache {
fn fetch(&mut self, _id: &()) -> Result<&ariadne::Source, Box<dyn std::fmt::Debug + '_>> {
Ok(&self.1)
}
fn display<'a>(&self, _id: &'a ()) -> Option<Box<dyn std::fmt::Display + 'a>> {
Some(Box::new(self.0.clone()))
}
}
#[derive(Debug, Error)]
#[non_exhaustive]
#[error(transparent)]
pub enum CooklangError {
Parser(#[from] crate::parser::ParserError),
Analysis(#[from] crate::analysis::AnalysisError),
Io(#[from] std::io::Error),
}
#[derive(Debug, Error)]
#[non_exhaustive]
#[error(transparent)]
pub enum CooklangWarning {
Parser(#[from] crate::parser::ParserWarning),
Analysis(#[from] crate::analysis::AnalysisWarning),
}
impl RichError for CooklangError {
fn labels(&self) -> Vec<(Span, Option<Cow<'static, str>>)> {
match self {
CooklangError::Parser(e) => e.labels(),
CooklangError::Analysis(e) => e.labels(),
CooklangError::Io(_) => vec![],
}
}
fn help(&self) -> Option<Cow<'static, str>> {
match self {
CooklangError::Parser(e) => e.help(),
CooklangError::Analysis(e) => e.help(),
CooklangError::Io(_) => None,
}
}
fn note(&self) -> Option<Cow<'static, str>> {
match self {
CooklangError::Parser(e) => e.note(),
CooklangError::Analysis(e) => e.note(),
CooklangError::Io(_) => None,
}
}
fn code(&self) -> Option<&'static str> {
match self {
CooklangError::Parser(e) => e.code(),
CooklangError::Analysis(e) => e.code(),
CooklangError::Io(_) => Some("io"),
}
}
}
impl RichError for CooklangWarning {
fn labels(&self) -> Vec<(Span, Option<Cow<'static, str>>)> {
match self {
CooklangWarning::Parser(e) => e.labels(),
CooklangWarning::Analysis(e) => e.labels(),
}
}
fn help(&self) -> Option<Cow<'static, str>> {
match self {
CooklangWarning::Parser(e) => e.help(),
CooklangWarning::Analysis(e) => e.help(),
}
}
fn code(&self) -> Option<&'static str> {
match self {
CooklangWarning::Parser(e) => e.code(),
CooklangWarning::Analysis(e) => e.code(),
}
}
fn kind(&self) -> ariadne::ReportKind {
ariadne::ReportKind::Warning
}
}