use std::fmt::{self, Display, Formatter};
use std::io;
use std::path::{Path, PathBuf};
use std::str::Utf8Error;
use std::string::FromUtf8Error;
use comemo::Tracked;
use ecow::{eco_vec, EcoVec};
use crate::syntax::package::{PackageSpec, PackageVersion};
use crate::syntax::{Span, Spanned, SyntaxError};
use crate::{World, WorldExt};
#[macro_export]
#[doc(hidden)]
macro_rules! __bail {
(
$fmt:literal $(, $arg:expr)*
$(; hint: $hint:literal $(, $hint_arg:expr)*)*
$(,)?
) => {
return Err($crate::diag::error!(
$fmt $(, $arg)*
$(; hint: $hint $(, $hint_arg)*)*
))
};
($error:expr) => {
return Err(::ecow::eco_vec![$error])
};
($($tts:tt)*) => {
return Err(::ecow::eco_vec![$crate::diag::error!($($tts)*)])
};
}
#[macro_export]
#[doc(hidden)]
macro_rules! __error {
($fmt:literal $(, $arg:expr)* $(,)?) => {
$crate::diag::eco_format!($fmt, $($arg),*).into()
};
(
$fmt:literal $(, $arg:expr)*
$(; hint: $hint:literal $(, $hint_arg:expr)*)*
$(,)?
) => {
$crate::diag::HintedString::new(
$crate::diag::eco_format!($fmt, $($arg),*)
) $(.with_hint($crate::diag::eco_format!($hint, $($hint_arg),*)))*
};
(
$span:expr, $fmt:literal $(, $arg:expr)*
$(; hint: $hint:literal $(, $hint_arg:expr)*)*
$(,)?
) => {
$crate::diag::SourceDiagnostic::error(
$span,
$crate::diag::eco_format!($fmt, $($arg),*),
) $(.with_hint($crate::diag::eco_format!($hint, $($hint_arg),*)))*
};
}
#[macro_export]
#[doc(hidden)]
macro_rules! __warning {
(
$span:expr,
$fmt:literal $(, $arg:expr)*
$(; hint: $hint:literal $(, $hint_arg:expr)*)*
$(,)?
) => {
$crate::diag::SourceDiagnostic::warning(
$span,
$crate::diag::eco_format!($fmt, $($arg),*),
) $(.with_hint($crate::diag::eco_format!($hint, $($hint_arg),*)))*
};
}
#[rustfmt::skip]
#[doc(inline)]
pub use {
crate::__bail as bail,
crate::__error as error,
crate::__warning as warning,
ecow::{eco_format, EcoString},
};
pub type SourceResult<T> = Result<T, EcoVec<SourceDiagnostic>>;
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub struct Warned<T> {
pub output: T,
pub warnings: EcoVec<SourceDiagnostic>,
}
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub struct SourceDiagnostic {
pub severity: Severity,
pub span: Span,
pub message: EcoString,
pub trace: EcoVec<Spanned<Tracepoint>>,
pub hints: EcoVec<EcoString>,
}
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
pub enum Severity {
Error,
Warning,
}
impl SourceDiagnostic {
pub fn error(span: Span, message: impl Into<EcoString>) -> Self {
Self {
severity: Severity::Error,
span,
trace: eco_vec![],
message: message.into(),
hints: eco_vec![],
}
}
pub fn warning(span: Span, message: impl Into<EcoString>) -> Self {
Self {
severity: Severity::Warning,
span,
trace: eco_vec![],
message: message.into(),
hints: eco_vec![],
}
}
pub fn hint(&mut self, hint: impl Into<EcoString>) {
self.hints.push(hint.into());
}
pub fn with_hint(mut self, hint: impl Into<EcoString>) -> Self {
self.hint(hint);
self
}
pub fn with_hints(mut self, hints: impl IntoIterator<Item = EcoString>) -> Self {
self.hints.extend(hints);
self
}
}
impl From<SyntaxError> for SourceDiagnostic {
fn from(error: SyntaxError) -> Self {
Self {
severity: Severity::Error,
span: error.span,
message: error.message,
trace: eco_vec![],
hints: error.hints,
}
}
}
#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
pub enum Tracepoint {
Call(Option<EcoString>),
Show(EcoString),
Import,
}
impl Display for Tracepoint {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
match self {
Tracepoint::Call(Some(name)) => {
write!(f, "error occurred in this call of function `{name}`")
}
Tracepoint::Call(None) => {
write!(f, "error occurred in this function call")
}
Tracepoint::Show(name) => {
write!(f, "error occurred while applying show rule to this {name}")
}
Tracepoint::Import => {
write!(f, "error occurred while importing this module")
}
}
}
}
pub trait Trace<T> {
fn trace<F>(self, world: Tracked<dyn World + '_>, make_point: F, span: Span) -> Self
where
F: Fn() -> Tracepoint;
}
impl<T> Trace<T> for SourceResult<T> {
fn trace<F>(self, world: Tracked<dyn World + '_>, make_point: F, span: Span) -> Self
where
F: Fn() -> Tracepoint,
{
self.map_err(|mut errors| {
let Some(trace_range) = world.range(span) else { return errors };
for error in errors.make_mut().iter_mut() {
if let Some(error_range) = world.range(error.span) {
if error.span.id() == span.id()
&& trace_range.start <= error_range.start
&& trace_range.end >= error_range.end
{
continue;
}
}
error.trace.push(Spanned::new(make_point(), span));
}
errors
})
}
}
pub type StrResult<T> = Result<T, EcoString>;
pub trait At<T> {
fn at(self, span: Span) -> SourceResult<T>;
}
impl<T, S> At<T> for Result<T, S>
where
S: Into<EcoString>,
{
fn at(self, span: Span) -> SourceResult<T> {
self.map_err(|message| {
let mut diagnostic = SourceDiagnostic::error(span, message);
if diagnostic.message.contains("(access denied)") {
diagnostic.hint("cannot read file outside of project root");
diagnostic
.hint("you can adjust the project root with the --root argument");
}
eco_vec![diagnostic]
})
}
}
pub type HintedStrResult<T> = Result<T, HintedString>;
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub struct HintedString(EcoVec<EcoString>);
impl HintedString {
pub fn new(message: EcoString) -> Self {
Self(eco_vec![message])
}
pub fn message(&self) -> &EcoString {
self.0.first().unwrap()
}
pub fn hints(&self) -> &[EcoString] {
self.0.get(1..).unwrap_or(&[])
}
pub fn hint(&mut self, hint: impl Into<EcoString>) {
self.0.push(hint.into());
}
pub fn with_hint(mut self, hint: impl Into<EcoString>) -> Self {
self.hint(hint);
self
}
pub fn with_hints(mut self, hints: impl IntoIterator<Item = EcoString>) -> Self {
self.0.extend(hints);
self
}
}
impl<S> From<S> for HintedString
where
S: Into<EcoString>,
{
fn from(value: S) -> Self {
Self::new(value.into())
}
}
impl<T> At<T> for HintedStrResult<T> {
fn at(self, span: Span) -> SourceResult<T> {
self.map_err(|err| {
let mut components = err.0.into_iter();
let message = components.next().unwrap();
let diag = SourceDiagnostic::error(span, message).with_hints(components);
eco_vec![diag]
})
}
}
pub trait Hint<T> {
fn hint(self, hint: impl Into<EcoString>) -> HintedStrResult<T>;
}
impl<T, S> Hint<T> for Result<T, S>
where
S: Into<EcoString>,
{
fn hint(self, hint: impl Into<EcoString>) -> HintedStrResult<T> {
self.map_err(|message| HintedString::new(message.into()).with_hint(hint))
}
}
impl<T> Hint<T> for HintedStrResult<T> {
fn hint(self, hint: impl Into<EcoString>) -> HintedStrResult<T> {
self.map_err(|mut error| {
error.hint(hint.into());
error
})
}
}
pub type FileResult<T> = Result<T, FileError>;
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub enum FileError {
NotFound(PathBuf),
AccessDenied,
IsDirectory,
NotSource,
InvalidUtf8,
Package(PackageError),
Other(Option<EcoString>),
}
impl FileError {
pub fn from_io(err: io::Error, path: &Path) -> Self {
match err.kind() {
io::ErrorKind::NotFound => Self::NotFound(path.into()),
io::ErrorKind::PermissionDenied => Self::AccessDenied,
io::ErrorKind::InvalidData
if err.to_string().contains("stream did not contain valid UTF-8") =>
{
Self::InvalidUtf8
}
_ => Self::Other(Some(eco_format!("{err}"))),
}
}
}
impl std::error::Error for FileError {}
impl Display for FileError {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
match self {
Self::NotFound(path) => {
write!(f, "file not found (searched at {})", path.display())
}
Self::AccessDenied => f.pad("failed to load file (access denied)"),
Self::IsDirectory => f.pad("failed to load file (is a directory)"),
Self::NotSource => f.pad("not a typst source file"),
Self::InvalidUtf8 => f.pad("file is not valid utf-8"),
Self::Package(error) => error.fmt(f),
Self::Other(Some(err)) => write!(f, "failed to load file ({err})"),
Self::Other(None) => f.pad("failed to load file"),
}
}
}
impl From<Utf8Error> for FileError {
fn from(_: Utf8Error) -> Self {
Self::InvalidUtf8
}
}
impl From<FromUtf8Error> for FileError {
fn from(_: FromUtf8Error) -> Self {
Self::InvalidUtf8
}
}
impl From<PackageError> for FileError {
fn from(err: PackageError) -> Self {
Self::Package(err)
}
}
impl From<FileError> for EcoString {
fn from(err: FileError) -> Self {
eco_format!("{err}")
}
}
pub type PackageResult<T> = Result<T, PackageError>;
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub enum PackageError {
NotFound(PackageSpec),
VersionNotFound(PackageSpec, PackageVersion),
NetworkFailed(Option<EcoString>),
MalformedArchive(Option<EcoString>),
Other(Option<EcoString>),
}
impl std::error::Error for PackageError {}
impl Display for PackageError {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
match self {
Self::NotFound(spec) => {
write!(f, "package not found (searched for {spec})",)
}
Self::VersionNotFound(spec, latest) => {
write!(
f,
"package found, but version {} does not exist (latest is {})",
spec.version, latest,
)
}
Self::NetworkFailed(Some(err)) => {
write!(f, "failed to download package ({err})")
}
Self::NetworkFailed(None) => f.pad("failed to download package"),
Self::MalformedArchive(Some(err)) => {
write!(f, "failed to decompress package ({err})")
}
Self::MalformedArchive(None) => {
f.pad("failed to decompress package (archive malformed)")
}
Self::Other(Some(err)) => write!(f, "failed to load package ({err})"),
Self::Other(None) => f.pad("failed to load package"),
}
}
}
impl From<PackageError> for EcoString {
fn from(err: PackageError) -> Self {
eco_format!("{err}")
}
}
pub fn format_xml_like_error(format: &str, error: roxmltree::Error) -> EcoString {
match error {
roxmltree::Error::UnexpectedCloseTag(expected, actual, pos) => {
eco_format!(
"failed to parse {format} (found closing tag '{actual}' \
instead of '{expected}' in line {})",
pos.row
)
}
roxmltree::Error::UnknownEntityReference(entity, pos) => {
eco_format!(
"failed to parse {format} (unknown entity '{entity}' in line {})",
pos.row
)
}
roxmltree::Error::DuplicatedAttribute(attr, pos) => {
eco_format!(
"failed to parse {format} (duplicate attribute '{attr}' in line {})",
pos.row
)
}
roxmltree::Error::NoRootNode => {
eco_format!("failed to parse {format} (missing root node)")
}
err => eco_format!("failed to parse {format} ({err})"),
}
}