diff options
author | Cre3per <12541974+Cre3per@users.noreply.github.com> | 2022-10-26 15:37:45 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2022-10-26 15:37:45 +0200 |
commit | 193b8828c5b437b61d6310391fff6a9c6f393b0a (patch) | |
tree | 57e6dd95eab2026ba1278fac8f39f5b1469df7e4 /runtime/fmt_errors.rs | |
parent | 6cd9343e8b6f6ede22ec56b06e6918a80c9a3ddd (diff) |
feat(cli): show error cause recursion information (#16384)
Diffstat (limited to 'runtime/fmt_errors.rs')
-rw-r--r-- | runtime/fmt_errors.rs | 135 |
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) = ¤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 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)] |