mirror of
https://github.com/denoland/deno.git
synced 2024-12-25 16:49:18 -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,
|
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 {
|
itest!(error_cause_recursive_tail {
|
||||||
args: "run error_cause_recursive_tail.ts",
|
args: "run error_cause_recursive_tail.ts",
|
||||||
output: "error_cause_recursive_tail.ts.out",
|
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 });
|
const baz = new Error("baz", { cause: bar });
|
||||||
^
|
^
|
||||||
at file:///[WILDCARD]/error_cause_recursive_tail.ts:3:13
|
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
|
at file:///[WILDCARD]/error_cause_recursive_tail.ts:2:13
|
||||||
Caused by: Error: foo
|
Caused by: Error: foo
|
||||||
at file:///[WILDCARD]/error_cause_recursive_tail.ts:1:13
|
at file:///[WILDCARD]/error_cause_recursive_tail.ts:1:13
|
||||||
Caused by: Error: bar
|
Caused by: [Circular *1]
|
||||||
at file:///[WILDCARD]/error_cause_recursive_tail.ts:2:13
|
|
||||||
[WILDCARD]
|
[WILDCARD]
|
||||||
|
|
|
@ -1,10 +1,8 @@
|
||||||
[WILDCARD]
|
[WILDCARD]
|
||||||
error: Uncaught Error: bar
|
error: Uncaught Error: bar <ref *1>
|
||||||
const y = new Error("bar", { cause: x });
|
const y = new Error("bar", { cause: x });
|
||||||
^
|
^
|
||||||
at file:///[WILDCARD]/error_cause_recursive.ts:2:11
|
at file:///[WILDCARD]/error_cause_recursive.ts:2:11
|
||||||
Caused by: Error: foo
|
Caused by: Error: foo
|
||||||
at file:///[WILDCARD]/error_cause_recursive.ts:1:11
|
at file:///[WILDCARD]/error_cause_recursive.ts:1:11
|
||||||
Caused by: Error: bar
|
Caused by: [Circular *1]
|
||||||
at file:///[WILDCARD]/error_cause_recursive.ts:2:11
|
|
||||||
[WILDCARD]
|
|
||||||
|
|
|
@ -119,6 +119,8 @@ pub fn to_v8_error<'a>(
|
||||||
/// A `JsError` represents an exception coming from V8, with stack frames and
|
/// A `JsError` represents an exception coming from V8, with stack frames and
|
||||||
/// line numbers. The deno_cli crate defines another `JsError` type, which wraps
|
/// line numbers. The deno_cli crate defines another `JsError` type, which wraps
|
||||||
/// the one defined here, that adds source map support and colorful formatting.
|
/// 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)]
|
#[derive(Debug, PartialEq, Clone, serde::Deserialize, serde::Serialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct JsError {
|
pub struct JsError {
|
||||||
|
|
|
@ -9,6 +9,37 @@ use deno_core::error::JsError;
|
||||||
use deno_core::error::JsStackFrame;
|
use deno_core::error::JsStackFrame;
|
||||||
use std::fmt::Write as _;
|
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`.
|
// Keep in sync with `/core/error.js`.
|
||||||
pub fn format_location(frame: &JsStackFrame) -> String {
|
pub fn format_location(frame: &JsStackFrame) -> String {
|
||||||
let _internal = frame
|
let _internal = frame
|
||||||
|
@ -150,25 +181,90 @@ fn format_maybe_source_line(
|
||||||
format!("\n{}{}\n{}{}", indent, source_line, indent, color_underline)
|
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();
|
let mut s = String::new();
|
||||||
s.push_str(&js_error.exception_message);
|
let mut nested_circular_reference_index = circular_reference_index;
|
||||||
if let Some(aggregated) = &js_error.aggregated {
|
|
||||||
for aggregated_error in aggregated {
|
for js_error in aggregated_errors {
|
||||||
let error_string = format_js_error_inner(aggregated_error, true);
|
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() {
|
for line in error_string.trim_start_matches("Uncaught ").lines() {
|
||||||
write!(s, "\n {}", line).unwrap();
|
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
|
let column_number = js_error
|
||||||
.source_line_frame_index
|
.source_line_frame_index
|
||||||
.and_then(|i| js_error.frames.get(i).unwrap().column_number);
|
.and_then(|i| js_error.frames.get(i).unwrap().column_number);
|
||||||
s.push_str(&format_maybe_source_line(
|
s.push_str(&format_maybe_source_line(
|
||||||
if is_child {
|
if include_source_code {
|
||||||
None
|
|
||||||
} else {
|
|
||||||
js_error.source_line.as_deref()
|
js_error.source_line.as_deref()
|
||||||
|
} else {
|
||||||
|
None
|
||||||
},
|
},
|
||||||
column_number,
|
column_number,
|
||||||
true,
|
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();
|
write!(s, "\n at {}", format_frame(frame)).unwrap();
|
||||||
}
|
}
|
||||||
if let Some(cause) = &js_error.cause {
|
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!(
|
write!(
|
||||||
s,
|
s,
|
||||||
"\nCaused by: {}",
|
"\nCaused by: {}",
|
||||||
|
@ -191,7 +296,13 @@ fn format_js_error_inner(js_error: &JsError, is_child: bool) -> String {
|
||||||
|
|
||||||
/// Format a [`JsError`] for terminal output.
|
/// Format a [`JsError`] for terminal output.
|
||||||
pub fn format_js_error(js_error: &JsError) -> String {
|
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)]
|
#[cfg(test)]
|
||||||
|
|
Loading…
Reference in a new issue