sailfish/sailfish-compiler/src/error.rs

278 lines
7.4 KiB
Rust

use std::env;
use std::fmt;
use std::fs;
use std::io;
use std::path::{Path, PathBuf};
#[non_exhaustive]
#[derive(Debug)]
pub enum ErrorKind {
FmtError(fmt::Error),
IoError(io::Error),
RustSyntaxError(syn::Error),
ConfigError(String),
ParseError(String),
AnalyzeError(String),
Unimplemented(String),
Other(String),
}
impl fmt::Display for ErrorKind {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match *self {
ErrorKind::FmtError(ref e) => e.fmt(f),
ErrorKind::IoError(ref e) => e.fmt(f),
ErrorKind::RustSyntaxError(ref e) => write!(f, "Rust Syntax Error ({})", e),
ErrorKind::ConfigError(ref e) => write!(f, "Invalid configuration ({})", e),
ErrorKind::ParseError(ref msg) => write!(f, "Parse error ({})", msg),
ErrorKind::AnalyzeError(ref msg) => write!(f, "Analyzation error ({})", msg),
ErrorKind::Unimplemented(ref msg) => f.write_str(&**msg),
ErrorKind::Other(ref msg) => f.write_str(&**msg),
}
}
}
macro_rules! impl_errorkind_conversion {
($source:ty, $kind:ident, $conv:expr, [ $($lifetimes:tt),* ]) => {
impl<$($lifetimes),*> From<$source> for ErrorKind {
#[inline]
fn from(other: $source) -> Self {
ErrorKind::$kind($conv(other))
}
}
};
($source:ty, $kind:ident) => {
impl_errorkind_conversion!($source, $kind, std::convert::identity, []);
}
}
impl_errorkind_conversion!(fmt::Error, FmtError);
impl_errorkind_conversion!(io::Error, IoError);
impl_errorkind_conversion!(syn::Error, RustSyntaxError);
impl_errorkind_conversion!(String, Other);
impl_errorkind_conversion!(&'a str, Other, |s: &str| s.to_owned(), ['a]);
#[derive(Debug, Default)]
pub struct Error {
pub(crate) source_file: Option<PathBuf>,
pub(crate) source: Option<String>,
pub(crate) offset: Option<usize>,
pub(crate) chains: Vec<ErrorKind>,
}
impl Error {
pub fn from_kind(kind: ErrorKind) -> Self {
Self {
chains: vec![kind],
..Self::default()
}
}
pub fn kind(&self) -> &ErrorKind {
self.chains.last().unwrap()
}
pub fn iter(&self) -> impl Iterator<Item = &ErrorKind> {
self.chains.iter().rev()
}
}
impl<T> From<T> for Error
where
ErrorKind: From<T>,
{
fn from(other: T) -> Self {
Self::from_kind(ErrorKind::from(other))
}
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let source = match (self.source.as_ref(), self.source_file.as_deref()) {
(Some(s), _) => Some(s.to_owned()),
(None, Some(f)) => fs::read_to_string(f).ok(),
(None, None) => None,
};
writeln!(f, "{}", self.chains.last().unwrap())?;
for e in self.chains.iter().rev().skip(1) {
writeln!(f, "caused by: {}", e)?;
}
f.write_str("\n")?;
if let Some(ref source_file) = self.source_file {
let source_file =
if env::var("SAILFISH_INTEGRATION_TESTS").map_or(false, |s| s == "1") {
match source_file.file_name() {
Some(f) => Path::new(f),
None => Path::new(""),
}
} else {
source_file
};
writeln!(f, "file: {}", source_file.display())?;
}
if let Some(ref source) = source {
if let Some(offset) = self.offset {
let (lineno, colno) = into_line_column(source, offset);
writeln!(f, "position: line {}, column {}\n", lineno, colno)?;
// TODO: display adjacent lines
let line = source.lines().nth(lineno - 1).unwrap();
let lpad = count_digits(lineno);
writeln!(f, "{:<lpad$} |", "", lpad = lpad)?;
writeln!(f, "{} | {}", lineno, line)?;
writeln!(
f,
"{:<lpad$} | {:<rpad$}^",
"",
"",
lpad = lpad,
rpad = colno - 1
)?;
} else {
let line_count = source.lines().count();
let lpad = count_digits(line_count);
for (i, line) in source.lines().enumerate() {
writeln!(f, "{:>lpad$} | {line}", i + 1, lpad = lpad)?;
}
}
}
Ok(())
}
}
impl std::error::Error for Error {}
pub trait ResultExt<T> {
fn chain_err<F, EK>(self, kind: F) -> Result<T, Error>
where
F: FnOnce() -> EK,
EK: Into<ErrorKind>;
}
impl<T> ResultExt<T> for Result<T, Error> {
fn chain_err<F, EK>(self, kind: F) -> Result<T, Error>
where
F: FnOnce() -> EK,
EK: Into<ErrorKind>,
{
self.map_err(|mut e| {
e.chains.push(kind().into());
e
})
}
}
impl<T, E: Into<ErrorKind>> ResultExt<T> for Result<T, E> {
fn chain_err<F, EK>(self, kind: F) -> Result<T, Error>
where
F: FnOnce() -> EK,
EK: Into<ErrorKind>,
{
self.map_err(|e| {
let mut e = Error::from(e.into());
e.chains.push(kind().into());
e
})
}
}
fn into_line_column(source: &str, offset: usize) -> (usize, usize) {
assert!(
offset <= source.len(),
"Internal error: error position offset overflow (error code: 56066)"
);
let mut lineno = 1;
let mut colno = 1;
let mut current = 0;
for line in source.lines() {
let end = current + line.len() + 1;
if offset < end {
colno = offset - current + 1;
break;
}
lineno += 1;
current = end;
}
(lineno, colno)
}
fn count_digits(n: usize) -> usize {
let mut current = 10;
let mut digits = 1;
while current <= n {
current *= 10;
digits += 1;
}
digits
}
macro_rules! make_error {
($kind:expr) => {
$crate::Error::from_kind($kind)
};
($kind:expr, $($remain:tt)*) => {{
#[allow(unused_mut)]
let mut err = $crate::Error::from_kind($kind);
make_error!(@opt err $($remain)*);
err
}};
(@opt $var:ident $key:ident = $value:expr, $($remain:tt)*) => {
$var.$key = Some($value.into());
make_error!(@opt $var $($remain)*);
};
(@opt $var:ident $key:ident = $value:expr) => {
$var.$key = Some($value.into());
};
(@opt $var:ident $key:ident, $($remain:tt)*) => {
$var.$key = Some($key);
make_error!(@opt $var $($remain)*);
};
(@opt $var:ident $key:ident) => {
$var.$key = Some($key);
};
(@opt $var:ident) => {};
}
#[cfg(test)]
mod tests {
use super::*;
use pretty_assertions::assert_eq;
#[test]
fn display_error() {
let mut err = make_error!(
ErrorKind::AnalyzeError("mismatched types".to_owned()),
source_file = PathBuf::from("apple.rs"),
source = "fn func() {\n 1\n}".to_owned(),
offset = 16usize
);
err.chains.push(ErrorKind::Other("some error".to_owned()));
assert!(matches!(err.kind(), &ErrorKind::Other(_)));
assert_eq!(
err.to_string(),
r#"some error
caused by: Analyzation error (mismatched types)
file: apple.rs
position: line 2, column 5
|
2 | 1
| ^
"#
);
}
}