use deno_core::v8;
use super::inspect::*;
fn object_proto_to_string<'s>(
scope: &mut v8::PinScope<'s, '_>,
value: v8::Local<'s, v8::Value>,
) -> String {
let tag: Option<String> = match v8::Local::<v8::Object>::try_from(value) {
Ok(obj) => {
let sym = v8::Symbol::get_to_string_tag(scope);
v8::tc_scope!(tc, scope);
match obj.get(tc, sym.into()) {
Some(v) if v.is_string() => Some(v.to_rust_string_lossy(tc)),
_ => None,
}
}
Err(_) => None,
};
let builtin = if let Some(tag) = tag {
tag
} else if value.is_array() {
"Array".to_string()
} else if value.is_function() {
"Function".to_string()
} else if value.is_native_error() {
"Error".to_string()
} else if value.is_date() {
"Date".to_string()
} else if value.is_reg_exp() {
"RegExp".to_string()
} else {
"Object".to_string()
};
format!("[object {builtin}]")
}
fn error_proto_to_string<'s, 'i>(
scope: &mut v8::PinScope<'s, 'i>,
error: v8::Local<'s, v8::Object>,
) -> R<String> {
let name_val = js_get_str(scope, error, "name")?;
let name = if name_val.is_undefined() {
"Error".to_string()
} else {
let s = {
v8::tc_scope!(tc, scope);
match name_val.to_string(tc) {
Some(s) => Ok(s.to_rust_string_lossy(tc)),
None => {
let exc = tc.exception();
Err(grab_err(tc, exc))
}
}
};
s?
};
let message_val = js_get_str(scope, error, "message")?;
let message = if message_val.is_undefined() {
String::new()
} else {
let s = {
v8::tc_scope!(tc, scope);
match message_val.to_string(tc) {
Some(s) => Ok(s.to_rust_string_lossy(tc)),
None => {
let exc = tc.exception();
Err(grab_err(tc, exc))
}
}
};
s?
};
if name.is_empty() {
return Ok(message);
}
if message.is_empty() {
return Ok(name);
}
Ok(format!("{name}: {message}"))
}
pub fn get_stack_string<'s, 'i>(
scope: &mut v8::PinScope<'s, 'i>,
intr: &Intrinsics<'_>,
ctx: &mut Ctx<'s>,
error: v8::Local<'s, v8::Object>,
) -> R<String> {
let stack = {
v8::tc_scope!(tc, scope);
let key = v8_str(tc, "stack");
obj_get_swallow(tc, error, key.into())
};
if let Some(stack) = stack {
if stack.boolean_value(scope) {
if stack.is_string() {
return Ok(stack.to_rust_string_lossy(scope));
}
ctx.seen.push(error.into());
ctx.indentation_lvl += 4;
let result = format_value(scope, intr, ctx, stack, f64::NAN, false)?;
ctx.indentation_lvl -= 4;
ctx.seen.pop();
let prefix = error_proto_to_string(scope, error)?;
return Ok(format!("{prefix}\n {result}"));
}
}
error_proto_to_string(scope, error)
}
fn obj_get_swallow<'s>(
tc: &mut v8::PinScope<'s, '_>,
obj: v8::Local<'s, v8::Object>,
key: v8::Local<'s, v8::Value>,
) -> Option<v8::Local<'s, v8::Value>> {
obj.get(tc, key)
}
fn improve_stack(
mut stack: String,
constructor: Option<&str>,
name: &NameInfo,
tag: &str,
) -> String {
let len: Option<usize> = name.utf16_len;
if !name.is_string {
let prefix = get_prefix(constructor, tag, "Error", "");
let prefix_trimmed = &prefix[..prefix.len().saturating_sub(1)];
let replacement = format!("{} [{}]", name.display, prefix_trimmed);
if let Some(pos) = stack.find(&name.display) {
stack = format!(
"{}{}{}",
&stack[..pos],
replacement,
&stack[pos + name.display.len()..]
);
}
}
let name_check = name.is_string
&& name.display.ends_with("Error")
&& stack.starts_with(&name.display)
&& (stack.len() == name.display.len()
|| stack[name.display.len()..].starts_with(':')
|| stack[name.display.len()..].starts_with('\n'));
if constructor.is_none() || name_check {
let mut fallback = "Error".to_string();
let mut len = len;
if constructor.is_none() {
let start = match_null_proto_error_start(&stack);
fallback = start.clone().unwrap_or_default();
len = Some(fallback.encode_utf16().count());
if fallback.is_empty() {
fallback = "Error".to_string();
}
}
let prefix_full = get_prefix(constructor, tag, &fallback, "");
let prefix = &prefix_full[..prefix_full.len().saturating_sub(1)];
if name.display != prefix || !name.is_string {
if prefix.contains(&name.display) {
if len == Some(0) {
stack = format!("{prefix}: {stack}");
} else {
let byte_len = match len {
Some(l) => utf16_to_byte_len(&stack, l),
None => 0,
};
stack = format!("{prefix}{}", &stack[byte_len..]);
}
} else {
let byte_len = match len {
Some(l) => utf16_to_byte_len(&stack, l),
None => 0,
};
stack = format!("{prefix} [{}]{}", name.display, &stack[byte_len..]);
}
}
}
stack
}
fn match_null_proto_error_start(stack: &str) -> Option<String> {
let chars: Vec<char> = stack.chars().collect();
if let Some(first) = chars.first() {
if first.is_ascii_uppercase() {
let mut end = 1;
while end < chars.len() {
let c = chars[end];
if c.is_ascii_lowercase()
|| c == '_'
|| c == ' '
|| c.is_ascii_uppercase()
|| c.is_ascii_digit()
|| matches!(c, '[' | ']' | '(' | ')' | '-')
{
end += 1;
} else {
break;
}
}
let mut e = end;
while e >= 2 {
let rest: String = chars[e..].iter().collect();
if rest.starts_with(':') || rest.starts_with("\n at") {
return Some(chars[..e].iter().collect());
}
e -= 1;
}
}
}
if stack.ends_with("Error")
&& stack
.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-')
{
return Some(stack.to_string());
}
None
}
fn utf16_to_byte_len(s: &str, utf16_len: usize) -> usize {
let mut count = 0usize;
for (byte_idx, c) in s.char_indices() {
if count >= utf16_len {
return byte_idx;
}
count += c.len_utf16();
}
s.len()
}
struct NameInfo {
display: String,
is_string: bool,
utf16_len: Option<usize>,
}
fn get_duplicate_error_frame_ranges(frames: &[String]) -> Vec<usize> {
let mut result: Vec<usize> = Vec::new();
let mut line_to_positions: Vec<(usize, Vec<usize>)> = Vec::new();
let mut index_of: std::collections::HashMap<&str, usize> =
std::collections::HashMap::new();
for (i, frame) in frames.iter().enumerate() {
match index_of.get(frame.as_str()) {
Some(&idx) => line_to_positions[idx].1.push(i),
None => {
index_of.insert(frame.as_str(), line_to_positions.len());
line_to_positions.push((i, vec![i]));
}
}
}
let minimum_duplicate_range = 3usize;
if frames.len() - line_to_positions.len() <= minimum_duplicate_range {
return result;
}
let mut i = 0usize;
while i + minimum_duplicate_range < frames.len() {
let positions =
&line_to_positions[*index_of.get(frames[i].as_str()).unwrap()].1;
if positions.len() == 1 || *positions.last().unwrap() == i {
i += 1;
continue;
}
let current = positions.iter().position(|&p| p == i).unwrap() + 1;
if current == positions.len() {
i += 1;
continue;
}
let mut range = positions[positions.len() - 1] - i;
if range < minimum_duplicate_range {
i += 1;
continue;
}
let mut extra_steps: Option<Vec<usize>> = None;
if current + 1 < positions.len() {
let mut gcd_range = 0usize;
let mut steps: Vec<usize> = Vec::new();
for &p in &positions[current..] {
let mut distance = p - i;
while distance != 0 {
let remainder = if distance == 0 {
0
} else {
gcd_range % distance
};
if gcd_range != 0 {
if !steps.contains(&gcd_range) {
steps.push(gcd_range);
}
}
gcd_range = distance;
distance = remainder;
}
if gcd_range == 1 {
break;
}
}
range = gcd_range;
steps.retain(|&s| s != range);
if !steps.is_empty() {
extra_steps = Some(steps);
}
}
let mut max_range = range;
let mut max_duplicates = 0usize;
let mut duplicate_ranges = 0usize;
let mut next_start = i + range;
loop {
let mut equal_frames = 0usize;
for j in 0..range {
if frames.get(i + j) != frames.get(next_start + j) {
break;
}
equal_frames += 1;
}
if equal_frames != range {
let has_extra =
extra_steps.as_ref().map(|s| !s.is_empty()).unwrap_or(false);
if !has_extra {
break;
}
if duplicate_ranges != 0
&& max_range * max_duplicates < range * duplicate_ranges
{
max_range = range;
max_duplicates = duplicate_ranges;
}
range = extra_steps.as_mut().unwrap().pop().unwrap();
next_start = i;
duplicate_ranges = 0;
next_start += range;
continue;
}
duplicate_ranges += 1;
next_start += range;
}
if max_duplicates != 0
&& max_range * max_duplicates >= range * duplicate_ranges
{
range = max_range;
duplicate_ranges = max_duplicates;
}
if duplicate_ranges * range >= 3 {
result.push(i + range);
result.push(range);
result.push(duplicate_ranges);
i += range * (duplicate_ranges + 1) - 1;
}
i += 1;
}
result
}
fn identical_sequence_range(a: &[String], b: &[String]) -> (usize, usize) {
if a.len() < 4 {
return (0, 0);
}
for i in 0..a.len() - 3 {
if let Some(pos) = b.iter().position(|x| *x == a[i]) {
let rest = b.len() - pos;
if rest > 3 {
let mut len = 1usize;
let max_len = (a.len() - i).min(rest);
while max_len > len && a[i + len] == b[pos + len] {
len += 1;
}
if len > 3 {
return (len, i);
}
}
}
}
(0, 0)
}
fn get_stack_frames<'s, 'i>(
scope: &mut v8::PinScope<'s, 'i>,
intr: &Intrinsics<'_>,
ctx: &mut Ctx<'s>,
err: v8::Local<'s, v8::Object>,
stack: &str,
) -> R<Vec<String>> {
let mut frames: Vec<String> =
stack.split('\n').map(|s| s.to_string()).collect();
let cause = {
v8::tc_scope!(tc, scope);
let key = v8_str(tc, "cause");
err.get(tc, key.into())
};
if let Some(cause) = cause {
let is_error =
cause.is_native_error() || is_error_instance(scope, intr, cause);
if !cause.is_null_or_undefined() && is_error {
if let Ok(cause_obj) = v8::Local::<v8::Object>::try_from(cause) {
let cause_stack = get_stack_string(scope, intr, ctx, cause_obj)?;
if let Some(cause_stack_start) = cause_stack.find("\n at") {
let cause_frames: Vec<String> = cause_stack[cause_stack_start + 1..]
.split('\n')
.map(|s| s.to_string())
.collect();
let (len, offset) = identical_sequence_range(&frames, &cause_frames);
if len > 0 {
let skipped = len - 2;
let msg =
format!(" ... {skipped} lines matching cause stack trace ...");
let styled = ctx.stylize(scope, &msg, "undefined")?;
let end = (offset + 1 + skipped).min(frames.len());
frames.splice(offset + 1..end, [styled]);
}
}
}
}
}
if frames.len() > 10 {
let ranges = get_duplicate_error_frame_ranges(&frames);
let mut i = ranges.len() as i64 - 3;
while i >= 0 {
let idx = i as usize;
let offset = ranges[idx];
let length = ranges[idx + 1];
let duplicate_ranges = ranges[idx + 2];
let msg = format!(
" ... collapsed {} duplicate lines matching above {}",
length * duplicate_ranges,
if duplicate_ranges > 1 {
format!("{length} lines {duplicate_ranges} times...")
} else {
"lines ...".to_string()
}
);
let styled = ctx.stylize(scope, &msg, "undefined")?;
let end = (offset + length * duplicate_ranges).min(frames.len());
frames.splice(offset..end, [styled]);
i -= 3;
}
}
Ok(frames)
}
fn is_error_instance<'s>(
scope: &mut v8::PinScope<'s, '_>,
intr: &Intrinsics<'_>,
value: v8::Local<'s, v8::Value>,
) -> bool {
let error_prototype = intr.error_prototype(scope);
is_prototype_of(scope, error_prototype.into(), value)
}
fn mark_node_modules<'s, 'i>(
scope: &mut v8::PinScope<'s, 'i>,
ctx: &mut Ctx<'s>,
line: &str,
) -> R<String> {
let mut temp_line = String::new();
let mut last_pos = 0usize;
let mut search_from = 0usize;
loop {
let Some(rel) =
line.get(search_from..).and_then(|s| s.find("node_modules"))
else {
break;
};
let node_module_position = search_from + rel;
let separator = if node_module_position == 0 {
None
} else {
line[..node_module_position].chars().last()
};
let after = line[node_module_position + 12..].chars().next();
let sep_ok = matches!(separator, Some('/') | Some('\\'));
let after_ok = matches!(after, Some('/') | Some('\\'));
if !sep_ok || !after_ok {
search_from = node_module_position + 1;
continue;
}
let separator = separator.unwrap();
let module_start = node_module_position + 13; temp_line.push_str(&line[last_pos..module_start]);
let mut module_end = line[module_start..]
.find(separator)
.map(|p| module_start + p);
if line[module_start..].starts_with('@') {
if let Some(me) = module_end {
module_end = line[me + 1..].find(separator).map(|p| me + 1 + p);
}
}
let Some(module_end) = module_end else {
temp_line.push_str(&line[module_start..]);
last_pos = line.len();
break;
};
let node_module = &line[module_start..module_end];
temp_line.push_str(&ctx.stylize(scope, node_module, "module")?);
last_pos = module_end;
search_from = module_end;
}
if last_pos != 0 {
Ok(format!("{temp_line}{}", &line[last_pos..]))
} else {
Ok(line.to_string())
}
}
fn mark_cwd<'s, 'i>(
scope: &mut v8::PinScope<'s, 'i>,
ctx: &mut Ctx<'s>,
line: &str,
working_directory: &str,
) -> R<String> {
let Some(mut cwd_start_pos) = line.find(working_directory) else {
return Ok(line.to_string());
};
let mut temp_line = String::new();
let mut cwd_length = working_directory.len();
if cwd_start_pos >= 7
&& line.get(cwd_start_pos - 7..cwd_start_pos) == Some("file://")
{
cwd_length += 7;
cwd_start_pos -= 7;
}
let start = if cwd_start_pos > 0 && line[..cwd_start_pos].ends_with('(') {
cwd_start_pos - 1
} else {
cwd_start_pos
};
let strip_close = start != cwd_start_pos && line.ends_with(')');
let end = if strip_close {
line.len() - 1
} else {
line.len()
};
let after_cwd = cwd_start_pos + cwd_length;
let working_directory_end_pos =
after_cwd + line[after_cwd..].chars().next().map_or(0, |c| c.len_utf8());
let cwd_slice = &line[start..working_directory_end_pos];
temp_line.push_str(&line[..start]);
temp_line.push_str(&ctx.stylize(scope, cwd_slice, "undefined")?);
if working_directory_end_pos < end {
temp_line.push_str(&line[working_directory_end_pos..end]);
}
if strip_close {
temp_line.push_str(&ctx.stylize(scope, ")", "undefined")?);
}
Ok(temp_line)
}
fn frame_after_at(line: &str) -> Option<&str> {
line.strip_prefix(" at ")
}
fn match_module_frame<'a>(line: &'a str, prefix: &str) -> Option<&'a str> {
let rest = frame_after_at(line)?;
let body = if let Some(paren) = rest.find(" (") {
let func = &rest[..paren];
if !func.is_empty()
&& !func.contains('/')
&& !func.contains('\\')
&& !func.contains('(')
{
&rest[paren + 2..]
} else {
rest
}
} else {
rest
};
let spec = body.strip_prefix(prefix)?;
let spec = spec.strip_suffix(')').unwrap_or(spec);
let (rest1, col) = spec.rsplit_once(':')?;
if col.is_empty() || !col.bytes().all(|b| b.is_ascii_digit()) {
return None;
}
let (module_path, lineno) = rest1.rsplit_once(':')?;
if lineno.is_empty() || !lineno.bytes().all(|b| b.is_ascii_digit()) {
return None;
}
if module_path.is_empty() {
return None;
}
Some(module_path)
}
fn is_core_module_frame(line: &str) -> bool {
match_module_frame(line, "node:").is_some()
}
fn is_ext_module_frame(line: &str) -> bool {
match_module_frame(line, "ext:").is_some()
}
fn is_filtered_ext_frame(line: &str) -> bool {
let Some(rest) = frame_after_at(line) else {
return false;
};
for name in [
"eventLoopTick",
"denoErrorToNodeError",
"__drainNextTickAndMacrotasks",
] {
if rest.starts_with(name) && rest[name.len()..].starts_with(' ') {
return true;
}
}
if let Some(after) = rest.strip_prefix("__node_internal_") {
if let Some(space) = after.find(' ') {
if space > 0 {
return true;
}
}
}
false
}
pub fn format_error<'s, 'i>(
scope: &mut v8::PinScope<'s, 'i>,
intr: &Intrinsics<'_>,
ctx: &mut Ctx<'s>,
err: v8::Local<'s, v8::Object>,
constructor: Option<&str>,
tag: &str,
keys: &mut Vec<v8::Local<'s, v8::Value>>,
) -> R<String> {
let mut stack = match get_stack_string(scope, intr, ctx, err) {
Ok(s) => s,
Err(_) => return Ok(object_proto_to_string(scope, err.into())),
};
let mut message: Option<v8::Local<'s, v8::Value>> = None;
let mut message_is_getter_that_throws = false;
{
v8::tc_scope!(tc, scope);
let key = v8_str(tc, "message");
match err.get(tc, key.into()) {
Some(v) => message = Some(v),
None => message_is_getter_that_throws = true,
}
}
let mut name: Option<v8::Local<'s, v8::Value>> = None;
let mut name_is_getter_that_throws = false;
{
v8::tc_scope!(tc, scope);
let key = v8_str(tc, "name");
match err.get(tc, key.into()) {
Some(v) => name = Some(v),
None => name_is_getter_that_throws = true,
}
}
let key_string = |scope: &mut v8::PinScope<'s, '_>,
key: &v8::Local<'s, v8::Value>|
-> Option<String> {
if key.is_string() {
Some(key.to_rust_string_lossy(scope))
} else {
None
}
};
if !ctx.show_hidden && !keys.is_empty() {
if let Some(pos) = keys
.iter()
.position(|k| key_string(scope, k).as_deref() == Some("stack"))
{
keys.remove(pos);
}
if !message_is_getter_that_throws {
let msg_val = message.unwrap_or_else(|| v8::undefined(scope).into());
let msg_str = if msg_val.is_string() {
Some(msg_val.to_rust_string_lossy(scope))
} else {
None
};
if let Some(pos) = keys
.iter()
.position(|k| key_string(scope, k).as_deref() == Some("message"))
{
let hide = match &msg_str {
None => true,
Some(m) => stack.contains(m.as_str()),
};
if hide {
keys.remove(pos);
}
}
}
if !name_is_getter_that_throws {
let name_val = name.unwrap_or_else(|| v8::undefined(scope).into());
let name_str = if name_val.is_string() {
Some(name_val.to_rust_string_lossy(scope))
} else {
None
};
if let Some(pos) = keys
.iter()
.position(|k| key_string(scope, k).as_deref() == Some("name"))
{
let hide = match &name_str {
None => true,
Some(n) => stack.contains(n.as_str()),
};
if hide {
keys.remove(pos);
}
}
}
}
let name_info = {
let name_val = if name_is_getter_that_throws {
None
} else {
name
};
match name_val {
Some(v) if !v.is_null_or_undefined() => {
let display = v.to_rust_string_lossy(scope);
let is_string = v.is_string();
let utf16_len = if is_string {
Some(display.encode_utf16().count())
} else {
None
};
NameInfo {
display,
is_string,
utf16_len,
}
}
_ => NameInfo {
display: "Error".to_string(),
is_string: true,
utf16_len: Some(5),
},
}
};
let has_cause = {
v8::tc_scope!(tc, scope);
let key = v8_str(tc, "cause");
err.has(tc, key.into()).unwrap_or(false)
};
if has_cause {
let already = keys
.iter()
.any(|k| key_string(scope, k).as_deref() == Some("cause"));
if keys.is_empty() || !already {
let cause_key = v8_str(scope, "cause");
keys.push(cause_key.into());
}
}
{
let errors = {
v8::tc_scope!(tc, scope);
let key = v8_str(tc, "errors");
err.get(tc, key.into())
};
if let Some(errors) = errors {
if errors.is_array() {
let already = keys
.iter()
.any(|k| key_string(scope, k).as_deref() == Some("errors"));
if keys.is_empty() || !already {
let errors_key = v8_str(scope, "errors");
keys.push(errors_key.into());
}
}
}
}
stack = improve_stack(stack, constructor, &name_info, tag);
let mut pos = 0usize;
if let Some(message) = message {
if !message_is_getter_that_throws && message.is_string() {
let msg = message.to_rust_string_lossy(scope);
if !msg.is_empty() {
if let Some(p) = stack.find(&msg) {
pos = p + msg.len();
}
}
}
}
let stack_start = stack
.get(pos..)
.and_then(|s| s.find("\n at"))
.map(|p| pos + p);
match stack_start {
None => {
stack = format!("[{stack}]");
}
Some(stack_start) => {
let mut new_stack = stack[..stack_start].to_string();
let stack_frame_part = &stack[stack_start + 1..];
let lines = get_stack_frames(scope, intr, ctx, err, stack_frame_part)?;
if ctx.colors {
let working_directory = {
let undef: v8::Local<v8::Value> = v8::undefined(scope).into();
let ret = {
let f = intr.get_cwd(scope);
js_call(scope, f, undef, &[])
}?;
if ret.is_string() {
Some(ret.to_rust_string_lossy(scope))
} else {
None
}
};
let mut esm_working_directory: Option<String> = None;
for line in &lines {
if is_filtered_ext_frame(line) && is_ext_module_frame(line) {
continue;
}
if is_core_module_frame(line) || is_ext_module_frame(line) {
let styled = ctx.stylize(scope, line, "undefined")?;
new_stack.push_str(&format!("\n{styled}"));
} else {
new_stack.push('\n');
let mut line = mark_node_modules(scope, ctx, line)?;
if let Some(wd) = &working_directory {
let new_line = mark_cwd(scope, ctx, &line, wd)?;
if new_line == line {
if esm_working_directory.is_none() {
esm_working_directory = Some(path_to_file_url_href(wd));
}
line = mark_cwd(
scope,
ctx,
&line,
esm_working_directory.as_ref().unwrap(),
)?;
} else {
line = new_line;
}
}
new_stack.push_str(&line);
}
}
} else {
for line in &lines {
if is_filtered_ext_frame(line) && is_ext_module_frame(line) {
continue;
}
new_stack.push_str(&format!("\n{line}"));
}
}
stack = new_stack;
}
}
if ctx.indentation_lvl != 0 {
let indentation = " ".repeat(ctx.indentation_lvl);
stack = stack.replace('\n', &format!("\n{indentation}"));
}
Ok(stack)
}
fn path_to_file_url_href(path: &str) -> String {
match deno_core::url::Url::from_file_path(path) {
Ok(url) => url.to_string(),
Err(_) => String::new(),
}
}