use std::path::PathBuf;
use deno_core::error::CoreErrorKind;
use thiserror::Error;
use crate::Module;
#[allow(clippy::struct_excessive_bools)]
#[derive(Debug, Clone)]
pub struct ErrorFormattingOptions {
pub include_filename: bool,
pub include_line_number: bool,
pub include_column_number: bool,
pub hide_current_directory: bool,
pub current_directory: Option<PathBuf>,
}
impl Default for ErrorFormattingOptions {
fn default() -> Self {
Self {
include_filename: true,
include_line_number: true,
include_column_number: true,
hide_current_directory: false,
current_directory: None,
}
}
}
#[derive(Error, Debug, Clone, serde::Serialize, serde::Deserialize, deno_error::JsError)]
pub enum Error {
#[class(generic)]
#[error("{0} has no entrypoint. Register one, or add a default to the runtime")]
MissingEntrypoint(Module),
#[class(generic)]
#[error("{0} could not be found in global, or module exports")]
ValueNotFound(String),
#[class(generic)]
#[error("{0} is not a function")]
ValueNotCallable(String),
#[class(generic)]
#[error("{0} could not be encoded as a v8 value")]
V8Encoding(String),
#[class(generic)]
#[error("value could not be deserialized: {0}")]
JsonDecode(String),
#[class(generic)]
#[error("{0}")]
ModuleNotFound(String),
#[class(generic)]
#[error("This worker has been destroyed")]
WorkerHasStopped,
#[class(generic)]
#[error("{0}")]
Runtime(String),
#[class(generic)]
#[error("{0}")]
JsError(Box<deno_core::error::JsError>),
#[class(generic)]
#[error("Module timed out: {0}")]
Timeout(String),
#[class(generic)]
#[error("Heap exhausted")]
HeapExhausted,
}
impl From<deno_core::error::JsError> for Error {
fn from(err: deno_core::error::JsError) -> Self {
Self::JsError(Box::new(err))
}
}
impl Error {
#[must_use]
pub fn as_highlighted(&self, options: ErrorFormattingOptions) -> String {
let mut e = if let Error::JsError(e) = self {
let (filename, row, col) = match e.frames.first() {
Some(f) => (
match &f.file_name {
Some(f) if f.is_empty() => None::<&str>,
Some(f) => Some(f.as_ref()),
None => None,
},
usize::try_from(f.line_number.unwrap_or(1)).unwrap_or_default(),
usize::try_from(f.column_number.unwrap_or(1)).unwrap_or_default(),
),
None => (None, 1, 1),
};
let mut line = e.source_line.as_ref().map(|s| s.trim_end());
let col = col - 1;
let mut padding = String::new();
match line {
None => {}
Some(s) => {
let (start, end) = if s.len() < 50 {
(0, s.len())
} else if col < 25 {
(0, 50)
} else if col > s.len() - 25 {
(s.len() - 50, s.len())
} else {
(col - 25, col + 25)
};
line = Some(s.get(start..end).unwrap_or(s));
padding = " ".repeat(col - start);
}
}
let msg_lines = e.exception_message.split('\n').collect::<Vec<_>>();
let line_number_part = if options.include_line_number {
format!("{row}:")
} else {
String::new()
};
let col_number_part = if options.include_column_number {
format!("{col}:")
} else {
String::new()
};
let source_line_part = match line {
Some(s) => format!("| {s}\n| {padding}^\n"),
None => String::new(),
};
let msg_part = msg_lines
.into_iter()
.map(|l| format!("= {l}"))
.collect::<Vec<_>>()
.join("\n");
let position_part = format!("{line_number_part}{col_number_part}");
let position_part = match filename {
None if position_part.is_empty() => String::new(),
Some(f) if options.include_filename => format!("At {f}:{position_part}\n"),
_ => format!("At {position_part}\n"),
};
format!("{position_part}{source_line_part}{msg_part}",)
} else {
self.to_string()
};
if options.hide_current_directory {
let dir = options.current_directory.or(std::env::current_dir().ok());
if let Some(dir) = dir {
let dir = dir.to_string_lossy().replace('\\', "/");
e = e.replace(&dir, "");
e = e.replace("file:////", "file:///");
}
}
e
}
}
#[macro_use]
mod error_macro {
macro_rules! map_error {
($source_error:path, $impl:expr) => {
impl From<$source_error> for Error {
fn from(e: $source_error) -> Self {
let fmt: &dyn Fn($source_error) -> Self = &$impl;
fmt(e)
}
}
};
}
}
#[cfg(feature = "node_experimental")]
map_error!(node_resolver::analyze::TranslateCjsToEsmError, |e| {
Error::Runtime(e.to_string())
});
map_error!(deno_ast::TranspileError, |e| Error::Runtime(e.to_string()));
map_error!(deno_core::error::CoreError, |e| {
let e = e.into_kind();
match e {
CoreErrorKind::Js(js_error) => Error::JsError(Box::new(js_error)),
_ => Error::Runtime(e.to_string()),
}
});
map_error!(std::cell::BorrowMutError, |e| Error::Runtime(e.to_string()));
map_error!(std::io::Error, |e| Error::ModuleNotFound(e.to_string()));
map_error!(deno_core::v8::DataError, |e| Error::Runtime(e.to_string()));
map_error!(deno_core::ModuleResolutionError, |e| Error::Runtime(
e.to_string()
));
map_error!(deno_core::url::ParseError, |e| Error::Runtime(
e.to_string()
));
map_error!(deno_core::serde_json::Error, |e| Error::JsonDecode(
e.to_string()
));
map_error!(deno_core::serde_v8::Error, |e| Error::JsonDecode(
e.to_string()
));
map_error!(deno_core::anyhow::Error, |e| {
Error::Runtime(e.to_string())
});
map_error!(tokio::time::error::Elapsed, |e| {
Error::Timeout(e.to_string())
});
map_error!(tokio::task::JoinError, |e| {
Error::Timeout(e.to_string())
});
map_error!(deno_core::futures::channel::oneshot::Canceled, |e| {
Error::Timeout(e.to_string())
});
#[cfg(feature = "broadcast_channel")]
map_error!(deno_broadcast_channel::BroadcastChannelError, |e| {
Error::Runtime(e.to_string())
});
#[cfg(test)]
mod test {
use crate::{error::ErrorFormattingOptions, Module, Runtime, RuntimeOptions, Undefined};
#[test]
#[rustfmt::skip]
fn test_highlights() {
let mut runtime = Runtime::new(RuntimeOptions::default()).unwrap();
let e = runtime.eval::<Undefined>("1+1;\n1 + x").unwrap_err().as_highlighted(ErrorFormattingOptions::default());
assert_eq!(e, concat!(
"At 2:4:\n",
"= Uncaught ReferenceError: x is not defined"
));
let module = Module::new("test.js", "1+1;\n1 + x");
let e = runtime.load_module(&module).unwrap_err().as_highlighted(ErrorFormattingOptions {
include_filename: false,
..Default::default()
});
assert_eq!(e, concat!(
"At 2:4:\n",
"| 1 + x\n",
"| ^\n",
"= Uncaught (in promise) ReferenceError: x is not defined"
));
let module = Module::new("test.js", "1+1;\n1 + x");
let e = runtime.load_module(&module).unwrap_err().as_highlighted(ErrorFormattingOptions {
hide_current_directory: true,
..Default::default()
});
assert_eq!(e, concat!(
"At file:///test.js:2:4:\n",
"| 1 + x\n",
"| ^\n",
"= Uncaught (in promise) ReferenceError: x is not defined"
));
}
}