use std::borrow::Cow;
use std::collections::HashSet;
use std::fmt;
use std::fmt::Debug;
use std::fmt::Display;
use std::fmt::Formatter;
use std::fmt::Write as _;
use anyhow::Error;
use v8::Object;
use crate::runtime::v8_static_strings;
use crate::runtime::JsRealm;
use crate::runtime::JsRuntime;
use crate::source_map::SourceMapApplication;
use crate::url::Url;
use crate::FastStaticString;
pub type AnyError = anyhow::Error;
pub type JsErrorCreateFn = dyn Fn(JsError) -> Error;
pub type GetErrorClassFn = &'static dyn for<'e> Fn(&'e Error) -> &'static str;
pub fn custom_error(
class: &'static str,
message: impl Into<Cow<'static, str>>,
) -> Error {
CustomError {
class,
message: message.into(),
}
.into()
}
pub fn generic_error(message: impl Into<Cow<'static, str>>) -> Error {
custom_error("Error", message)
}
pub fn type_error(message: impl Into<Cow<'static, str>>) -> Error {
custom_error("TypeError", message)
}
pub fn range_error(message: impl Into<Cow<'static, str>>) -> Error {
custom_error("RangeError", message)
}
pub fn invalid_hostname(hostname: &str) -> Error {
type_error(format!("Invalid hostname: '{hostname}'"))
}
pub fn uri_error(message: impl Into<Cow<'static, str>>) -> Error {
custom_error("URIError", message)
}
pub fn bad_resource(message: impl Into<Cow<'static, str>>) -> Error {
custom_error("BadResource", message)
}
pub fn bad_resource_id() -> Error {
custom_error("BadResource", "Bad resource ID")
}
pub fn not_supported() -> Error {
custom_error("NotSupported", "The operation is not supported")
}
pub fn resource_unavailable() -> Error {
custom_error(
"Busy",
"Resource is unavailable because it is in use by a promise",
)
}
#[derive(Debug)]
struct CustomError {
class: &'static str,
message: Cow<'static, str>,
}
impl Display for CustomError {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
f.write_str(&self.message)
}
}
impl std::error::Error for CustomError {}
pub fn get_custom_error_class(error: &Error) -> Option<&'static str> {
error.downcast_ref::<CustomError>().map(|e| e.class)
}
#[repr(transparent)]
pub struct StdAnyError(pub Error);
impl std::fmt::Debug for StdAnyError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{:?}", self.0)
}
}
impl std::fmt::Display for StdAnyError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
impl std::error::Error for StdAnyError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
self.0.source()
}
}
impl From<Error> for StdAnyError {
fn from(err: Error) -> Self {
Self(err)
}
}
pub fn to_v8_error<'a>(
scope: &mut v8::HandleScope<'a>,
get_class: GetErrorClassFn,
error: &Error,
) -> v8::Local<'a, v8::Value> {
let tc_scope = &mut v8::TryCatch::new(scope);
let cb = JsRealm::exception_state_from_scope(tc_scope)
.js_build_custom_error_cb
.borrow()
.clone()
.expect("Custom error builder must be set");
let cb = cb.open(tc_scope);
let this = v8::undefined(tc_scope).into();
let class = v8::String::new(tc_scope, get_class(error)).unwrap();
let message = v8::String::new(tc_scope, &format!("{error:#}")).unwrap();
let mut args = vec![class.into(), message.into()];
if let Some(code) = crate::error_codes::get_error_code(error) {
args.push(v8::String::new(tc_scope, code).unwrap().into());
}
let maybe_exception = cb.call(tc_scope, this, &args);
match maybe_exception {
Some(exception) => exception,
None => {
let mut msg =
"Custom error class must have a builder registered".to_string();
if tc_scope.has_caught() {
let e = tc_scope.exception().unwrap();
let js_error = JsError::from_v8_exception(tc_scope, e);
msg = format!("{}: {}", msg, js_error.exception_message);
}
panic!("{}", msg);
}
}
}
#[inline(always)]
pub(crate) fn call_site_evals_key<'a>(
scope: &mut v8::HandleScope<'a>,
) -> v8::Local<'a, v8::Private> {
let name = v8_static_strings::CALL_SITE_EVALS.v8_string(scope);
v8::Private::for_api(scope, Some(name))
}
#[derive(Debug, PartialEq, Clone, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct JsError {
pub name: Option<String>,
pub message: Option<String>,
pub stack: Option<String>,
pub cause: Option<Box<JsError>>,
pub exception_message: String,
pub frames: Vec<JsStackFrame>,
pub source_line: Option<String>,
pub source_line_frame_index: Option<usize>,
pub aggregated: Option<Vec<JsError>>,
}
#[derive(Debug, Eq, PartialEq, Clone, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct JsStackFrame {
pub type_name: Option<String>,
pub function_name: Option<String>,
pub method_name: Option<String>,
pub file_name: Option<String>,
pub line_number: Option<i64>,
pub column_number: Option<i64>,
pub eval_origin: Option<String>,
#[serde(rename = "isToplevel")]
pub is_top_level: Option<bool>,
pub is_eval: bool,
pub is_native: bool,
pub is_constructor: bool,
pub is_async: bool,
pub is_promise_all: bool,
pub promise_index: Option<i64>,
}
fn apply_source_map<'a>(
source_mapper: &mut crate::source_map::SourceMapper,
file_name: Cow<'a, str>,
line_number: i64,
column_number: i64,
) -> (Cow<'a, str>, i64, i64) {
match source_mapper.apply_source_map(
&file_name,
line_number as u32,
column_number as u32,
) {
SourceMapApplication::Unchanged => (file_name, line_number, column_number),
SourceMapApplication::LineAndColumn {
line_number,
column_number,
} => (file_name, line_number.into(), column_number.into()),
SourceMapApplication::LineAndColumnAndFileName {
file_name,
line_number,
column_number,
} => (file_name.into(), line_number.into(), column_number.into()),
}
}
fn parse_eval_origin(
eval_origin: &str,
) -> Option<(&str, (&str, i64, i64), &str)> {
let eval_at = "eval at ";
let mut innermost_start = eval_origin.rfind(eval_at)? + eval_at.len();
innermost_start += eval_origin[innermost_start..].find('(')? + 1;
if innermost_start >= eval_origin.len() {
return None;
}
let mut parts = eval_origin[innermost_start..].rsplitn(3, ':');
let column_number_with_rest = parts.next()?;
let column_number_end = column_number_with_rest.find(')')?;
let column_number = column_number_with_rest[..column_number_end]
.parse::<i64>()
.ok()?;
let line_number = parts.next()?.parse::<i64>().ok()?;
let file_name = parts.next()?;
let column_start = eval_origin.rfind(':')? + 1;
let innermost_end = column_start + column_number_end;
Some((
&eval_origin[..innermost_start],
(file_name, line_number, column_number),
&eval_origin[innermost_end..],
))
}
impl JsStackFrame {
pub fn from_location(
file_name: Option<String>,
line_number: Option<i64>,
column_number: Option<i64>,
) -> Self {
Self {
type_name: None,
function_name: None,
method_name: None,
file_name,
line_number,
column_number,
eval_origin: None,
is_top_level: None,
is_eval: false,
is_native: false,
is_constructor: false,
is_async: false,
is_promise_all: false,
promise_index: None,
}
}
fn from_callsite_object<'s>(
scope: &mut v8::HandleScope<'s>,
callsite: v8::Local<'s, v8::Object>,
) -> Option<Self> {
macro_rules! call {
($key: ident : $t: ty) => {{
let res = call_method(scope, callsite, $key, &[])?;
let res: $t = match serde_v8::from_v8(scope, res) {
Ok(res) => res,
Err(err) => {
let message = format!(
"Failed to deserialize return value from callsite property '{}' to correct type: {err:?}.",
$key
);
let message = v8::String::new(scope, &message).unwrap();
let exception = v8::Exception::type_error(scope, message);
scope.throw_exception(exception);
return None;
}
};
res
}};
($key: ident) => { call!($key : _) };
}
let state = JsRuntime::state_from(scope);
let mut source_mapper = state.source_mapper.borrow_mut();
let (file_name, line_number, column_number) = match (
call!(GET_FILE_NAME : Option<String>),
call!(GET_LINE_NUMBER),
call!(GET_COLUMN_NUMBER),
) {
(Some(f), Some(l), Some(c)) => {
let (file_name, line_num, col_num) =
apply_source_map(&mut source_mapper, f.into(), l, c);
(Some(file_name.into_owned()), Some(line_num), Some(col_num))
}
(f, l, c) => (f, l, c),
};
let eval_origin = call!(GET_EVAL_ORIGIN: Option<String>).and_then(|o| {
let Some((before, (file, line, col), after)) = parse_eval_origin(&o)
else {
return Some(o);
};
let (file, line, col) =
apply_source_map(&mut source_mapper, file.into(), line, col);
Some(format!("{before}{file}:{line}:{col}{after}"))
});
Some(Self {
file_name,
line_number,
column_number,
eval_origin,
type_name: call!(GET_TYPE_NAME),
function_name: call!(GET_FUNCTION_NAME),
method_name: call!(GET_METHOD_NAME),
is_top_level: call!(IS_TOPLEVEL),
is_eval: call!(IS_EVAL),
is_native: call!(IS_NATIVE),
is_constructor: call!(IS_CONSTRUCTOR),
is_async: call!(IS_ASYNC),
is_promise_all: call!(IS_PROMISE_ALL),
promise_index: call!(GET_PROMISE_INDEX),
})
}
pub fn from_v8_message<'a>(
scope: &'a mut v8::HandleScope,
message: v8::Local<'a, v8::Message>,
) -> Option<Self> {
let f = message.get_script_resource_name(scope)?;
let f: v8::Local<v8::String> = f.try_into().ok()?;
let f = f.to_rust_string_lossy(scope);
let l = message.get_line_number(scope)? as i64;
let c = message.get_start_column() as i64 + 1;
let state = JsRuntime::state_from(scope);
let mut source_mapper = state.source_mapper.borrow_mut();
let (file_name, line_num, col_num) =
apply_source_map(&mut source_mapper, f.into(), l, c);
Some(JsStackFrame::from_location(
Some(file_name.into_owned()),
Some(line_num),
Some(col_num),
))
}
pub fn maybe_format_location(&self) -> Option<String> {
Some(format!(
"{}:{}:{}",
self.file_name.as_ref()?,
self.line_number?,
self.column_number?
))
}
}
#[inline(always)]
fn get_property<'a>(
scope: &mut v8::HandleScope<'a>,
object: v8::Local<v8::Object>,
key: FastStaticString,
) -> Option<v8::Local<'a, v8::Value>> {
let key = key.v8_string(scope);
object.get(scope, key.into())
}
fn call_method<'a, T>(
scope: &mut v8::HandleScope<'a>,
object: v8::Local<v8::Object>,
key: FastStaticString,
args: &[v8::Local<'a, v8::Value>],
) -> Option<v8::Local<'a, T>>
where
v8::Local<'a, T>: TryFrom<v8::Local<'a, v8::Value>, Error: Debug>,
{
let func = match get_property(scope, object, key)?.try_cast::<v8::Function>()
{
Ok(func) => func,
Err(err) => {
let message =
format!("Callsite property '{key}' is not a function: {err}");
let message = v8::String::new(scope, &message).unwrap();
let exception = v8::Exception::type_error(scope, message);
scope.throw_exception(exception);
return None;
}
};
let res = func.call(scope, object.into(), args)?;
let result = match v8::Local::try_from(res) {
Ok(result) => result,
Err(err) => {
let message = format!(
"Failed to cast callsite method '{key}' return value to correct value: {err:?}."
);
let message = v8::String::new(scope, &message).unwrap();
let exception = v8::Exception::type_error(scope, message);
scope.throw_exception(exception);
return None;
}
};
Some(result)
}
#[derive(Default, serde::Deserialize)]
pub(crate) struct NativeJsError {
pub name: Option<String>,
pub message: Option<String>,
}
impl JsError {
pub fn is_same_error(&self, other: &JsError) -> bool {
let a = self;
let b = other;
a.name == b.name
&& a.message == b.message
&& a.stack == b.stack
&& (a.exception_message == b.exception_message
|| a.exception_message.replace(" (in promise) ", " ") == b.exception_message.replace(" (in promise) ", " "))
&& a.frames == b.frames
&& a.source_line == b.source_line
&& a.source_line_frame_index == b.source_line_frame_index
&& a.aggregated == b.aggregated
}
pub fn from_v8_exception(
scope: &mut v8::HandleScope,
exception: v8::Local<v8::Value>,
) -> Self {
Self::inner_from_v8_exception(scope, exception, Default::default())
}
pub fn from_v8_message<'a>(
scope: &'a mut v8::HandleScope,
msg: v8::Local<'a, v8::Message>,
) -> Self {
let scope = &mut v8::HandleScope::new(scope);
let exception_message = msg.get(scope).to_rust_string_lossy(scope);
let mut frames: Vec<JsStackFrame> = vec![];
let mut source_line = None;
let mut source_line_frame_index = None;
if let Some(stack_frame) = JsStackFrame::from_v8_message(scope, msg) {
frames = vec![stack_frame];
}
{
let state = JsRuntime::state_from(scope);
let mut source_mapper = state.source_mapper.borrow_mut();
for (i, frame) in frames.iter().enumerate() {
if let (Some(file_name), Some(line_number)) =
(&frame.file_name, frame.line_number)
{
if !file_name.trim_start_matches('[').starts_with("ext:") {
source_line = source_mapper.get_source_line(file_name, line_number);
source_line_frame_index = Some(i);
break;
}
}
}
}
Self {
name: None,
message: None,
exception_message,
cause: None,
source_line,
source_line_frame_index,
frames,
stack: None,
aggregated: None,
}
}
fn inner_from_v8_exception<'a>(
scope: &'a mut v8::HandleScope,
exception: v8::Local<'a, v8::Value>,
mut seen: HashSet<v8::Local<'a, v8::Object>>,
) -> Self {
let scope = &mut v8::HandleScope::new(scope);
let msg = v8::Exception::create_message(scope, exception);
let mut exception_message = None;
let exception_state = JsRealm::exception_state_from_scope(scope);
let js_format_exception_cb =
exception_state.js_format_exception_cb.borrow().clone();
if let Some(format_exception_cb) = js_format_exception_cb {
let format_exception_cb = format_exception_cb.open(scope);
let this = v8::undefined(scope).into();
let formatted = format_exception_cb.call(scope, this, &[exception]);
if let Some(formatted) = formatted {
if formatted.is_string() {
exception_message = Some(formatted.to_rust_string_lossy(scope));
}
}
}
if is_instance_of_error(scope, exception) {
let v8_exception = exception;
let exception: v8::Local<v8::Object> = exception.try_into().unwrap();
let cause = get_property(scope, exception, v8_static_strings::CAUSE);
let e: NativeJsError =
serde_v8::from_v8(scope, exception.into()).unwrap_or_default();
let name = e.name.clone().unwrap_or_else(|| "Error".to_string());
let message_prop = e.message.clone().unwrap_or_default();
let exception_message = exception_message.unwrap_or_else(|| {
if !name.is_empty() && !message_prop.is_empty() {
format!("Uncaught {name}: {message_prop}")
} else if !name.is_empty() {
format!("Uncaught {name}")
} else if !message_prop.is_empty() {
format!("Uncaught {message_prop}")
} else {
"Uncaught".to_string()
}
});
let cause = cause.and_then(|cause| {
if cause.is_undefined() || seen.contains(&exception) {
None
} else {
seen.insert(exception);
Some(Box::new(JsError::inner_from_v8_exception(
scope, cause, seen,
)))
}
});
let stack = get_property(scope, exception, v8_static_strings::STACK);
let stack: Option<v8::Local<v8::String>> =
stack.and_then(|s| s.try_into().ok());
let stack = stack.map(|s| s.to_rust_string_lossy(scope));
let frames_v8 = {
let key = call_site_evals_key(scope);
exception.get_private(scope, key)
};
let frames_v8: Option<v8::Local<v8::Array>> =
frames_v8.and_then(|a| a.try_into().ok());
let mut frames: Vec<JsStackFrame> = match frames_v8 {
Some(frames_v8) => {
let mut buf = Vec::with_capacity(frames_v8.length() as usize);
for i in 0..frames_v8.length() {
let callsite = frames_v8.get_index(scope, i).unwrap().cast();
let tc_scope = &mut v8::TryCatch::new(scope);
let Some(stack_frame) =
JsStackFrame::from_callsite_object(tc_scope, callsite)
else {
let message = tc_scope
.exception()
.expect(
"JsStackFrame::from_callsite_object raised an exception",
)
.to_rust_string_lossy(tc_scope);
#[allow(clippy::print_stderr)]
{
eprintln!(
"warning: Failed to create JsStackFrame from callsite object: {message}. This is a bug in deno"
);
}
break;
};
buf.push(stack_frame);
}
buf
}
None => vec![],
};
let mut source_line = None;
let mut source_line_frame_index = None;
if frames.is_empty() {
if let Some(stack_frame) = JsStackFrame::from_v8_message(scope, msg) {
frames = vec![stack_frame];
}
}
{
let state = JsRuntime::state_from(scope);
let mut source_mapper = state.source_mapper.borrow_mut();
for (i, frame) in frames.iter().enumerate() {
if let (Some(file_name), Some(line_number)) =
(&frame.file_name, frame.line_number)
{
if !file_name.trim_start_matches('[').starts_with("ext:") {
source_line =
source_mapper.get_source_line(file_name, line_number);
source_line_frame_index = Some(i);
break;
}
}
}
}
let mut aggregated: Option<Vec<JsError>> = None;
if is_aggregate_error(scope, v8_exception) {
let aggregated_errors =
get_property(scope, exception, v8_static_strings::ERRORS);
let aggregated_errors: Option<v8::Local<v8::Array>> =
aggregated_errors.and_then(|a| a.try_into().ok());
if let Some(errors) = aggregated_errors {
if errors.length() > 0 {
let mut agg = vec![];
for i in 0..errors.length() {
let error = errors.get_index(scope, i).unwrap();
let js_error = Self::from_v8_exception(scope, error);
agg.push(js_error);
}
aggregated = Some(agg);
}
}
};
Self {
name: e.name,
message: e.message,
exception_message,
cause,
source_line,
source_line_frame_index,
frames,
stack,
aggregated,
}
} else {
let exception_message = exception_message
.unwrap_or_else(|| msg.get(scope).to_rust_string_lossy(scope));
Self {
name: None,
message: None,
exception_message,
cause: None,
source_line: None,
source_line_frame_index: None,
frames: vec![],
stack: None,
aggregated: None,
}
}
}
}
impl std::error::Error for JsError {}
impl Display for JsError {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
if let Some(stack) = &self.stack {
let stack_lines = stack.lines();
if stack_lines.count() > 1 {
return write!(f, "{stack}");
}
}
write!(f, "{}", self.exception_message)?;
let location = self.frames.first().and_then(|f| f.maybe_format_location());
if let Some(location) = location {
write!(f, "\n at {location}")?;
}
Ok(())
}
}
pub(crate) fn to_v8_type_error(
scope: &mut v8::HandleScope,
err: Error,
) -> v8::Global<v8::Value> {
let err_string = err.to_string();
let error_chain = err
.chain()
.skip(1)
.filter(|e| e.to_string() != err_string)
.map(|e| e.to_string())
.collect::<Vec<_>>();
let message = if !error_chain.is_empty() {
format!(
"{}\n Caused by:\n {}",
err_string,
error_chain.join("\n ")
)
} else {
err_string
};
let message = v8::String::new(scope, &message).unwrap();
let exception = v8::Exception::type_error(scope, message);
v8::Global::new(scope, exception)
}
pub(crate) fn is_instance_of_error(
scope: &mut v8::HandleScope,
value: v8::Local<v8::Value>,
) -> bool {
if !value.is_object() {
return false;
}
let message = v8::String::empty(scope);
let error_prototype = v8::Exception::error(scope, message)
.to_object(scope)
.unwrap()
.get_prototype(scope)
.unwrap();
let mut maybe_prototype =
value.to_object(scope).unwrap().get_prototype(scope);
while let Some(prototype) = maybe_prototype {
if !prototype.is_object() {
return false;
}
if prototype.strict_equals(error_prototype) {
return true;
}
maybe_prototype = prototype
.to_object(scope)
.and_then(|o| o.get_prototype(scope));
}
false
}
pub(crate) fn is_aggregate_error(
scope: &mut v8::HandleScope,
value: v8::Local<v8::Value>,
) -> bool {
let mut maybe_prototype = Some(value);
while let Some(prototype) = maybe_prototype {
if !prototype.is_object() {
return false;
}
let prototype = prototype.to_object(scope).unwrap();
let prototype_name =
match get_property(scope, prototype, v8_static_strings::CONSTRUCTOR) {
Some(constructor) => {
let ctor = constructor.to_object(scope).unwrap();
get_property(scope, ctor, v8_static_strings::NAME)
.map(|v| v.to_rust_string_lossy(scope))
}
None => return false,
};
if prototype_name == Some(String::from("AggregateError")) {
return true;
}
maybe_prototype = prototype.get_prototype(scope);
}
false
}
pub(crate) fn has_call_site(
scope: &mut v8::HandleScope,
exception: v8::Local<v8::Value>,
) -> bool {
if !exception.is_object() {
return false;
}
let exception = exception.to_object(scope).unwrap();
get_property(scope, exception, v8_static_strings::STACK);
let frames_v8 = {
let key = call_site_evals_key(scope);
exception.get_private(scope, key)
};
let frames_v8: Option<v8::Local<v8::Array>> =
frames_v8.and_then(|a| a.try_into().ok());
if let Some(frames_v8) = frames_v8 {
if frames_v8.length() > 0 {
return true;
}
}
false
}
const DATA_URL_ABBREV_THRESHOLD: usize = 150;
pub fn format_file_name(file_name: &str) -> String {
abbrev_file_name(file_name).unwrap_or_else(|| {
match percent_encoding::percent_decode_str(file_name).decode_utf8() {
Ok(s) => s.to_string(),
Err(_) => file_name.to_string(),
}
})
}
fn abbrev_file_name(file_name: &str) -> Option<String> {
if !file_name.starts_with("data:") {
return None;
}
if file_name.len() <= DATA_URL_ABBREV_THRESHOLD {
return Some(file_name.to_string());
}
let url = Url::parse(file_name).ok()?;
let (head, tail) = url.path().split_once(',')?;
let len = tail.len();
let start = tail.get(0..20)?;
let end = tail.get(len - 20..)?;
Some(format!("{}:{},{}......{}", url.scheme(), head, start, end))
}
pub(crate) fn exception_to_err_result<T>(
scope: &mut v8::HandleScope,
exception: v8::Local<v8::Value>,
mut in_promise: bool,
clear_error: bool,
) -> Result<T, Error> {
let state = JsRealm::exception_state_from_scope(scope);
let mut was_terminating_execution = scope.is_execution_terminating();
scope.set_microtasks_policy(v8::MicrotasksPolicy::Explicit);
scope.cancel_terminate_execution();
let exception = if let Some(dispatched_exception) =
state.get_dispatched_exception_as_local(scope)
{
in_promise = state.is_dispatched_exception_promise();
if clear_error {
state.clear_error();
was_terminating_execution = false;
}
dispatched_exception
} else if was_terminating_execution && exception.is_null_or_undefined() {
let message = v8::String::new(scope, "execution terminated").unwrap();
v8::Exception::error(scope, message)
} else {
exception
};
let mut js_error = JsError::from_v8_exception(scope, exception);
if in_promise {
js_error.exception_message = format!(
"Uncaught (in promise) {}",
js_error.exception_message.trim_start_matches("Uncaught ")
);
}
if was_terminating_execution {
scope.terminate_execution();
}
scope.set_microtasks_policy(v8::MicrotasksPolicy::Auto);
Err(js_error.into())
}
pub fn throw_type_error(scope: &mut v8::HandleScope, message: impl AsRef<str>) {
let message = v8::String::new(scope, message.as_ref()).unwrap();
let exception = v8::Exception::type_error(scope, message);
scope.throw_exception(exception);
}
v8_static_strings::v8_static_strings! {
ERROR = "Error",
GET_FILE_NAME = "getFileName",
GET_SCRIPT_NAME_OR_SOURCE_URL = "getScriptNameOrSourceURL",
GET_THIS = "getThis",
GET_TYPE_NAME = "getTypeName",
GET_FUNCTION = "getFunction",
GET_FUNCTION_NAME = "getFunctionName",
GET_METHOD_NAME = "getMethodName",
GET_LINE_NUMBER = "getLineNumber",
GET_COLUMN_NUMBER = "getColumnNumber",
GET_EVAL_ORIGIN = "getEvalOrigin",
IS_TOPLEVEL = "isToplevel",
IS_EVAL = "isEval",
IS_NATIVE = "isNative",
IS_CONSTRUCTOR = "isConstructor",
IS_ASYNC = "isAsync",
IS_PROMISE_ALL = "isPromiseAll",
GET_PROMISE_INDEX = "getPromiseIndex",
TO_STRING = "toString",
PREPARE_STACK_TRACE = "prepareStackTrace",
ORIGINAL = "deno_core::original_call_site",
ERROR_RECEIVER_IS_NOT_VALID_CALLSITE_OBJECT = "The receiver is not a valid callsite object.",
}
#[inline(always)]
pub(crate) fn original_call_site_key<'a>(
scope: &mut v8::HandleScope<'a>,
) -> v8::Local<'a, v8::Private> {
let name = ORIGINAL.v8_string(scope);
v8::Private::for_api(scope, Some(name))
}
fn make_patched_callsite<'s>(
scope: &mut v8::HandleScope<'s>,
callsite: v8::Local<'s, v8::Object>,
prototype: v8::Local<'s, v8::Object>,
) -> v8::Local<'s, v8::Object> {
let out_obj =
Object::with_prototype_and_properties(scope, prototype.into(), &[], &[]);
let orig_key = original_call_site_key(scope);
out_obj.set_private(scope, orig_key, callsite.into());
out_obj
}
fn original_call_site<'a>(
scope: &mut v8::HandleScope<'a>,
this: v8::Local<'_, v8::Object>,
) -> Option<v8::Local<'a, v8::Object>> {
let orig_key = original_call_site_key(scope);
let Some(orig) = this
.get_private(scope, orig_key)
.and_then(|v| v8::Local::<v8::Object>::try_from(v).ok())
else {
let message = ERROR_RECEIVER_IS_NOT_VALID_CALLSITE_OBJECT.v8_string(scope);
let exception = v8::Exception::type_error(scope, message);
scope.throw_exception(exception);
return None;
};
Some(orig)
}
macro_rules! make_callsite_fn {
($fn:ident, $field:ident) => {
pub fn $fn(
scope: &mut v8::HandleScope<'_>,
args: v8::FunctionCallbackArguments<'_>,
mut rv: v8::ReturnValue<'_>,
) {
let Some(orig) = original_call_site(scope, args.this()) else {
return;
};
let key = $field.v8_string(scope).into();
let orig_ret = orig
.cast::<v8::Object>()
.get(scope, key)
.unwrap()
.cast::<v8::Function>()
.call(scope, orig.into(), &[]);
rv.set(orig_ret.unwrap_or_else(|| v8::undefined(scope).into()));
}
};
}
fn maybe_to_path_str(string: &str) -> Option<String> {
if string.starts_with("file://") {
Some(
Url::parse(string)
.unwrap()
.to_file_path()
.unwrap()
.to_string_lossy()
.into_owned(),
)
} else {
None
}
}
pub mod callsite_fns {
use super::*;
make_callsite_fn!(get_this, GET_THIS);
make_callsite_fn!(get_type_name, GET_TYPE_NAME);
make_callsite_fn!(get_function, GET_FUNCTION);
make_callsite_fn!(get_function_name, GET_FUNCTION_NAME);
make_callsite_fn!(get_method_name, GET_METHOD_NAME);
pub fn get_file_name(
scope: &mut v8::HandleScope<'_>,
args: v8::FunctionCallbackArguments<'_>,
mut rv: v8::ReturnValue<'_>,
) {
let Some(orig) = original_call_site(scope, args.this()) else {
return;
};
let orig_ret =
call_method::<v8::Value>(scope, orig, super::GET_FILE_NAME, &[]);
if let Some(ret_val) =
orig_ret.and_then(|v| v.try_cast::<v8::String>().ok())
{
let string = ret_val.to_rust_string_lossy(scope);
if let Some(file_name) = maybe_to_path_str(&string) {
let v8_str = crate::FastString::from(file_name).v8_string(scope).into();
rv.set(v8_str);
} else {
rv.set(ret_val.into());
}
}
}
make_callsite_fn!(get_line_number, GET_LINE_NUMBER);
make_callsite_fn!(get_column_number, GET_COLUMN_NUMBER);
make_callsite_fn!(get_eval_origin, GET_EVAL_ORIGIN);
make_callsite_fn!(is_toplevel, IS_TOPLEVEL);
make_callsite_fn!(is_eval, IS_EVAL);
make_callsite_fn!(is_native, IS_NATIVE);
make_callsite_fn!(is_constructor, IS_CONSTRUCTOR);
make_callsite_fn!(is_async, IS_ASYNC);
make_callsite_fn!(is_promise_all, IS_PROMISE_ALL);
make_callsite_fn!(get_promise_index, GET_PROMISE_INDEX);
make_callsite_fn!(
get_script_name_or_source_url,
GET_SCRIPT_NAME_OR_SOURCE_URL
);
pub fn to_string(
scope: &mut v8::HandleScope<'_>,
args: v8::FunctionCallbackArguments<'_>,
mut rv: v8::ReturnValue<'_>,
) {
let Some(orig) = original_call_site(scope, args.this()) else {
return;
};
let Some(orig_to_string_v8) =
call_method::<v8::String>(scope, orig, TO_STRING, &[])
else {
return;
};
let orig_to_string = serde_v8::to_utf8(orig_to_string_v8, scope);
let orig_ret_file_name =
call_method::<v8::Value>(scope, orig, GET_FILE_NAME, &[]);
let Some(orig_file_name) =
orig_ret_file_name.and_then(|v| v.try_cast::<v8::String>().ok())
else {
return;
};
let orig_file_name = serde_v8::to_utf8(orig_file_name, scope);
if let Some(file_name) = maybe_to_path_str(&orig_file_name) {
let to_string = orig_to_string.replace(&orig_file_name, &file_name);
let v8_str = crate::FastString::from(to_string).v8_string(scope).into();
rv.set(v8_str);
} else {
rv.set(orig_to_string_v8.into());
}
}
}
pub(crate) fn make_callsite_prototype<'s>(
scope: &mut v8::HandleScope<'s>,
) -> v8::Local<'s, v8::Object> {
let template = v8::ObjectTemplate::new(scope);
macro_rules! set_attr {
($scope:ident, $template:ident, $fn:ident, $field:ident) => {
let key = $field.v8_string($scope).into();
$template.set_with_attr(
key,
v8::FunctionBuilder::<v8::FunctionTemplate>::new(callsite_fns::$fn)
.build($scope)
.into(),
v8::PropertyAttribute::DONT_DELETE
| v8::PropertyAttribute::DONT_ENUM
| v8::PropertyAttribute::READ_ONLY,
);
};
}
set_attr!(scope, template, get_this, GET_THIS);
set_attr!(scope, template, get_type_name, GET_TYPE_NAME);
set_attr!(scope, template, get_function, GET_FUNCTION);
set_attr!(scope, template, get_function_name, GET_FUNCTION_NAME);
set_attr!(scope, template, get_method_name, GET_METHOD_NAME);
set_attr!(scope, template, get_file_name, GET_FILE_NAME);
set_attr!(scope, template, get_line_number, GET_LINE_NUMBER);
set_attr!(scope, template, get_column_number, GET_COLUMN_NUMBER);
set_attr!(scope, template, get_eval_origin, GET_EVAL_ORIGIN);
set_attr!(scope, template, is_toplevel, IS_TOPLEVEL);
set_attr!(scope, template, is_eval, IS_EVAL);
set_attr!(scope, template, is_native, IS_NATIVE);
set_attr!(scope, template, is_constructor, IS_CONSTRUCTOR);
set_attr!(scope, template, is_async, IS_ASYNC);
set_attr!(scope, template, is_promise_all, IS_PROMISE_ALL);
set_attr!(scope, template, get_promise_index, GET_PROMISE_INDEX);
set_attr!(
scope,
template,
get_script_name_or_source_url,
GET_SCRIPT_NAME_OR_SOURCE_URL
);
set_attr!(scope, template, to_string, TO_STRING);
template.new_instance(scope).unwrap()
}
#[inline(always)]
fn prepare_stack_trace_inner<'s, const PATCH_CALLSITES: bool>(
scope: &mut v8::HandleScope<'s>,
error: v8::Local<'s, v8::Value>,
callsites: v8::Local<'s, v8::Array>,
) -> v8::Local<'s, v8::Value> {
if let Ok(obj) = error.try_cast::<v8::Object>() {
let key = call_site_evals_key(scope);
obj.set_private(scope, key, callsites.into());
}
let global = scope.get_current_context().global(scope);
let global_error =
get_property(scope, global, ERROR).and_then(|g| g.try_cast().ok());
let prepare_fn = global_error.and_then(|g| {
get_property(scope, g, PREPARE_STACK_TRACE)
.and_then(|f| f.try_cast::<v8::Function>().ok())
});
if let Some(prepare_fn) = prepare_fn {
let callsites = if PATCH_CALLSITES {
let len = callsites.length();
let mut patched = Vec::with_capacity(len as usize);
let template = JsRuntime::state_from(scope)
.callsite_prototype
.borrow()
.clone()
.unwrap();
let prototype = v8::Local::new(scope, template);
for i in 0..len {
let callsite =
callsites.get_index(scope, i).unwrap().cast::<v8::Object>();
patched.push(make_patched_callsite(scope, callsite, prototype).into());
}
v8::Array::new_with_elements(scope, &patched)
} else {
callsites
};
let this = global_error.unwrap().into();
let args = &[error, callsites.into()];
return prepare_fn
.call(scope, this, args)
.unwrap_or_else(|| v8::undefined(scope).into());
}
format_stack_trace(scope, error, callsites)
}
pub fn prepare_stack_trace_callback_with_original_callsites<'s>(
scope: &mut v8::HandleScope<'s>,
error: v8::Local<'s, v8::Value>,
callsites: v8::Local<'s, v8::Array>,
) -> v8::Local<'s, v8::Value> {
prepare_stack_trace_inner::<false>(scope, error, callsites)
}
pub fn prepare_stack_trace_callback<'s>(
scope: &mut v8::HandleScope<'s>,
error: v8::Local<'s, v8::Value>,
callsites: v8::Local<'s, v8::Array>,
) -> v8::Local<'s, v8::Value> {
prepare_stack_trace_inner::<true>(scope, error, callsites)
}
pub fn format_stack_trace<'s>(
scope: &mut v8::HandleScope<'s>,
error: v8::Local<'s, v8::Value>,
callsites: v8::Local<'s, v8::Array>,
) -> v8::Local<'s, v8::Value> {
let mut result = String::new();
if let Ok(obj) = error.try_cast() {
let msg = get_property(scope, obj, v8_static_strings::MESSAGE)
.filter(|v| !v.is_undefined())
.map(|v| v.to_rust_string_lossy(scope))
.unwrap_or_default();
let name = get_property(scope, obj, v8_static_strings::NAME)
.filter(|v| !v.is_undefined())
.map(|v| v.to_rust_string_lossy(scope))
.unwrap_or_else(|| "Error".to_string());
match (!msg.is_empty(), !name.is_empty()) {
(true, true) => write!(result, "{}: {}", name, msg).unwrap(),
(true, false) => write!(result, "{}", msg).unwrap(),
(false, true) => write!(result, "{}", name).unwrap(),
(false, false) => {}
}
}
for i in 0..callsites.length() {
let callsite = callsites.get_index(scope, i).unwrap().cast::<v8::Object>();
let tc_scope = &mut v8::TryCatch::new(scope);
let Some(frame) = JsStackFrame::from_callsite_object(tc_scope, callsite)
else {
let message = tc_scope
.exception()
.expect("JsStackFrame::from_callsite_object raised an exception")
.to_rust_string_lossy(tc_scope);
#[allow(clippy::print_stderr)]
{
eprintln!("warning: Failed to create JsStackFrame from callsite object: {message}; Result so far: {result}. This is a bug in deno");
}
break;
};
write!(result, "\n at {}", format_frame::<NoAnsiColors>(&frame))
.unwrap();
}
let result = v8::String::new(scope, &result).unwrap();
result.into()
}
pub struct NoAnsiColors;
#[derive(Debug, Clone, Copy)]
pub enum ErrorElement {
Anonymous,
NativeFrame,
LineNumber,
ColumnNumber,
FunctionName,
FileName,
EvalOrigin,
PromiseAll,
}
pub trait ErrorFormat {
fn fmt_element(element: ErrorElement, s: &str) -> Cow<'_, str>;
}
impl ErrorFormat for NoAnsiColors {
fn fmt_element(_element: ErrorElement, s: &str) -> Cow<'_, str> {
s.into()
}
}
pub fn format_location<F: ErrorFormat>(frame: &JsStackFrame) -> String {
use ErrorElement::*;
let _internal = frame
.file_name
.as_ref()
.map(|f| f.starts_with("ext:"))
.unwrap_or(false);
if frame.is_native {
return F::fmt_element(NativeFrame, "native").to_string();
}
let mut result = String::new();
let file_name = frame.file_name.clone().unwrap_or_default();
if !file_name.is_empty() {
result += &F::fmt_element(FileName, &format_file_name(&file_name))
} else {
if frame.is_eval {
result += &(F::fmt_element(
ErrorElement::EvalOrigin,
frame.eval_origin.as_ref().unwrap(),
)
.to_string()
+ ", ");
}
result += &F::fmt_element(Anonymous, "<anonymous>");
}
if let Some(line_number) = frame.line_number {
write!(
result,
":{}",
F::fmt_element(LineNumber, &line_number.to_string())
)
.unwrap();
if let Some(column_number) = frame.column_number {
write!(
result,
":{}",
F::fmt_element(ColumnNumber, &column_number.to_string())
)
.unwrap();
}
}
result
}
pub fn format_frame<F: ErrorFormat>(frame: &JsStackFrame) -> String {
use ErrorElement::*;
let _internal = frame
.file_name
.as_ref()
.map(|f| f.starts_with("ext:"))
.unwrap_or(false);
let is_method_call =
!(frame.is_top_level.unwrap_or_default() || frame.is_constructor);
let mut result = String::new();
if frame.is_async {
result += "async ";
}
if frame.is_promise_all {
result += &F::fmt_element(
PromiseAll,
&format!(
"Promise.all (index {})",
frame.promise_index.unwrap_or_default()
),
);
return result;
}
if is_method_call {
let mut formatted_method = String::new();
if let Some(function_name) = &frame.function_name {
if let Some(type_name) = &frame.type_name {
if !function_name.starts_with(type_name) {
write!(formatted_method, "{type_name}.").unwrap();
}
}
formatted_method += function_name;
if let Some(method_name) = &frame.method_name {
if !function_name.ends_with(method_name) {
write!(formatted_method, " [as {method_name}]").unwrap();
}
}
} else {
if let Some(type_name) = &frame.type_name {
write!(formatted_method, "{type_name}.").unwrap();
}
if let Some(method_name) = &frame.method_name {
formatted_method += method_name
} else {
formatted_method += "<anonymous>";
}
}
result += F::fmt_element(FunctionName, &formatted_method).as_ref();
} else if frame.is_constructor {
result += "new ";
if let Some(function_name) = &frame.function_name {
write!(result, "{}", F::fmt_element(FunctionName, function_name))
.unwrap();
} else {
result += F::fmt_element(Anonymous, "<anonymous>").as_ref();
}
} else if let Some(function_name) = &frame.function_name {
result += F::fmt_element(FunctionName, function_name).as_ref();
} else {
result += &format_location::<F>(frame);
return result;
}
write!(result, " ({})", format_location::<F>(frame)).unwrap();
result
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_bad_resource() {
let err = bad_resource("Resource has been closed");
assert_eq!(err.to_string(), "Resource has been closed");
}
#[test]
fn test_bad_resource_id() {
let err = bad_resource_id();
assert_eq!(err.to_string(), "Bad resource ID");
}
#[test]
fn test_format_file_name() {
let file_name = format_file_name("data:,Hello%2C%20World%21");
assert_eq!(file_name, "data:,Hello%2C%20World%21");
let too_long_name = "a".repeat(DATA_URL_ABBREV_THRESHOLD + 1);
let file_name = format_file_name(&format!(
"data:text/plain;base64,{too_long_name}_%F0%9F%A6%95"
));
assert_eq!(
file_name,
"data:text/plain;base64,aaaaaaaaaaaaaaaaaaaa......aaaaaaa_%F0%9F%A6%95"
);
let file_name = format_file_name("file:///foo/bar.ts");
assert_eq!(file_name, "file:///foo/bar.ts");
let file_name =
format_file_name("file:///%E6%9D%B1%E4%BA%AC/%F0%9F%A6%95.ts");
assert_eq!(file_name, "file:///東京/🦕.ts");
}
#[test]
fn test_parse_eval_origin() {
let cases = [
(
"eval at <anonymous> (file://path.ts:1:2)",
Some(("eval at <anonymous> (", ("file://path.ts", 1, 2), ")")),
),
(
"eval at (s:1:2",
None,
),
(
"at ()", None,
),
(
"eval at foo (http://website.zzz/my-script).ts:1:2)",
Some((
"eval at foo (",
("http://website.zzz/my-script).ts", 1, 2),
")",
)),
),
(
"eval at foo (eval at bar (file://path.ts:1:2))",
Some(("eval at foo (eval at bar (", ("file://path.ts", 1, 2), "))")),
),
];
for (input, expect) in cases {
match expect {
Some((
expect_before,
(expect_file, expect_line, expect_col),
expect_after,
)) => {
let (before, (file_name, line_number, column_number), after) =
parse_eval_origin(input).unwrap();
assert_eq!(before, expect_before);
assert_eq!(file_name, expect_file);
assert_eq!(line_number, expect_line);
assert_eq!(column_number, expect_col);
assert_eq!(after, expect_after);
}
None => {
assert!(parse_eval_origin(input).is_none());
}
}
}
}
}