mirror of
https://github.com/denoland/deno.git
synced 2024-11-24 15:19:26 -05:00
f930000415
This commit adds a suggestion with information and hint how to resolve situation when user tries to run an npm package with Node-API addons using global cache (which is currently not supported). Closes https://github.com/denoland/deno/issues/25974
330 lines
8.2 KiB
Rust
330 lines
8.2 KiB
Rust
// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
|
|
//! 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;
|
|
use std::fmt::Write as _;
|
|
|
|
#[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)]
|
|
enum FixSuggestionMessage<'a> {
|
|
Single(&'a str),
|
|
Multiline(&'a [&'a str]),
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub struct FixSuggestion<'a> {
|
|
kind: FixSuggestionKind,
|
|
message: FixSuggestionMessage<'a>,
|
|
}
|
|
|
|
impl<'a> FixSuggestion<'a> {
|
|
pub fn info(message: &'a str) -> Self {
|
|
Self {
|
|
kind: FixSuggestionKind::Info,
|
|
message: FixSuggestionMessage::Single(message),
|
|
}
|
|
}
|
|
|
|
pub fn info_multiline(messages: &'a [&'a str]) -> Self {
|
|
Self {
|
|
kind: FixSuggestionKind::Info,
|
|
message: FixSuggestionMessage::Multiline(messages),
|
|
}
|
|
}
|
|
|
|
pub fn hint(message: &'a str) -> Self {
|
|
Self {
|
|
kind: FixSuggestionKind::Hint,
|
|
message: FixSuggestionMessage::Single(message),
|
|
}
|
|
}
|
|
|
|
pub fn hint_multiline(messages: &'a [&'a str]) -> Self {
|
|
Self {
|
|
kind: FixSuggestionKind::Hint,
|
|
message: FixSuggestionMessage::Multiline(messages),
|
|
}
|
|
}
|
|
}
|
|
|
|
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(),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// 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>,
|
|
is_error: bool,
|
|
level: usize,
|
|
) -> String {
|
|
if source_line.is_none() || column_number.is_none() {
|
|
return "".to_string();
|
|
}
|
|
|
|
let source_line = source_line.unwrap();
|
|
// 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() {
|
|
return "".to_string();
|
|
}
|
|
if source_line.contains("Couldn't format source line: ") {
|
|
return format!("\n{source_line}");
|
|
}
|
|
|
|
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('^');
|
|
let color_underline = if is_error {
|
|
red(&s).to_string()
|
|
} else {
|
|
cyan(&s).to_string()
|
|
};
|
|
|
|
let indent = format!("{:indent$}", "", indent = level);
|
|
|
|
format!("\n{indent}{source_line}\n{indent}{color_underline}")
|
|
}
|
|
|
|
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| cause.is_same_error(el)) {
|
|
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,
|
|
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![])
|
|
};
|
|
|
|
write!(
|
|
s,
|
|
"\nCaused by: {}",
|
|
error_string.trim_start_matches("Uncaught ")
|
|
)
|
|
.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(),
|
|
};
|
|
match suggestion.message {
|
|
FixSuggestionMessage::Single(msg) => {
|
|
write!(s, "{}", msg).unwrap();
|
|
}
|
|
FixSuggestionMessage::Multiline(messages) => {
|
|
for (idx, message) in messages.iter().enumerate() {
|
|
if idx != 0 {
|
|
writeln!(s).unwrap();
|
|
write!(s, " ").unwrap();
|
|
}
|
|
write!(s, "{}", 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)
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use test_util::strip_ansi_codes;
|
|
|
|
#[test]
|
|
fn test_format_none_source_line() {
|
|
let actual = format_maybe_source_line(None, None, false, 0);
|
|
assert_eq!(actual, "");
|
|
}
|
|
|
|
#[test]
|
|
fn test_format_some_source_line() {
|
|
let actual =
|
|
format_maybe_source_line(Some("console.log('foo');"), Some(9), true, 0);
|
|
assert_eq!(
|
|
strip_ansi_codes(&actual),
|
|
"\nconsole.log(\'foo\');\n ^"
|
|
);
|
|
}
|
|
}
|