diff options
Diffstat (limited to 'src/js_errors.rs')
-rw-r--r-- | src/js_errors.rs | 479 |
1 files changed, 99 insertions, 380 deletions
diff --git a/src/js_errors.rs b/src/js_errors.rs index 5ba36c0cf..f42d9cb51 100644 --- a/src/js_errors.rs +++ b/src/js_errors.rs @@ -1,16 +1,8 @@ // 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. - +//! This mod adds source maps and ANSI color display to deno_core::JSError. use crate::ansi; -use serde_json; +use deno_core::JSError; +use deno_core::StackFrame; use source_map_mappings::parse_mappings; use source_map_mappings::Bias; use source_map_mappings::Mappings; @@ -18,57 +10,36 @@ use std::collections::HashMap; use std::fmt; use std::str; +/// Wrapper around JSError which provides color to_string. +pub struct JSErrorColor<'a>(pub &'a JSError); + +struct StackFrameColor<'a>(&'a StackFrame); + pub trait SourceMapGetter { /// Returns the raw source map file. fn get_source_map(&self, script_name: &str) -> Option<Vec<u8>>; } -struct SourceMap { - mappings: Mappings, - sources: Vec<String>, -} - /// Cached filename lookups. The key can be None if a previous lookup failed to /// find a SourceMap. type CachedMaps = HashMap<String, Option<SourceMap>>; -#[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>, +struct SourceMap { + mappings: Mappings, + sources: Vec<String>, } -impl fmt::Display for StackFrame { +impl<'a> fmt::Display for StackFrameColor<'a> { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let frame = self.0; // Note when we print to string, we change from 0-indexed to 1-indexed. - let function_name = ansi::italic_bold(self.function_name.clone()); + let function_name = ansi::italic_bold(frame.function_name.clone()); let script_line_column = - format_script_line_column(&self.script_name, self.line, self.column); + format_script_line_column(&frame.script_name, frame.line, frame.column); - if !self.function_name.is_empty() { + if !frame.function_name.is_empty() { write!(f, " at {} ({})", function_name, script_line_column) - } else if self.is_eval { + } else if frame.is_eval { write!(f, " at eval ({})", script_line_column) } else { write!(f, " at {}", script_line_column) @@ -88,29 +59,30 @@ fn format_script_line_column( format!("{}:{}:{}", script_name, line, column) } -impl fmt::Display for JSError { +impl<'a> fmt::Display for JSErrorColor<'a> { 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(); + let e = self.0; + if e.script_resource_name.is_some() { + let script_resource_name = e.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()); + if e.line_number.is_some() && e.start_column.is_some() { + assert!(e.line_number.is_some()); + assert!(e.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, + e.line_number.unwrap() - 1, + e.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())?; + if e.source_line.is_some() { + write!(f, "\n{}\n", e.source_line.as_ref().unwrap())?; let mut s = String::new(); - for i in 0..self.end_column.unwrap() { - if i >= self.start_column.unwrap() { + for i in 0..e.end_column.unwrap() { + if i >= e.start_column.unwrap() { s.push('^'); } else { s.push(' '); @@ -121,133 +93,15 @@ impl fmt::Display for JSError { } } - write!(f, "{}", ansi::bold(self.message.clone()))?; + write!(f, "{}", ansi::bold(e.message.clone()))?; - for frame in &self.frames { - write!(f, "\n{}", &frame.to_string())?; + for frame in &e.frames { + write!(f, "\n{}", StackFrameColor(&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, - }) - } - - fn apply_source_map( - &self, - mappings_map: &mut CachedMaps, - getter: &dyn SourceMapGetter, - ) -> StackFrame { - let maybe_sm = - get_mappings(self.script_name.as_ref(), mappings_map, getter); - let frame_pos = ( - self.script_name.to_owned(), - self.line as i64, - self.column as i64, - ); - let (script_name, line, column) = match maybe_sm { - None => frame_pos, - Some(sm) => match sm.mappings.original_location_for( - self.line as u32, - self.column as u32, - Bias::default(), - ) { - None => frame_pos, - Some(mapping) => match &mapping.original { - None => frame_pos, - Some(original) => { - let orig_source = sm.sources[original.source as usize].clone(); - ( - orig_source, - i64::from(original.original_line), - i64::from(original.original_column), - ) - } - }, - }, - }; - - StackFrame { - script_name, - function_name: self.function_name.clone(), - line, - column, - is_eval: self.is_eval, - is_constructor: self.is_constructor, - is_wasm: self.is_wasm, - } - } -} - impl SourceMap { fn from_json(json_str: &str) -> Option<Self> { // Ugly. Maybe use serde_derive. @@ -283,87 +137,72 @@ impl SourceMap { } } -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, - }) +fn frame_apply_source_map( + frame: &StackFrame, + mappings_map: &mut CachedMaps, + getter: &dyn SourceMapGetter, +) -> StackFrame { + let maybe_sm = get_mappings(frame.script_name.as_ref(), mappings_map, getter); + let frame_pos = ( + frame.script_name.to_owned(), + frame.line as i64, + frame.column as i64, + ); + let (script_name, line, column) = match maybe_sm { + None => frame_pos, + Some(sm) => match sm.mappings.original_location_for( + frame.line as u32, + frame.column as u32, + Bias::default(), + ) { + None => frame_pos, + Some(mapping) => match &mapping.original { + None => frame_pos, + Some(original) => { + let orig_source = sm.sources[original.source as usize].clone(); + ( + orig_source, + i64::from(original.original_line), + i64::from(original.original_column), + ) + } + }, + }, + }; + + StackFrame { + script_name, + function_name: frame.function_name.clone(), + line, + column, + is_eval: frame.is_eval, + is_constructor: frame.is_constructor, + is_wasm: frame.is_wasm, } +} - pub fn apply_source_map(&self, getter: &dyn SourceMapGetter) -> Self { - let mut mappings_map: CachedMaps = HashMap::new(); - let mut frames = Vec::<StackFrame>::new(); - for frame in &self.frames { - let f = frame.apply_source_map(&mut mappings_map, getter); - frames.push(f); - } - JSError { - message: self.message.clone(), - frames, - error_level: self.error_level, - source_line: self.source_line.clone(), - // TODO the following need to be source mapped: - script_resource_name: self.script_resource_name.clone(), - line_number: self.line_number, - start_position: self.start_position, - end_position: self.end_position, - start_column: self.start_column, - end_column: self.end_column, - } +pub fn apply_source_map( + js_error: &JSError, + getter: &dyn SourceMapGetter, +) -> JSError { + let mut mappings_map: CachedMaps = HashMap::new(); + let mut frames = Vec::<StackFrame>::new(); + for frame in &js_error.frames { + let f = frame_apply_source_map(&frame, &mut mappings_map, getter); + frames.push(f); + } + JSError { + message: js_error.message.clone(), + frames, + error_level: js_error.error_level, + source_line: js_error.source_line.clone(), + // TODO the following need to be source mapped: + script_resource_name: js_error.script_resource_name.clone(), + line_number: js_error.line_number, + start_position: js_error.start_position, + end_position: js_error.end_position, + start_column: js_error.start_column, + end_column: js_error.end_column, } } @@ -469,126 +308,6 @@ mod tests { } #[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)", - strip_ansi_codes(&e.frames[0].to_string()) - ); - assert_eq!( - " at qat (bar_baz.ts:6:21)", - strip_ansi_codes(&e.frames[1].to_string()) - ); - } - - #[test] fn js_error_to_string() { let e = error1(); assert_eq!("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", strip_ansi_codes(&e.to_string())); @@ -598,7 +317,7 @@ mod tests { fn js_error_apply_source_map_1() { let e = error1(); let getter = MockSourceMapGetter {}; - let actual = e.apply_source_map(&getter); + let actual = apply_source_map(&e, &getter); let expected = JSError { message: "Error: foo bar".to_string(), source_line: None, @@ -665,7 +384,7 @@ mod tests { }], }; let getter = MockSourceMapGetter {}; - let actual = e.apply_source_map(&getter); + let actual = apply_source_map(&e, &getter); assert_eq!(actual.message, "TypeError: baz"); // Because this is accessing the live bundle, this test might be more fragile assert_eq!(actual.frames.len(), 1); |