Skip to content

Commit 2e27bf7

Browse files
authored
fix: apply source map to callsite objects (#1020)
Ref denoland/deno#25794. Lazily source map the callsite if `getFileName`, `getLineNumber`, `getColumnNumber`, or `toString` is called. Cache the result on the callsite wrapper object as an array of `[fileName, lineNumber, columnNumber]`, and then use the cached value for subsequent calls.
1 parent e373639 commit 2e27bf7

File tree

9 files changed

+276
-59
lines changed

9 files changed

+276
-59
lines changed

core/error.rs

Lines changed: 236 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1015,6 +1015,7 @@ v8_static_strings::v8_static_strings! {
10151015
TO_STRING = "toString",
10161016
PREPARE_STACK_TRACE = "prepareStackTrace",
10171017
ORIGINAL = "deno_core::original_call_site",
1018+
SOURCE_MAPPED_INFO = "deno_core::source_mapped_call_site_info",
10181019
ERROR_RECEIVER_IS_NOT_VALID_CALLSITE_OBJECT = "The receiver is not a valid callsite object.",
10191020
}
10201021

@@ -1026,6 +1027,13 @@ pub(crate) fn original_call_site_key<'a>(
10261027
v8::Private::for_api(scope, Some(name))
10271028
}
10281029

1030+
pub(crate) fn source_mapped_info_key<'a>(
1031+
scope: &mut v8::HandleScope<'a>,
1032+
) -> v8::Local<'a, v8::Private> {
1033+
let name = SOURCE_MAPPED_INFO.v8_string(scope).unwrap();
1034+
v8::Private::for_api(scope, Some(name))
1035+
}
1036+
10291037
fn make_patched_callsite<'s>(
10301038
scope: &mut v8::HandleScope<'s>,
10311039
callsite: v8::Local<'s, v8::Object>,
@@ -1095,44 +1103,186 @@ fn maybe_to_path_str(string: &str) -> Option<String> {
10951103
}
10961104

