Skip to content

Commit

Permalink
apply source map to callsites objects
Browse files Browse the repository at this point in the history
  • Loading branch information
nathanwhit committed Jan 1, 2025
1 parent 56c88a8 commit b466e2c
Show file tree
Hide file tree
Showing 7 changed files with 268 additions and 53 deletions.
285 changes: 236 additions & 49 deletions core/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1008,6 +1008,7 @@ v8_static_strings::v8_static_strings! {
TO_STRING = "toString",
PREPARE_STACK_TRACE = "prepareStackTrace",
ORIGINAL = "deno_core::original_call_site",
SOURCE_MAPPED_INFO = "deno_core::source_mapped_call_site_info",
ERROR_RECEIVER_IS_NOT_VALID_CALLSITE_OBJECT = "The receiver is not a valid callsite object.",
}

Expand All @@ -1019,6 +1020,13 @@ pub(crate) fn original_call_site_key<'a>(
v8::Private::for_api(scope, Some(name))
}

pub(crate) fn source_mapped_info_key<'a>(
scope: &mut v8::HandleScope<'a>,
) -> v8::Local<'a, v8::Private> {
let name = SOURCE_MAPPED_INFO.v8_string(scope).unwrap();
v8::Private::for_api(scope, Some(name))
}

fn make_patched_callsite<'s>(
scope: &mut v8::HandleScope<'s>,
callsite: v8::Local<'s, v8::Object>,
Expand Down Expand Up @@ -1088,44 +1096,184 @@ fn maybe_to_path_str(string: &str) -> Option<String> {
}

pub mod callsite_fns {
use capacity_builder::StringBuilder;

use crate::{convert, FromV8, ToV8};

use super::*;

enum SourceMappedCallsiteInfo<'a> {
Ref(v8::Local<'a, v8::Array>),
Value {
file_name: v8::Local<'a, v8::Value>,
line_number: v8::Local<'a, v8::Value>,
column_number: v8::Local<'a, v8::Value>,
},
}
impl<'a> SourceMappedCallsiteInfo<'a> {
#[inline]
fn file_name(
&self,
scope: &mut v8::HandleScope<'a>,
) -> v8::Local<'a, v8::Value> {
match self {
Self::Ref(array) => array.get_index(scope, 0).unwrap(),
Self::Value { file_name, .. } => file_name.clone(),
}
}
#[inline]
fn line_number(
&self,
scope: &mut v8::HandleScope<'a>,
) -> v8::Local<'a, v8::Value> {
match self {
Self::Ref(array) => array.get_index(scope, 1).unwrap(),
Self::Value { line_number, .. } => line_number.clone(),
}
}
#[inline]
fn column_number(
&self,
scope: &mut v8::HandleScope<'a>,
) -> v8::Local<'a, v8::Value> {
match self {
Self::Ref(array) => array.get_index(scope, 2).unwrap(),
Self::Value { column_number, .. } => column_number.clone(),
}
}
}

type MaybeValue<'a> = Option<v8::Local<'a, v8::Value>>;

fn maybe_apply_source_map<'a>(
scope: &mut v8::HandleScope<'a>,
file_name: MaybeValue<'a>,
line_number: MaybeValue<'a>,
column_number: MaybeValue<'a>,
) -> Option<(String, i64, i64)> {
let file_name = serde_v8::to_utf8(file_name?.try_cast().ok()?, scope);
let convert::Number(line_number) =
FromV8::from_v8(scope, line_number?).ok()?;
let convert::Number(column_number) =
FromV8::from_v8(scope, column_number?).ok()?;

let state = JsRuntime::state_from(scope);
let mut source_mapper = state.source_mapper.borrow_mut();
let (mapped_file_name, mapped_line_number, mapped_column_number) =
apply_source_map(
&mut *source_mapper,
Cow::Owned(file_name),
line_number,
column_number,
);
Some((
mapped_file_name.into_owned(),
mapped_line_number,
mapped_column_number,
))
}
fn source_mapped_call_site_info<'a>(
scope: &mut v8::HandleScope<'a>,
callsite: v8::Local<'a, v8::Object>,
) -> Option<SourceMappedCallsiteInfo<'a>> {
let key = source_mapped_info_key(scope);
// return the cached value if it exists
if let Some(info) = callsite.get_private(scope, key) {
if let Ok(array) = info.try_cast::<v8::Array>() {
return Some(SourceMappedCallsiteInfo::Ref(array));
}
}
let orig_callsite = original_call_site(scope, callsite)?;

let file_name =
call_method::<v8::Value>(scope, orig_callsite, super::GET_FILE_NAME, &[]);
let line_number = call_method::<v8::Value>(
scope,
orig_callsite,
super::GET_LINE_NUMBER,
&[],
);
let column_number = call_method::<v8::Value>(
scope,
orig_callsite,
super::GET_COLUMN_NUMBER,
&[],
);

