1
0
Fork 0
mirror of https://github.com/denoland/deno.git synced 2024-12-24 16:19:12 -05:00

feat(cli): show error cause recursion information (#16384)

This commit is contained in:
Cre3per 2022-10-26 15:37:45 +02:00 committed by GitHub
parent 6cd9343e8b
commit 193b8828c5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 161 additions and 21 deletions

View file

@ -3460,6 +3460,12 @@ itest!(error_cause {
exit_code: 1,
});
itest!(error_cause_recursive_aggregate {
args: "run error_cause_recursive_aggregate.ts",
output: "error_cause_recursive_aggregate.ts.out",
exit_code: 1,
});
itest!(error_cause_recursive_tail {
args: "run error_cause_recursive_tail.ts",
output: "error_cause_recursive_tail.ts.out",

View file

@ -0,0 +1,9 @@
const foo = new Error("foo");
const bar = new Error("bar", { cause: foo });
foo.cause = bar;
const qux = new Error("qux");
const quux = new Error("quux", { cause: qux });
qux.cause = quux;
throw new AggregateError([bar, quux]);

View file

@ -0,0 +1,15 @@
[WILDCARD]
error: Uncaught AggregateError
Error: bar <ref *1>
at file:///[WILDCARD]/error_cause_recursive_aggregate.ts:2:13
Caused by: Error: foo
at file:///[WILDCARD]/error_cause_recursive_aggregate.ts:1:13
Caused by: [Circular *1]
Error: quux <ref *2>
at file:///[WILDCARD]/error_cause_recursive_aggregate.ts:6:14
Caused by: Error: qux
at file:///[WILDCARD]/error_cause_recursive_aggregate.ts:5:13
Caused by: [Circular *2]
throw new AggregateError([bar, quux]);
^
at file:///[WILDCARD]/error_cause_recursive_aggregate.ts:9:7

View file

@ -3,10 +3,9 @@ error: Uncaught Error: baz
const baz = new Error("baz", { cause: bar });
^
at file:///[WILDCARD]/error_cause_recursive_tail.ts:3:13
Caused by: Error: bar
Caused by: Error: bar <ref *1>
at file:///[WILDCARD]/error_cause_recursive_tail.ts:2:13
Caused by: Error: foo
at file:///[WILDCARD]/error_cause_recursive_tail.ts:1:13
Caused by: Error: bar
at file:///[WILDCARD]/error_cause_recursive_tail.ts:2:13
Caused by: [Circular *1]
[WILDCARD]

View file

@ -1,10 +1,8 @@
[WILDCARD]
error: Uncaught Error: bar
error: Uncaught Error: bar <ref *1>
const y = new Error("bar", { cause: x });
^
at file:///[WILDCARD]/error_cause_recursive.ts:2:11
Caused by: Error: foo
at file:///[WILDCARD]/error_cause_recursive.ts:1:11
Caused by: Error: bar
at file:///[WILDCARD]/error_cause_recursive.ts:2:11
[WILDCARD]
Caused by: [Circular *1]

View file

@ -119,6 +119,8 @@ pub fn to_v8_error<'a>(
/// 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)]
#[serde(rename_all = "camelCase")]
pub struct JsError {

View file

@ -9,6 +9,37 @@ use deno_core::error::JsError;
use deno_core::error::JsStackFrame;
use std::fmt::Write as _;
/// 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.
/// 02_console.js, which also detects recursive causes, can use JS object
/// comparisons to compare errors. We don't have access to JS object identity in
/// format_js_error().
fn errors_are_equal_without_cause(a: &JsError, b: &JsError) -> bool {
a.name == b.name
&& a.message == b.message
&& a.stack == b.stack
// `a.cause == b.cause` omitted, because it is absent in recursive errors,
// despite the error being identical to a previously seen one.
&& a.exception_message == b.exception_message
&& a.frames == b.frames
&& a.source_line == b.source_line
&& a.source_line_frame_index == b.source_line_frame_index
&& a.aggregated == b.aggregated
}
#[derive(Debug, Clone)]
struct ErrorReference<'a> {
from: &'a JsError,
to: &'a JsError,
}
#[derive(Debug, Clone)]
struct IndexedErrorReference<'a> {
reference: ErrorReference<'a>,
index: usize,
}
// Keep in sync with `/core/error.js`.
pub fn format_location(frame: &JsStackFrame) -> String {
let _internal = frame
@ -150,25 +181,90 @@ fn format_maybe_source_line(
format!("\n{}{}\n{}{}", indent, source_line, indent, color_underline)
}
fn format_js_error_inner(js_error: &JsError, is_child: bool) -> String {
let mut s = String::new();
s.push_str(&js_error.exception_message);
if let Some(aggregated) = &js_error.aggregated {
for aggregated_error in aggregated {
let error_string = format_js_error_inner(aggregated_error, true);
for line in error_string.trim_start_matches("Uncaught ").lines() {
write!(s, "\n {}", line).unwrap();
}
fn find_recursive_cause(js_error: &JsError) -> Option<ErrorReference> {
let mut history = Vec::<&JsError>::new();
let mut current_error: &JsError = js_error;
while let Some(cause) = &current_error.cause {
history.push(current_error);
if let Some(seen) = history
.iter()
.find(|&el| errors_are_equal_without_cause(el, cause.as_ref()))
{
return Some(ErrorReference {
from: current_error,
to: *seen,
});
} else {
current_error = cause;
}
}
None
}
fn format_aggregated_error(
aggregated_errors: &Vec<JsError>,
circular_reference_index: usize,
) -> String {
let mut s = String::new();
let mut nested_circular_reference_index = circular_reference_index;
for js_error in aggregated_errors {
let aggregated_circular = find_recursive_cause(js_error);
if aggregated_circular.is_some() {
nested_circular_reference_index += 1;
}
let error_string = format_js_error_inner(
js_error,
aggregated_circular.map(|reference| IndexedErrorReference {
reference,
index: nested_circular_reference_index,
}),
false,
);
for line in error_string.trim_start_matches("Uncaught ").lines() {
write!(s, "\n {}", line).unwrap();
}
}
s
}
fn format_js_error_inner(
js_error: &JsError,
circular: Option<IndexedErrorReference>,
include_source_code: bool,
) -> String {
let mut s = String::new();
s.push_str(&js_error.exception_message);
if let Some(circular) = &circular {
if errors_are_equal_without_cause(js_error, circular.reference.to) {
write!(s, " {}", cyan(format!("<ref *{}>", circular.index))).unwrap();
}
}
if let Some(aggregated) = &js_error.aggregated {
let aggregated_message = format_aggregated_error(
aggregated,
circular.as_ref().map_or(0, |circular| circular.index),
);
s.push_str(&aggregated_message);
}
let column_number = js_error
.source_line_frame_index
.and_then(|i| js_error.frames.get(i).unwrap().column_number);
s.push_str(&format_maybe_source_line(
if is_child {
None
} else {
if include_source_code {
js_error.source_line.as_deref()
} else {
None
},
column_number,
true,
@ -178,7 +274,16 @@ fn format_js_error_inner(js_error: &JsError, is_child: bool) -> String {
write!(s, "\n at {}", format_frame(frame)).unwrap();
}
if let Some(cause) = &js_error.cause {
let error_string = format_js_error_inner(cause, true);
let is_caused_by_circular = circular.as_ref().map_or(false, |circular| {
errors_are_equal_without_cause(circular.reference.from, js_error)
});
let error_string = if is_caused_by_circular {
cyan(format!("[Circular *{}]", circular.unwrap().index)).to_string()
} else {
format_js_error_inner(cause, circular, false)
};
write!(
s,
"\nCaused by: {}",
@ -191,7 +296,13 @@ fn format_js_error_inner(js_error: &JsError, is_child: bool) -> String {
/// Format a [`JsError`] for terminal output.
pub fn format_js_error(js_error: &JsError) -> String {
format_js_error_inner(js_error, false)
let circular =
find_recursive_cause(js_error).map(|reference| IndexedErrorReference {
reference,
index: 1,
});
format_js_error_inner(js_error, circular, true)
}
#[cfg(test)]