// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. use crate::ErrBox; use rusty_v8 as v8; use std::convert::TryFrom; use std::convert::TryInto; use std::error::Error; use std::fmt; /// 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, pub script_resource_name: Option, pub line_number: Option, pub start_column: Option, // 0-based pub end_column: Option, // 0-based pub frames: Vec, pub formatted_frames: Vec, } #[derive(Debug, PartialEq, Clone)] pub struct JSStackFrame { pub type_name: Option, pub function_name: Option, pub method_name: Option, pub file_name: Option, pub line_number: Option, pub column_number: Option, pub eval_origin: Option, pub is_top_level: Option, 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, } fn get_property<'a>( scope: &mut impl v8::ToLocal<'a>, context: v8::Local, object: v8::Local, key: &str, ) -> Option> { let key = v8::String::new(scope, key).unwrap(); object.get(scope, context, key.into()) } impl JSError { pub(crate) fn create(js_error: Self) -> ErrBox { ErrBox::from(js_error) } pub fn from_v8_exception( scope: &mut impl v8::InIsolate, exception: v8::Local, ) -> Self { // Create a new HandleScope because we're creating a lot of new local // handles below. let mut hs = v8::HandleScope::new(scope); let scope = hs.enter(); let context = { scope.get_current_context().unwrap() }; 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 = exception.clone().try_into().unwrap(); // Get the message by formatting error.name and error.message. let name = get_property(scope, context, 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, context, 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, context, exception, "stack"); // Read an array of structured frames from error.__callSiteEvals. let frames_v8 = get_property(scope, context, exception, "__callSiteEvals"); let frames_v8: Option> = 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, context, exception, "__formattedFrames"); let formatted_frames_v8: Option> = formatted_frames_v8.and_then(|a| a.try_into().ok()); // Convert them into Vec and Vec respectively. let mut frames: Vec = vec![]; let mut formatted_frames: Vec = 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 = frames_v8 .get_index(scope, context, i) .unwrap() .try_into() .unwrap(); let type_name: Option> = get_property(scope, context, call_site, "typeName") .unwrap() .try_into() .ok(); let type_name = type_name.map(|s| s.to_rust_string_lossy(scope)); let function_name: Option> = get_property(scope, context, call_site, "functionName") .unwrap() .try_into() .ok(); let function_name = function_name.map(|s| s.to_rust_string_lossy(scope)); let method_name: Option> = get_property(scope, context, call_site, "methodName") .unwrap() .try_into() .ok(); let method_name = method_name.map(|s| s.to_rust_string_lossy(scope)); let file_name: Option> = get_property(scope, context, call_site, "fileName") .unwrap() .try_into() .ok(); let file_name = file_name.map(|s| s.to_rust_string_lossy(scope)); let line_number: Option> = get_property(scope, context, call_site, "lineNumber") .unwrap() .try_into() .ok(); let line_number = line_number.map(|n| n.value()); let column_number: Option> = get_property(scope, context, call_site, "columnNumber") .unwrap() .try_into() .ok(); let column_number = column_number.map(|n| n.value()); let eval_origin: Option> = get_property(scope, context, 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> = get_property(scope, context, call_site, "isTopLevel") .unwrap() .try_into() .ok(); let is_top_level = is_top_level.map(|b| b.is_true()); let is_eval: v8::Local = get_property(scope, context, call_site, "isEval") .unwrap() .try_into() .unwrap(); let is_eval = is_eval.is_true(); let is_native: v8::Local = get_property(scope, context, call_site, "isNative") .unwrap() .try_into() .unwrap(); let is_native = is_native.is_true(); let is_constructor: v8::Local = get_property(scope, context, call_site, "isConstructor") .unwrap() .try_into() .unwrap(); let is_constructor = is_constructor.is_true(); let is_async: v8::Local = get_property(scope, context, call_site, "isAsync") .unwrap() .try_into() .unwrap(); let is_async = is_async.is_true(); let is_promise_all: v8::Local = get_property(scope, context, call_site, "isPromiseAll") .unwrap() .try_into() .unwrap(); let is_promise_all = is_promise_all.is_true(); let promise_index: Option> = get_property(scope, context, 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 = formatted_frames_v8 .get_index(scope, context, 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::::try_from(v).ok()) .map(|v| v.to_rust_string_lossy(scope)), source_line: msg .get_source_line(scope, context) .map(|v| v.to_rust_string_lossy(scope)), line_number: msg.get_line_number(context).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 fmt::Display for JSError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { if self.script_resource_name.is_some() { let script_resource_name = self.script_resource_name.as_ref().unwrap(); 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() { write!(f, "\n{}\n", self.source_line.as_ref().unwrap())?; let mut s = String::new(); for i in 0..self.end_column.unwrap() { if i >= self.start_column.unwrap() { s.push('^'); } 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(()) } }