let info = v8::Array::new(scope, 3);

// if the types are right, apply the source map, otherwise just take them as is
if let Some((mapped_file_name, mapped_line_number, mapped_column_number)) =
maybe_apply_source_map(scope, file_name, line_number, column_number)
{
let mapped_file_name_trimmed = maybe_to_path_str(&mapped_file_name)
.unwrap_or_else(|| mapped_file_name);
let mapped_file_name = crate::FastString::from(mapped_file_name_trimmed)
.v8_string(scope)
.unwrap();
let Ok(mapped_line_number) =
convert::Number(mapped_line_number).to_v8(scope);
let Ok(mapped_column_number) =
convert::Number(mapped_column_number).to_v8(scope);
info.set_index(scope, 0, mapped_file_name.into());
info.set_index(scope, 1, mapped_line_number.into());
info.set_index(scope, 2, mapped_column_number.into());
callsite.set_private(scope, key, info.into());
Some(SourceMappedCallsiteInfo::Value {
file_name: mapped_file_name.into(),
line_number: mapped_line_number.into(),
column_number: mapped_column_number.into(),
})
} else {
let file_name = file_name.unwrap_or_else(|| v8::undefined(scope).into());
let line_number =
line_number.unwrap_or_else(|| v8::undefined(scope).into());
let column_number =
column_number.unwrap_or_else(|| v8::undefined(scope).into());
info.set_index(scope, 0, file_name);
info.set_index(scope, 1, line_number);
info.set_index(scope, 2, column_number);
callsite.set_private(scope, key, info.into());
Some(SourceMappedCallsiteInfo::Ref(info))
}
}

make_callsite_fn!(get_this, GET_THIS);
make_callsite_fn!(get_type_name, GET_TYPE_NAME);
make_callsite_fn!(get_function, GET_FUNCTION);
make_callsite_fn!(get_function_name, GET_FUNCTION_NAME);
make_callsite_fn!(get_method_name, GET_METHOD_NAME);

pub fn get_file_name(
scope: &mut v8::HandleScope<'_>,
args: v8::FunctionCallbackArguments<'_>,
mut rv: v8::ReturnValue<'_>,
pub fn get_file_name<'a, 'b, 'c>(
scope: &'c mut v8::HandleScope<'a>,
args: v8::FunctionCallbackArguments<'a>,
mut rv: v8::ReturnValue<'b>,
) {
let Some(orig) = original_call_site(scope, args.this()) else {
return;
};
// call getFileName
let orig_ret =
call_method::<v8::Value>(scope, orig, super::GET_FILE_NAME, &[]);
if let Some(ret_val) =
orig_ret.and_then(|v| v.try_cast::<v8::String>().ok())
{
// strip off `file://`
let string = ret_val.to_rust_string_lossy(scope);
if let Some(file_name) = maybe_to_path_str(&string) {
let v8_str = crate::FastString::from(file_name)
.v8_string(scope)
.unwrap()
.into();
rv.set(v8_str);
} else {
rv.set(ret_val.into());
}
if let Some(info) = source_mapped_call_site_info(scope, args.this()) {
rv.set(info.file_name(scope));
}
}

pub fn get_line_number<'a, 'b, 'c>(
scope: &'c mut v8::HandleScope<'a>,
args: v8::FunctionCallbackArguments<'a>,
mut rv: v8::ReturnValue<'b>,
) {
if let Some(info) = source_mapped_call_site_info(scope, args.this()) {
rv.set(info.line_number(scope));
}
}

pub fn get_column_number<'a, 'b, 'c>(
scope: &'c mut v8::HandleScope<'a>,
args: v8::FunctionCallbackArguments<'a>,
mut rv: v8::ReturnValue<'b>,
) {
if let Some(info) = source_mapped_call_site_info(scope, args.this()) {
rv.set(info.column_number(scope));
}
}

make_callsite_fn!(get_line_number, GET_LINE_NUMBER);
make_callsite_fn!(get_column_number, GET_COLUMN_NUMBER);
make_callsite_fn!(get_eval_origin, GET_EVAL_ORIGIN);
make_callsite_fn!(is_toplevel, IS_TOPLEVEL);
make_callsite_fn!(is_eval, IS_EVAL);
Expand All @@ -1139,12 +1287,65 @@ pub mod callsite_fns {
GET_SCRIPT_NAME_OR_SOURCE_URL
);

