deno/runtime/fmt_errors.rs

297 lines
7.3 KiB
Rust
Raw Normal View History

// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
2019-06-20 02:07:01 +00:00
//! This mod provides DenoError to unify errors across Deno.
use deno_core::error::format_frame;
use deno_core::error::JsError;
use deno_terminal::colors::cyan;
use deno_terminal::colors::italic_bold;
use deno_terminal::colors::red;
use deno_terminal::colors::yellow;
2022-07-01 13:28:06 +00:00
use std::fmt::Write as _;
2019-06-20 02:07:01 +00:00
#[derive(Debug, Clone)]
struct ErrorReference<'a> {
from: &'a JsError,
to: &'a JsError,
}
#[derive(Debug, Clone)]
struct IndexedErrorReference<'a> {
reference: ErrorReference<'a>,
index: usize,
}
#[derive(Debug)]
enum FixSuggestionKind {
Info,
Hint,
}
#[derive(Debug)]
pub struct FixSuggestion<'a> {
kind: FixSuggestionKind,
message: &'a str,
}
impl<'a> FixSuggestion<'a> {
pub fn info(message: &'a str) -> Self {
Self {
kind: FixSuggestionKind::Info,
message,
}
}
pub fn hint(message: &'a str) -> Self {
Self {
kind: FixSuggestionKind::Hint,
message,
}
}
}
struct AnsiColors;
impl deno_core::error::ErrorFormat for AnsiColors {
fn fmt_element(
element: deno_core::error::ErrorElement,
s: &str,
) -> std::borrow::Cow<'_, str> {
use deno_core::error::ErrorElement::*;
match element {
Anonymous | NativeFrame | FileName | EvalOrigin => {
cyan(s).to_string().into()
}
LineNumber | ColumnNumber => yellow(s).to_string().into(),
FunctionName | PromiseAll => italic_bold(s).to_string().into(),
}
}
2020-06-08 12:06:20 +00:00
}
2019-06-20 02:07:01 +00:00
/// Take an optional source line and associated information to format it into
/// a pretty printed version of that line.
fn format_maybe_source_line(
source_line: Option<&str>,
column_number: Option<i64>,
2019-06-20 02:07:01 +00:00
is_error: bool,
level: usize,
) -> String {
if source_line.is_none() || column_number.is_none() {
2019-06-20 02:07:01 +00:00
return "".to_string();
}
let source_line = source_line.unwrap();
2019-06-20 02:07:01 +00:00
// sometimes source_line gets set with an empty string, which then outputs
// an empty source line when displayed, so need just short circuit here.
if source_line.is_empty() {
2019-06-20 02:07:01 +00:00
return "".to_string();
}
if source_line.contains("Couldn't format source line: ") {
return format!("\n{source_line}");
}
2019-06-20 02:07:01 +00:00
let mut s = String::new();
let column_number = column_number.unwrap();
if column_number as usize > source_line.len() {
return format!(
"\n{} Couldn't format source line: Column {} is out of bounds (source may have changed at runtime)",
yellow("Warning"), column_number,
);
}
for _i in 0..(column_number - 1) {
if source_line.chars().nth(_i as usize).unwrap() == '\t' {
s.push('\t');
} else {
s.push(' ');
}
}
s.push('^');
2019-06-20 02:07:01 +00:00
let color_underline = if is_error {
red(&s).to_string()
2019-06-20 02:07:01 +00:00
} else {
cyan(&s).to_string()
2019-06-20 02:07:01 +00:00
};
let indent = format!("{:indent$}", "", indent = level);
format!("\n{indent}{source_line}\n{indent}{color_underline}")
2019-06-20 02:07:01 +00:00
}
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| cause.is_same_error(el)) {
return Some(ErrorReference {
from: current_error,
2022-11-18 01:59:10 +00:00
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,
vec![],
);
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,
suggestions: Vec<FixSuggestion>,
) -> String {
let mut s = String::new();
s.push_str(&js_error.exception_message);
if let Some(circular) = &circular {
if js_error.is_same_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(|circular| circular.index)
.unwrap_or(0),
);
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 include_source_code {
js_error.source_line.as_deref()
} else {
None
},
column_number,
true,
0,
));
for frame in &js_error.frames {
write!(s, "\n at {}", format_frame::<AnsiColors>(frame)).unwrap();
}
if let Some(cause) = &js_error.cause {
let is_caused_by_circular = circular
.as_ref()
.map(|circular| js_error.is_same_error(circular.reference.from))
.unwrap_or(false);
let error_string = if is_caused_by_circular {
cyan(format!("[Circular *{}]", circular.unwrap().index)).to_string()
} else {
format_js_error_inner(cause, circular, false, vec![])
};
2022-07-01 13:28:06 +00:00
write!(
s,
"\nCaused by: {}",
error_string.trim_start_matches("Uncaught ")
2022-07-01 13:28:06 +00:00
)
.unwrap();
}
if !suggestions.is_empty() {
write!(s, "\n\n").unwrap();
for (index, suggestion) in suggestions.iter().enumerate() {
write!(s, " ").unwrap();
match suggestion.kind {
FixSuggestionKind::Hint => write!(s, "{} ", cyan("hint:")).unwrap(),
FixSuggestionKind::Info => write!(s, "{} ", yellow("info:")).unwrap(),
};
write!(s, "{}", suggestion.message).unwrap();
if index != (suggestions.len() - 1) {
writeln!(s).unwrap();
}
}
}
s
}
/// Format a [`JsError`] for terminal output.
pub fn format_js_error(js_error: &JsError) -> String {
let circular =
find_recursive_cause(js_error).map(|reference| IndexedErrorReference {
reference,
index: 1,
});
format_js_error_inner(js_error, circular, true, vec![])
}
/// Format a [`JsError`] for terminal output, printing additional suggestions.
pub fn format_js_error_with_suggestions(
js_error: &JsError,
suggestions: Vec<FixSuggestion>,
) -> String {
let circular =
find_recursive_cause(js_error).map(|reference| IndexedErrorReference {
reference,
index: 1,
});
format_js_error_inner(js_error, circular, true, suggestions)
}
2019-06-20 02:07:01 +00:00
#[cfg(test)]
mod tests {
use super::*;
use test_util::strip_ansi_codes;
2019-06-20 02:07:01 +00:00
#[test]
fn test_format_none_source_line() {
let actual = format_maybe_source_line(None, None, false, 0);
2019-06-20 02:07:01 +00:00
assert_eq!(actual, "");
}
#[test]
fn test_format_some_source_line() {
let actual =
format_maybe_source_line(Some("console.log('foo');"), Some(9), true, 0);
2019-06-20 02:07:01 +00:00
assert_eq!(
strip_ansi_codes(&actual),
"\nconsole.log(\'foo\');\n ^"
2019-06-20 02:07:01 +00:00
);
}
}