use crate::common::Spanned;
use anstyle::{Color, Style};
use pochoir_common::span::SpanPosition;
use std::{
borrow::Cow,
error, fmt,
ops::{Deref, DerefMut, Range},
path::Path,
result,
};
use thiserror::Error;
pub type Result<T> = result::Result<T, SpannedWithComponent<Error>>;
#[derive(Error, Debug, PartialEq, Eq, Clone)]
pub enum Error {
#[error("component {name:?} is importing itself resulting in a cycle")]
CyclicComponent {
name: String,
},
#[error("component {name:?} not found")]
ComponentNotFound {
name: String,
},
#[error("unknown statement {stmt:?}")]
UnknownStatement {
stmt: String,
},
#[error("unbounded ranges are forbidden in `for` loops, if you want to index an array you can use its length as the end bound")]
UnboundedRangeInForLoop,
#[error("an `elif` statement cannot be used after an `else` statement, you may have forgotten an `endif` statement")]
ElifAfterElse,
#[error(transparent)]
TemplateError(#[from] crate::template_engine::Error),
#[error(transparent)]
LangError(#[from] crate::lang::Error),
#[error(transparent)]
ParserError(#[from] crate::parser::Error),
#[error(transparent)]
StreamParserError(#[from] crate::common::Error),
}
#[derive(Debug, PartialEq, Eq, Clone)]
pub struct SpannedWithComponent<T> {
original_spanned: Box<Spanned<T>>,
component_name: String,
}
impl<T> SpannedWithComponent<T> {
pub fn new(node: T) -> Self {
Self {
original_spanned: Box::new(Spanned::new(node)),
component_name: String::default(),
}
}
#[must_use]
pub fn with_component_name<N: Into<String>>(mut self, component_name: N) -> Self {
self.component_name = component_name.into();
self
}
pub fn set_component_name<N: Into<String>>(&mut self, component_name: N) {
self.component_name = component_name.into();
}
pub fn component_name(&self) -> &str {
&self.component_name
}
#[must_use]
pub fn with_span(mut self, span: Range<usize>) -> Self {
self.original_spanned.set_span(span);
self
}
#[must_use]
pub fn with_file_path<P: AsRef<Path>>(mut self, file_path: P) -> Self {
self.original_spanned.set_file_path(file_path.as_ref());
self
}
}
impl<T> From<Spanned<T>> for SpannedWithComponent<T> {
fn from(original_spanned: Spanned<T>) -> Self {
Self {
original_spanned: Box::new(original_spanned),
component_name: String::default(),
}
}
}
impl<T> Deref for SpannedWithComponent<T> {
type Target = Spanned<T>;
fn deref(&self) -> &Self::Target {
&self.original_spanned
}
}
impl<T> DerefMut for SpannedWithComponent<T> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.original_spanned
}
}
impl<T: fmt::Display> fmt::Display for SpannedWithComponent<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.original_spanned.fmt(f)
}
}
impl<T: error::Error> error::Error for SpannedWithComponent<T> {
fn source(&self) -> Option<&(dyn error::Error + 'static)> {
self.original_spanned.source()
}
}
pub trait AutoError<T>
where
Self: Sized,
{
fn auto_error(self) -> T;
}
impl<T> AutoError<Result<T>> for result::Result<T, Spanned<crate::parser::Error>> {
fn auto_error(self) -> Result<T> {
self.map_err(|e| SpannedWithComponent::from(e.map_spanned(Error::ParserError)))
}
}
impl<T> AutoError<result::Result<T, Spanned<Error>>>
for result::Result<T, Spanned<crate::lang::Error>>
{
fn auto_error(self) -> result::Result<T, Spanned<Error>> {
self.map_err(|e| e.map_spanned(Error::LangError))
}
}
impl<T> AutoError<result::Result<T, Spanned<Error>>>
for result::Result<T, Spanned<crate::template_engine::Error>>
{
fn auto_error(self) -> result::Result<T, Spanned<Error>> {
self.map_err(|e| e.map_spanned(Error::TemplateError))
}
}
pub(crate) trait AutoErrorWithName<T>
where
Self: Sized,
{
fn auto_error_with_name<N: Into<String>>(self, component_name: N) -> T;
}
pub(crate) trait AutoErrorWithNameOffset<T>
where
Self: Sized,
{
fn auto_error_with_name_offset<N: Into<String>>(
self,
component_name: N,
file_offset: usize,
) -> T;
}
impl<T> AutoErrorWithName<Result<T>> for result::Result<T, Spanned<crate::parser::Error>> {
fn auto_error_with_name<N: Into<String>>(self, component_name: N) -> Result<T> {
self.map_err(|e| {
SpannedWithComponent::from(e.map_spanned(Error::ParserError))
.with_component_name(component_name)
})
}
}
impl<T> AutoErrorWithNameOffset<Result<T>> for result::Result<T, Spanned<crate::common::Error>> {
fn auto_error_with_name_offset<N: Into<String>>(
self,
component_name: N,
file_offset: usize,
) -> Result<T> {
self.map_err(|e| {
let span = e.span().clone();
SpannedWithComponent::from(e.map_spanned(Error::StreamParserError))
.with_span(span.start + file_offset..span.end + file_offset)
.with_component_name(component_name)
})
}
}
impl<T> AutoErrorWithName<Result<T>> for result::Result<T, Spanned<crate::lang::Error>> {
fn auto_error_with_name<N: Into<String>>(self, component_name: N) -> Result<T> {
self.map_err(|e| {
SpannedWithComponent::from(e.map_spanned(Error::LangError))
.with_component_name(component_name)
})
}
}
impl<T> AutoErrorWithName<Result<T>> for result::Result<T, Spanned<crate::template_engine::Error>> {
fn auto_error_with_name<N: Into<String>>(self, component_name: N) -> Result<T> {
self.map_err(|e| {
SpannedWithComponent::from(e.map_spanned(Error::TemplateError))
.with_component_name(component_name)
})
}
}
pub fn component_not_found<N: Into<String>>(name: N) -> SpannedWithComponent<Error> {
SpannedWithComponent::new(Error::ComponentNotFound { name: name.into() })
}
pub fn display_html_error<E: error::Error>(error: &Spanned<E>, source: &str) -> String {
let (source_context, pos) = if error.is_dummy_span() {
(
Cow::Borrowed("(no source)"),
SpanPosition { row: 0, col: 0 },
)
} else if let (Some(section), Some(pos)) =
(source.get(error.span().clone()), error.get_position(source))
{
let context_row_start = pos.row.saturating_sub(3);
#[allow(clippy::cast_sign_loss)]
#[allow(clippy::cast_possible_wrap)]
let context_row_middle = if pos.row == context_row_start {
0
} else {
2 - (context_row_start as isize - (pos.row as isize - 3isize)) as usize
};
let context_row_end = context_row_start + 4;
let max_digits = ((context_row_end + 1).checked_ilog10().unwrap_or(0) + 1) as usize;
let lines = source
.lines()
.skip(context_row_start)
.take(5)
.enumerate()
.map(|(i, l)| {
if i == context_row_middle {
let remaining = (pos.col + section.len() + usize::from(pos.col == 0))
.checked_sub(1)
.unwrap_or(pos.col + section.len());
Cow::Owned(format!(
r#"{}{} | {}<span style="font-weight: 900; color: red;">{}</span>{}"#,
" ".repeat(
max_digits
- ((context_row_start + i + 1).checked_ilog10().unwrap_or(0) + 1)
as usize
),
context_row_start + i + 1,
crate::template_engine::Escaping::Html
.escape(&l[..pos.col.checked_sub(1).unwrap_or(pos.col)]),
if remaining > l.len() {
Cow::Borrowed("")
} else {
crate::template_engine::Escaping::Html.escape(section)
},
if remaining > l.len() {
Cow::Borrowed(r#"<span style="color: red;">\u{2588}</span>"#)
} else {
crate::template_engine::Escaping::Html.escape(&l[remaining..])
},
))
} else {
crate::template_engine::Escaping::Html.escape(format!(
"{}{} | {}",
" ".repeat(
max_digits
- ((context_row_start + i + 1).checked_ilog10().unwrap_or(0) + 1)
as usize
),
context_row_start + i + 1,
l
))
}
})
.collect::<Vec<Cow<str>>>();
(Cow::Owned(lines.join("\n")), pos)
} else {
(
Cow::Borrowed("(no source)"),
SpanPosition { row: 0, col: 0 },
)
};
let file_path = error.file_path().to_string_lossy();
format!(
r#"<pre style="font-family: monospace, sans-serif;"><span style="color: red; font-weight: 600;">error</span><span style="font-weight: 600;">: {}</span>
<span style="margin-left: 2rem; color: #666; font-size: 0.9em;">{}:{}:{}</span>
<code style="background-color: #F5F5F5; width: max-content; display: inline-block; padding: 14px; line-height: 1.5;">{}</code></pre>"#,
crate::template_engine::Escaping::Html.escape(error.to_string()),
if file_path.is_empty() {
Cow::Borrowed("(no path)")
} else {
file_path
},
pos.row,
pos.col,
source_context,
)
}
pub fn display_ansi_error<E: error::Error>(error: &Spanned<E>, source: &str) -> String {
let error_style = Style::new().bold().fg_color(Some(Color::Ansi(anstyle::AnsiColor::Red)));
let bold_style = Style::new().bold();
let (source_context, pos) = if error.is_dummy_span() {
(
Cow::Borrowed("(no source)"),
SpanPosition { row: 0, col: 0 },
)
} else if let (Some(section), Some(pos)) =
(source.get(error.span().clone()), error.get_position(source))
{
let context_row_start = pos.row.saturating_sub(3);
#[allow(clippy::cast_sign_loss)]
#[allow(clippy::cast_possible_wrap)]
let context_row_middle = if pos.row == context_row_start {
0
} else {
2 - (context_row_start as isize - (pos.row as isize - 3isize)) as usize
};
let context_row_end = context_row_start + 4;
let max_digits = ((context_row_end + 1).checked_ilog10().unwrap_or(0) + 1) as usize;
let line_style = Style::new().bold().fg_color(Some(Color::Ansi(anstyle::AnsiColor::Blue)));
let lines = source
.lines()
.skip(context_row_start)
.take(5)
.enumerate()
.map(|(i, l)| {
if i == context_row_middle {
let remaining = (pos.col + section.len() + usize::from(pos.col == 0))
.checked_sub(1)
.unwrap_or(pos.col + section.len());
format!(
r" {}{line_style}{} |{line_style:#} {}{}{}",
" ".repeat(
max_digits
- ((context_row_start + i + 1).checked_ilog10().unwrap_or(0) + 1)
as usize
),
context_row_start + i + 1,
&l[..pos.col.checked_sub(1).unwrap_or(pos.col)],
if remaining > l.len() {
Cow::Borrowed("")
} else {
Cow::Owned(format!("{error_style}{section}{error_style:#}"))
},
if remaining > l.len() {
Cow::Owned(format!("{error_style}\u{2588}{error_style:#}"))
} else {
Cow::Borrowed(&l[remaining..])
},
)
} else {
format!(
" {}{line_style}{} |{line_style:#} {}",
" ".repeat(
max_digits
- ((context_row_start + i + 1).checked_ilog10().unwrap_or(0) + 1)
as usize
),
context_row_start + i + 1,
l
)
}
})
.collect::<Vec<String>>();
(Cow::Owned(lines.join("\n")), pos)
} else {
(
Cow::Borrowed("(no source)"),
SpanPosition { row: 0, col: 0 },
)
};
let file_path = error.file_path().to_string_lossy();
format!(
"{error_style}error{error_style:#}{bold_style}: {error}{bold_style:#}\n {}:{}:{}\n\n{source_context}",
if file_path.is_empty() {
Cow::Borrowed("(no path)")
} else {
file_path
},
pos.row,
pos.col,
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn source_too_short() {
assert_eq!(
display_html_error(
&Spanned::new(Error::ElifAfterElse).with_span(0..120),
"hello world"
),
r#"<pre style="font-family: monospace, sans-serif;"><span style="color: red; font-weight: 600;">error</span><span style="font-weight: 600;">: an `elif` statement cannot be used after an `else` statement, you may have forgotten an `endif` statement</span>
<span style="margin-left: 2rem; color: #666; font-size: 0.9em;">(no path):0:0</span>
<code style="background-color: #F5F5F5; width: max-content; display: inline-block; padding: 14px; line-height: 1.5;">(no source)</code></pre>"#
);
assert_eq!(
display_ansi_error(&Spanned::new(Error::ElifAfterElse).with_span(0..120), "hello world"),
"\u{1b}[1m\u{1b}[31merror\u{1b}[0m\u{1b}[1m: an `elif` statement cannot be used after an `else` statement, you may have forgotten an `endif` statement\u{1b}[0m
(no path):0:0
(no source)"
);
}
}