pub fn to_string(
scope: &mut v8::HandleScope<'_>,
args: v8::FunctionCallbackArguments<'_>,
mut rv: v8::ReturnValue<'_>,
// the bulk of the to_string logic
fn to_string_inner<'d, 'e>(
scope: &'d mut v8::HandleScope<'e>,
this: v8::Local<'e, v8::Object>,
orig: v8::Local<'e, Object>,
orig_to_string_v8: v8::Local<'e, v8::String>,
) -> Option<v8::Local<'e, v8::String>> {
let orig_to_string = serde_v8::to_utf8(orig_to_string_v8, scope);
// `this[kOriginalCallsite].getFileName()`
let orig_file_name =
call_method::<v8::Value>(scope, orig, GET_FILE_NAME, &[])
.and_then(|v| v.try_cast::<v8::String>().ok())?;
let orig_line_number =
call_method::<v8::Value>(scope, orig, GET_LINE_NUMBER, &[])
.and_then(|v| v.try_cast::<v8::Number>().ok())?;
let orig_column_number =
call_method::<v8::Value>(scope, orig, GET_COLUMN_NUMBER, &[])
.and_then(|v| v.try_cast::<v8::Number>().ok())?;
let orig_file_name = serde_v8::to_utf8(orig_file_name, scope);
let orig_line_number = orig_line_number.value() as i64;
let orig_column_number = orig_column_number.value() as i64;
let orig_file_name_line_col =
fmt_file_line_col(&orig_file_name, orig_line_number, orig_column_number);
let mapped = source_mapped_call_site_info(scope, this)?;
let mapped_file_name = mapped.file_name(scope).to_rust_string_lossy(scope);
let mapped_line_num = mapped
.line_number(scope)
.try_cast::<v8::Number>()
.ok()
.map(|n| n.value() as i64)?;
let mapped_col_num =
mapped.column_number(scope).cast::<v8::Number>().value() as i64;
let file_name_line_col =
fmt_file_line_col(&mapped_file_name, mapped_line_num, mapped_col_num);
// replace file URL with file path, and source map in original `toString`
let to_string = orig_to_string
.replace(&orig_file_name_line_col, &file_name_line_col)
.replace(&orig_file_name, &mapped_file_name); // maybe unnecessary?
Some(crate::FastString::from(to_string).v8_string(scope).unwrap())
}

fn fmt_file_line_col(file: &str, line: i64, col: i64) -> String {
StringBuilder::build(|builder| {
builder.append(file);
builder.append(':');
builder.append(line);
builder.append(':');
builder.append(col);
})
.unwrap()
}

pub fn to_string<'a, 'b, 'c>(
scope: &'b mut v8::HandleScope<'a>,
args: v8::FunctionCallbackArguments<'a>,
mut rv: v8::ReturnValue<'c>,
) {
let Some(orig) = original_call_site(scope, args.this()) else {
let this = args.this();
let Some(orig) = original_call_site(scope, this) else {
return;
};
// `this[kOriginalCallsite].toString()`
Expand All @@ -1153,24 +1354,10 @@ pub mod callsite_fns {
else {
return;
};
let orig_to_string = serde_v8::to_utf8(orig_to_string_v8, scope);
// `this[kOriginalCallsite].getFileName()`
let orig_ret_file_name =
call_method::<v8::Value>(scope, orig, GET_FILE_NAME, &[]);
let Some(orig_file_name) =
orig_ret_file_name.and_then(|v| v.try_cast::<v8::String>().ok())
else {
return;
};
// replace file URL with file path in original `toString`
let orig_file_name = serde_v8::to_utf8(orig_file_name, scope);
if let Some(file_name) = maybe_to_path_str(&orig_file_name) {
let to_string = orig_to_string.replace(&orig_file_name, &file_name);
let v8_str = crate::FastString::from(to_string)
.v8_string(scope)
.unwrap()
.into();
rv.set(v8_str);

if let Some(v8_str) = to_string_inner(scope, this, orig, orig_to_string_v8)
{
rv.set(v8_str.into());
} else {
rv.set(orig_to_string_v8.into());
}
Expand Down
2 changes: 1 addition & 1 deletion rust-toolchain.toml
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
[toolchain]
channel = "1.81.0"
channel = "1.82.0"
components = ["rustfmt", "clippy"]
4 changes: 2 additions & 2 deletions testing/integration/error_callsite/error_callsite.out

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
type Thing = {
name: string;
};

try {
throw new Error("This is an error");
} catch (e) {
Error.prepareStackTrace = (_, stack) => {
return stack.map((s) => ({
filename: s.getFileName(),
methodName: s.getMethodName(),
functionName: s.getFunctionName(),
lineNumber: s.getLineNumber(),
columnNumber: s.getColumnNumber(),
}));
};
console.log(e.stack);
}
Loading

0 comments on commit b466e2c

Please sign in to comment.