10971105
pub mod callsite_fns {
1106+
use capacity_builder::StringBuilder;
1107+
1108+
use crate::convert;
1109+
use crate::FromV8;
1110+
use crate::ToV8;
1111+
10981112
use super::*;
10991113

1114+
enum SourceMappedCallsiteInfo<'a> {
1115+
Ref(v8::Local<'a, v8::Array>),
1116+
Value {
1117+
file_name: v8::Local<'a, v8::Value>,
1118+
line_number: v8::Local<'a, v8::Value>,
1119+
column_number: v8::Local<'a, v8::Value>,
1120+
},
1121+
}
1122+
impl<'a> SourceMappedCallsiteInfo<'a> {
1123+
#[inline]
1124+
fn file_name(
1125+
&self,
1126+
scope: &mut v8::HandleScope<'a>,
1127+
) -> v8::Local<'a, v8::Value> {
1128+
match self {
1129+
Self::Ref(array) => array.get_index(scope, 0).unwrap(),
1130+
Self::Value { file_name, .. } => *file_name,
1131+
}
1132+
}
1133+
#[inline]
1134+
fn line_number(
1135+
&self,
1136+
scope: &mut v8::HandleScope<'a>,
1137+
) -> v8::Local<'a, v8::Value> {
1138+
match self {
1139+
Self::Ref(array) => array.get_index(scope, 1).unwrap(),
1140+
Self::Value { line_number, .. } => *line_number,
1141+
}
1142+
}
1143+
#[inline]
1144+
fn column_number(
1145+
&self,
1146+
scope: &mut v8::HandleScope<'a>,
1147+
) -> v8::Local<'a, v8::Value> {
1148+
match self {
1149+
Self::Ref(array) => array.get_index(scope, 2).unwrap(),
1150+
Self::Value { column_number, .. } => *column_number,
1151+
}
1152+
}
1153+
}
1154+
1155+
type MaybeValue<'a> = Option<v8::Local<'a, v8::Value>>;
1156+
1157+
fn maybe_apply_source_map<'a>(
1158+
scope: &mut v8::HandleScope<'a>,
1159+
file_name: MaybeValue<'a>,
1160+
line_number: MaybeValue<'a>,
1161+
column_number: MaybeValue<'a>,
1162+
) -> Option<(String, i64, i64)> {
1163+
let file_name = serde_v8::to_utf8(file_name?.try_cast().ok()?, scope);
1164+
let convert::Number(line_number) =
1165+
FromV8::from_v8(scope, line_number?).ok()?;
1166+
let convert::Number(column_number) =
1167+
FromV8::from_v8(scope, column_number?).ok()?;
1168+
1169+
let state = JsRuntime::state_from(scope);
1170+
let mut source_mapper = state.source_mapper.borrow_mut();
1171+
let (mapped_file_name, mapped_line_number, mapped_column_number) =
1172+
apply_source_map(
1173+
&mut source_mapper,
1174+
Cow::Owned(file_name),
1175+
line_number,
1176+
column_number,
1177+
);
1178+
Some((
1179+
mapped_file_name.into_owned(),
1180+
mapped_line_number,
1181+
mapped_column_number,
1182+
))
1183+
}
1184+
fn source_mapped_call_site_info<'a>(
1185+
scope: &mut v8::HandleScope<'a>,
1186+
callsite: v8::Local<'a, v8::Object>,
1187+
) -> Option<SourceMappedCallsiteInfo<'a>> {
1188+
let key = source_mapped_info_key(scope);
1189+
// return the cached value if it exists
1190+
if let Some(info) = callsite.get_private(scope, key) {
1191+
if let Ok(array) = info.try_cast::<v8::Array>() {
1192+
return Some(SourceMappedCallsiteInfo::Ref(array));
1193+
}
1194+
}
1195+
let orig_callsite = original_call_site(scope, callsite)?;
1196+
1197+
let file_name =
1198+
call_method::<v8::Value>(scope, orig_callsite, super::GET_FILE_NAME, &[]);
1199+
let line_number = call_method::<v8::Value>(
1200+
scope,
1201+
orig_callsite,
1202+
super::GET_LINE_NUMBER,
1203+
&[],
1204+
);
1205+
let column_number = call_method::<v8::Value>(
1206+
scope,
1207+
orig_callsite,
1208+
super::GET_COLUMN_NUMBER,
1209+
&[],
1210+
);
1211+
1212+
let info = v8::Array::new(scope, 3);
1213+
1214+
// if the types are right, apply the source map, otherwise just take them as is
1215+
if let Some((mapped_file_name, mapped_line_number, mapped_column_number)) =
1216+
maybe_apply_source_map(scope, file_name, line_number, column_number)
1217+
{
1218+
let mapped_file_name_trimmed =
1219+
maybe_to_path_str(&mapped_file_name).unwrap_or(mapped_file_name);
1220+
let mapped_file_name = crate::FastString::from(mapped_file_name_trimmed)
1221+
.v8_string(scope)
1222+
.unwrap();
1223+
let Ok(mapped_line_number) =
1224+
convert::Number(mapped_line_number).to_v8(scope);
1225+
let Ok(mapped_column_number) =
1226+
convert::Number(mapped_column_number).to_v8(scope);
1227+
info.set_index(scope, 0, mapped_file_name.into());
1228+
info.set_index(scope, 1, mapped_line_number);
1229+
info.set_index(scope, 2, mapped_column_number);
1230+
callsite.set_private(scope, key, info.into());
1231+
Some(SourceMappedCallsiteInfo::Value {
1232+
file_name: mapped_file_name.into(),
1233+
line_number: mapped_line_number,
1234+
column_number: mapped_column_number,
1235+
})
1236+
} else {
1237+
let file_name = file_name.unwrap_or_else(|| v8::undefined(scope).into());
1238+
let line_number =
1239+
line_number.unwrap_or_else(|| v8::undefined(scope).into());
1240+
let column_number =
1241+
column_number.unwrap_or_else(|| v8::undefined(scope).into());
1242+
info.set_index(scope, 0, file_name);
1243+
info.set_index(scope, 1, line_number);
1244+
info.set_index(scope, 2, column_number);
1245+
callsite.set_private(scope, key, info.into());
1246+
Some(SourceMappedCallsiteInfo::Ref(info))
1247+
}
1248+
}
1249+
11001250
make_callsite_fn!(get_this, GET_THIS);
11011251
make_callsite_fn!(get_type_name, GET_TYPE_NAME);
11021252
make_callsite_fn!(get_function, GET_FUNCTION);
11031253
make_callsite_fn!(get_function_name, GET_FUNCTION_NAME);
11041254
make_callsite_fn!(get_method_name, GET_METHOD_NAME);
11051255

