diff options
author | Ryan Dahl <ry@tinyclouds.org> | 2019-02-26 17:36:05 -0500 |
---|---|---|
committer | GitHub <noreply@github.com> | 2019-02-26 17:36:05 -0500 |
commit | b8a537d020f5e4495572daa4d8a59f51fa3b20d1 (patch) | |
tree | 468164b8dcc8ce0bcd64e75b63e4a3af0da36f15 /core/js_errors.rs | |
parent | 5dfbbbb07a8f484dca27bbe0d7a3bafb0bdb91fd (diff) |
deno_core (#1827)
A new low-level crate with focus on speed.
This doesn't yet hook into the existing code base.
Diffstat (limited to 'core/js_errors.rs')
-rw-r--r-- | core/js_errors.rs | 416 |
1 files changed, 416 insertions, 0 deletions
diff --git a/core/js_errors.rs b/core/js_errors.rs new file mode 100644 index 000000000..c07af136f --- /dev/null +++ b/core/js_errors.rs @@ -0,0 +1,416 @@ +// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license. + +// Note that source_map_mappings requires 0-indexed line and column numbers but +// V8 Exceptions are 1-indexed. + +// TODO: This currently only applies to uncaught exceptions. It would be nice to +// also have source maps for situations like this: +// const err = new Error("Boo!"); +// console.log(err.stack); +// It would require calling into Rust from Error.prototype.prepareStackTrace. + +use serde_json; +use std::fmt; +use std::str; + +#[derive(Debug, PartialEq)] +pub struct StackFrame { + pub line: i64, // zero indexed + pub column: i64, // zero indexed + pub script_name: String, + pub function_name: String, + pub is_eval: bool, + pub is_constructor: bool, + pub is_wasm: bool, +} + +#[derive(Debug, PartialEq)] +pub struct JSError { + pub message: String, + + pub source_line: Option<String>, + pub script_resource_name: Option<String>, + pub line_number: Option<i64>, + pub start_position: Option<i64>, + pub end_position: Option<i64>, + pub error_level: Option<i64>, + pub start_column: Option<i64>, + pub end_column: Option<i64>, + + pub frames: Vec<StackFrame>, +} + +impl fmt::Display for StackFrame { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + // Note when we print to string, we change from 0-indexed to 1-indexed. + let function_name = self.function_name.clone(); + let script_line_column = + format_script_line_column(&self.script_name, self.line, self.column); + + if !self.function_name.is_empty() { + write!(f, " at {} ({})", function_name, script_line_column) + } else if self.is_eval { + write!(f, " at eval ({})", script_line_column) + } else { + write!(f, " at {}", script_line_column) + } + } +} + +fn format_script_line_column( + script_name: &str, + line: i64, + column: i64, +) -> String { + // TODO match this style with how typescript displays errors. + let line = (1 + line).to_string(); + let column = (1 + column).to_string(); + let script_name = script_name.to_string(); + format!("{}:{}:{}", script_name, line, column) +} + +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(); + // Avoid showing internal code from gen/bundle/main.js + if script_resource_name != "gen/bundle/main.js" + && script_resource_name != "gen/bundle/compiler.js" + { + if self.line_number.is_some() && self.start_column.is_some() { + assert!(self.line_number.is_some()); + assert!(self.start_column.is_some()); + let script_line_column = format_script_line_column( + script_resource_name, + self.line_number.unwrap() - 1, + self.start_column.unwrap() - 1, + ); + write!(f, "{}", script_line_column)?; + } + 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.clone())?; + + for frame in &self.frames { + write!(f, "\n{}", &frame.to_string())?; + } + Ok(()) + } +} + +impl StackFrame { + // TODO Maybe use serde_derive? + fn from_json_value(v: &serde_json::Value) -> Option<Self> { + if !v.is_object() { + return None; + } + let obj = v.as_object().unwrap(); + + let line_v = &obj["line"]; + if !line_v.is_u64() { + return None; + } + let line = line_v.as_u64().unwrap() as i64; + + let column_v = &obj["column"]; + if !column_v.is_u64() { + return None; + } + let column = column_v.as_u64().unwrap() as i64; + + let script_name_v = &obj["scriptName"]; + if !script_name_v.is_string() { + return None; + } + let script_name = String::from(script_name_v.as_str().unwrap()); + + // Optional fields. See EncodeExceptionAsJSON() in libdeno. + // Sometimes V8 doesn't provide all the frame information. + + let mut function_name = String::from(""); // default + if obj.contains_key("functionName") { + let function_name_v = &obj["functionName"]; + if function_name_v.is_string() { + function_name = String::from(function_name_v.as_str().unwrap()); + } + } + + let mut is_eval = false; // default + if obj.contains_key("isEval") { + let is_eval_v = &obj["isEval"]; + if is_eval_v.is_boolean() { + is_eval = is_eval_v.as_bool().unwrap(); + } + } + + let mut is_constructor = false; // default + if obj.contains_key("isConstructor") { + let is_constructor_v = &obj["isConstructor"]; + if is_constructor_v.is_boolean() { + is_constructor = is_constructor_v.as_bool().unwrap(); + } + } + + let mut is_wasm = false; // default + if obj.contains_key("isWasm") { + let is_wasm_v = &obj["isWasm"]; + if is_wasm_v.is_boolean() { + is_wasm = is_wasm_v.as_bool().unwrap(); + } + } + + Some(StackFrame { + line: line - 1, + column: column - 1, + script_name, + function_name, + is_eval, + is_constructor, + is_wasm, + }) + } +} + +impl JSError { + /// Creates a new JSError by parsing the raw exception JSON string from V8. + pub fn from_v8_exception(json_str: &str) -> Option<Self> { + let v = serde_json::from_str::<serde_json::Value>(json_str); + if v.is_err() { + return None; + } + let v = v.unwrap(); + + if !v.is_object() { + return None; + } + let obj = v.as_object().unwrap(); + + let message_v = &obj["message"]; + if !message_v.is_string() { + return None; + } + let message = String::from(message_v.as_str().unwrap()); + + let source_line = obj + .get("sourceLine") + .and_then(|v| v.as_str().map(String::from)); + let script_resource_name = obj + .get("scriptResourceName") + .and_then(|v| v.as_str().map(String::from)); + let line_number = obj.get("lineNumber").and_then(|v| v.as_i64()); + let start_position = obj.get("startPosition").and_then(|v| v.as_i64()); + let end_position = obj.get("endPosition").and_then(|v| v.as_i64()); + let error_level = obj.get("errorLevel").and_then(|v| v.as_i64()); + let start_column = obj.get("startColumn").and_then(|v| v.as_i64()); + let end_column = obj.get("endColumn").and_then(|v| v.as_i64()); + + let frames_v = &obj["frames"]; + if !frames_v.is_array() { + return None; + } + let frame_values = frames_v.as_array().unwrap(); + + let mut frames = Vec::<StackFrame>::new(); + for frame_v in frame_values { + match StackFrame::from_json_value(frame_v) { + None => return None, + Some(frame) => frames.push(frame), + } + } + + Some(JSError { + message, + source_line, + script_resource_name, + line_number, + start_position, + end_position, + error_level, + start_column, + end_column, + frames, + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn error1() -> JSError { + JSError { + message: "Error: foo bar".to_string(), + source_line: None, + script_resource_name: None, + line_number: None, + start_position: None, + end_position: None, + error_level: None, + start_column: None, + end_column: None, + frames: vec![ + StackFrame { + line: 4, + column: 16, + script_name: "foo_bar.ts".to_string(), + function_name: "foo".to_string(), + is_eval: false, + is_constructor: false, + is_wasm: false, + }, + StackFrame { + line: 5, + column: 20, + script_name: "bar_baz.ts".to_string(), + function_name: "qat".to_string(), + is_eval: false, + is_constructor: false, + is_wasm: false, + }, + StackFrame { + line: 1, + column: 1, + script_name: "deno_main.js".to_string(), + function_name: "".to_string(), + is_eval: false, + is_constructor: false, + is_wasm: false, + }, + ], + } + } + + #[test] + fn stack_frame_from_json_value_1() { + let v = serde_json::from_str::<serde_json::Value>( + r#"{ + "line":2, + "column":11, + "functionName":"foo", + "scriptName":"/Users/rld/src/deno/tests/error_001.ts", + "isEval":true, + "isConstructor":false, + "isWasm":false + }"#, + ).unwrap(); + let r = StackFrame::from_json_value(&v); + assert_eq!( + r, + Some(StackFrame { + line: 1, + column: 10, + script_name: "/Users/rld/src/deno/tests/error_001.ts".to_string(), + function_name: "foo".to_string(), + is_eval: true, + is_constructor: false, + is_wasm: false, + }) + ); + } + + #[test] + fn stack_frame_from_json_value_2() { + let v = serde_json::from_str::<serde_json::Value>( + r#"{ + "scriptName": "/Users/rld/src/deno/tests/error_001.ts", + "line": 2, + "column": 11 + }"#, + ).unwrap(); + let r = StackFrame::from_json_value(&v); + assert!(r.is_some()); + let f = r.unwrap(); + assert_eq!(f.line, 1); + assert_eq!(f.column, 10); + assert_eq!(f.script_name, "/Users/rld/src/deno/tests/error_001.ts"); + } + + #[test] + fn js_error_from_v8_exception() { + let r = JSError::from_v8_exception( + r#"{ + "message":"Uncaught Error: bad", + "frames":[ + { + "line":2, + "column":11, + "functionName":"foo", + "scriptName":"/Users/rld/src/deno/tests/error_001.ts", + "isEval":true, + "isConstructor":false, + "isWasm":false + }, { + "line":5, + "column":5, + "functionName":"bar", + "scriptName":"/Users/rld/src/deno/tests/error_001.ts", + "isEval":true, + "isConstructor":false, + "isWasm":false + } + ]}"#, + ); + assert!(r.is_some()); + let e = r.unwrap(); + assert_eq!(e.message, "Uncaught Error: bad"); + assert_eq!(e.frames.len(), 2); + assert_eq!( + e.frames[0], + StackFrame { + line: 1, + column: 10, + script_name: "/Users/rld/src/deno/tests/error_001.ts".to_string(), + function_name: "foo".to_string(), + is_eval: true, + is_constructor: false, + is_wasm: false, + } + ) + } + + #[test] + fn js_error_from_v8_exception2() { + let r = JSError::from_v8_exception( + "{\"message\":\"Error: boo\",\"sourceLine\":\"throw Error('boo');\",\"scriptResourceName\":\"a.js\",\"lineNumber\":3,\"startPosition\":8,\"endPosition\":9,\"errorLevel\":8,\"startColumn\":6,\"endColumn\":7,\"isSharedCrossOrigin\":false,\"isOpaque\":false,\"frames\":[{\"line\":3,\"column\":7,\"functionName\":\"\",\"scriptName\":\"a.js\",\"isEval\":false,\"isConstructor\":false,\"isWasm\":false}]}" + ); + assert!(r.is_some()); + let e = r.unwrap(); + assert_eq!(e.message, "Error: boo"); + assert_eq!(e.source_line, Some("throw Error('boo');".to_string())); + assert_eq!(e.script_resource_name, Some("a.js".to_string())); + assert_eq!(e.line_number, Some(3)); + assert_eq!(e.start_position, Some(8)); + assert_eq!(e.end_position, Some(9)); + assert_eq!(e.error_level, Some(8)); + assert_eq!(e.start_column, Some(6)); + assert_eq!(e.end_column, Some(7)); + assert_eq!(e.frames.len(), 1); + } + + #[test] + fn stack_frame_to_string() { + let e = error1(); + assert_eq!(" at foo (foo_bar.ts:5:17)", &e.frames[0].to_string()); + assert_eq!(" at qat (bar_baz.ts:6:21)", &e.frames[1].to_string()); + } + + #[test] + fn js_error_to_string() { + let e = error1(); + let expected = "Error: foo bar\n at foo (foo_bar.ts:5:17)\n at qat (bar_baz.ts:6:21)\n at deno_main.js:2:2"; + assert_eq!(expected, &e.to_string()); + } +} |