diff options
author | Nayeem Rahman <nayeemrmn99@gmail.com> | 2022-04-15 15:08:09 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2022-04-15 16:08:09 +0200 |
commit | 8b31fc23cd80de9baa62535e95367da7a21c9cfd (patch) | |
tree | 994748bd06ed5b4953929392107b6beaa1c1c337 /core | |
parent | b4af648c1515a8e79d7a5d1b14d8a4ba9d966a72 (diff) |
refactor: Move source map lookups to core (#14274)
The following transformations gradually faced by "JsError" have all been
moved up front to "JsError::from_v8_exception()":
- finding the first non-"deno:" source line;
- moving "JsError::script_resource_name" etc. into the first error stack
in case of syntax errors;
- source mapping "JsError::script_resource_name" etc. when wrapping
the error even though the frame locations are source mapped earlier;
- removing "JsError::{script_resource_name,line_number,start_column,end_column}"
entirely in favour of "js_error.frames.get(0)".
We also no longer pass a js-side callback to "core/02_error.js" from cli.
I avoided doing this on previous occasions because the source map lookups
were in an awkward place.
Diffstat (limited to 'core')
-rw-r--r-- | core/02_error.js | 28 | ||||
-rw-r--r-- | core/Cargo.toml | 1 | ||||
-rw-r--r-- | core/bindings.rs | 41 | ||||
-rw-r--r-- | core/error.rs | 112 | ||||
-rw-r--r-- | core/lib.rs | 3 | ||||
-rw-r--r-- | core/runtime.rs | 15 | ||||
-rw-r--r-- | core/source_map.rs | 99 |
7 files changed, 242 insertions, 57 deletions
diff --git a/core/02_error.js b/core/02_error.js index 756098738..c6b808169 100644 --- a/core/02_error.js +++ b/core/02_error.js @@ -2,6 +2,7 @@ "use strict"; ((window) => { + const core = Deno.core; const { Error, ObjectFreeze, @@ -188,24 +189,8 @@ }; } - /** - * Returns a function that can be used as `Error.prepareStackTrace`. - * - * This function accepts an optional argument, a function that performs - * source mapping. It is not required to pass this argument, but - * in such case only JavaScript sources will have proper position in - * stack frames. - * @param {( - * fileName: string, - * lineNumber: number, - * columnNumber: number - * ) => { - * fileName: string, - * lineNumber: number, - * columnNumber: number - * }} sourceMappingFn - */ - function createPrepareStackTrace(sourceMappingFn, formatFileNameFn) { + /** Returns a function that can be used as `Error.prepareStackTrace`. */ + function createPrepareStackTrace(formatFileNameFn) { return function prepareStackTrace( error, callSites, @@ -214,13 +199,10 @@ const fileName = callSite.getFileName(); const lineNumber = callSite.getLineNumber(); const columnNumber = callSite.getColumnNumber(); - if ( - sourceMappingFn && fileName && lineNumber != null && - columnNumber != null - ) { + if (fileName && lineNumber != null && columnNumber != null) { return patchCallSite( callSite, - sourceMappingFn({ + core.applySourceMap({ fileName, lineNumber, columnNumber, diff --git a/core/Cargo.toml b/core/Cargo.toml index a46608035..c3503ff0d 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -25,6 +25,7 @@ pin-project = "1.0.7" serde = { version = "1.0.129", features = ["derive"] } serde_json = { version = "1.0.66", features = ["preserve_order"] } serde_v8 = { version = "0.40.0", path = "../serde_v8" } +sourcemap = "=6.0.1" url = { version = "2.2.2", features = ["serde"] } v8 = "0.41.0" diff --git a/core/bindings.rs b/core/bindings.rs index 46dcefe38..889229bb5 100644 --- a/core/bindings.rs +++ b/core/bindings.rs @@ -10,6 +10,7 @@ use crate::modules::ModuleMap; use crate::ops::OpCtx; use crate::ops_builtin::WasmStreamingResource; use crate::resolve_url_or_path; +use crate::source_map::apply_source_map as apply_source_map_; use crate::JsRuntime; use crate::PromiseId; use crate::ResourceId; @@ -21,6 +22,7 @@ use serde::Deserialize; use serde::Serialize; use serde_v8::to_v8; use std::cell::RefCell; +use std::collections::HashMap; use std::option::Option; use std::os::raw::c_void; use url::Url; @@ -109,6 +111,9 @@ pub static EXTERNAL_REFERENCES: Lazy<v8::ExternalReferences> = v8::ExternalReference { function: terminate.map_fn_to(), }, + v8::ExternalReference { + function: apply_source_map.map_fn_to(), + }, ]) }); @@ -237,6 +242,7 @@ pub fn initialize_context<'s>( set_func(scope, core_val, "abortWasmStreaming", abort_wasm_streaming); set_func(scope, core_val, "destructureError", destructure_error); set_func(scope, core_val, "terminate", terminate); + set_func(scope, core_val, "applySourceMap", apply_source_map); // Direct bindings on `window`. set_func(scope, global, "queueMicrotask", queue_microtask); @@ -1324,6 +1330,41 @@ fn terminate( scope.terminate_execution(); } +fn apply_source_map( + scope: &mut v8::HandleScope, + args: v8::FunctionCallbackArguments, + mut rv: v8::ReturnValue, +) { + #[derive(Deserialize, Serialize)] + #[serde(rename_all = "camelCase")] + struct Location { + file_name: String, + line_number: u32, + column_number: u32, + } + let state_rc = JsRuntime::state(scope); + let state = state_rc.borrow(); + if let Some(source_map_getter) = &state.source_map_getter { + let mut location = match serde_v8::from_v8::<Location>(scope, args.get(0)) { + Ok(location) => location, + Err(error) => return throw_type_error(scope, error.to_string()), + }; + let (f, l, c, _) = apply_source_map_( + location.file_name, + location.line_number.into(), + location.column_number.into(), + &mut HashMap::new(), + source_map_getter.as_ref(), + ); + location.file_name = f; + location.line_number = l as u32; + location.column_number = c as u32; + rv.set(serde_v8::to_v8(scope, location).unwrap()); + } else { + rv.set(args.get(0)); + } +} + fn create_host_object( scope: &mut v8::HandleScope, _args: v8::FunctionCallbackArguments, diff --git a/core/error.rs b/core/error.rs index 362bf2fa5..59fe170c8 100644 --- a/core/error.rs +++ b/core/error.rs @@ -1,7 +1,10 @@ // Copyright 2018-2022 the Deno authors. All rights reserved. MIT license. +use crate::runtime::JsRuntime; +use crate::source_map::apply_source_map; use anyhow::Error; use std::borrow::Cow; +use std::collections::HashMap; use std::collections::HashSet; use std::fmt; use std::fmt::Debug; @@ -95,15 +98,12 @@ pub fn get_custom_error_class(error: &Error) -> Option<&'static str> { pub struct JsError { pub name: Option<String>, pub message: Option<String>, - pub exception_message: String, + pub stack: Option<String>, pub cause: Option<Box<JsError>>, - 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 exception_message: String, pub frames: Vec<JsStackFrame>, - pub stack: Option<String>, + pub source_line: Option<String>, + pub source_line_frame_index: Option<usize>, } #[derive(Debug, PartialEq, Clone, serde::Deserialize, serde::Serialize)] @@ -236,25 +236,82 @@ impl JsError { frames_v8.and_then(|a| a.try_into().ok()); // Convert them into Vec<JsStackFrame> - let frames: Vec<JsStackFrame> = match frames_v8 { + let mut frames: Vec<JsStackFrame> = match frames_v8 { Some(frames_v8) => serde_v8::from_v8(scope, frames_v8.into()).unwrap(), None => vec![], }; + + let state_rc = JsRuntime::state(scope); + let state = state_rc.borrow(); + + // When the stack frame array is empty, but the source location given by + // (script_resource_name, line_number, start_column + 1) exists, this is + // likely a syntax error. For the sake of formatting we treat it like it + // was given as a single stack frame. + if frames.is_empty() { + let 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)); + let line_number: Option<i64> = + msg.get_line_number(scope).and_then(|v| v.try_into().ok()); + let column_number: Option<i64> = msg.get_start_column().try_into().ok(); + if let (Some(f), Some(l), Some(c)) = + (script_resource_name, line_number, column_number) + { + // V8's column numbers are 0-based, we want 1-based. + let c = c + 1; + if let Some(source_map_getter) = &state.source_map_getter { + let (f, l, c, _) = apply_source_map( + f, + l, + c, + &mut HashMap::new(), + source_map_getter.as_ref(), + ); + frames = + vec![JsStackFrame::from_location(Some(f), Some(l), Some(c))]; + } else { + frames = + vec![JsStackFrame::from_location(Some(f), Some(l), Some(c))]; + } + } + } + + let mut source_line = None; + let mut source_line_frame_index = None; + if let Some(source_map_getter) = &state.source_map_getter { + for (i, frame) in frames.iter().enumerate() { + if let (Some(file_name), Some(line_number)) = + (&frame.file_name, frame.line_number) + { + if !file_name.trim_start_matches('[').starts_with("deno:") { + // Source lookup expects a 0-based line number, ours are 1-based. + source_line = source_map_getter + .get_source_line(file_name, (line_number - 1) as usize); + source_line_frame_index = Some(i); + break; + } + } + } + } else if let Some(frame) = frames.first() { + if let Some(file_name) = &frame.file_name { + if !file_name.trim_start_matches('[').starts_with("deno:") { + source_line = msg + .get_source_line(scope) + .map(|v| v.to_rust_string_lossy(scope)); + source_line_frame_index = Some(0); + } + } + } + Self { name: e.name, message: e.message, exception_message, cause, - 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(), + source_line, + source_line_frame_index, frames, stack, } @@ -267,11 +324,8 @@ impl JsError { message: None, exception_message: msg.get(scope).to_rust_string_lossy(scope), cause: None, - script_resource_name: None, source_line: None, - line_number: None, - start_column: None, - end_column: None, + source_line_frame_index: None, frames: vec![], stack: None, } @@ -299,15 +353,13 @@ impl Display for JsError { return write!(f, "{}", stack); } } - write!(f, "{}", self.exception_message)?; - if let Some(script_resource_name) = &self.script_resource_name { - if self.line_number.is_some() && self.start_column.is_some() { - let source_loc = format_source_loc( - script_resource_name, - self.line_number.unwrap(), - self.start_column.unwrap(), - ); + let frame = self.frames.first(); + if let Some(frame) = frame { + if let (Some(f_), Some(l), Some(c)) = + (&frame.file_name, frame.line_number, frame.column_number) + { + let source_loc = format_source_loc(f_, l, c); write!(f, "\n at {}", source_loc)?; } } diff --git a/core/lib.rs b/core/lib.rs index 652ad2cd6..9a8cc8ef9 100644 --- a/core/lib.rs +++ b/core/lib.rs @@ -16,6 +16,7 @@ mod ops_builtin; mod ops_metrics; mod resources; mod runtime; +mod source_map; // Re-exports pub use anyhow; @@ -28,6 +29,7 @@ pub use serde_v8::Buffer as ZeroCopyBuf; pub use serde_v8::ByteString; pub use serde_v8::StringOrBuffer; pub use serde_v8::U16String; +pub use sourcemap; pub use url; pub use v8; @@ -96,6 +98,7 @@ pub use crate::runtime::JsRuntime; pub use crate::runtime::RuntimeOptions; pub use crate::runtime::SharedArrayBufferStore; pub use crate::runtime::Snapshot; +pub use crate::source_map::SourceMapGetter; pub use deno_ops::op; pub fn v8_version() -> &'static str { diff --git a/core/runtime.rs b/core/runtime.rs index a6442ce97..714f1aba8 100644 --- a/core/runtime.rs +++ b/core/runtime.rs @@ -17,6 +17,7 @@ use crate::modules::NoopModuleLoader; use crate::op_void_async; use crate::op_void_sync; use crate::ops::*; +use crate::source_map::SourceMapGetter; use crate::Extension; use crate::OpMiddlewareFn; use crate::OpResult; @@ -159,6 +160,7 @@ pub(crate) struct JsRuntimeState { /// A counter used to delay our dynamic import deadlock detection by one spin /// of the event loop. dyn_module_evaluate_idle_counter: u32, + pub(crate) source_map_getter: Option<Box<dyn SourceMapGetter>>, pub(crate) js_error_create_fn: Rc<JsErrorCreateFn>, pub(crate) pending_ops: FuturesUnordered<PendingOpFuture>, pub(crate) unrefed_ops: HashSet<i32>, @@ -225,6 +227,9 @@ fn v8_init(v8_platform: Option<v8::SharedRef<v8::Platform>>) { #[derive(Default)] pub struct RuntimeOptions { + /// Source map reference for errors. + pub source_map_getter: Option<Box<dyn SourceMapGetter>>, + /// Allows a callback to be set whenever a V8 exception is made. This allows /// the caller to wrap the JsError into an error. By default this callback /// is set to `JsError::create()`. @@ -380,6 +385,7 @@ impl JsRuntime { js_uncaught_exception_cb: None, has_tick_scheduled: false, js_wasm_streaming_cb: None, + source_map_getter: options.source_map_getter, js_error_create_fn, pending_ops: FuturesUnordered::new(), unrefed_ops: HashSet::new(), @@ -1023,7 +1029,6 @@ pub(crate) fn exception_to_err_result<'s, T>( in_promise: bool, ) -> Result<T, Error> { let state_rc = JsRuntime::state(scope); - let mut state = state_rc.borrow_mut(); let is_terminating_exception = scope.is_execution_terminating(); let mut exception = exception; @@ -1036,6 +1041,7 @@ pub(crate) fn exception_to_err_result<'s, T>( // If the termination is the result of a `Deno.core.terminate` call, we want // to use the exception that was passed to it rather than the exception that // was passed to this function. + let mut state = state_rc.borrow_mut(); exception = state .explicit_terminate_exception .take() @@ -1058,7 +1064,7 @@ pub(crate) fn exception_to_err_result<'s, T>( js_error.exception_message.trim_start_matches("Uncaught ") ); } - let js_error = (state.js_error_create_fn)(js_error); + let js_error = (state_rc.borrow().js_error_create_fn)(js_error); if is_terminating_exception { // Re-enable exception termination. @@ -2158,7 +2164,8 @@ pub mod tests { let r = runtime.execute_script("i.js", src); let e = r.unwrap_err(); let js_error = e.downcast::<JsError>().unwrap(); - assert_eq!(js_error.end_column, Some(11)); + let frame = js_error.frames.first().unwrap(); + assert_eq!(frame.column_number, Some(12)); } #[test] @@ -2500,7 +2507,7 @@ main(); "#, ); let expected_error = r#"Uncaught SyntaxError: Invalid or unexpected token - at error_without_stack.js:3:14"#; + at error_without_stack.js:3:15"#; assert_eq!(result.unwrap_err().to_string(), expected_error); } diff --git a/core/source_map.rs b/core/source_map.rs new file mode 100644 index 000000000..d33b66586 --- /dev/null +++ b/core/source_map.rs @@ -0,0 +1,99 @@ +// Copyright 2018-2022 the Deno authors. All rights reserved. MIT license. + +//! This mod provides functions to remap a `JsError` based on a source map. + +use crate::resolve_url; +pub use sourcemap::SourceMap; +use std::collections::HashMap; +use std::str; + +pub trait SourceMapGetter: Sync + Send { + /// Returns the raw source map file. + fn get_source_map(&self, file_name: &str) -> Option<Vec<u8>>; + fn get_source_line( + &self, + file_name: &str, + line_number: usize, + ) -> Option<String>; +} + +/// Cached filename lookups. The key can be None if a previous lookup failed to +/// find a SourceMap. +pub type CachedMaps = HashMap<String, Option<SourceMap>>; + +pub fn apply_source_map<G: SourceMapGetter + ?Sized>( + file_name: String, + line_number: i64, + column_number: i64, + mappings_map: &mut CachedMaps, + getter: &G, +) -> (String, i64, i64, Option<String>) { + // Lookup expects 0-based line and column numbers, but ours are 1-based. + let line_number = line_number - 1; + let column_number = column_number - 1; + + let default_pos = (file_name.clone(), line_number, column_number, None); + let maybe_source_map = get_mappings(&file_name, mappings_map, getter); + let (file_name, line_number, column_number, source_line) = + match maybe_source_map { + None => default_pos, + Some(source_map) => { + match source_map.lookup_token(line_number as u32, column_number as u32) + { + None => default_pos, + Some(token) => match token.get_source() { + None => default_pos, + Some(source_file_name) => { + // The `source_file_name` written by tsc in the source map is + // sometimes only the basename of the URL, or has unwanted `<`/`>` + // around it. Use the `file_name` we get from V8 if + // `source_file_name` does not parse as a URL. + let file_name = match resolve_url(source_file_name) { + Ok(m) if m.scheme() == "blob" => file_name, + Ok(m) => m.to_string(), + Err(_) => file_name, + }; + let source_line = + if let Some(source_view) = token.get_source_view() { + source_view + .get_line(token.get_src_line()) + .map(|s| s.to_string()) + } else { + None + }; + ( + file_name, + i64::from(token.get_src_line()), + i64::from(token.get_src_col()), + source_line, + ) + } + }, + } + } + }; + let source_line = source_line + .or_else(|| getter.get_source_line(&file_name, line_number as usize)); + (file_name, line_number + 1, column_number + 1, source_line) +} + +fn get_mappings<'a, G: SourceMapGetter + ?Sized>( + file_name: &str, + mappings_map: &'a mut CachedMaps, + getter: &G, +) -> &'a Option<SourceMap> { + mappings_map + .entry(file_name.to_string()) + .or_insert_with(|| parse_map_string(file_name, getter)) +} + +// TODO(kitsonk) parsed source maps should probably be cached in state in +// the module meta data. +fn parse_map_string<G: SourceMapGetter + ?Sized>( + file_name: &str, + getter: &G, +) -> Option<SourceMap> { + getter + .get_source_map(file_name) + .and_then(|raw_source_map| SourceMap::from_slice(&raw_source_map).ok()) +} |