1106-
pub fn get_file_name(
1107-
scope: &mut v8::HandleScope<'_>,
1108-
args: v8::FunctionCallbackArguments<'_>,
1256+
pub fn get_file_name<'a>(
1257+
scope: &mut v8::HandleScope<'a>,
1258+
args: v8::FunctionCallbackArguments<'a>,
11091259
mut rv: v8::ReturnValue<'_>,
11101260
) {
1111-
let Some(orig) = original_call_site(scope, args.this()) else {
1112-
return;
1113-
};
1114-
// call getFileName
1115-
let orig_ret =
1116-
call_method::<v8::Value>(scope, orig, super::GET_FILE_NAME, &[]);
1117-
if let Some(ret_val) =
1118-
orig_ret.and_then(|v| v.try_cast::<v8::String>().ok())
1119-
{
1120-
// strip off `file://`
1121-
let string = ret_val.to_rust_string_lossy(scope);
1122-
if let Some(file_name) = maybe_to_path_str(&string) {
1123-
let v8_str = crate::FastString::from(file_name)
1124-
.v8_string(scope)
1125-
.unwrap()
1126-
.into();
1127-
rv.set(v8_str);
1128-
} else {
1129-
rv.set(ret_val.into());
1130-
}
1261+
if let Some(info) = source_mapped_call_site_info(scope, args.this()) {
1262+
rv.set(info.file_name(scope));
1263+
}
1264+
}
1265+
1266+
pub fn get_line_number<'a>(
1267+
scope: &mut v8::HandleScope<'a>,
1268+
args: v8::FunctionCallbackArguments<'a>,
1269+
mut rv: v8::ReturnValue<'_>,
1270+
) {
1271+
if let Some(info) = source_mapped_call_site_info(scope, args.this()) {
1272+
rv.set(info.line_number(scope));
1273+
}
1274+
}
1275+
1276+
pub fn get_column_number<'a>(
1277+
scope: &mut v8::HandleScope<'a>,
1278+
args: v8::FunctionCallbackArguments<'a>,
1279+
mut rv: v8::ReturnValue<'_>,
1280+
) {
1281+
if let Some(info) = source_mapped_call_site_info(scope, args.this()) {
1282+
rv.set(info.column_number(scope));
11311283
}
11321284
}
11331285

1134-
make_callsite_fn!(get_line_number, GET_LINE_NUMBER);
1135-
make_callsite_fn!(get_column_number, GET_COLUMN_NUMBER);
11361286
make_callsite_fn!(get_eval_origin, GET_EVAL_ORIGIN);
11371287
make_callsite_fn!(is_toplevel, IS_TOPLEVEL);
11381288
make_callsite_fn!(is_eval, IS_EVAL);
@@ -1146,12 +1296,65 @@ pub mod callsite_fns {
11461296
GET_SCRIPT_NAME_OR_SOURCE_URL
11471297
);
11481298

