1
0
Fork 0
mirror of https://github.com/denoland/deno.git synced 2024-11-24 15:19:26 -05:00

fix(core): preserve syntax error locations in dynamic imports (#18664)

Fixes #6259.

Adds the location for v8 syntax errors to the message (`message += " at
{location}"`) when rethrowing them for dynamic imports.

Discussing with @bartlomieju on discord I proposed just preserving v8's
error and not reconstructing it, allowing the standard stack trace to
just point to the syntax error instead of the dynamic import. But on
further thought this way has parity with SWC's syntax errors + has the
advantage of showing both the syntax error and dynamic import location.

```ts
// temp.js
await import("./temp2.js");

// temp2.js
function foo() {
  await Promise.resolve();
}

// Before:
// error: Uncaught (in promise) SyntaxError: Unexpected reserved word
// await import("./temp2.js");
// ^
//     at async file:///.../temp.js:1:1

// After:
// error: Uncaught (in promise) SyntaxError: Unexpected reserved word at file:///.../temp2.js:2:3
// await import("./temp2.js");
// ^
//     at async file:///.../temp.js:1:1
```
This commit is contained in:
Nayeem Rahman 2023-04-12 12:45:36 +01:00 committed by Levente Kurusa
parent 477f49bca2
commit b183737fc9
6 changed files with 88 additions and 99 deletions

View file

@ -4352,6 +4352,12 @@ itest!(node_prefix_missing {
exit_code: 1, exit_code: 1,
}); });
itest!(dynamic_import_syntax_error {
args: "run -A run/dynamic_import_syntax_error.js",
output: "run/dynamic_import_syntax_error.js.out",
exit_code: 1,
});
itest!(extension_import { itest!(extension_import {
args: "run run/extension_import.ts", args: "run run/extension_import.ts",
output: "run/extension_import.ts.out", output: "run/extension_import.ts.out",

View file

@ -0,0 +1 @@
await import("./dynamic_import_syntax_error_import.js");

View file

@ -0,0 +1,4 @@
error: Uncaught (in promise) SyntaxError: Unexpected reserved word at [WILDCARD]/dynamic_import_syntax_error_import.js:3:3
await import("./dynamic_import_syntax_error_import.js");
^
at async [WILDCARD]/dynamic_import_syntax_error.js:1:1

View file

@ -0,0 +1,5 @@
// deno-lint-ignore-file
function foo() {
await Promise.resolve();
}
foo();

View file

@ -7,6 +7,7 @@ use log::debug;
use v8::MapFnTo; use v8::MapFnTo;
use crate::error::is_instance_of_error; use crate::error::is_instance_of_error;
use crate::error::JsStackFrame;
use crate::modules::get_asserted_module_type_from_assertions; use crate::modules::get_asserted_module_type_from_assertions;
use crate::modules::parse_import_assertions; use crate::modules::parse_import_assertions;
use crate::modules::resolve_helper; use crate::modules::resolve_helper;
@ -436,26 +437,26 @@ fn catch_dynamic_import_promise_error(
if is_instance_of_error(scope, arg) { if is_instance_of_error(scope, arg) {
let e: crate::error::NativeJsError = serde_v8::from_v8(scope, arg).unwrap(); let e: crate::error::NativeJsError = serde_v8::from_v8(scope, arg).unwrap();
let name = e.name.unwrap_or_else(|| "Error".to_string()); let name = e.name.unwrap_or_else(|| "Error".to_string());
let message = v8::Exception::create_message(scope, arg); let msg = v8::Exception::create_message(scope, arg);
if message.get_stack_trace(scope).unwrap().get_frame_count() == 0 { if msg.get_stack_trace(scope).unwrap().get_frame_count() == 0 {
let arg: v8::Local<v8::Object> = arg.try_into().unwrap(); let arg: v8::Local<v8::Object> = arg.try_into().unwrap();
let message_key = let message_key =
v8::String::new_external_onebyte_static(scope, b"message").unwrap(); v8::String::new_external_onebyte_static(scope, b"message").unwrap();
let message = arg.get(scope, message_key.into()).unwrap(); let message = arg.get(scope, message_key.into()).unwrap();
let mut message: v8::Local<v8::String> = message.try_into().unwrap();
if let Some(stack_frame) = JsStackFrame::from_v8_message(scope, msg) {
if let Some(location) = stack_frame.maybe_format_location() {
let str =
format!("{} at {location}", message.to_rust_string_lossy(scope));
message = v8::String::new(scope, &str).unwrap();
}
}
let exception = match name.as_str() { let exception = match name.as_str() {
"RangeError" => { "RangeError" => v8::Exception::range_error(scope, message),
v8::Exception::range_error(scope, message.try_into().unwrap()) "TypeError" => v8::Exception::type_error(scope, message),
} "SyntaxError" => v8::Exception::syntax_error(scope, message),
"TypeError" => { "ReferenceError" => v8::Exception::reference_error(scope, message),
v8::Exception::type_error(scope, message.try_into().unwrap()) _ => v8::Exception::error(scope, message),
}
"SyntaxError" => {
v8::Exception::syntax_error(scope, message.try_into().unwrap())
}
"ReferenceError" => {
v8::Exception::reference_error(scope, message.try_into().unwrap())
}
_ => v8::Exception::error(scope, message.try_into().unwrap()),
}; };
let code_key = let code_key =
v8::String::new_external_onebyte_static(scope, b"code").unwrap(); v8::String::new_external_onebyte_static(scope, b"code").unwrap();

View file

@ -195,6 +195,44 @@ impl JsStackFrame {
promise_index: None, promise_index: None,
} }
} }
/// Gets the source mapped stack frame corresponding to the
/// (script_resource_name, line_number, column_number) from a v8 message.
/// For non-syntax errors, it should also correspond to the first stack frame.
pub fn from_v8_message<'a>(
scope: &'a mut v8::HandleScope,
message: v8::Local<'a, v8::Message>,
) -> Option<Self> {
let f = message.get_script_resource_name(scope)?;
let f: v8::Local<v8::String> = f.try_into().ok()?;
let f = f.to_rust_string_lossy(scope);
let l = message.get_line_number(scope)? as i64;
// V8's column numbers are 0-based, we want 1-based.
let c = message.get_start_column() as i64 + 1;
let state_rc = JsRuntime::state(scope);
let state = &mut *state_rc.borrow_mut();
if let Some(source_map_getter) = &state.source_map_getter {
let (f, l, c) = apply_source_map(
f,
l,
c,
&mut state.source_map_cache,
source_map_getter.as_ref(),
);
Some(JsStackFrame::from_location(Some(f), Some(l), Some(c)))
} else {
Some(JsStackFrame::from_location(Some(f), Some(l), Some(c)))
}
}
pub fn maybe_format_location(&self) -> Option<String> {
Some(format!(
"{}:{}:{}",
self.file_name.as_ref()?,
self.line_number?,
self.column_number?
))
}
} }
fn get_property<'a>( fn get_property<'a>(
@ -231,42 +269,18 @@ impl JsError {
let scope = &mut v8::HandleScope::new(scope); let scope = &mut v8::HandleScope::new(scope);
let exception_message = msg.get(scope).to_rust_string_lossy(scope); let exception_message = msg.get(scope).to_rust_string_lossy(scope);
let state_rc = JsRuntime::state(scope);
// Convert them into Vec<JsStackFrame> // Convert them into Vec<JsStackFrame>
let mut frames: Vec<JsStackFrame> = vec![]; let mut frames: Vec<JsStackFrame> = vec![];
let mut source_line = None; let mut source_line = None;
let mut source_line_frame_index = None; let mut source_line_frame_index = None;
if let Some(stack_frame) = JsStackFrame::from_v8_message(scope, msg) {
frames = vec![stack_frame];
}
{ {
let state_rc = JsRuntime::state(scope);
let state = &mut *state_rc.borrow_mut(); let state = &mut *state_rc.borrow_mut();
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 state.source_map_cache,
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))];
}
}
if let Some(source_map_getter) = &state.source_map_getter { if let Some(source_map_getter) = &state.source_map_getter {
for (i, frame) in frames.iter().enumerate() { for (i, frame) in frames.iter().enumerate() {
if let (Some(file_name), Some(line_number)) = if let (Some(file_name), Some(line_number)) =
@ -377,48 +391,21 @@ impl JsError {
Some(frames_v8) => serde_v8::from_v8(scope, frames_v8.into()).unwrap(), Some(frames_v8) => serde_v8::from_v8(scope, frames_v8.into()).unwrap(),
None => vec![], None => vec![],
}; };
let mut source_line = None; let mut source_line = None;
let mut source_line_frame_index = None; let mut source_line_frame_index = None;
{
let state_rc = JsRuntime::state(scope);
let state = &mut *state_rc.borrow_mut();
// When the stack frame array is empty, but the source location given by // When the stack frame array is empty, but the source location given by
// (script_resource_name, line_number, start_column + 1) exists, this is // (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 // likely a syntax error. For the sake of formatting we treat it like it
// was given as a single stack frame. // was given as a single stack frame.
if frames.is_empty() { if frames.is_empty() {
let script_resource_name = msg if let Some(stack_frame) = JsStackFrame::from_v8_message(scope, msg) {
.get_script_resource_name(scope) frames = vec![stack_frame];
.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 state_rc = JsRuntime::state(scope);
let c = c + 1; let state = &mut *state_rc.borrow_mut();
if let Some(source_map_getter) = &state.source_map_getter {
let (f, l, c) = apply_source_map(
f,
l,
c,
&mut state.source_map_cache,
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))];
}
}
}
if let Some(source_map_getter) = &state.source_map_getter { if let Some(source_map_getter) = &state.source_map_getter {
for (i, frame) in frames.iter().enumerate() { for (i, frame) in frames.iter().enumerate() {
if let (Some(file_name), Some(line_number)) = if let (Some(file_name), Some(line_number)) =
@ -502,16 +489,6 @@ impl JsError {
impl std::error::Error for JsError {} impl std::error::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 { impl Display for JsError {
fn fmt(&self, f: &mut Formatter) -> fmt::Result { fn fmt(&self, f: &mut Formatter) -> fmt::Result {
if let Some(stack) = &self.stack { if let Some(stack) = &self.stack {
@ -521,14 +498,9 @@ impl Display for JsError {
} }
} }
write!(f, "{}", self.exception_message)?; write!(f, "{}", self.exception_message)?;
let frame = self.frames.first(); let location = self.frames.first().and_then(|f| f.maybe_format_location());
if let Some(frame) = frame { if let Some(location) = location {
if let (Some(f_), Some(l), Some(c)) = write!(f, "\n at {location}")?;
(&frame.file_name, frame.line_number, frame.column_number)
{
let source_loc = format_source_loc(f_, l, c);
write!(f, "\n at {source_loc}")?;
}
} }
Ok(()) Ok(())
} }