diff options
author | Bert Belder <bertbelder@gmail.com> | 2020-09-14 18:48:57 +0200 |
---|---|---|
committer | Bert Belder <bertbelder@gmail.com> | 2020-09-15 01:50:52 +0200 |
commit | f5b40c918c7d602827622d167728a3e7bae87d9d (patch) | |
tree | fb51722e043f4d6bce64a2c7e897cce4ead06c82 /core/error.rs | |
parent | 3da20d19a14ab6838897d281f1b11e49d68bd1a7 (diff) |
refactor: use the 'anyhow' crate instead of 'ErrBox' (#7476)
Diffstat (limited to 'core/error.rs')
-rw-r--r-- | core/error.rs | 439 |
1 files changed, 439 insertions, 0 deletions
diff --git a/core/error.rs b/core/error.rs new file mode 100644 index 000000000..198f7baee --- /dev/null +++ b/core/error.rs @@ -0,0 +1,439 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. + +use rusty_v8 as v8; +use std::borrow::Cow; +use std::convert::TryFrom; +use std::convert::TryInto; +use std::error::Error; +use std::fmt; +use std::fmt::Debug; +use std::fmt::Display; +use std::fmt::Formatter; +use std::io; + +/// A generic wrapper that can encapsulate any concrete error type. +pub type AnyError = anyhow::Error; + +/// Creates a new error with a caller-specified error class name and message. +pub fn custom_error( + class: &'static str, + message: impl Into<Cow<'static, str>>, +) -> AnyError { + CustomError { + class, + message: message.into(), + } + .into() +} + +pub fn generic_error(message: impl Into<Cow<'static, str>>) -> AnyError { + custom_error("Error", message) +} + +pub fn type_error(message: impl Into<Cow<'static, str>>) -> AnyError { + custom_error("TypeError", message) +} + +pub fn uri_error(message: impl Into<Cow<'static, str>>) -> AnyError { + custom_error("URIError", message) +} + +pub fn last_os_error() -> AnyError { + io::Error::last_os_error().into() +} + +pub fn bad_resource(message: impl Into<Cow<'static, str>>) -> AnyError { + custom_error("BadResource", message) +} + +pub fn bad_resource_id() -> AnyError { + custom_error("BadResource", "Bad resource ID") +} + +pub fn not_supported() -> AnyError { + custom_error("NotSupported", "The operation is supported") +} + +pub fn resource_unavailable() -> AnyError { + custom_error( + "Busy", + "Resource is unavailable because it is in use by a promise", + ) +} + +/// A simple error type that lets the creator specify both the error message and +/// the error class name. This type is private; externally it only ever appears +/// wrapped in an `AnyError`. To retrieve the error class name from a wrapped +/// `CustomError`, use the function `get_custom_error_class()`. +#[derive(Debug)] +struct CustomError { + class: &'static str, + message: Cow<'static, str>, +} + +impl Display for CustomError { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + f.write_str(&self.message) + } +} + +impl Error for CustomError {} + +/// If this error was crated with `custom_error()`, return the specified error +/// class name. In all other cases this function returns `None`. +pub fn get_custom_error_class(error: &AnyError) -> Option<&'static str> { + error.downcast_ref::<CustomError>().map(|e| e.class) +} + +/// A `JsError` represents an exception coming from V8, with stack frames and +/// line numbers. The deno_cli crate defines another `JsError` type, which wraps +/// the one defined here, that adds source map support and colorful formatting. +#[derive(Debug, PartialEq, Clone)] +pub struct JsError { + pub message: String, + pub source_line: Option<String>, + pub script_resource_name: Option<String>, + pub line_number: Option<i64>, + pub start_column: Option<i64>, // 0-based + pub end_column: Option<i64>, // 0-based + pub frames: Vec<JsStackFrame>, + pub formatted_frames: Vec<String>, +} + +#[derive(Debug, PartialEq, Clone)] +pub struct JsStackFrame { + pub type_name: Option<String>, + pub function_name: Option<String>, + pub method_name: Option<String>, + pub file_name: Option<String>, + pub line_number: Option<i64>, + pub column_number: Option<i64>, + pub eval_origin: Option<String>, + pub is_top_level: Option<bool>, + pub is_eval: bool, + pub is_native: bool, + pub is_constructor: bool, + pub is_async: bool, + pub is_promise_all: bool, + pub promise_index: Option<i64>, +} + +fn get_property<'a>( + scope: &mut v8::HandleScope<'a>, + object: v8::Local<v8::Object>, + key: &str, +) -> Option<v8::Local<'a, v8::Value>> { + let key = v8::String::new(scope, key).unwrap(); + object.get(scope, key.into()) +} + +impl JsError { + pub(crate) fn create(js_error: Self) -> AnyError { + js_error.into() + } + + pub fn from_v8_exception( + scope: &mut v8::HandleScope, + exception: v8::Local<v8::Value>, + ) -> Self { + // Create a new HandleScope because we're creating a lot of new local + // handles below. + let scope = &mut v8::HandleScope::new(scope); + + let msg = v8::Exception::create_message(scope, exception); + + let (message, frames, formatted_frames) = if exception.is_native_error() { + // The exception is a JS Error object. + let exception: v8::Local<v8::Object> = + exception.clone().try_into().unwrap(); + + // Get the message by formatting error.name and error.message. + let name = get_property(scope, exception, "name") + .and_then(|m| m.to_string(scope)) + .map(|s| s.to_rust_string_lossy(scope)) + .unwrap_or_else(|| "undefined".to_string()); + let message_prop = get_property(scope, exception, "message") + .and_then(|m| m.to_string(scope)) + .map(|s| s.to_rust_string_lossy(scope)) + .unwrap_or_else(|| "undefined".to_string()); + let message = format!("Uncaught {}: {}", name, message_prop); + + // Access error.stack to ensure that prepareStackTrace() has been called. + // This should populate error.__callSiteEvals and error.__formattedFrames. + let _ = get_property(scope, exception, "stack"); + + // Read an array of structured frames from error.__callSiteEvals. + let frames_v8 = get_property(scope, exception, "__callSiteEvals"); + let frames_v8: Option<v8::Local<v8::Array>> = + frames_v8.and_then(|a| a.try_into().ok()); + + // Read an array of pre-formatted frames from error.__formattedFrames. + let formatted_frames_v8 = + get_property(scope, exception, "__formattedFrames"); + let formatted_frames_v8: Option<v8::Local<v8::Array>> = + formatted_frames_v8.and_then(|a| a.try_into().ok()); + + // Convert them into Vec<JSStack> and Vec<String> respectively. + let mut frames: Vec<JsStackFrame> = vec![]; + let mut formatted_frames: Vec<String> = vec![]; + if let (Some(frames_v8), Some(formatted_frames_v8)) = + (frames_v8, formatted_frames_v8) + { + for i in 0..frames_v8.length() { + let call_site: v8::Local<v8::Object> = + frames_v8.get_index(scope, i).unwrap().try_into().unwrap(); + let type_name: Option<v8::Local<v8::String>> = + get_property(scope, call_site, "typeName") + .unwrap() + .try_into() + .ok(); + let type_name = type_name.map(|s| s.to_rust_string_lossy(scope)); + let function_name: Option<v8::Local<v8::String>> = + get_property(scope, call_site, "functionName") + .unwrap() + .try_into() + .ok(); + let function_name = + function_name.map(|s| s.to_rust_string_lossy(scope)); + let method_name: Option<v8::Local<v8::String>> = + get_property(scope, call_site, "methodName") + .unwrap() + .try_into() + .ok(); + let method_name = method_name.map(|s| s.to_rust_string_lossy(scope)); + let file_name: Option<v8::Local<v8::String>> = + get_property(scope, call_site, "fileName") + .unwrap() + .try_into() + .ok(); + let file_name = file_name.map(|s| s.to_rust_string_lossy(scope)); + let line_number: Option<v8::Local<v8::Integer>> = + get_property(scope, call_site, "lineNumber") + .unwrap() + .try_into() + .ok(); + let line_number = line_number.map(|n| n.value()); + let column_number: Option<v8::Local<v8::Integer>> = + get_property(scope, call_site, "columnNumber") + .unwrap() + .try_into() + .ok(); + let column_number = column_number.map(|n| n.value()); + let eval_origin: Option<v8::Local<v8::String>> = + get_property(scope, call_site, "evalOrigin") + .unwrap() + .try_into() + .ok(); + let eval_origin = eval_origin.map(|s| s.to_rust_string_lossy(scope)); + let is_top_level: Option<v8::Local<v8::Boolean>> = + get_property(scope, call_site, "isTopLevel") + .unwrap() + .try_into() + .ok(); + let is_top_level = is_top_level.map(|b| b.is_true()); + let is_eval: v8::Local<v8::Boolean> = + get_property(scope, call_site, "isEval") + .unwrap() + .try_into() + .unwrap(); + let is_eval = is_eval.is_true(); + let is_native: v8::Local<v8::Boolean> = + get_property(scope, call_site, "isNative") + .unwrap() + .try_into() + .unwrap(); + let is_native = is_native.is_true(); + let is_constructor: v8::Local<v8::Boolean> = + get_property(scope, call_site, "isConstructor") + .unwrap() + .try_into() + .unwrap(); + let is_constructor = is_constructor.is_true(); + let is_async: v8::Local<v8::Boolean> = + get_property(scope, call_site, "isAsync") + .unwrap() + .try_into() + .unwrap(); + let is_async = is_async.is_true(); + let is_promise_all: v8::Local<v8::Boolean> = + get_property(scope, call_site, "isPromiseAll") + .unwrap() + .try_into() + .unwrap(); + let is_promise_all = is_promise_all.is_true(); + let promise_index: Option<v8::Local<v8::Integer>> = + get_property(scope, call_site, "columnNumber") + .unwrap() + .try_into() + .ok(); + let promise_index = promise_index.map(|n| n.value()); + frames.push(JsStackFrame { + type_name, + function_name, + method_name, + file_name, + line_number, + column_number, + eval_origin, + is_top_level, + is_eval, + is_native, + is_constructor, + is_async, + is_promise_all, + promise_index, + }); + let formatted_frame: v8::Local<v8::String> = formatted_frames_v8 + .get_index(scope, i) + .unwrap() + .try_into() + .unwrap(); + let formatted_frame = formatted_frame.to_rust_string_lossy(scope); + formatted_frames.push(formatted_frame) + } + } + (message, frames, formatted_frames) + } 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![], vec![]) + }; + + Self { + message, + script_resource_name: msg + .get_script_resource_name(scope) + .and_then(|v| v8::Local::<v8::String>::try_from(v).ok()) + .map(|v| v.to_rust_string_lossy(scope)), + source_line: msg + .get_source_line(scope) + .map(|v| v.to_rust_string_lossy(scope)), + line_number: msg.get_line_number(scope).and_then(|v| v.try_into().ok()), + start_column: msg.get_start_column().try_into().ok(), + end_column: msg.get_end_column().try_into().ok(), + frames, + formatted_frames, + } + } +} + +impl Error for JsError {} + +fn format_source_loc( + file_name: &str, + line_number: i64, + column_number: i64, +) -> String { + let line_number = line_number; + let column_number = column_number; + format!("{}:{}:{}", file_name, line_number, column_number) +} + +impl Display for JsError { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + if let Some(script_resource_name) = &self.script_resource_name { + if self.line_number.is_some() && self.start_column.is_some() { + assert!(self.line_number.is_some()); + assert!(self.start_column.is_some()); + let source_loc = format_source_loc( + script_resource_name, + self.line_number.unwrap(), + self.start_column.unwrap(), + ); + write!(f, "{}", source_loc)?; + } + if self.source_line.is_some() { + let source_line = self.source_line.as_ref().unwrap(); + write!(f, "\n{}\n", source_line)?; + let mut s = String::new(); + for i in 0..self.end_column.unwrap() { + if i >= self.start_column.unwrap() { + s.push('^'); + } else if source_line.chars().nth(i as usize).unwrap() == '\t' { + s.push('\t'); + } else { + s.push(' '); + } + } + writeln!(f, "{}", s)?; + } + } + + write!(f, "{}", self.message)?; + + for formatted_frame in &self.formatted_frames { + // TODO: Strip ANSI color from formatted_frame. + write!(f, "\n at {}", formatted_frame)?; + } + Ok(()) + } +} + +pub(crate) fn attach_handle_to_error( + scope: &mut v8::Isolate, + err: AnyError, + handle: v8::Local<v8::Value>, +) -> AnyError { + // TODO(bartomieju): this is a special case... + ErrWithV8Handle::new(scope, err, handle).into() +} + +// TODO(piscisaureus): rusty_v8 should implement the Error trait on +// values of type v8::Global<T>. +pub struct ErrWithV8Handle { + err: AnyError, + handle: v8::Global<v8::Value>, +} + +impl ErrWithV8Handle { + pub fn new( + scope: &mut v8::Isolate, + err: AnyError, + handle: v8::Local<v8::Value>, + ) -> Self { + let handle = v8::Global::new(scope, handle); + Self { err, handle } + } + + pub fn get_handle<'s>( + &self, + scope: &mut v8::HandleScope<'s>, + ) -> v8::Local<'s, v8::Value> { + v8::Local::new(scope, &self.handle) + } +} + +unsafe impl Send for ErrWithV8Handle {} +unsafe impl Sync for ErrWithV8Handle {} + +impl Error for ErrWithV8Handle {} + +impl Display for ErrWithV8Handle { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + <AnyError as Display>::fmt(&self.err, f) + } +} + +impl Debug for ErrWithV8Handle { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + <Self as Display>::fmt(self, f) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_bad_resource() { + let err = bad_resource("Resource has been closed"); + assert_eq!(err.to_string(), "Resource has been closed"); + } + + #[test] + fn test_bad_resource_id() { + let err = bad_resource_id(); + assert_eq!(err.to_string(), "Bad resource ID"); + } +} |