1149-
pub fn to_string(
1150-
scope: &mut v8::HandleScope<'_>,
1151-
args: v8::FunctionCallbackArguments<'_>,
1299+
// the bulk of the to_string logic
1300+
fn to_string_inner<'e>(
1301+
scope: &mut v8::HandleScope<'e>,
1302+
this: v8::Local<'e, v8::Object>,
1303+
orig: v8::Local<'e, Object>,
1304+
orig_to_string_v8: v8::Local<'e, v8::String>,
1305+
) -> Option<v8::Local<'e, v8::String>> {
1306+
let orig_to_string = serde_v8::to_utf8(orig_to_string_v8, scope);
1307+
// `this[kOriginalCallsite].getFileName()`
1308+
let orig_file_name =
1309+
call_method::<v8::Value>(scope, orig, GET_FILE_NAME, &[])
1310+
.and_then(|v| v.try_cast::<v8::String>().ok())?;
1311+
let orig_line_number =
1312+
call_method::<v8::Value>(scope, orig, GET_LINE_NUMBER, &[])
1313+
.and_then(|v| v.try_cast::<v8::Number>().ok())?;
1314+
let orig_column_number =
1315+
call_method::<v8::Value>(scope, orig, GET_COLUMN_NUMBER, &[])
1316+
.and_then(|v| v.try_cast::<v8::Number>().ok())?;
1317+
let orig_file_name = serde_v8::to_utf8(orig_file_name, scope);
1318+
let orig_line_number = orig_line_number.value() as i64;
1319+
let orig_column_number = orig_column_number.value() as i64;
1320+
let orig_file_name_line_col =
1321+
fmt_file_line_col(&orig_file_name, orig_line_number, orig_column_number);
1322+
let mapped = source_mapped_call_site_info(scope, this)?;
1323+
let mapped_file_name = mapped.file_name(scope).to_rust_string_lossy(scope);
1324+
let mapped_line_num = mapped
1325+
.line_number(scope)
1326+
.try_cast::<v8::Number>()
1327+
.ok()
1328+
.map(|n| n.value() as i64)?;
1329+
let mapped_col_num =
1330+
mapped.column_number(scope).cast::<v8::Number>().value() as i64;
1331+
let file_name_line_col =
1332+
fmt_file_line_col(&mapped_file_name, mapped_line_num, mapped_col_num);
1333+
// replace file URL with file path, and source map in original `toString`
1334+
let to_string = orig_to_string
1335+
.replace(&orig_file_name_line_col, &file_name_line_col)
1336+
.replace(&orig_file_name, &mapped_file_name); // maybe unnecessary?
1337+
Some(crate::FastString::from(to_string).v8_string(scope).unwrap())
1338+
}
1339+
1340+
fn fmt_file_line_col(file: &str, line: i64, col: i64) -> String {
1341+
StringBuilder::build(|builder| {
1342+
builder.append(file);
1343+
builder.append(':');
1344+
builder.append(line);
1345+
builder.append(':');
1346+
builder.append(col);
1347+
})
1348+
.unwrap()
1349+
}
1350+
1351+
pub fn to_string<'a>(
1352+
scope: &mut v8::HandleScope<'a>,
1353+
args: v8::FunctionCallbackArguments<'a>,
11521354
mut rv: v8::ReturnValue<'_>,
11531355
) {
1154-
let Some(orig) = original_call_site(scope, args.this()) else {
1356+
let this = args.this();
1357+
let Some(orig) = original_call_site(scope, this) else {
11551358
return;
11561359
};
11571360
// `this[kOriginalCallsite].toString()`
@@ -1160,24 +1363,10 @@ pub mod callsite_fns {
11601363
else {
11611364
return;
11621365
};
1163-
let orig_to_string = serde_v8::to_utf8(orig_to_string_v8, scope);
1164-
// `this[kOriginalCallsite].getFileName()`
1165-
let orig_ret_file_name =
1166-
call_method::<v8::Value>(scope, orig, GET_FILE_NAME, &[]);
1167-
let Some(orig_file_name) =
1168-
orig_ret_file_name.and_then(|v| v.try_cast::<v8::String>().ok())
1169-
else {
1170-
return;
1171-
};
1172-
// replace file URL with file path in original `toString`
1173-
let orig_file_name = serde_v8::to_utf8(orig_file_name, scope);
1174-
if let Some(file_name) = maybe_to_path_str(&orig_file_name) {
1175-
let to_string = orig_to_string.replace(&orig_file_name, &file_name);
1176-
let v8_str = crate::FastString::from(to_string)
1177-
.v8_string(scope)
1178-
.unwrap()
1179-
.into();
1180-
rv.set(v8_str);
1366+
1367+
if let Some(v8_str) = to_string_inner(scope, this, orig, orig_to_string_v8)
1368+
{
1369+
rv.set(v8_str.into());
11811370
} else {
11821371
rv.set(orig_to_string_v8.into());
11831372
}

core/external.rs

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,7 @@ macro_rules! external {
1515
// SAFETY: Wash the pointer through black_box so the compiler cannot see what we're going to do with it and needs
1616
// to assume it will be used for valid purposes. We are taking the address of a static item, but we avoid taking an
1717
// intermediate mutable reference to make this safe.
18-
let ptr = ::std::hint::black_box(unsafe {
19-
::std::ptr::addr_of_mut!(DEFINITION)
20-
});
18+
let ptr = ::std::hint::black_box(::std::ptr::addr_of_mut!(DEFINITION));
2119
ptr as ::core::primitive::usize
2220
}
2321

rust-toolchain.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
[toolchain]
2-
channel = "1.81.0"
2+
channel = "1.82.0"
33
components = ["rustfmt", "clippy"]

testing/integration/error_callsite/error_callsite.out

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

testing/integration/error_get_file_name_to_string/error_get_file_name_to_string.out

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

testing/integration/error_prepare_stack_trace/error_prepare_stack_trace.out

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)