summaryrefslogtreecommitdiff
path: root/runtime/fmt_errors.rs
diff options
context:
space:
mode:
authorCre3per <12541974+Cre3per@users.noreply.github.com>2022-10-26 15:37:45 +0200
committerGitHub <noreply@github.com>2022-10-26 15:37:45 +0200
commit193b8828c5b437b61d6310391fff6a9c6f393b0a (patch)
tree57e6dd95eab2026ba1278fac8f39f5b1469df7e4 /runtime/fmt_errors.rs
parent6cd9343e8b6f6ede22ec56b06e6918a80c9a3ddd (diff)
feat(cli): show error cause recursion information (#16384)
Diffstat (limited to 'runtime/fmt_errors.rs')
-rw-r--r--runtime/fmt_errors.rs135
1 files changed, 123 insertions, 12 deletions
diff --git a/runtime/fmt_errors.rs b/runtime/fmt_errors.rs
index aa1f60450..e285d07b4 100644
--- a/runtime/fmt_errors.rs
+++ b/runtime/fmt_errors.rs
@@ -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) = &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(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();
- }
+
+ 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)]