mirror of
https://github.com/denoland/deno.git
synced 2025-01-03 12:58:54 -05:00
feat: output cause
on JS runtime errors (#13209)
This commit is contained in:
parent
42777f2541
commit
167982be9e
8 changed files with 154 additions and 41 deletions
|
@ -128,9 +128,11 @@ fn format_frame(frame: &JsStackFrame) -> String {
|
|||
result
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn format_stack(
|
||||
is_error: bool,
|
||||
message_line: &str,
|
||||
cause: Option<&str>,
|
||||
source_line: Option<&str>,
|
||||
start_column: Option<i64>,
|
||||
end_column: Option<i64>,
|
||||
|
@ -154,6 +156,14 @@ fn format_stack(
|
|||
indent = level
|
||||
));
|
||||
}
|
||||
if let Some(cause) = cause {
|
||||
s.push_str(&format!(
|
||||
"\n{:indent$}Caused by: {}",
|
||||
"",
|
||||
cause,
|
||||
indent = level
|
||||
));
|
||||
}
|
||||
s
|
||||
}
|
||||
|
||||
|
@ -262,12 +272,19 @@ impl fmt::Display for PrettyJsError {
|
|||
)];
|
||||
}
|
||||
|
||||
let cause = self
|
||||
.0
|
||||
.cause
|
||||
.clone()
|
||||
.map(|cause| format!("{}", PrettyJsError(*cause)));
|
||||
|
||||
write!(
|
||||
f,
|
||||
"{}",
|
||||
&format_stack(
|
||||
true,
|
||||
&self.0.message,
|
||||
cause.as_deref(),
|
||||
self.0.source_line.as_deref(),
|
||||
self.0.start_column,
|
||||
self.0.end_column,
|
||||
|
|
|
@ -79,8 +79,14 @@ pub fn apply_source_map<G: SourceMapGetter>(
|
|||
}
|
||||
}
|
||||
|
||||
let cause = js_error
|
||||
.cause
|
||||
.clone()
|
||||
.map(|cause| Box::new(apply_source_map(&*cause, getter)));
|
||||
|
||||
JsError {
|
||||
message: js_error.message.clone(),
|
||||
cause,
|
||||
source_line,
|
||||
script_resource_name,
|
||||
line_number,
|
||||
|
@ -238,6 +244,7 @@ mod tests {
|
|||
fn apply_source_map_line() {
|
||||
let e = JsError {
|
||||
message: "TypeError: baz".to_string(),
|
||||
cause: None,
|
||||
source_line: Some("foo".to_string()),
|
||||
script_resource_name: Some("foo_bar.ts".to_string()),
|
||||
line_number: Some(4),
|
||||
|
|
|
@ -464,6 +464,18 @@ fn broken_stdout() {
|
|||
assert!(!stderr.contains("panic"));
|
||||
}
|
||||
|
||||
itest!(error_cause {
|
||||
args: "run error_cause.ts",
|
||||
output: "error_cause.ts.out",
|
||||
exit_code: 1,
|
||||
});
|
||||
|
||||
itest!(error_cause_recursive {
|
||||
args: "run error_cause_recursive.ts",
|
||||
output: "error_cause_recursive.ts.out",
|
||||
exit_code: 1,
|
||||
});
|
||||
|
||||
itest_flaky!(cafile_url_imports {
|
||||
args: "run --quiet --reload --cert tls/RootCA.pem cafile_url_imports.ts",
|
||||
output: "cafile_url_imports.ts.out",
|
||||
|
|
13
cli/tests/testdata/error_cause.ts
vendored
Normal file
13
cli/tests/testdata/error_cause.ts
vendored
Normal file
|
@ -0,0 +1,13 @@
|
|||
function a() {
|
||||
throw new Error("foo", { cause: new Error("bar", { cause: "deno" }) });
|
||||
}
|
||||
|
||||
function b() {
|
||||
a();
|
||||
}
|
||||
|
||||
function c() {
|
||||
b();
|
||||
}
|
||||
|
||||
c();
|
17
cli/tests/testdata/error_cause.ts.out
vendored
Normal file
17
cli/tests/testdata/error_cause.ts.out
vendored
Normal file
|
@ -0,0 +1,17 @@
|
|||
[WILDCARD]
|
||||
error: Uncaught Error: foo
|
||||
throw new Error("foo", { cause: new Error("bar", { cause: "deno" }) });
|
||||
^
|
||||
at a (file:///[WILDCARD]/error_cause.ts:2:9)
|
||||
at b (file:///[WILDCARD]/error_cause.ts:6:3)
|
||||
at c (file:///[WILDCARD]/error_cause.ts:10:3)
|
||||
at file:///[WILDCARD]/error_cause.ts:13:1
|
||||
Caused by: Uncaught Error: bar
|
||||
throw new Error("foo", { cause: new Error("bar", { cause: "deno" }) });
|
||||
^
|
||||
at a (file:///[WILDCARD]/error_cause.ts:2:35)
|
||||
at b (file:///[WILDCARD]/error_cause.ts:6:3)
|
||||
at c (file:///[WILDCARD]/error_cause.ts:10:3)
|
||||
at file:///[WILDCARD]/error_cause.ts:13:1
|
||||
Caused by: Uncaught deno
|
||||
[WILDCARD]
|
4
cli/tests/testdata/error_cause_recursive.ts
vendored
Normal file
4
cli/tests/testdata/error_cause_recursive.ts
vendored
Normal file
|
@ -0,0 +1,4 @@
|
|||
const x = new Error("foo");
|
||||
const y = new Error("bar", { cause: x });
|
||||
x.cause = y;
|
||||
throw y;
|
14
cli/tests/testdata/error_cause_recursive.ts.out
vendored
Normal file
14
cli/tests/testdata/error_cause_recursive.ts.out
vendored
Normal file
|
@ -0,0 +1,14 @@
|
|||
[WILDCARD]
|
||||
error: Uncaught Error: bar
|
||||
const y = new Error("bar", { cause: x });
|
||||
^
|
||||
at file:///[WILDCARD]/error_cause_recursive.ts:2:11
|
||||
Caused by: Uncaught Error: foo
|
||||
const x = new Error("foo");
|
||||
^
|
||||
at file:///[WILDCARD]/error_cause_recursive.ts:1:11
|
||||
Caused by: Uncaught Error: bar
|
||||
const y = new Error("bar", { cause: x });
|
||||
^
|
||||
at file:///[WILDCARD]/error_cause_recursive.ts:2:11
|
||||
[WILDCARD]
|
111
core/error.rs
111
core/error.rs
|
@ -2,6 +2,7 @@
|
|||
|
||||
use anyhow::Error;
|
||||
use std::borrow::Cow;
|
||||
use std::collections::HashSet;
|
||||
use std::fmt;
|
||||
use std::fmt::Debug;
|
||||
use std::fmt::Display;
|
||||
|
@ -92,6 +93,7 @@ pub fn get_custom_error_class(error: &Error) -> Option<&'static str> {
|
|||
#[derive(Debug, PartialEq, Clone)]
|
||||
pub struct JsError {
|
||||
pub message: String,
|
||||
pub cause: Option<Box<JsError>>,
|
||||
pub source_line: Option<String>,
|
||||
pub script_resource_name: Option<String>,
|
||||
pub line_number: Option<i64>,
|
||||
|
@ -173,6 +175,14 @@ impl JsError {
|
|||
pub fn from_v8_exception(
|
||||
scope: &mut v8::HandleScope,
|
||||
exception: v8::Local<v8::Value>,
|
||||
) -> Self {
|
||||
Self::inner_from_v8_exception(scope, exception, Default::default())
|
||||
}
|
||||
|
||||
fn inner_from_v8_exception<'a>(
|
||||
scope: &'a mut v8::HandleScope,
|
||||
exception: v8::Local<'a, v8::Value>,
|
||||
mut seen: HashSet<v8::Local<'a, v8::Value>>,
|
||||
) -> Self {
|
||||
// Create a new HandleScope because we're creating a lot of new local
|
||||
// handles below.
|
||||
|
@ -180,53 +190,72 @@ impl JsError {
|
|||
|
||||
let msg = v8::Exception::create_message(scope, exception);
|
||||
|
||||
let (message, frames, stack) = if is_instance_of_error(scope, exception) {
|
||||
// The exception is a JS Error object.
|
||||
let exception: v8::Local<v8::Object> = exception.try_into().unwrap();
|
||||
let (message, frames, stack, cause) =
|
||||
if is_instance_of_error(scope, exception) {
|
||||
// The exception is a JS Error object.
|
||||
let exception: v8::Local<v8::Object> = exception.try_into().unwrap();
|
||||
let cause = get_property(scope, exception, "cause");
|
||||
let e: NativeJsError =
|
||||
serde_v8::from_v8(scope, exception.into()).unwrap();
|
||||
// Get the message by formatting error.name and error.message.
|
||||
let name = e.name.unwrap_or_else(|| "Error".to_string());
|
||||
let message_prop = e.message.unwrap_or_else(|| "".to_string());
|
||||
let message = if !name.is_empty() && !message_prop.is_empty() {
|
||||
format!("Uncaught {}: {}", name, message_prop)
|
||||
} else if !name.is_empty() {
|
||||
format!("Uncaught {}", name)
|
||||
} else if !message_prop.is_empty() {
|
||||
format!("Uncaught {}", message_prop)
|
||||
} else {
|
||||
"Uncaught".to_string()
|
||||
};
|
||||
let cause = cause.and_then(|cause| {
|
||||
if cause.is_undefined() || seen.contains(&cause) {
|
||||
None
|
||||
} else {
|
||||
seen.insert(cause);
|
||||
Some(Box::new(JsError::inner_from_v8_exception(
|
||||
scope, cause, seen,
|
||||
)))
|
||||
}
|
||||
});
|
||||
|
||||
let e: NativeJsError =
|
||||
serde_v8::from_v8(scope, exception.into()).unwrap();
|
||||
// Get the message by formatting error.name and error.message.
|
||||
let name = e.name.unwrap_or_else(|| "Error".to_string());
|
||||
let message_prop = e.message.unwrap_or_else(|| "".to_string());
|
||||
let message = if !name.is_empty() && !message_prop.is_empty() {
|
||||
format!("Uncaught {}: {}", name, message_prop)
|
||||
} else if !name.is_empty() {
|
||||
format!("Uncaught {}", name)
|
||||
} else if !message_prop.is_empty() {
|
||||
format!("Uncaught {}", message_prop)
|
||||
// Access error.stack to ensure that prepareStackTrace() has been called.
|
||||
// This should populate error.__callSiteEvals.
|
||||
let stack = get_property(scope, exception, "stack");
|
||||
let stack: Option<v8::Local<v8::String>> =
|
||||
stack.and_then(|s| s.try_into().ok());
|
||||
let stack = stack.map(|s| s.to_rust_string_lossy(scope));
|
||||
|
||||
// Read an array of structured frames from error.__callSiteEvals.
|
||||
let frames_v8 = get_property(scope, exception, "__callSiteEvals");
|
||||
// Ignore non-array values
|
||||
let frames_v8: Option<v8::Local<v8::Array>> =
|
||||
frames_v8.and_then(|a| a.try_into().ok());
|
||||
|
||||
// Convert them into Vec<JsStackFrame>
|
||||
let frames: Vec<JsStackFrame> = match frames_v8 {
|
||||
Some(frames_v8) => {
|
||||
serde_v8::from_v8(scope, frames_v8.into()).unwrap()
|
||||
}
|
||||
None => vec![],
|
||||
};
|
||||
(message, frames, stack, cause)
|
||||
} else {
|
||||
"Uncaught".to_string()
|
||||
// The exception is not a JS Error object.
|
||||
// Get the message given by V8::Exception::create_message(), and provide
|
||||
// empty frames.
|
||||
(
|
||||
msg.get(scope).to_rust_string_lossy(scope),
|
||||
vec![],
|
||||
None,
|
||||
None,
|
||||
)
|
||||
};
|
||||
|
||||
// Access error.stack to ensure that prepareStackTrace() has been called.
|
||||
// This should populate error.__callSiteEvals.
|
||||
let stack = get_property(scope, exception, "stack");
|
||||
let stack: Option<v8::Local<v8::String>> =
|
||||
stack.and_then(|s| s.try_into().ok());
|
||||
let stack = stack.map(|s| s.to_rust_string_lossy(scope));
|
||||
|
||||
// Read an array of structured frames from error.__callSiteEvals.
|
||||
let frames_v8 = get_property(scope, exception, "__callSiteEvals");
|
||||
// Ignore non-array values
|
||||
let frames_v8: Option<v8::Local<v8::Array>> =
|
||||
frames_v8.and_then(|a| a.try_into().ok());
|
||||
|
||||
// Convert them into Vec<JsStackFrame>
|
||||
let frames: Vec<JsStackFrame> = match frames_v8 {
|
||||
Some(frames_v8) => serde_v8::from_v8(scope, frames_v8.into()).unwrap(),
|
||||
None => vec![],
|
||||
};
|
||||
(message, frames, stack)
|
||||
} else {
|
||||
// The exception is not a JS Error object.
|
||||
// Get the message given by V8::Exception::create_message(), and provide
|
||||
// empty frames.
|
||||
(msg.get(scope).to_rust_string_lossy(scope), vec![], None)
|
||||
};
|
||||
|
||||
Self {
|
||||
message,
|
||||
cause,
|
||||
script_resource_name: msg
|
||||
.get_script_resource_name(scope)
|
||||
.and_then(|v| v8::Local::<v8::String>::try_from(v).ok())
|
||||
|
|
Loading…
Reference in a new issue