mirror of
https://github.com/denoland/deno.git
synced 2024-12-25 00:29:09 -05:00
feat(cli): show error cause recursion information (#16384)
This commit is contained in:
parent
6cd9343e8b
commit
193b8828c5
7 changed files with 161 additions and 21 deletions
|
@ -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",
|
||||
|
|
9
cli/tests/testdata/error_cause_recursive_aggregate.ts
vendored
Normal file
9
cli/tests/testdata/error_cause_recursive_aggregate.ts
vendored
Normal 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]);
|
15
cli/tests/testdata/error_cause_recursive_aggregate.ts.out
vendored
Normal file
15
cli/tests/testdata/error_cause_recursive_aggregate.ts.out
vendored
Normal 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
|
|
@ -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]
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
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) = ¤t_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();
|
||||
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);
|
||||
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)]
|
||||
|
|
Loading…
Reference in a new issue