/
githubmirror
/
deno
Обзор
Документация
Войти
/
githubmirror
/
deno
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
libs/core/error.rs
3 214 строк
103 KB
Nathan Whitaker
fix(core): bound error graph conversion (#36070)
17 июл 2026, 00:44
Не верифицирован
17 июл 2026, 00:44
6f570cf
Код
Авторство
О чём код?
// Copyright 2018-2026 the Deno authors. MIT license. use std::borrow::Cow; use std::collections::HashSet; use std::error::Error; use std::fmt; use std::fmt::Debug; use std::fmt::Display; use std::fmt::Formatter; use std::fmt::Write as _; use std::sync::Arc; use boxed_error::Boxed; use deno_error::JsError; use deno_error::JsErrorClass; use deno_error::PropertyValue; use deno_error::builtin_classes::*; use thiserror::Error; pub use super::modules::ModuleConcreteError; use crate::FastStaticString; pub use crate::io::ResourceError; pub use crate::modules::ModuleLoaderError; use crate::runtime::JsRealm; use crate::runtime::JsRuntime; use crate::runtime::v8_static_strings; use crate::source_map::SourceMapApplication; use crate::url::Url; /// A generic wrapper that can encapsulate any concrete error type. // TODO(ry) Deprecate AnyError and encourage deno_core::anyhow::Error instead. pub type AnyError = anyhow::Error; deno_error::js_error_wrapper!(v8::DataError, DataError, TYPE_ERROR); impl PartialEq<DataError> for DataError { fn eq(&self, other: &DataError) -> bool { match (self.0, other.0) { ( v8::DataError::BadType { actual, expected }, v8::DataError::BadType { actual: other_actual, expected: other_expected, }, ) => actual == other_actual && expected == other_expected, ( v8::DataError::NoData { expected }, v8::DataError::NoData { expected: other_expected, }, ) => expected == other_expected, _ => false, } } } impl Eq for DataError {} #[derive(Debug, Error, JsError)] #[class(generic)] #[error("Failed to parse {0}")] pub struct CoreModuleParseError(pub FastStaticString); #[derive(Debug, Error, JsError)] #[class(generic)] #[error("Failed to execute {0}")] pub struct CoreModuleExecuteError(pub FastStaticString); #[derive(Debug, Error, JsError)] #[class(generic)] #[error("Unable to get code cache from unbound module script for {0}")] pub struct CreateCodeCacheError(pub Url); #[derive(Debug, Error, JsError)] #[class(generic)] #[error( "Extensions from snapshot loaded in wrong order: expected {} but got {}", .expected, .actual )] pub struct ExtensionSnapshotMismatchError { pub expected: &'static str, pub actual: &'static str, } #[derive(Debug, Error, JsError)] #[class(generic)] #[error( "Number of lazy-initialized extensions ({}) does not match number of arguments ({})", .lazy_init_extensions_len, .arguments_len )] pub struct ExtensionLazyInitCountMismatchError { pub lazy_init_extensions_len: usize, pub arguments_len: usize, } #[derive(Debug, Error, JsError)] #[class(generic)] #[error( "Lazy-initialized extensions loaded in wrong order: expected {} but got {}", .expected, .actual )] pub struct ExtensionLazyInitOrderMismatchError { pub expected: &'static str, pub actual: &'static str, } #[derive(Debug, Boxed, JsError)] pub struct CoreError(pub Box<CoreErrorKind>); #[derive(Debug, thiserror::Error, JsError)] pub enum CoreErrorKind { #[class(generic)] #[error("Top-level await is not allowed in synchronous evaluation")] TLA, #[class(inherit)] #[error(transparent)] Js(#[from] Box<JsError>), #[class(inherit)] #[error(transparent)] Io(#[from] std::io::Error), #[class(inherit)] #[error(transparent)] ExtensionTranspiler(deno_error::JsErrorBox), #[class(inherit)] #[error(transparent)] Parse(#[from] CoreModuleParseError), #[class(inherit)] #[error(transparent)] Execute(#[from] CoreModuleExecuteError), #[class(generic)] #[error( "Following modules were passed to ExtModuleLoader but never used:\n{}", .0.iter().map(|s| format!(" - {}\n", s)).collect::<Vec<_>>().join("") )] UnusedModules(Vec<String>), #[class(generic)] #[error( "Following modules were not evaluated; make sure they are imported from other code:\n{}", .0.iter().map(|s| format!(" - {}\n", s)).collect::<Vec<_>>().join("") )] NonEvaluatedModules(Vec<String>), #[class(generic)] #[error("{0} not present in the module map")] MissingFromModuleMap(String), #[class(generic)] #[error("Could not execute {specifier}")] CouldNotExecute { #[source] error: Box<Self>, specifier: String, }, #[class(inherit)] #[error(transparent)] JsBox(#[from] deno_error::JsErrorBox), #[class(inherit)] #[error(transparent)] Url(#[from] url::ParseError), #[class(generic)] #[error( "Cannot evaluate module, because JavaScript execution has been terminated" )] ExecutionTerminated, #[class(generic)] #[error( "Promise resolution is still pending but the event loop has already resolved" )] PendingPromiseResolution, #[class(generic)] #[error( "Module evaluation is still pending after multiple event loop iterations, \ but no stalled top-level await was found. This is a bug in Deno." )] ModuleEvaluationDeadlock, #[class(generic)] #[error( "Cannot evaluate dynamically imported module, because JavaScript execution has been terminated" )] EvaluateDynamicImportedModule, #[class(inherit)] #[error(transparent)] Module(ModuleConcreteError), #[class(inherit)] #[error(transparent)] Data(DataError), #[class(inherit)] #[error(transparent)] CreateCodeCache(#[from] CreateCodeCacheError), #[class(inherit)] #[error(transparent)] ExtensionSnapshotMismatch(ExtensionSnapshotMismatchError), #[class(inherit)] #[error(transparent)] ExtensionLazyInitCountMismatch(ExtensionLazyInitCountMismatchError), #[class(inherit)] #[error(transparent)] ExtensionLazyInitOrderMismatch(ExtensionLazyInitOrderMismatchError), } impl CoreError { pub fn print_with_cause(&self) -> String { use std::error::Error; let mut err_message = self.to_string(); if let Some(source) = self.source() { err_message.push_str(&format!( "\n\nCaused by:\n {}", source.to_string().replace("\n", "\n ") )); } err_message } pub fn to_v8_error(&self, scope: &mut v8::PinScope) -> v8::Global<v8::Value> { self.as_kind().to_v8_error(scope) } } impl CoreErrorKind { pub fn to_v8_error(&self, scope: &mut v8::PinScope) -> v8::Global<v8::Value> { let err_string = self.get_message().to_string(); let mut error_chain = vec![]; let mut intermediary_error: Option<&dyn Error> = Some(&self); while let Some(err) = intermediary_error { if let Some(source) = err.source() { let source_str = source.to_string(); if source_str != err_string { error_chain.push(source_str); } intermediary_error = Some(source); } else { intermediary_error = None; } } let message = if !error_chain.is_empty() { format!( "{}\n Caused by:\n {}", err_string, error_chain.join("\n ") ) } else { err_string }; let exception = js_class_and_message_to_exception(scope, &self.get_class(), &message); v8::Global::new(scope, exception) } } impl From<v8::DataError> for CoreError { fn from(err: v8::DataError) -> Self { CoreErrorKind::Data(DataError(err)).into_box() } } /// Single-pass V8 string → Rust string via `write_utf8_into` (uses ValueView /// internally). Replaces the two-pass `to_rust_string_lossy` which does a /// `utf8_length` pre-scan before writing. #[inline] fn v8_to_rust_string(s: &v8::String, scope: &mut v8::Isolate) -> String { let mut buf = String::new(); s.write_utf8_into(scope, &mut buf); buf } pub fn throw_js_error_class( scope: &mut v8::PinScope, error: &dyn JsErrorClass, ) { let exception = build_js_error_class_exception(scope, error); scope.throw_exception(exception); } /// Builds a JS exception for `error` using only native V8 APIs, without /// re-entering JavaScript. /// /// This mirrors what the JS `Deno.core.buildCustomError` callback does /// (restoring the registered error class so `instanceof` holds, and attaching /// the additional properties), but never invokes user-reachable JS. That makes /// it safe to call from inside a V8 fast call, where re-entering JS is /// forbidden: the callback could run an attacker-controlled prototype setter /// that detaches an `ArrayBuffer` argument out from under the JIT, a /// use-after-free (GHSA-p4r3-6jgx-4cj5). /// /// Reading data properties off the cached `errorConstructors` map and calling /// `SetPrototype` / `CreateDataProperty` / `DefineOwnProperty` never execute /// user JS, so the fast-call contract is upheld. fn build_js_error_class_exception<'s, 'i>( scope: &mut v8::PinScope<'s, 'i>, error: &dyn JsErrorClass, ) -> v8::Local<'s, v8::Value> { let class = error.get_class(); let message = v8::String::new(scope, &error.get_message()).unwrap(); let exception = v8::Exception::error(scope, message); // `v8::Exception::error` always produces an object, but be defensive. let Ok(exception_obj) = TryInto::<v8::Local<v8::Object>>::try_into(exception) else { return exception; }; // Restore the exact registered error class (e.g. `RangeError`, // `Deno.errors.NotFound`) by re-parenting the prototype, so that // `err instanceof Deno.errors.NotFound` holds. We never call the class // constructor (that would run JS); we only borrow its `.prototype`. let constructors = JsRealm::exception_state_from_scope(scope) .js_error_constructors .borrow() .clone(); if let Some(constructors) = constructors { let constructors = v8::Local::new(scope, constructors); let class_key = v8::String::new(scope, &class).unwrap(); if let Some(ctor) = constructors.get(scope, class_key.into()) && let Ok(ctor) = TryInto::<v8::Local<v8::Object>>::try_into(ctor) { let prototype_key = v8::String::new(scope, "prototype").unwrap(); if let Some(prototype) = ctor.get(scope, prototype_key.into()) && prototype.is_object() { exception_obj.set_prototype(scope, prototype); } } } // Set `name` explicitly. The registered classes assign `this.name` in their // constructor (an own property), which the prototype swap above does not // reproduce since we never run the constructor. The registration key matches // that assigned name, so using the class here mirrors `new ErrorClass(...)`. let name_key = v8::String::new(scope, "name").unwrap(); let class_value = v8::String::new(scope, &class).unwrap(); exception_obj.create_data_property( scope, name_key.into(), class_value.into(), ); // Copy the additional properties (e.g. `code`) and record their keys under // the same symbol `buildCustomError` uses, so the error round-trips back to // Rust via `JsError::from_v8_exception`. let mut added_keys = vec![]; for (key, value) in error.get_additional_properties() { let key = v8::String::new(scope, &key).unwrap(); // Match `buildCustomError`: don't clobber a property the class defines. if exception_obj.has(scope, key.into()) == Some(true) { continue; } let value = match value { PropertyValue::String(value) => { v8::String::new(scope, &value).unwrap().into() } PropertyValue::Number(value) => v8::Number::new(scope, value).into(), }; exception_obj.create_data_property(scope, key.into(), value); added_keys.push(v8::Local::<v8::Value>::from(key)); } if !added_keys.is_empty() { let keys_array = v8::Array::new_with_elements(scope, &added_keys); let symbol_name = v8::String::new(scope, "errorAdditionalPropertyKeys").unwrap(); let symbol = v8::Symbol::for_key(scope, symbol_name); exception_obj.define_own_property( scope, symbol.into(), keys_array.into(), v8::PropertyAttribute::READ_ONLY | v8::PropertyAttribute::DONT_ENUM | v8::PropertyAttribute::DONT_DELETE, ); } exception } fn js_class_and_message_to_exception<'s, 'i>( scope: &mut v8::PinScope<'s, 'i>, _class: &str, message: &str, ) -> v8::Local<'s, v8::Value> { let message = v8::String::new(scope, message).unwrap(); /* commented out since this was previously only handling type errors, but this change is breaking CLI, so visiting on a later date match class { TYPE_ERROR => v8::Exception::type_error(scope, message), RANGE_ERROR => v8::Exception::range_error(scope, message), REFERENCE_ERROR => v8::Exception::reference_error(scope, message), SYNTAX_ERROR => v8::Exception::syntax_error(scope, message), _ => v8::Exception::error(scope, message), }*/ v8::Exception::type_error(scope, message) } pub fn to_v8_error<'s, 'i>( scope: &mut v8::PinScope<'s, 'i>, error: &dyn JsErrorClass, ) -> v8::Local<'s, v8::Value> { v8::tc_scope!(let tc_scope, 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, &error.get_class()).unwrap(); let message = v8::String::new(tc_scope, &error.get_message()).unwrap(); let mut args = vec![class.into(), message.into()]; let additional_properties = error .get_additional_properties() .map(|(key, value)| { let key = v8::String::new(tc_scope, &key).unwrap().into(); let value = match value { PropertyValue::String(value) => { v8::String::new(tc_scope, &value).unwrap().into() } PropertyValue::Number(value) => v8::Number::new(tc_scope, value).into(), }; v8::Array::new_with_elements(tc_scope, &[key, value]).into() }) .collect::<Vec<_>>(); if !additional_properties.is_empty() { args.push( v8::Array::new_with_elements(tc_scope, &additional_properties).into(), ); } let maybe_exception = cb.call(tc_scope, this, &args); match maybe_exception { Some(exception) => exception, None => { // The JS error builder callback failed. This can happen when the // stack is exhausted (e.g. Maximum call stack size exceeded) and // the callback itself throws. Fall back to a plain V8 error // instead of panicking. if tc_scope.has_caught() { tc_scope.reset(); } message.into() } } } /// Effectively throw an uncatchable error. This will terminate runtime /// execution before any more JS code can run, except in the REPL where it /// should just output the error to the console. pub fn dispatch_exception<'s, 'i>( scope: &mut v8::PinScope<'s, 'i>, exception: v8::Local<'s, v8::Value>, promise: bool, ) { let state = JsRuntime::state_from(scope); if let Some(true) = state.with_inspector(|inspector| { inspector.exception_thrown(scope, exception, false); inspector.is_dispatching_message() }) { // This indicates that the fn is being called from a REPL. Skip termination. return; } JsRealm::exception_state_from_scope(scope) .set_dispatched_exception(v8::Global::new(scope, exception), promise); scope.terminate_execution(); } #[inline(always)] pub(crate) fn call_site_evals_key<'s, 'i>( scope: &mut v8::PinScope<'s, 'i>, ) -> v8::Local<'s, v8::Private> { let name = v8_static_strings::CALL_SITE_EVALS.v8_string(scope).unwrap(); v8::Private::for_api(scope, Some(name)) } /// A `JsError` represents an exception coming from V8, with stack frames and /// line numbers. The deno_cli crate defines another `JsError` type, which wraps /// the one defined here, that adds source map support and colorful formatting. /// When updating this struct, also update errors_are_equal_without_cause() in /// fmt_error.rs. #[derive( Debug, PartialEq, Clone, serde::Deserialize, serde::Serialize, deno_ops::FromV8, deno_ops::ToV8, )] #[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>>, pub additional_properties: Vec<(String, String)>, /// True when the error's `.stack` was not produced by our /// `prepareStackTrace` (e.g. a custom getter from libraries like Effect, or /// a manually assigned value). In that case the structured `frames` are not /// authoritative and formatters should preserve the custom `.stack` string. #[serde(default)] pub stack_is_custom: bool, } const MAX_ERROR_CONVERSION_DEPTH: usize = 32; const MAX_ERROR_CONVERSION_NODES: usize = 256; struct ErrorConversionState { remaining_nodes: usize, limit_marker_emitted: bool, } enum ErrorConversionStep { Convert, Truncate, Omit, } impl ErrorConversionState { fn new() -> Self { Self { remaining_nodes: MAX_ERROR_CONVERSION_NODES, limit_marker_emitted: false, } } fn enter(&mut self, depth: usize) -> ErrorConversionStep { if depth < MAX_ERROR_CONVERSION_DEPTH && self.remaining_nodes > 0 { self.remaining_nodes -= 1; ErrorConversionStep::Convert } else { self.truncate() } } fn is_exhausted(&self) -> bool { self.remaining_nodes == 0 } fn truncate(&mut self) -> ErrorConversionStep { if !self.limit_marker_emitted { self.limit_marker_emitted = true; ErrorConversionStep::Truncate } else { ErrorConversionStep::Omit } } } impl JsErrorClass for JsError { fn get_class(&self) -> Cow<'static, str> { if let Some(name) = &self.name { Cow::Owned(name.clone()) } else { Cow::Borrowed(GENERIC_ERROR) } } fn get_message(&self) -> Cow<'static, str> { if let Some(message) = &self.message { Cow::Owned(message.clone()) } else { Cow::Borrowed("") } } fn get_additional_properties(&self) -> deno_error::AdditionalProperties { Box::new( self .additional_properties // todo(dsherret): why does JsErrorClass not allow having references within this struct? .clone() .into_iter() .map(|(k, v)| { ( Cow::Owned(k.to_string()), PropertyValue::String(Cow::Owned(v.to_string())), ) }), ) } fn get_ref(&self) -> &(dyn std::error::Error + Send + Sync + 'static) { self } } #[derive( Debug, Eq, PartialEq, Clone, serde::Deserialize, serde::Serialize, deno_ops::FromV8, deno_ops::ToV8, )] #[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>, // Warning! isToplevel has inconsistent snake<>camel case, "typo" originates in v8: // https://source.chromium.org/search?q=isToplevel&sq=&ss=chromium%2Fchromium%2Fsrc:v8%2F #[serde(rename = "isToplevel")] #[v8(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 is_wasm: bool, pub promise_index: Option<i64>, } /// Applies source map to the given location 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()), } } /// Parses an eval origin string from V8, returning /// the contents before the location, /// (the file name, line number, and column number), and /// the contents after the location. /// /// # Example /// ```ignore /// assert_eq!( /// parse_eval_origin("eval at foo (bar at (file://a.ts:1:2))"), /// Some(("eval at foo (bar at (", ("file://a.ts", 1, 2), "))")), /// ); /// ``` /// fn parse_eval_origin( eval_origin: &str, ) -> Option<(&str, (&str, i64, i64), &str)> { // The eval origin string we get from V8 looks like // `eval at ${function_name} (${origin})` // where origin can be either a file name, like // "eval at foo (file:///path/to/script.ts:1:2)" // or a nested eval origin, like // "eval at foo (eval at bar (file:///path/to/script.ts:1:2))" // let eval_at = "eval at "; // only the innermost eval origin can have location info, so find the last // "eval at", then continue parsing the rest of the string let mut innermost_start = eval_origin.rfind(eval_at)? + eval_at.len(); // skip over the function name innermost_start += eval_origin[innermost_start..].find('(')? + 1; if innermost_start >= eval_origin.len() { // malformed return None; } // from the right, split by ":" to get the column number, line number, file name // (in that order, since we're iterating from the right). e.g. // eval at foo (eval at bar (file://foo.ts:1:2)) // ^^^^^^^^^^^^^ ^ ^^^ let mut parts = eval_origin[innermost_start..].rsplitn(3, ':'); // the part with the column number will include extra stuff, the actual number ends at // the closing paren 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()?; // The column number starts after the last occurring ":". let column_start = eval_origin.rfind(':')? + 1; // the innermost origin ends at the end of the column number 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, is_wasm: false, promise_index: None, } } /// Creates a `JsStackFrame` from a `CallSite`` JS object, /// provided by V8. fn from_callsite_object<'s, 'i>( scope: &mut v8::PinScope<'s, 'i>, 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 raw_file_name = call!(GET_FILE_NAME : Option<String>); let is_wasm = raw_file_name .as_deref() .map(|f| f.starts_with("wasm://")) .unwrap_or(false); // For wasm frames, skip source map application — source maps don't apply to wasm. // V8 returns the function index via getLineNumber() and byte offset via getColumnNumber(). let (file_name, line_number, column_number) = if is_wasm { ( raw_file_name, call!(GET_LINE_NUMBER), call!(GET_COLUMN_NUMBER), ) } else { let state = JsRuntime::state_from(scope); let mut source_mapper = state.source_mapper.borrow_mut(); // apply source map match ( raw_file_name, 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), } }; // apply source map to the eval origin, if the error originates from `eval`ed code let eval_origin = if is_wasm { None } else { 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 state = JsRuntime::state_from(scope); let mut source_mapper = state.source_mapper.borrow_mut(); 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), is_wasm, promise_index: call!(GET_PROMISE_INDEX), }) } /// Gets the source mapped stack frame corresponding to the /// (script_resource_name, line_number, column_number) from a v8 message. /// For non-syntax errors, it should also correspond to the first stack frame. pub fn from_v8_message<'s, 'i>( scope: &mut v8::PinScope<'s, 'i>, message: v8::Local<'s, v8::Message>, ) -> Option<Self> { let f = message.get_script_resource_name(scope)?; let f: v8::Local<v8::String> = f.try_into().ok()?; let f = v8_to_rust_string(&f, scope); let l = message.get_line_number(scope)? as i64; // V8's column numbers are 0-based, we want 1-based. 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<'s, 'i>( scope: &mut v8::PinScope<'s, 'i>, object: v8::Local<'s, v8::Object>, key: FastStaticString, ) -> Option<v8::Local<'s, v8::Value>> { let key = key.v8_string(scope).unwrap(); object.get(scope, key.into()) } fn call_method<'s, 'i, T>( scope: &mut v8::PinScope<'s, 'i>, object: v8::Local<'s, v8::Object>, key: FastStaticString, args: &[v8::Local<'s, v8::Value>], ) -> Option<v8::Local<'s, T>> where v8::Local<'s, T>: TryFrom<v8::Local<'s, 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(Debug, Default, serde::Deserialize)] pub(crate) struct NativeJsError { pub name: Option<String>, pub message: Option<String>, // Warning! .stack is special so handled by itself // stack: Option<String>, } impl JsError { /// Compares all properties of JsError, except for JsError::cause. This function is used to /// detect that 2 JsError objects in a JsError::cause chain are identical, ie. there is a recursive cause. /// /// We don't have access to object identity here, so we do it via field comparison. Ideally this should /// be able to maintain object identity somehow. pub fn is_same_error(&self, other: &JsError) -> bool { let a = self; let b = other; // `a.cause == b.cause` omitted, because it is absent in recursive errors, // despite the error being identical to a previously seen one. a.name == b.name && a.message == b.message && a.stack == b.stack // TODO(mmastrac): we need consistency around when we insert "in promise" and when we don't. For now, we // are going to manually replace this part of the string. && (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<'s, 'i>( scope: &mut v8::PinScope<'s, 'i>, exception: v8::Local<'s, v8::Value>, ) -> Box<Self> { let mut state = ErrorConversionState::new(); Box::new( Self::inner_from_v8_exception( scope, exception, Default::default(), 0, &mut state, ) .expect("the conversion budget includes the root exception"), ) } pub fn from_v8_message<'s, 'i>( scope: &mut v8::PinScope<'s, 'i>, msg: v8::Local<'s, v8::Message>, ) -> Box<Self> { // Create a new HandleScope because we're creating a lot of new local // handles below. v8::scope!(let scope, scope); let exception_message = v8_to_rust_string(&msg.get(scope), scope); // Convert them into Vec<JsStackFrame> 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) && !file_name.trim_start_matches('[').starts_with("ext:") && !file_name.starts_with("node:") { source_line = source_mapper.get_source_line(file_name, line_number); source_line_frame_index = Some(i); break; } } } Box::new(Self { name: None, message: None, exception_message, cause: None, source_line, source_line_frame_index, frames, stack: None, aggregated: None, additional_properties: vec![], stack_is_custom: false, }) } fn inner_from_v8_exception<'s, 'i>( scope: &mut v8::PinScope<'s, 'i>, exception: v8::Local<'s, v8::Value>, seen: HashSet<v8::Local<'s, v8::Object>>, depth: usize, state: &mut ErrorConversionState, ) -> Option<Self> { match state.enter(depth) { ErrorConversionStep::Convert => {} ErrorConversionStep::Truncate => { return Some(Self::error_conversion_limit()); } ErrorConversionStep::Omit => return None, } // Create a new HandleScope because we're creating a lot of new local // handles below. v8::scope!(let scope, 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 && let Ok(formatted) = formatted.try_cast::<v8::String>() { exception_message = Some(v8_to_rust_string(&formatted, scope)); } } if is_instance_of_error(scope, exception) { let v8_exception = exception; // The exception is a JS Error object. 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(); // Get the message by formatting error.name and error.message. let name = e.name.clone().unwrap_or_else(|| GENERIC_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() } }); // A cycle terminates by converting the repeated error one final time and // dropping its `cause` and aggregate members. Formatters rely on that // repeat: `find_recursive_cause` matches it against the earlier // occurrence by value to label a cycle `<ref *n>` / `[Circular *n]`, so // it cannot be replaced with a placeholder. `seen` is path-local, so an // error reachable by several distinct paths is not mistaken for a cycle. let is_repeat = seen.contains(&exception); let cause = cause.and_then(|cause| { if cause.is_undefined() || is_repeat { None } else { let mut seen = seen.clone(); seen.insert(exception); JsError::inner_from_v8_exception(scope, cause, seen, depth + 1, state) .map(Box::new) } }); // Access error.stack to ensure that prepareStackTrace() has been called. // This should populate error.#callSiteEvals. 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| v8_to_rust_string(&s, scope)); // Read an array of structured frames from error.#callSiteEvals. let frames_v8 = { let key = call_site_evals_key(scope); exception.get_private(scope, key) }; // Ignore non-array values let frames_v8: Option<v8::Local<v8::Array>> = frames_v8.and_then(|a| a.try_into().ok()); // If accessing `.stack` above did not populate `#callSiteEvals`, the // error's stack was not produced by our `prepareStackTrace`. This happens // when the user overrides `.stack` with a custom getter (e.g. Effect, // fiber runtimes) or assigns a plain string. In that case the structured // frames are not authoritative, so we mark the stack as custom and let // formatters preserve the `.stack` string. // // V8's default `.stack` for a frame-less error is exactly its // "name: message" string (and `message` may itself be multi-line). We // only treat the stack as custom when it carries content beyond that // header, so a multi-line *message* isn't mistaken for a stack. let default_stack = if message_prop.is_empty() { name.clone() } else if name.is_empty() { message_prop.clone() } else { format!("{name}: {message_prop}") }; let has_structured_frames = frames_v8.map(|a| a.length() > 0).unwrap_or(false); let stack_is_custom = !has_structured_frames && stack.as_ref().is_some_and(|s| { s.lines().count() > 1 && s.trim_end() != default_stack.trim_end() }); // Convert them into Vec<JsStackFrame> 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(); v8::tc_scope!(let tc_scope, scope); let Some(stack_frame) = JsStackFrame::from_callsite_object(tc_scope, callsite) else { let exc = tc_scope.exception().expect( "JsStackFrame::from_callsite_object raised an exception", ); let message = match exc.to_string(tc_scope) { Some(s) => v8_to_rust_string(&s, tc_scope), None => String::new(), }; #[allow( clippy::print_stderr, reason = "intentional warning output" )] { 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; // When the stack frame array is empty, but the source location given by // (script_resource_name, line_number, start_column + 1) exists, this is // likely a syntax error. For the sake of formatting we treat it like it // was given as a single stack frame. if frames.is_empty() && 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) && !file_name.trim_start_matches('[').starts_with("ext:") && !file_name.starts_with("node:") { 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) && !is_repeat { // Read an array of stored errors, this is only defined for `AggregateError` let (aggregated_errors, errors_getter_failed) = { v8::tc_scope!(let tc_scope, scope); let aggregated_errors = get_property(tc_scope, exception, v8_static_strings::ERRORS) .and_then(|value| value.try_cast::<v8::Array>().ok()) .map(|errors| v8::Global::new(tc_scope, errors)); let errors_getter_failed = tc_scope.has_caught(); if errors_getter_failed { tc_scope.reset(); } (aggregated_errors, errors_getter_failed) }; if errors_getter_failed { aggregated = Some(vec![Self::aggregate_element_unavailable()]); } else if let Some(errors) = aggregated_errors { let errors = v8::Local::new(scope, errors); if errors.length() > 0 { let mut seen = seen.clone(); seen.insert(exception); let mut agg = vec![]; for i in 0..errors.length() { if state.is_exhausted() { if matches!(state.truncate(), ErrorConversionStep::Truncate) { agg.push(Self::error_conversion_limit()); } break; } let (error, element_getter_failed) = { v8::tc_scope!(let tc_scope, scope); let error = errors .get_index(tc_scope, i) .map(|error| v8::Global::new(tc_scope, error)); let element_getter_failed = tc_scope.has_caught(); if element_getter_failed { tc_scope.reset(); } (error, element_getter_failed) }; if element_getter_failed { agg.push(Self::aggregate_element_unavailable()); break; } let Some(error) = error else { agg.push(Self::aggregate_element_unavailable()); break; }; let error = v8::Local::new(scope, error); let Some(js_error) = Self::inner_from_v8_exception( scope, error, seen.clone(), depth + 1, state, ) else { break; }; agg.push(js_error); } aggregated = Some(agg); } } }; let additional_properties_string = v8::String::new(scope, "errorAdditionalPropertyKeys").unwrap(); let additional_properties_key = v8::Symbol::for_key(scope, additional_properties_string); let additional_properties = exception.get(scope, additional_properties_key.into()); let additional_properties = if let Some(arr) = additional_properties.and_then(|keys| keys.try_cast::<v8::Array>().ok()) { let mut out = Vec::with_capacity(arr.length() as usize); for i in 0..arr.length() { let Some(key) = arr.get_index(scope, i) else { continue; }; let key_name = match key.to_string(scope) { Some(s) => v8_to_rust_string(&s, scope), None => continue, }; let Some(val) = exception.get(scope, key) else { continue; }; let val_str = match val.to_string(scope) { Some(s) => v8_to_rust_string(&s, scope), None => continue, }; out.push((key_name, val_str)); } out } else { vec![] }; Some(Self { name: e.name, message: e.message, exception_message, cause, source_line, source_line_frame_index, frames, stack, aggregated, additional_properties, stack_is_custom, }) } else { let exception_message = exception_message .unwrap_or_else(|| v8_to_rust_string(&msg.get(scope), scope)); // The exception is not a JS Error object. // Get the message given by V8::Exception::create_message(), and provide // empty frames. Some(Self { name: None, message: None, exception_message, cause: None, source_line: None, source_line_frame_index: None, frames: vec![], stack: None, aggregated: None, additional_properties: vec![], stack_is_custom: false, }) } } fn error_conversion_limit() -> Self { Self::conversion_placeholder("[Error details truncated]") } fn aggregate_element_unavailable() -> Self { Self::conversion_placeholder("[Error details unavailable]") } fn conversion_placeholder(message: &str) -> Self { Self { name: None, message: Some(message.to_string()), exception_message: format!("Uncaught {message}"), cause: None, source_line: None, source_line_frame_index: None, frames: vec![], stack: None, aggregated: None, additional_properties: vec![], stack_is_custom: false, } } } 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(()) } } /// Implements `value instanceof primordials.Error` in JS. Similar to /// `Value::is_native_error()` but more closely matches the semantics /// of `instanceof`. `Value::is_native_error()` also checks for static class /// inheritance rather than just scanning the prototype chain, which doesn't /// work with our WebIDL implementation of `DOMException`. pub(crate) fn is_instance_of_error<'s, 'i>( scope: &mut v8::PinScope<'s, 'i>, value: v8::Local<'s, 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 } /// Implements `value instanceof primordials.AggregateError` in JS, /// by walking the prototype chain, and comparing each links constructor `name` property. /// /// NOTE: There is currently no way to detect `AggregateError` via `rusty_v8`, /// as v8 itself doesn't expose `v8__Exception__AggregateError`, /// and we cannot create bindings for it. This forces us to rely on `name` inference. pub(crate) fn is_aggregate_error<'s, 'i>( scope: &mut v8::PinScope<'s, 'i>, value: v8::Local<'s, v8::Value>, ) -> bool { // Prototype and property lookups can invoke arbitrary accessors or proxy // traps. Keep exceptions raised while probing contained here: failure to // classify an error should not interfere with converting the original // exception. v8::tc_scope!(let tc_scope, scope); let result = is_aggregate_error_inner(tc_scope, value); if tc_scope.has_caught() { tc_scope.reset(); false } else { result } } fn is_aggregate_error_inner<'s, 'i>( scope: &mut v8::PinScope<'s, 'i>, value: v8::Local<'s, v8::Value>, ) -> bool { let mut maybe_prototype = Some(value); while let Some(prototype) = maybe_prototype { if !prototype.is_object() { return false; } let Ok(prototype) = prototype.try_cast::<v8::Object>() else { return false; }; let prototype_name = match get_property(scope, prototype, v8_static_strings::CONSTRUCTOR) { Some(constructor) => { let Ok(ctor) = constructor.try_cast::<v8::Object>() else { return false; }; get_property(scope, ctor, v8_static_strings::NAME) .and_then(|v| v.try_cast::<v8::String>().ok()) .map(|s| v8_to_rust_string(&s, scope)) } None => return false, }; if prototype_name == Some(String::from("AggregateError")) { return true; } maybe_prototype = prototype.get_prototype(scope); } false } /// Check if the error has a proper stack trace. The stack trace checked is the /// one passed to `prepareStackTrace()`, not `msg.get_stack_trace()`. pub(crate) fn has_call_site<'s, 'i>( scope: &mut v8::PinScope<'s, 'i>, exception: v8::Local<'s, v8::Value>, ) -> bool { if !exception.is_object() { return false; } let exception = exception.to_object(scope).unwrap(); // Access error.stack to ensure that prepareStackTrace() has been called. // This should populate error.#callSiteEvals. 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 && frames_v8.length() > 0 { return true; } false } const DATA_URL_ABBREV_THRESHOLD: usize = 150; fn to_percent_decoded_str(s: &str) -> String { match percent_encoding::percent_decode_str(s).decode_utf8() { Ok(s) => s.to_string(), // when failed to decode, return the original string Err(_) => s.to_string(), } } pub fn relative_specifier(from: &Url, to: &Url) -> Option<String> { let is_dir = to.path().ends_with('/'); if is_dir && from == to { return Some("./".to_string()); } let text = from.make_relative(to)?; let text = if text.starts_with("../") || text.starts_with("./") { text } else { format!("./{text}") }; Some(to_percent_decoded_str(&text)) } /// Like `relative_specifier`, but returns None if the path would require going up directories. fn relative_specifier_within(from: &Url, to: &Url) -> Option<String> { let relative = relative_specifier(from, to)?; if relative.starts_with("../") { return None; } Some(relative) } pub struct FileNameParts<'a> { pub working_dir_path: Option<Cow<'a, str>>, pub file_name: Cow<'a, str>, } impl<'a> FileNameParts<'a> { pub fn into_owned(self) -> FileNameParts<'static> { FileNameParts { working_dir_path: self.working_dir_path.map(|s| match s { Cow::Borrowed(s) => Cow::Owned(s.to_owned()), Cow::Owned(s) => Cow::Owned(s), }), file_name: match self.file_name { Cow::Borrowed(s) => Cow::Owned(s.to_owned()), Cow::Owned(s) => Cow::Owned(s), }, } } } pub fn format_file_name<'a>( file_name: &'a str, maybe_initial_cwd: Option<&Url>, ) -> FileNameParts<'a> { abbrev_file_name(file_name, maybe_initial_cwd).unwrap_or_else(|| { // same as to_percent_decoded_str() in cli/util/path.rs match percent_encoding::percent_decode_str(file_name).decode_utf8() { Ok(file_name) => FileNameParts { working_dir_path: None, file_name, }, // when failing as utf-8, just return the original string Err(_) => FileNameParts { working_dir_path: None, file_name: file_name.into(), }, } }) } fn abbrev_file_name<'a>( file_name: &'a str, maybe_initial_cwd: Option<&Url>, ) -> Option<FileNameParts<'a>> { let Ok(url) = Url::parse(file_name) else { return None; }; // Handle data URLs - abbreviate if too long if url.scheme() == "data" { if file_name.len() <= DATA_URL_ABBREV_THRESHOLD { return Some(FileNameParts { working_dir_path: None, file_name: file_name.into(), }); } let (head, tail) = url.path().split_once(',')?; let len = tail.len(); let start = tail.get(0..20)?; let end = tail.get(len - 20..)?; return Some(FileNameParts { working_dir_path: None, file_name: format!("{}:{},{}......{}", url.scheme(), head, start, end) .into(), }); } // For file:// URLs, use relative paths only if the file is within the cwd if let Some(initial_cwd) = maybe_initial_cwd && let Some(relative) = relative_specifier_within(initial_cwd, &url) { return Some(FileNameParts { working_dir_path: Some(initial_cwd.to_string().into()), file_name: relative.into(), }); } // For all other cases, use absolute paths None } fn format_eval_origin<'a>( eval_origin: &'a str, maybe_initial_cwd: Option<&Url>, ) -> Cow<'a, str> { let Some((before, (file, line, col), after)) = parse_eval_origin(eval_origin) else { return eval_origin.into(); }; let formatted_file = format_file_name(file, maybe_initial_cwd); Cow::Owned(format!( "{before}{}:{line}:{col}{after}", if let Some(working_dir_path) = formatted_file.working_dir_path { Cow::Owned(format!( "{}{}", working_dir_path, formatted_file.file_name.trim_start_matches("./") )) } else { formatted_file.file_name } )) } pub(crate) fn exception_to_err_result<'s, 'i, T>( scope: &mut v8::PinScope<'s, 'i>, exception: v8::Local<'s, v8::Value>, in_promise: bool, clear_error: bool, ) -> Result<T, Box<JsError>> { Err(exception_to_err(scope, exception, in_promise, clear_error)) } pub fn exception_to_err<'s, 'i>( scope: &mut v8::PinScope<'s, 'i>, exception: v8::Local<'s, v8::Value>, mut in_promise: bool, clear_error: bool, ) -> Box<JsError> { let state = JsRealm::exception_state_from_scope(scope); let mut was_terminating_execution = scope.is_execution_terminating(); // If TerminateExecution was called, cancel isolate termination so that the // exception can be created. Note that `scope.is_execution_terminating()` may // have returned false if TerminateExecution was indeed called but there was // no JS to execute after the call. scope.cancel_terminate_execution(); let exception = match state.get_dispatched_exception_as_local(scope) { Some(dispatched_exception) => { // If termination is the result of a `reportUnhandledException` call, we want // to use the exception that was passed to it rather than the exception that // was passed to this function. in_promise = state.is_dispatched_exception_promise(); if clear_error { state.clear_error(); was_terminating_execution = false; } dispatched_exception } _ => { if was_terminating_execution && exception.is_null_or_undefined() { // If we are terminating and there is no exception, throw `new Error("execution terminated")``. let message = v8::String::new(scope, "execution terminated").unwrap(); v8::Exception::error(scope, message) } else { // Otherwise re-use the exception 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 { // Resume exception termination. scope.terminate_execution(); } js_error } 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", SOURCE_MAPPED_INFO = "deno_core::source_mapped_call_site_info", ERROR_RECEIVER_IS_NOT_VALID_CALLSITE_OBJECT = "The receiver is not a valid callsite object.", } #[inline(always)] pub(crate) fn original_call_site_key<'s, 'i>( scope: &mut v8::PinScope<'s, 'i>, ) -> v8::Local<'s, v8::Private> { let name = ORIGINAL.v8_string(scope).unwrap(); v8::Private::for_api(scope, Some(name)) } pub(crate) fn source_mapped_info_key<'s, 'i>( scope: &mut v8::PinScope<'s, 'i>, ) -> v8::Local<'s, v8::Private> { let name = SOURCE_MAPPED_INFO.v8_string(scope).unwrap(); v8::Private::for_api(scope, Some(name)) } fn make_patched_callsite<'s, 'i>( scope: &mut v8::PinScope<'s, 'i>, callsite: v8::Local<'s, v8::Object>, prototype: v8::Local<'s, v8::Object>, ) -> v8::Local<'s, v8::Object> { let out_obj = v8::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<'s, 'i>( scope: &mut v8::PinScope<'s, 'i>, this: v8::Local<'s, v8::Object>, ) -> Option<v8::Local<'s, 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) .unwrap(); 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<'s, 'i>( scope: &mut v8::PinScope<'s, 'i>, args: v8::FunctionCallbackArguments<'s>, mut rv: v8::ReturnValue<'_>, ) { let Some(orig) = original_call_site(scope, args.this()) else { return; }; let key = $field.v8_string(scope).unwrap().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( deno_path_util::url_to_file_path(&Url::parse(string).ok()?) .ok()? .to_string_lossy() .into_owned(), ) } else { None } } pub mod callsite_fns { use capacity_builder::StringBuilder; use super::*; use crate::FromV8; use crate::ToV8; use crate::convert; enum SourceMappedCallsiteInfo<'a> { Ref(v8::Local<'a, v8::Array>), Value { file_name: v8::Local<'a, v8::Value>, line_number: v8::Local<'a, v8::Value>, column_number: v8::Local<'a, v8::Value>, }, } impl<'s> SourceMappedCallsiteInfo<'s> { #[inline] fn file_name<'i>( &self, scope: &mut v8::PinScope<'s, 'i>, ) -> v8::Local<'s, v8::Value> { match self { Self::Ref(array) => array.get_index(scope, 0).unwrap(), Self::Value { file_name, .. } => *file_name, } } #[inline] fn line_number<'i>( &self, scope: &mut v8::PinScope<'s, 'i>, ) -> v8::Local<'s, v8::Value> { match self { Self::Ref(array) => array.get_index(scope, 1).unwrap(), Self::Value { line_number, .. } => *line_number, } } #[inline] fn column_number<'i>( &self, scope: &mut v8::PinScope<'s, 'i>, ) -> v8::Local<'s, v8::Value> { match self { Self::Ref(array) => array.get_index(scope, 2).unwrap(), Self::Value { column_number, .. } => *column_number, } } } type MaybeValue<'a> = Option<v8::Local<'a, v8::Value>>; fn maybe_apply_source_map<'s, 'i>( scope: &mut v8::PinScope<'s, 'i>, file_name: MaybeValue<'s>, line_number: MaybeValue<'s>, column_number: MaybeValue<'s>, ) -> Option<(String, i64, i64)> { let file_name = serde_v8::to_utf8(file_name?.try_cast().ok()?, scope); let convert::Number(line_number) = FromV8::from_v8(scope, line_number?).ok()?; let convert::Number(column_number) = FromV8::from_v8(scope, column_number?).ok()?; let state = JsRuntime::state_from(scope); let mut source_mapper = state.source_mapper.borrow_mut(); let (mapped_file_name, mapped_line_number, mapped_column_number) = apply_source_map( &mut source_mapper, Cow::Owned(file_name), line_number, column_number, ); Some(( mapped_file_name.into_owned(), mapped_line_number, mapped_column_number, )) } fn source_mapped_call_site_info<'s, 'i>( scope: &mut v8::PinScope<'s, 'i>, callsite: v8::Local<'s, v8::Object>, ) -> Option<SourceMappedCallsiteInfo<'s>> { let key = source_mapped_info_key(scope); // return the cached value if it exists if let Some(info) = callsite.get_private(scope, key) && let Ok(array) = info.try_cast::<v8::Array>() { return Some(SourceMappedCallsiteInfo::Ref(array)); } let orig_callsite = original_call_site(scope, callsite)?; let file_name = call_method::<v8::Value>(scope, orig_callsite, super::GET_FILE_NAME, &[]); let line_number = call_method::<v8::Value>( scope, orig_callsite, super::GET_LINE_NUMBER, &[], ); let column_number = call_method::<v8::Value>( scope, orig_callsite, super::GET_COLUMN_NUMBER, &[], ); // For wasm frames, skip source map application and cache the original values let is_wasm = file_name .and_then(|v| v.try_cast::<v8::String>().ok()) .map(|s| serde_v8::to_utf8(s, scope).starts_with("wasm://")) .unwrap_or(false); let info = v8::Array::new(scope, 3); // if the types are right, apply the source map (unless wasm), otherwise just take them as is if !is_wasm && let Some((mapped_file_name, mapped_line_number, mapped_column_number)) = maybe_apply_source_map(scope, file_name, line_number, column_number) { let mapped_file_name_trimmed = maybe_to_path_str(&mapped_file_name).unwrap_or(mapped_file_name); let mapped_file_name = crate::FastString::from(mapped_file_name_trimmed) .v8_string(scope) .unwrap(); let Ok(mapped_line_number) = convert::Number(mapped_line_number).to_v8(scope); let Ok(mapped_column_number) = convert::Number(mapped_column_number).to_v8(scope); info.set_index(scope, 0, mapped_file_name.into()); info.set_index(scope, 1, mapped_line_number); info.set_index(scope, 2, mapped_column_number); callsite.set_private(scope, key, info.into()); Some(SourceMappedCallsiteInfo::Value { file_name: mapped_file_name.into(), line_number: mapped_line_number, column_number: mapped_column_number, }) } else { let file_name = file_name.unwrap_or_else(|| v8::undefined(scope).into()); let line_number = line_number.unwrap_or_else(|| v8::undefined(scope).into()); let column_number = column_number.unwrap_or_else(|| v8::undefined(scope).into()); info.set_index(scope, 0, file_name); info.set_index(scope, 1, line_number); info.set_index(scope, 2, column_number); callsite.set_private(scope, key, info.into()); Some(SourceMappedCallsiteInfo::Ref(info)) } } 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<'s, 'i>( scope: &mut v8::PinScope<'s, 'i>, args: v8::FunctionCallbackArguments<'s>, mut rv: v8::ReturnValue<'_>, ) { if let Some(info) = source_mapped_call_site_info(scope, args.this()) { rv.set(info.file_name(scope)); } } pub fn get_line_number<'s, 'i>( scope: &mut v8::PinScope<'s, 'i>, args: v8::FunctionCallbackArguments<'s>, mut rv: v8::ReturnValue<'_>, ) { if let Some(info) = source_mapped_call_site_info(scope, args.this()) { rv.set(info.line_number(scope)); } } pub fn get_column_number<'s, 'i>( scope: &mut v8::PinScope<'s, 'i>, args: v8::FunctionCallbackArguments<'s>, mut rv: v8::ReturnValue<'_>, ) { if let Some(info) = source_mapped_call_site_info(scope, args.this()) { rv.set(info.column_number(scope)); } } 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 ); // the bulk of the to_string logic fn to_string_inner<'s, 'i>( scope: &mut v8::PinScope<'s, 'i>, this: v8::Local<'s, v8::Object>, orig: v8::Local<'s, v8::Object>, orig_to_string_v8: v8::Local<'s, v8::String>, ) -> Option<v8::Local<'s, v8::String>> { let orig_to_string = serde_v8::to_utf8(orig_to_string_v8, scope); // `this[kOriginalCallsite].getFileName()` let orig_file_name = call_method::<v8::Value>(scope, orig, GET_FILE_NAME, &[]) .and_then(|v| v.try_cast::<v8::String>().ok())?; let orig_file_name = serde_v8::to_utf8(orig_file_name, scope); // For wasm frames, V8's original toString() already formats correctly // (with wasm-function[N]:0xOFFSET), so return it unchanged. if orig_file_name.starts_with("wasm://") { return Some(orig_to_string_v8); } let orig_line_number = call_method::<v8::Value>(scope, orig, GET_LINE_NUMBER, &[]) .and_then(|v| v.try_cast::<v8::Number>().ok())?; let orig_column_number = call_method::<v8::Value>(scope, orig, GET_COLUMN_NUMBER, &[]) .and_then(|v| v.try_cast::<v8::Number>().ok())?; let orig_line_number = orig_line_number.value() as i64; let orig_column_number = orig_column_number.value() as i64; let orig_file_name_line_col = fmt_file_line_col(&orig_file_name, orig_line_number, orig_column_number); let mapped = source_mapped_call_site_info(scope, this)?; let mapped_file_name = match mapped.file_name(scope).to_string(scope) { Some(s) => v8_to_rust_string(&s, scope), None => String::new(), }; let mapped_line_num = mapped .line_number(scope) .try_cast::<v8::Number>() .ok() .map(|n| n.value() as i64)?; let mapped_col_num = mapped.column_number(scope).cast::<v8::Number>().value() as i64; let file_name_line_col = fmt_file_line_col(&mapped_file_name, mapped_line_num, mapped_col_num); // replace file URL with file path, and source map in original `toString` let to_string = orig_to_string .replace(&orig_file_name_line_col, &file_name_line_col) .replace(&orig_file_name, &mapped_file_name); // maybe unnecessary? Some(crate::FastString::from(to_string).v8_string(scope).unwrap()) } fn fmt_file_line_col(file: &str, line: i64, col: i64) -> String { StringBuilder::build(|builder| { builder.append(file); builder.append(':'); builder.append(line); builder.append(':'); builder.append(col); }) .unwrap() } pub fn to_string<'s, 'i>( scope: &mut v8::PinScope<'s, 'i>, args: v8::FunctionCallbackArguments<'s>, mut rv: v8::ReturnValue<'_>, ) { let this = args.this(); let Some(orig) = original_call_site(scope, this) else { return; }; // `this[kOriginalCallsite].toString()` let Some(orig_to_string_v8) = call_method::<v8::String>(scope, orig, TO_STRING, &[]) else { return; }; if let Some(v8_str) = to_string_inner(scope, this, orig, orig_to_string_v8) { rv.set(v8_str.into()); } else { rv.set(orig_to_string_v8.into()); } } } /// Creates a template for a `Callsite`-like object, with /// a patched `getFileName`. /// Effectively: /// ```js /// const kOriginalCallsite = Symbol("_original"); /// { /// [kOriginalCallsite]: originalCallSite, /// getLineNumber() { /// return this[kOriginalCallsite].getLineNumber(); /// }, /// // etc /// getFileName() { /// const fileName = this[kOriginalCallsite].getFileName(); /// return fileUrlToPath(fileName); /// } /// } /// ``` pub(crate) fn make_callsite_prototype<'s, 'i>( scope: &mut v8::PinScope<'s, 'i>, ) -> 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).unwrap().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, 'i, const PATCH_CALLSITES: bool>( scope: &mut v8::PinScope<'s, 'i>, error: v8::Local<'s, v8::Value>, callsites: v8::Local<'s, v8::Array>, ) -> v8::Local<'s, v8::Value> { // stash the callsites on the error object. this is used by `JsError::from_exception_inner` // to get more details on an error, because the v8 StackFrame API is missing some info. if let Ok(obj) = error.try_cast::<v8::Object>() { let key = call_site_evals_key(scope); obj.set_private(scope, key, callsites.into()); } // `globalThis.Error.prepareStackTrace` 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()) }); // Note that the callback is called _instead_ of `Error.prepareStackTrace` // so we have to explicitly call the global function if let Some(prepare_fn) = prepare_fn { let callsites = if PATCH_CALLSITES { // User defined `Error.prepareStackTrace`. // Patch the callsites to have file paths, then call // the user's function 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 }; // call the user's `prepareStackTrace` with our "callsite" objects 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()); } // no user defined `prepareStackTrace`, just call our default format_stack_trace(scope, error, callsites) } /// Callback to prepare an error stack trace. This callback is invoked by V8 whenever a stack trace is created. /// This will patch callsite objects to translate file URLs to file paths before they're /// exposed to user-defined `prepareStackTrace` implementations. /// /// This function is not used by default, but it can /// be set directly on the [v8 isolate][v8::Isolate::set_prepare_stack_trace_callback] pub fn prepare_stack_trace_callback_with_original_callsites<'s, 'i>( scope: &mut v8::PinScope<'s, 'i>, 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) } /// Callback to prepare an error stack trace. This callback is invoked by V8 whenever a stack trace is created. /// This will patch callsite objects to translate file URLs to file paths before they're /// exposed to user-defined `prepareStackTrace` implementations. /// /// This function is the default callback, set on creating a `JsRuntime`, but the callback can also /// be set directly on the [v8 isolate][v8::Isolate::set_prepare_stack_trace_callback] pub fn prepare_stack_trace_callback<'s, 'i>( scope: &mut v8::PinScope<'s, 'i>, 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 struct InitialCwd(pub Arc<Url>); pub fn format_stack_trace<'s, 'i>( scope: &mut v8::PinScope<'s, 'i>, error: v8::Local<'s, v8::Value>, callsites: v8::Local<'s, v8::Array>, ) -> v8::Local<'s, v8::Value> { let state = JsRuntime::state_from(scope); let maybe_initial_cwd = state .op_state .borrow() .try_borrow::<InitialCwd>() .map(|i| &i.0) .cloned(); let mut result = String::new(); if let Ok(obj) = error.try_cast() { // Write out the error name + message, if any let msg = get_property(scope, obj, v8_static_strings::MESSAGE) .filter(|v| !v.is_undefined()) .and_then(|v| v.to_string(scope)) .map(|s| v8_to_rust_string(&s, scope)) .unwrap_or_default(); let name = get_property(scope, obj, v8_static_strings::NAME) .filter(|v| !v.is_undefined()) .and_then(|v| v.to_string(scope)) .map(|s| v8_to_rust_string(&s, scope)) .unwrap_or_else(|| GENERIC_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) => {} } } // format each stack frame for i in 0..callsites.length() { let callsite = callsites.get_index(scope, i).unwrap().cast::<v8::Object>(); v8::tc_scope!(let tc_scope, scope); let Some(frame) = JsStackFrame::from_callsite_object(tc_scope, callsite) else { let exc = tc_scope .exception() .expect("JsStackFrame::from_callsite_object raised an exception"); let message = match exc.to_string(tc_scope) { Some(s) => v8_to_rust_string(&s, tc_scope), None => String::new(), }; #[allow(clippy::print_stderr, reason = "intentional warning output")] { 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, maybe_initial_cwd.as_deref()) ) .unwrap(); } let result = v8::String::new(scope, &result).unwrap(); result.into() } /// Formats an error without using ansi colors. pub struct NoAnsiColors; #[derive(Debug, Clone, Copy)] /// Part of an error stack trace pub enum ErrorElement { /// An anonymous file or method name Anonymous, /// Text signifying a native stack frame NativeFrame, /// A source line number LineNumber, /// A source column number ColumnNumber, /// The name of a function (or method) FunctionName, /// The name of a source file FileName, /// The path to the working directory, as in part of a file name WorkingDirPath, /// The origin of an error coming from `eval`ed code EvalOrigin, /// Text signifying a call to `Promise.all` PromiseAll, /// Other, plain text appearing in the error stack trace PlainText, } /// Applies formatting to various parts of error stack traces. /// /// This is to allow the `deno` crate to reuse `format_frame` and /// `format_location` but apply ANSI colors for terminal output, /// without adding extra dependencies to `deno_core`. pub trait ErrorFormat { fn fmt_element( element: ErrorElement, in_extension_code: bool, s: &str, ) -> Cow<'_, str>; } impl ErrorFormat for NoAnsiColors { fn fmt_element( _element: ErrorElement, _in_extension_code: bool, s: &str, ) -> Cow<'_, str> { s.into() } } pub fn format_location<F: ErrorFormat>( frame: &JsStackFrame, maybe_initial_cwd: Option<&Url>, ) -> String { use ErrorElement::*; let in_extension_code = frame .file_name .as_ref() .map(|f| f.starts_with("ext:") || f.starts_with("node:")) .unwrap_or(false); if frame.is_native { return F::fmt_element(NativeFrame, in_extension_code, "native") .to_string(); } let mut result = String::new(); let file_name = frame.file_name.as_deref().unwrap_or(""); if !file_name.is_empty() { let parts = format_file_name(file_name, maybe_initial_cwd); if let Some(working_dir_path) = &parts.working_dir_path { result += &F::fmt_element(WorkingDirPath, in_extension_code, working_dir_path); result += &F::fmt_element( FileName, in_extension_code, parts.file_name.trim_start_matches("./"), ) } else { result += &F::fmt_element(FileName, in_extension_code, &parts.file_name) } } else { if frame.is_eval { let eval_origin = frame.eval_origin.as_ref().unwrap(); let formatted_eval_origin = format_eval_origin(eval_origin, maybe_initial_cwd); result += &F::fmt_element(EvalOrigin, in_extension_code, &formatted_eval_origin); result += &F::fmt_element(PlainText, in_extension_code, ", "); } result += &F::fmt_element(Anonymous, in_extension_code, "<anonymous>"); } if frame.is_wasm { // W3C WebAssembly Web API spec format: // {url}:wasm-function[{funcIndex}]:0x{pcOffset} // V8 returns function index (1-based) via getLineNumber() // and byte offset via getColumnNumber() if let Some(line_number) = frame.line_number { let func_index = line_number - 1; let wasm_func = format!("wasm-function[{func_index}]"); result += &F::fmt_element(PlainText, in_extension_code, ":"); result += &F::fmt_element(LineNumber, in_extension_code, &wasm_func); } if let Some(column_number) = frame.column_number { // V8's getColumnNumber() is 1-based, but wasm byte offsets are 0-based let pc_offset = format!("0x{:x}", column_number - 1); result += &F::fmt_element(PlainText, in_extension_code, ":"); result += &F::fmt_element(ColumnNumber, in_extension_code, &pc_offset); } } else if let Some(line_number) = frame.line_number { result += &F::fmt_element(PlainText, in_extension_code, ":"); result += &F::fmt_element(LineNumber, in_extension_code, &line_number.to_string()); if let Some(column_number) = frame.column_number { result += &F::fmt_element(PlainText, in_extension_code, ":"); result += &F::fmt_element( ColumnNumber, in_extension_code, &column_number.to_string(), ); } } result } pub fn format_frame<F: ErrorFormat>( frame: &JsStackFrame, maybe_initial_cwd: Option<&Url>, ) -> String { use ErrorElement::*; let in_extension_code = frame .file_name .as_ref() .map(|f| f.starts_with("ext:") || f.starts_with("node:")) .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 += &F::fmt_element(PlainText, in_extension_code, "async "); } if frame.is_promise_all { result += &F::fmt_element( PromiseAll, in_extension_code, &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 && !function_name.starts_with(type_name) { write!(formatted_method, "{type_name}.").unwrap(); } formatted_method += function_name; if let Some(method_name) = &frame.method_name && !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, in_extension_code, &formatted_method) .as_ref(); } else if frame.is_constructor { result += &F::fmt_element(PlainText, in_extension_code, "new "); if let Some(function_name) = &frame.function_name { write!( result, "{}", F::fmt_element(FunctionName, in_extension_code, function_name) ) .unwrap(); } else { result += F::fmt_element(Anonymous, in_extension_code, "<anonymous>").as_ref(); } } else if let Some(function_name) = &frame.function_name { result += F::fmt_element(FunctionName, in_extension_code, function_name).as_ref(); } else { result += &format_location::<F>(frame, maybe_initial_cwd); return result; } result += &F::fmt_element(PlainText, in_extension_code, " ("); result += &format_location::<F>(frame, maybe_initial_cwd); result += &F::fmt_element(PlainText, in_extension_code, ")"); result } pub fn throw_error_one_byte_info( info: &v8::FunctionCallbackInfo, message: &str, ) { v8::callback_scope!(unsafe scope, info); throw_error_one_byte(scope, message); } pub fn throw_error_js_error_class<'s, 'i>( scope: &mut v8::PinCallbackScope<'s, 'i>, err: &dyn JsErrorClass, ) { let exc = to_v8_error(scope, err); scope.throw_exception(exc); } pub fn throw_error_one_byte<'s, 'i>( scope: &mut v8::PinCallbackScope<'s, 'i>, message: &str, ) { let msg = deno_core::v8::String::new_from_one_byte( scope, message.as_bytes(), deno_core::v8::NewStringType::Normal, ) .unwrap(); let exc = deno_core::v8::Exception::type_error(scope, msg); scope.throw_exception(exc); } /// Throw a Node-flavoured `ERR_INVALID_THIS` `TypeError` -- a plain /// `TypeError` whose `code` own-property is `"ERR_INVALID_THIS"`. The op2 /// macro uses this to report cppgc brand-check failures so that the thrown /// error matches the `webidl.assertBranded` shape expected by web platform /// and Node compat tests. pub fn throw_invalid_this_error_one_byte_info( info: &v8::FunctionCallbackInfo, message: &str, ) { v8::callback_scope!(unsafe scope, info); throw_invalid_this_error_one_byte(scope, message); } pub fn throw_invalid_this_error_one_byte<'s, 'i>( scope: &mut v8::PinCallbackScope<'s, 'i>, message: &str, ) { let msg = deno_core::v8::String::new_from_one_byte( scope, message.as_bytes(), deno_core::v8::NewStringType::Normal, ) .unwrap(); let exc = deno_core::v8::Exception::type_error(scope, msg); if let Some(exc_obj) = exc.to_object(scope) { let code_key = deno_core::v8::String::new_from_one_byte( scope, b"code", deno_core::v8::NewStringType::Normal, ) .unwrap(); let code_val = deno_core::v8::String::new_from_one_byte( scope, b"ERR_INVALID_THIS", deno_core::v8::NewStringType::Normal, ) .unwrap(); exc_obj.set(scope, code_key.into(), code_val.into()); } scope.throw_exception(exc); } #[cfg(test)] mod tests { use super::*; #[cfg(not(miri))] #[test] fn test_js_error_from_v8_exception_handles_cyclic_aggregate_error() { let mut runtime = JsRuntime::new(Default::default()); deno_core::scope!(scope, runtime); let exception: v8::Local<v8::Object> = JsRuntime::eval( scope, r#"(() => { const error = new AggregateError([], "boom"); error.errors.push(error); return error; })()"#, ) .unwrap(); let error = JsError::from_v8_exception(scope, exception.into()); let aggregated = error.aggregated.as_ref().expect("aggregate errors"); assert_eq!(aggregated.len(), 1); // The cycle terminates by repeating the error once with its own members // dropped, rather than recursing into itself forever. assert_eq!(aggregated[0].exception_message, error.exception_message); assert!(aggregated[0].aggregated.is_none()); } #[cfg(not(miri))] #[test] fn test_js_error_from_v8_exception_handles_crossed_aggregate_cycle() { let mut runtime = JsRuntime::new(Default::default()); deno_core::scope!(scope, runtime); let exception: v8::Local<v8::Object> = JsRuntime::eval( scope, r#"(() => { const first = new AggregateError([], "first"); const second = new AggregateError([first], "second"); first.errors.push(second); return first; })()"#, ) .unwrap(); let error = JsError::from_v8_exception(scope, exception.into()); let second = &error.aggregated.as_ref().unwrap()[0]; let repeated_first = &second.aggregated.as_ref().unwrap()[0]; assert_eq!(repeated_first.exception_message, error.exception_message); assert!(repeated_first.aggregated.is_none()); } #[cfg(not(miri))] #[test] fn test_js_error_from_v8_exception_truncates_deep_cause_chain() { let mut runtime = JsRuntime::new(Default::default()); deno_core::scope!(scope, runtime); let exception: v8::Local<v8::Object> = JsRuntime::eval( scope, r#"(() => { let error = new Error("0"); for (let i = 1; i < 100; i++) { error = new Error(String(i), { cause: error }); } return error; })()"#, ) .unwrap(); let error = JsError::from_v8_exception(scope, exception.into()); let mut current = Some(error.as_ref()); let mut found_truncated = false; while let Some(error) = current { if error.exception_message == "Uncaught [Error details truncated]" { found_truncated = true; break; } current = error.cause.as_deref(); } assert!(found_truncated); } #[cfg(not(miri))] #[test] fn test_js_error_from_v8_exception_bounds_shared_aggregate_graph() { let mut runtime = JsRuntime::new(Default::default()); deno_core::scope!(scope, runtime); let exception: v8::Local<v8::Object> = JsRuntime::eval( scope, r#"(() => { globalThis.aggregateElementReads = 0; let error = new Error("leaf"); for (let i = 0; i < 20; i++) { error = new AggregateError([error, error], String(i)); const child = error.errors[0]; Object.defineProperties(error.errors, { 0: { get() { globalThis.aggregateElementReads++; return child; }, }, 1: { get() { globalThis.aggregateElementReads++; return child; }, }, }); } return error; })()"#, ) .unwrap(); let error = JsError::from_v8_exception(scope, exception.into()); let mut pending = vec![error.as_ref()]; let mut converted_nodes = 0; let mut truncated_nodes = 0; while let Some(error) = pending.pop() { converted_nodes += 1; if error.exception_message == "Uncaught [Error details truncated]" { truncated_nodes += 1; } if let Some(cause) = error.cause.as_deref() { pending.push(cause); } if let Some(aggregated) = &error.aggregated { pending.extend(aggregated); } } assert_eq!(truncated_nodes, 1); assert!(converted_nodes <= MAX_ERROR_CONVERSION_NODES + 1); let element_reads: v8::Local<v8::Integer> = JsRuntime::eval(scope, "globalThis.aggregateElementReads").unwrap(); assert!(element_reads.value() as usize <= MAX_ERROR_CONVERSION_NODES); } #[cfg(not(miri))] #[test] fn test_js_error_from_v8_exception_handles_throwing_aggregate_element() { let mut runtime = JsRuntime::new(Default::default()); deno_core::scope!(scope, runtime); let exception: v8::Local<v8::Object> = JsRuntime::eval( scope, r#"(() => { const error = new AggregateError([new Error("inner")], "outer"); Object.defineProperty(error.errors, 0, { get() { throw new Error("element getter failed"); }, }); return error; })()"#, ) .unwrap(); let error = JsError::from_v8_exception(scope, exception.into()); let aggregated = error.aggregated.as_ref().expect("aggregate errors"); assert_eq!(aggregated.len(), 1); assert_eq!( aggregated[0].exception_message, "Uncaught [Error details unavailable]" ); let result: v8::Local<v8::Integer> = JsRuntime::eval(scope, "1 + 1").expect("exception was contained"); assert_eq!(result.value(), 2); } #[cfg(not(miri))] #[test] fn test_js_error_from_v8_exception_handles_throwing_aggregate_errors_getter() { let mut runtime = JsRuntime::new(Default::default()); deno_core::scope!(scope, runtime); let exception: v8::Local<v8::Object> = JsRuntime::eval( scope, r#"(() => { const error = new AggregateError([], "outer"); Object.defineProperty(error, "errors", { get() { throw new Error("errors getter failed"); }, }); return error; })()"#, ) .unwrap(); let error = JsError::from_v8_exception(scope, exception.into()); let aggregated = error.aggregated.as_ref().expect("aggregate errors"); assert_eq!(aggregated.len(), 1); assert_eq!( aggregated[0].exception_message, "Uncaught [Error details unavailable]" ); let result: v8::Local<v8::Integer> = JsRuntime::eval(scope, "1 + 1").expect("exception was contained"); assert_eq!(result.value(), 2); } #[cfg(not(miri))] #[test] fn test_is_aggregate_error_recognizes_builtin() { let mut runtime = JsRuntime::new(Default::default()); deno_core::scope!(scope, runtime); let exception: v8::Local<v8::Object> = JsRuntime::eval( scope, "new AggregateError([new Error('first')], 'combined')", ) .unwrap(); let exception = exception.into(); assert!(is_aggregate_error(scope, exception)); let js_error = JsError::from_v8_exception(scope, exception); assert_eq!(js_error.name.as_deref(), Some("AggregateError")); assert_eq!(js_error.message.as_deref(), Some("combined")); assert_eq!(js_error.aggregated.as_ref().map(Vec::len), Some(1)); } #[cfg(not(miri))] #[test] fn test_is_aggregate_error_handles_malformed_constructors() { let mut runtime = JsRuntime::new(Default::default()); deno_core::scope!(scope, runtime); for constructor in ["null", "undefined", "42", "'AggregateError'"] { let exception: v8::Local<v8::Object> = JsRuntime::eval( scope, &format!( "(() => {{ const error = new Error('malformed'); Object.defineProperty(error, 'constructor', {{ value: {constructor} }}); return error; }})()" ), ) .unwrap(); let exception = exception.into(); { v8::tc_scope!(let tc_scope, scope); assert!(!is_aggregate_error(tc_scope, exception)); assert!(!tc_scope.has_caught()); } let js_error = JsError::from_v8_exception(scope, exception); assert_eq!(js_error.message.as_deref(), Some("malformed")); assert!(js_error.aggregated.is_none()); } } #[cfg(not(miri))] #[test] fn test_is_aggregate_error_handles_throwing_accessors() { let mut runtime = JsRuntime::new(Default::default()); deno_core::scope!(scope, runtime); for source in [ "(() => { const error = new Error('constructor getter'); Object.defineProperty(error, 'constructor', { get() { throw new Error('constructor lookup failed'); } }); return error; })()", "(() => { const error = new Error('name getter'); const constructor = {}; Object.defineProperty(constructor, 'name', { get() { throw new Error('name lookup failed'); } }); Object.defineProperty(error, 'constructor', { value: constructor }); return error; })()", ] { let exception: v8::Local<v8::Object> = JsRuntime::eval(scope, source).unwrap(); let exception = exception.into(); { v8::tc_scope!(let tc_scope, scope); assert!(!is_aggregate_error(tc_scope, exception)); assert!(!tc_scope.has_caught()); } let js_error = JsError::from_v8_exception(scope, exception); assert!(js_error.aggregated.is_none()); // Classification must leave the isolate ready to convert the next // exception normally. let next_exception: v8::Local<v8::Object> = JsRuntime::eval(scope, "new TypeError('next error')").unwrap(); let next_exception = next_exception.into(); let next_error = JsError::from_v8_exception(scope, next_exception); assert_eq!(next_error.name.as_deref(), Some("TypeError")); assert_eq!(next_error.message.as_deref(), Some("next error")); } } #[test] fn test_format_file_name() { let file_name = format_file_name("data:,Hello%2C%20World%21", None); assert_eq!(file_name.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"), None, ) .into_owned(); let expected = "data:text/plain;base64,aaaaaaaaaaaaaaaaaaaa......aaaaaaa_%F0%9F%A6%95"; assert_eq!(file_name.file_name, expected,); let file_name = format_file_name( &format!("data:text/plain;base64,{too_long_name}_%F0%9F%A6%95"), Some(&Url::parse("file:///foo").unwrap()), ) .into_owned(); assert_eq!(file_name.file_name, expected); let file_name = format_file_name("file:///foo/bar.ts", None); assert_eq!(file_name.file_name, "file:///foo/bar.ts"); let file_name = format_file_name( "file:///foo/bar.ts", Some(&Url::parse("file:///foo/").unwrap()), ); assert_eq!(file_name.file_name, "./bar.ts"); let file_name = format_file_name("file:///%E6%9D%B1%E4%BA%AC/%F0%9F%A6%95.ts", None); assert_eq!(file_name.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), ")")), ), ( // malformed "eval at (s:1:2", None, ), ( // malformed "at ()", None, ), ( // url with parens "eval at foo (http://website.zzz/my-script).ts:1:2)", Some(( "eval at foo (", ("http://website.zzz/my-script).ts", 1, 2), ")", )), ), ( // nested "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()); } } } } #[test] fn test_relative_specifier_within() { // File in the same directory - should return relative path let from = Url::parse("file:///Users/dev/project/").unwrap(); let to = Url::parse("file:///Users/dev/project/foo.ts").unwrap(); let result = relative_specifier_within(&from, &to); assert_eq!(result, Some("./foo.ts".to_string())); // File in a subdirectory - should return relative path let from = Url::parse("file:///Users/dev/project/").unwrap(); let to = Url::parse("file:///Users/dev/project/src/bar.ts").unwrap(); let result = relative_specifier_within(&from, &to); assert_eq!(result, Some("./src/bar.ts".to_string())); // File in a deeper subdirectory - should return relative path let from = Url::parse("file:///Users/dev/project/").unwrap(); let to = Url::parse("file:///Users/dev/project/src/lib/utils.ts").unwrap(); let result = relative_specifier_within(&from, &to); assert_eq!(result, Some("./src/lib/utils.ts".to_string())); // File requires going up one level - should return None let from = Url::parse("file:///Users/dev/project/src/").unwrap(); let to = Url::parse("file:///Users/dev/project/main.ts").unwrap(); let result = relative_specifier_within(&from, &to); assert_eq!(result, None); // File requires going up multiple levels - should return None let from = Url::parse("file:///Users/dev/project/src/lib/").unwrap(); let to = Url::parse("file:///Users/dev/other/file.ts").unwrap(); let result = relative_specifier_within(&from, &to); assert_eq!(result, None); // Completely different path (e.g., bundled synthetic path) - should return None let from = Url::parse("file:///Users/dev/workspace/deno/").unwrap(); let to = Url::parse("file:///a.ts").unwrap(); let result = relative_specifier_within(&from, &to); assert_eq!(result, None); // Same directory (edge case) - should return relative path let from = Url::parse("file:///Users/dev/project/").unwrap(); let to = Url::parse("file:///Users/dev/project/").unwrap(); let result = relative_specifier_within(&from, &to); assert_eq!(result, Some("./".to_string())); // File in sibling directory - should return None let from = Url::parse("file:///Users/dev/project1/src/").unwrap(); let to = Url::parse("file:///Users/dev/project2/foo.ts").unwrap(); let result = relative_specifier_within(&from, &to); assert_eq!(result, None); // HTTP URLs (not file://) - should work if in same path let from = Url::parse("http://example.com/app/").unwrap(); let to = Url::parse("http://example.com/app/main.js").unwrap(); let result = relative_specifier_within(&from, &to); assert_eq!(result, Some("./main.js".to_string())); // HTTP URLs requiring going up - should return None let from = Url::parse("http://example.com/app/src/").unwrap(); let to = Url::parse("http://example.com/app/main.js").unwrap(); let result = relative_specifier_within(&from, &to); assert_eq!(result, None); } #[cfg(not(miri))] #[test] fn test_to_v8_error_handles_null_builder_exception() { let mut runtime = JsRuntime::new(Default::default()); deno_core::scope!(scope, runtime); let throw_null_fn: v8::Local<v8::Function> = JsRuntime::eval(scope, "(function () { throw null; })").unwrap(); // Override custom error builder so it throws null let exception_state = JsRealm::exception_state_from_scope(scope); exception_state .js_build_custom_error_cb .borrow_mut() .replace(v8::Global::new(scope, throw_null_fn)); let err = CoreErrorKind::TLA; let expected_message = err.get_message(); let value = to_v8_error(scope, &err); let value: v8::Local<v8::String> = value .try_into() .expect("should fall back to message string"); assert_eq!(value.to_rust_string_lossy(scope), expected_message); } #[cfg(not(miri))] #[test] fn test_to_v8_error_handles_stack_overflow_in_builder() { let mut runtime = JsRuntime::new(Default::default()); deno_core::scope!(scope, runtime); // Simulate the error builder throwing RangeError (as happens during // stack overflow when the builder callback itself exceeds the call // stack limit). let throw_range_error_fn: v8::Local<v8::Function> = JsRuntime::eval( scope, "(function () { throw new RangeError('Maximum call stack size exceeded'); })", ) .unwrap(); let exception_state = JsRealm::exception_state_from_scope(scope); exception_state .js_build_custom_error_cb .borrow_mut() .replace(v8::Global::new(scope, throw_range_error_fn)); let err = CoreErrorKind::TLA; let expected_message = err.get_message(); // Should not panic, should fall back to the message string let value = to_v8_error(scope, &err); let value: v8::Local<v8::String> = value .try_into() .expect("should fall back to message string"); assert_eq!(value.to_rust_string_lossy(scope), expected_message); } #[cfg(not(miri))] #[test] fn test_js_error_v8_roundtrip_preserves_shape() { use crate::convert::FromV8; use crate::convert::ToV8; let mut runtime = JsRuntime::new(Default::default()); deno_core::scope!(scope, runtime); let frame = JsStackFrame { type_name: Some("Foo".to_string()), function_name: Some("bar".to_string()), method_name: None, file_name: Some("file:///a.ts".to_string()), line_number: Some(10), column_number: Some(5), eval_origin: None, is_top_level: Some(true), is_eval: false, is_native: false, is_constructor: false, is_async: false, is_promise_all: false, is_wasm: false, promise_index: None, }; let cause = JsError { name: Some("Error".to_string()), message: Some("inner".to_string()), stack: None, cause: None, exception_message: "Error: inner".to_string(), frames: vec![], source_line: None, source_line_frame_index: None, aggregated: None, additional_properties: vec![], stack_is_custom: false, }; let err = JsError { name: Some("TypeError".to_string()), message: Some("boom".to_string()), stack: Some( "TypeError: boom\n at Foo.bar (file:///a.ts:10:5)".to_string(), ), cause: Some(Box::new(cause)), exception_message: "TypeError: boom".to_string(), frames: vec![frame], source_line: Some("throw new TypeError('boom');".to_string()), source_line_frame_index: Some(0), aggregated: None, additional_properties: vec![("code".to_string(), "X1".to_string())], stack_is_custom: false, }; let v = err.clone().to_v8(scope).unwrap(); // Lock in JS-visible field names: serde-emitted shape (camelCase, with // the `isToplevel` quirk preserved) must match what the derive emits. let obj = v8::Local::<v8::Object>::try_from(v).expect("object"); let expected_keys = [ "name", "message", "stack", "cause", "exceptionMessage", "frames", "sourceLine", "sourceLineFrameIndex", "aggregated", "additionalProperties", "stackIsCustom", ]; for key in expected_keys { let key_v = v8::String::new(scope, key).unwrap(); assert!( obj.has(scope, key_v.into()).unwrap(), "JsError v8 shape missing key {key}", ); } let frames_key = v8::String::new(scope, "frames").unwrap(); let frames_v = obj.get(scope, frames_key.into()).unwrap(); let frames = v8::Local::<v8::Array>::try_from(frames_v).unwrap(); let frame0 = frames.get_index(scope, 0).unwrap(); let frame0 = v8::Local::<v8::Object>::try_from(frame0).unwrap(); let frame_keys = [ "typeName", "functionName", "fileName", "lineNumber", "columnNumber", // serde rename for the v8 "typo"; must be `isToplevel`, not `isTopLevel`. "isToplevel", "isEval", "isNative", "isConstructor", "isAsync", "isPromiseAll", "isWasm", ]; for key in frame_keys { let key_v = v8::String::new(scope, key).unwrap(); assert!( frame0.has(scope, key_v.into()).unwrap(), "JsStackFrame v8 shape missing key {key}", ); } let back = JsError::from_v8(scope, v).unwrap(); assert_eq!(err, back); } }