improve error display

This commit is contained in:
Moritz Hölting 2024-09-19 00:12:24 +02:00
parent 6f3c152e73
commit 2bc8281f19
5 changed files with 142 additions and 34 deletions

View File

@ -1,21 +1,24 @@
/// An error that occurred during compilation. /// An error that occurred during compilation.
#[allow(missing_docs)] #[allow(missing_docs)]
#[derive(Debug, thiserror::Error, Clone, PartialEq, Eq)] #[derive(Debug, thiserror::Error, Clone, PartialEq)]
pub enum Error { pub enum Error {
#[error("An error occurred while working with Input/Output: {0}")] #[error("FileProviderError: {0}")]
IoError(String), FileProviderError(#[from] super::FileProviderError),
#[error(transparent)] #[error(transparent)]
Utf8Error(#[from] std::str::Utf8Error),
#[error("An error occurred while lexing the source code: {0}")]
LexicalError(#[from] crate::lexical::Error), LexicalError(#[from] crate::lexical::Error),
#[error("An error occured while tokenizing the source code: {0}")]
TokenizeError(#[from] crate::lexical::token::TokenizeError),
#[error(transparent)] #[error(transparent)]
ParseError(#[from] crate::syntax::error::Error), ParseError(#[from] crate::syntax::error::Error),
#[error(transparent)] #[error(transparent)]
TranspileError(#[from] crate::transpile::TranspileError), TranspileError(#[from] crate::transpile::TranspileError),
#[error("An error occurred: {0}")] #[error("An error occurred: {0}")]
Other(&'static str), Other(String),
}
impl Error {
/// Creates a new error from a string.
pub fn other<S: Into<String>>(error: S) -> Self {
Self::Other(error.into())
}
} }
/// A specialized [`Result`] type for this crate. /// A specialized [`Result`] type for this crate.

View File

@ -1,10 +1,10 @@
use std::{ use std::{
borrow::Cow, borrow::Cow,
fmt::Display,
path::{Path, PathBuf}, path::{Path, PathBuf},
sync::Arc,
}; };
use super::Error;
/// A trait for providing file contents. /// A trait for providing file contents.
pub trait FileProvider { pub trait FileProvider {
/// Reads the contents of the file at the given path as bytes. /// Reads the contents of the file at the given path as bytes.
@ -22,7 +22,12 @@ pub trait FileProvider {
/// - If the file is not valid UTF-8. /// - If the file is not valid UTF-8.
fn read_str<P: AsRef<Path>>(&self, path: P) -> Result<Cow<str>, Error> { fn read_str<P: AsRef<Path>>(&self, path: P) -> Result<Cow<str>, Error> {
let bytes = self.read_bytes(path)?; let bytes = self.read_bytes(path)?;
let string = std::str::from_utf8(&bytes)?.to_string(); let string = std::str::from_utf8(&bytes)
.map_err(|err| {
let arc: Arc<dyn std::error::Error + Send + Sync> = Arc::new(err);
Error::other(arc)
})?
.to_string();
Ok(Cow::Owned(string)) Ok(Cow::Owned(string))
} }
} }
@ -56,20 +61,118 @@ impl FileProvider for FsProvider {
let full_path = self.root.join(path); let full_path = self.root.join(path);
std::fs::read(full_path) std::fs::read(full_path)
.map(Cow::Owned) .map(Cow::Owned)
.map_err(|err| Error::IoError(err.to_string())) .map_err(Error::from)
} }
fn read_str<P: AsRef<Path>>(&self, path: P) -> Result<Cow<str>, Error> { fn read_str<P: AsRef<Path>>(&self, path: P) -> Result<Cow<str>, Error> {
let full_path = self.root.join(path); let full_path = self.root.join(path);
std::fs::read_to_string(full_path) std::fs::read_to_string(full_path)
.map(Cow::Owned) .map(Cow::Owned)
.map_err(|err| Error::IoError(err.to_string())) .map_err(Error::from)
}
}
/// The error type for [`FileProvider`] operations.
#[allow(clippy::module_name_repetitions)]
#[derive(Debug, Clone, thiserror::Error)]
pub struct Error {
kind: std::io::ErrorKind,
#[source]
error: Option<Arc<dyn std::error::Error + Send + Sync>>,
}
impl Error {
/// Creates a new [`FileProviderError`] from a known kind of error as well as an
/// arbitrary error payload.
///
/// The `error` argument is an arbitrary
/// payload which will be contained in this [`FileProviderError`].
///
/// Note that this function allocates memory on the heap.
/// If no extra payload is required, use the `From` conversion from
/// `ErrorKind`.
pub fn new<E>(kind: std::io::ErrorKind, error: E) -> Self
where
E: Into<Arc<dyn std::error::Error + Send + Sync>>,
{
Self {
kind,
error: Some(error.into()),
}
}
/// Creates a new [`FileProviderError`] from an arbitrary error payload.
///
/// It is a shortcut for [`FileProviderError::new`]
/// with [`std::io::ErrorKind::Other`].
pub fn other<E>(error: E) -> Self
where
E: Into<Arc<dyn std::error::Error + Send + Sync>>,
{
Self::new(std::io::ErrorKind::Other, error)
}
/// Returns a reference to the inner error wrapped by this error (if any).
///
/// If this [`FileProviderError`] was constructed via [`Self::new`] then this function will
/// return [`Some`], otherwise it will return [`None`].
#[must_use]
pub fn get_ref(&self) -> Option<&(dyn std::error::Error + Send + Sync + 'static)> {
return self.error.as_deref();
}
/// Consumes the [`FileProviderError`], returning its inner error (if any).
///
/// If this [`Error`] was constructed via [`Self::new`] then this function will
/// return [`Some`], otherwise it will return [`None`].
#[must_use]
pub fn into_inner(self) -> Option<Arc<dyn std::error::Error + Send + Sync>> {
self.error
}
/// Returns the corresponding [`std::io::ErrorKind`] for this error.
#[must_use]
pub fn kind(&self) -> std::io::ErrorKind {
self.kind
}
}
impl Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match &self.error {
Some(err) => write!(f, "{}: {}", self.kind, err),
None => write!(f, "{}", self.kind),
}
}
}
impl PartialEq for Error {
fn eq(&self, _: &Self) -> bool {
false
}
}
impl From<std::io::ErrorKind> for Error {
fn from(value: std::io::ErrorKind) -> Self {
Self {
kind: value,
error: None,
}
}
}
impl From<std::io::Error> for Error {
fn from(value: std::io::Error) -> Self {
let kind = value.kind();
let error = value.into_inner().map(Arc::from);
Self { kind, error }
} }
} }
#[cfg(feature = "shulkerbox")] #[cfg(feature = "shulkerbox")]
mod vfs { mod vfs {
use std::borrow::Cow; use std::{borrow::Cow, sync::Arc};
use super::{Error, FileProvider, Path}; use super::{Error, FileProvider, Path};
use shulkerbox::virtual_fs::{VFile, VFolder}; use shulkerbox::virtual_fs::{VFile, VFolder};
@ -77,10 +180,10 @@ mod vfs {
impl FileProvider for VFolder { impl FileProvider for VFolder {
fn read_bytes<P: AsRef<Path>>(&self, path: P) -> Result<Cow<[u8]>, Error> { fn read_bytes<P: AsRef<Path>>(&self, path: P) -> Result<Cow<[u8]>, Error> {
normalize_path_str(path).map_or_else( normalize_path_str(path).map_or_else(
|| Err(Error::IoError("Invalid path".to_string())), || Err(Error::from(std::io::ErrorKind::InvalidData)),
|path| { |path| {
self.get_file(&path) self.get_file(&path)
.ok_or_else(|| Error::IoError("File not found".to_string())) .ok_or_else(|| Error::from(std::io::ErrorKind::NotFound))
.map(|file| Cow::Borrowed(file.as_bytes())) .map(|file| Cow::Borrowed(file.as_bytes()))
}, },
) )
@ -88,15 +191,18 @@ mod vfs {
fn read_str<P: AsRef<Path>>(&self, path: P) -> Result<Cow<str>, Error> { fn read_str<P: AsRef<Path>>(&self, path: P) -> Result<Cow<str>, Error> {
normalize_path_str(path).map_or_else( normalize_path_str(path).map_or_else(
|| Err(Error::IoError("Invalid path".to_string())), || Err(Error::from(std::io::ErrorKind::InvalidData)),
|path| { |path| {
self.get_file(&path) self.get_file(&path)
.ok_or_else(|| Error::IoError("File not found".to_string())) .ok_or_else(|| Error::from(std::io::ErrorKind::NotFound))
.and_then(|file| match file { .and_then(|file| match file {
VFile::Text(text) => Ok(Cow::Borrowed(text.as_str())), VFile::Text(text) => Ok(Cow::Borrowed(text.as_str())),
VFile::Binary(bin) => { VFile::Binary(bin) => {
let string = std::str::from_utf8(bin) let string = std::str::from_utf8(bin).map_err(|err| {
.map_err(|err| Error::IoError(err.to_string()))?; let arc: Arc<dyn std::error::Error + Send + Sync> =
Arc::new(err);
Error::new(std::io::ErrorKind::InvalidData, arc)
})?;
Ok(Cow::Borrowed(string)) Ok(Cow::Borrowed(string))
} }
@ -161,10 +267,7 @@ mod vfs {
dir.read_str("bar/baz.txt").unwrap().into_owned(), dir.read_str("bar/baz.txt").unwrap().into_owned(),
"bar, baz".to_string() "bar, baz".to_string()
); );
assert!(matches!( assert!(dir.read_str("nonexistent.txt").is_err());
dir.read_str("nonexistent.txt"),
Err(Error::IoError(_))
));
} }
} }
} }

View File

@ -10,6 +10,6 @@ mod diagnostic;
pub use diagnostic::{Handler, PrintHandler, SilentHandler, VoidHandler}; pub use diagnostic::{Handler, PrintHandler, SilentHandler, VoidHandler};
mod file_provider; mod file_provider;
pub use file_provider::{FileProvider, FsProvider}; pub use file_provider::{Error as FileProviderError, FileProvider, FsProvider};
pub mod log; pub mod log;

View File

@ -7,16 +7,18 @@ use crate::base::{
source_file::Span, source_file::Span,
}; };
use super::token_stream::Delimiter; use super::{token, token_stream::Delimiter};
/// Represents an error that occurred during the lexical analysis of the source code. /// Represents an error that occurred during the lexical analysis of the source code.
#[allow(missing_docs)] #[allow(missing_docs)]
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, thiserror::Error)] #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, thiserror::Error)]
pub enum Error { pub enum Error {
#[error("Comment is not terminated.")] #[error(transparent)]
UnterminatedDelimitedComment(#[from] UnterminatedDelimitedComment), UnterminatedDelimitedComment(#[from] UnterminatedDelimitedComment),
#[error("Delimiter is not terminated.")] #[error(transparent)]
UndelimitedDelimiter(#[from] UndelimitedDelimiter), UndelimitedDelimiter(#[from] UndelimitedDelimiter),
#[error("Tokenize error: {0}")]
TokenizeError(#[from] token::TokenizeError),
} }
/// Source code contains an unclosed `/*` comment. /// Source code contains an unclosed `/*` comment.

View File

@ -81,7 +81,7 @@ pub fn parse(
let tokens = tokenize(handler, file_provider, path, identifier)?; let tokens = tokenize(handler, file_provider, path, identifier)?;
if handler.has_received() { if handler.has_received() {
return Err(Error::Other( return Err(Error::other(
"An error occurred while tokenizing the source code.", "An error occurred while tokenizing the source code.",
)); ));
} }
@ -89,12 +89,12 @@ pub fn parse(
tracing::info!("Parsing the source code at path: {}", path.display()); tracing::info!("Parsing the source code at path: {}", path.display());
let mut parser = Parser::new(&tokens); let mut parser = Parser::new(&tokens);
let program = parser.parse_program(handler).ok_or(Error::Other( let program = parser
"An error occurred while parsing the source code.", .parse_program(handler)
))?; .ok_or_else(|| Error::other("An error occurred while parsing the source code."))?;
if handler.has_received() { if handler.has_received() {
return Err(Error::Other( return Err(Error::other(
"An error occurred while parsing the source code.", "An error occurred while parsing the source code.",
)); ));
} }
@ -168,7 +168,7 @@ where
let datapack = transpiler.into_datapack(); let datapack = transpiler.into_datapack();
if handler.has_received() { if handler.has_received() {
return Err(Error::Other( return Err(Error::other(
"An error occurred while transpiling the source code.", "An error occurred while transpiling the source code.",
)); ));
} }