From 2bc8281f19040affcfa1c127c5982d91385adb5e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20H=C3=B6lting?= <87192362+moritz-hoelting@users.noreply.github.com> Date: Thu, 19 Sep 2024 00:12:24 +0200 Subject: [PATCH] improve error display --- src/base/error.rs | 19 +++--- src/base/file_provider.rs | 135 +++++++++++++++++++++++++++++++++----- src/base/mod.rs | 2 +- src/lexical/error.rs | 8 ++- src/lib.rs | 12 ++-- 5 files changed, 142 insertions(+), 34 deletions(-) diff --git a/src/base/error.rs b/src/base/error.rs index 0d71f36..ec14dba 100644 --- a/src/base/error.rs +++ b/src/base/error.rs @@ -1,21 +1,24 @@ /// An error that occurred during compilation. #[allow(missing_docs)] -#[derive(Debug, thiserror::Error, Clone, PartialEq, Eq)] +#[derive(Debug, thiserror::Error, Clone, PartialEq)] pub enum Error { - #[error("An error occurred while working with Input/Output: {0}")] - IoError(String), + #[error("FileProviderError: {0}")] + FileProviderError(#[from] super::FileProviderError), #[error(transparent)] - Utf8Error(#[from] std::str::Utf8Error), - #[error("An error occurred while lexing the source code: {0}")] LexicalError(#[from] crate::lexical::Error), - #[error("An error occured while tokenizing the source code: {0}")] - TokenizeError(#[from] crate::lexical::token::TokenizeError), #[error(transparent)] ParseError(#[from] crate::syntax::error::Error), #[error(transparent)] TranspileError(#[from] crate::transpile::TranspileError), #[error("An error occurred: {0}")] - Other(&'static str), + Other(String), +} + +impl Error { + /// Creates a new error from a string. + pub fn other>(error: S) -> Self { + Self::Other(error.into()) + } } /// A specialized [`Result`] type for this crate. diff --git a/src/base/file_provider.rs b/src/base/file_provider.rs index f81ee53..6e704c8 100644 --- a/src/base/file_provider.rs +++ b/src/base/file_provider.rs @@ -1,10 +1,10 @@ use std::{ borrow::Cow, + fmt::Display, path::{Path, PathBuf}, + sync::Arc, }; -use super::Error; - /// A trait for providing file contents. pub trait FileProvider { /// 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. fn read_str>(&self, path: P) -> Result, Error> { 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 = Arc::new(err); + Error::other(arc) + })? + .to_string(); Ok(Cow::Owned(string)) } } @@ -56,20 +61,118 @@ impl FileProvider for FsProvider { let full_path = self.root.join(path); std::fs::read(full_path) .map(Cow::Owned) - .map_err(|err| Error::IoError(err.to_string())) + .map_err(Error::from) } fn read_str>(&self, path: P) -> Result, Error> { let full_path = self.root.join(path); std::fs::read_to_string(full_path) .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>, +} + +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(kind: std::io::ErrorKind, error: E) -> Self + where + E: Into>, + { + 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(error: E) -> Self + where + E: Into>, + { + 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> { + 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 for Error { + fn from(value: std::io::ErrorKind) -> Self { + Self { + kind: value, + error: None, + } + } +} + +impl From 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")] mod vfs { - use std::borrow::Cow; + use std::{borrow::Cow, sync::Arc}; use super::{Error, FileProvider, Path}; use shulkerbox::virtual_fs::{VFile, VFolder}; @@ -77,10 +180,10 @@ mod vfs { impl FileProvider for VFolder { fn read_bytes>(&self, path: P) -> Result, Error> { normalize_path_str(path).map_or_else( - || Err(Error::IoError("Invalid path".to_string())), + || Err(Error::from(std::io::ErrorKind::InvalidData)), |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())) }, ) @@ -88,15 +191,18 @@ mod vfs { fn read_str>(&self, path: P) -> Result, Error> { normalize_path_str(path).map_or_else( - || Err(Error::IoError("Invalid path".to_string())), + || Err(Error::from(std::io::ErrorKind::InvalidData)), |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 { VFile::Text(text) => Ok(Cow::Borrowed(text.as_str())), VFile::Binary(bin) => { - let string = std::str::from_utf8(bin) - .map_err(|err| Error::IoError(err.to_string()))?; + let string = std::str::from_utf8(bin).map_err(|err| { + let arc: Arc = + Arc::new(err); + Error::new(std::io::ErrorKind::InvalidData, arc) + })?; Ok(Cow::Borrowed(string)) } @@ -161,10 +267,7 @@ mod vfs { dir.read_str("bar/baz.txt").unwrap().into_owned(), "bar, baz".to_string() ); - assert!(matches!( - dir.read_str("nonexistent.txt"), - Err(Error::IoError(_)) - )); + assert!(dir.read_str("nonexistent.txt").is_err()); } } } diff --git a/src/base/mod.rs b/src/base/mod.rs index e5369bd..5e2fd1f 100644 --- a/src/base/mod.rs +++ b/src/base/mod.rs @@ -10,6 +10,6 @@ mod diagnostic; pub use diagnostic::{Handler, PrintHandler, SilentHandler, VoidHandler}; mod file_provider; -pub use file_provider::{FileProvider, FsProvider}; +pub use file_provider::{Error as FileProviderError, FileProvider, FsProvider}; pub mod log; diff --git a/src/lexical/error.rs b/src/lexical/error.rs index d80dfd4..36f6331 100644 --- a/src/lexical/error.rs +++ b/src/lexical/error.rs @@ -7,16 +7,18 @@ use crate::base::{ 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. #[allow(missing_docs)] #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, thiserror::Error)] pub enum Error { - #[error("Comment is not terminated.")] + #[error(transparent)] UnterminatedDelimitedComment(#[from] UnterminatedDelimitedComment), - #[error("Delimiter is not terminated.")] + #[error(transparent)] UndelimitedDelimiter(#[from] UndelimitedDelimiter), + #[error("Tokenize error: {0}")] + TokenizeError(#[from] token::TokenizeError), } /// Source code contains an unclosed `/*` comment. diff --git a/src/lib.rs b/src/lib.rs index ab696bf..0259ef4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -81,7 +81,7 @@ pub fn parse( let tokens = tokenize(handler, file_provider, path, identifier)?; if handler.has_received() { - return Err(Error::Other( + return Err(Error::other( "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()); let mut parser = Parser::new(&tokens); - let program = parser.parse_program(handler).ok_or(Error::Other( - "An error occurred while parsing the source code.", - ))?; + let program = parser + .parse_program(handler) + .ok_or_else(|| Error::other("An error occurred while parsing the source code."))?; if handler.has_received() { - return Err(Error::Other( + return Err(Error::other( "An error occurred while parsing the source code.", )); } @@ -168,7 +168,7 @@ where let datapack = transpiler.into_datapack(); if handler.has_received() { - return Err(Error::Other( + return Err(Error::other( "An error occurred while transpiling the source code.", )); }