Repository: denoland/deno_core
Pull request #224 (2023-09-20 20:13:35)

feat: add core.currentUserCallSite()
This function returns the first call site in the current stack that is
"user" code, i.e. has a file name starting with neither `ext:` or
`node:`.


Patch:
diff --git a/core/01_core.js b/core/01_core.js
index 5ea1179..804ad0a 100644
--- a/core/01_core.js
+++ b/core/01_core.js
@@ -800,6 +800,16 @@ for (let i = 0; i < 10; i++) {
     op_shutdown: shutdown,
   } = ensureFastOps();
 
+  const callSiteRetBuf = new Uint32Array(2);
+  const callSiteRetBufU8 = new Uint8Array(callSiteRetBuf.buffer);
+
+  function currentUserCallSite() {
+    const fileName = ops.op_current_user_call_site(callSiteRetBufU8);
+    const lineNumber = callSiteRetBuf[0];
+    const columnNumber = callSiteRetBuf[1];
+    return { fileName, lineNumber, columnNumber };
+  }
+
   // Extra Deno.core.* exports
   const core = ObjectAssign(globalThis.Deno.core, {
     asyncStub,
@@ -867,6 +877,7 @@ for (let i = 0; i < 10; i++) {
     byteLength: (str) => ops.op_str_byte_length(str),
     build,
     setBuildInfo,
+    currentUserCallSite,
   });
 
   const internals = {};
diff --git a/core/ops_builtin.rs b/core/ops_builtin.rs
index da7d1cd..fc4286b 100644
--- a/core/ops_builtin.rs
+++ b/core/ops_builtin.rs
@@ -75,6 +75,7 @@ crate::extension!(
     ops_builtin_v8::op_op_names,
     ops_builtin_v8::op_apply_source_map,
     ops_builtin_v8::op_apply_source_map_filename,
+    ops_builtin_v8::op_current_user_call_site,
     ops_builtin_v8::op_set_format_exception_callback,
     ops_builtin_v8::op_event_loop_has_more_work,
     ops_builtin_v8::op_store_pending_promise_rejection,
diff --git a/core/ops_builtin_v8.rs b/core/ops_builtin_v8.rs
index 57bd7fc..68b7e6f 100644
--- a/core/ops_builtin_v8.rs
+++ b/core/ops_builtin_v8.rs
@@ -803,6 +803,15 @@ pub struct Location {
   column_number: u32,
 }
 
+fn write_line_and_col_to_ret_buf(
+  ret_buf: &mut [u8],
+  line_number: u32,
+  column_number: u32,
+) {
+  ret_buf[0..4].copy_from_slice(&line_number.to_le_bytes());
+  ret_buf[4..8].copy_from_slice(&column_number.to_le_bytes());
+}
+
 // Returns:
 // 0: no source mapping performed, use original location
 // 1: mapped line and column, but not file name. new line and column are in
@@ -822,8 +831,8 @@ pub fn op_apply_source_map(
   if ret_buf.len() != 8 {
     return Err(type_error("retBuf must be 8 bytes"));
   }
-  let mut cache = state.source_map_cache.borrow_mut();
   if let Some(source_map_getter) = state.source_map_getter.as_ref() {
+    let mut cache = state.source_map_cache.borrow_mut();
     let application = apply_source_map(
       file_name,
       line_number,
@@ -831,17 +840,13 @@ pub fn op_apply_source_map(
       &mut cache,
       &***source_map_getter,
     );
-    fn write_ret_buf(ret_buf: &mut [u8], line_number: u32, column_number: u32) {
-      ret_buf[0..4].copy_from_slice(&line_number.to_le_bytes());
-      ret_buf[4..8].copy_from_slice(&column_number.to_le_bytes());
-    }
     match application {
       SourceMapApplication::Unchanged => Ok(0),
       SourceMapApplication::LineAndColumn {
         line_number,
         column_number,
       } => {
-        write_ret_buf(ret_buf, line_number, column_number);
+        write_line_and_col_to_ret_buf(ret_buf, line_number, column_number);
         Ok(1)
       }
       SourceMapApplication::LineAndColumnAndFileName {
@@ -849,7 +854,7 @@ pub fn op_apply_source_map(
         column_number,
         file_name,
       } => {
-        write_ret_buf(ret_buf, line_number, column_number);
+        write_line_and_col_to_ret_buf(ret_buf, line_number, column_number);
         cache.stashed_file_name.replace(file_name);
         Ok(2)
       }
@@ -874,6 +879,73 @@ pub fn op_apply_source_map_filename(
     .ok_or_else(|| type_error("No stashed file name"))
 }
 
+#[op2(core)]
+#[string]
+pub fn op_current_user_call_site(
+  scope: &mut v8::HandleScope,
+  js_runtime_state: &mut JsRuntimeState,
+  #[buffer] ret_buf: &mut [u8],
+) -> String {
+  let stack_trace = v8::StackTrace::current_stack_trace(scope, 10).unwrap();
+  let frame_count = stack_trace.get_frame_count();
+  for i in 0..frame_count {
+    let frame = stack_trace.get_frame(scope, i).unwrap();
+    if !frame.is_user_javascript() {
+      continue;
+    }
+    let file_name = frame
+      .get_script_name(scope)
+      .unwrap()
+      .to_rust_string_lossy(scope);
+    // TODO: this condition should be configurable. It's a CLI assumption.
+    if (file_name.starts_with("ext:") || file_name.starts_with("node:"))
+      && i != frame_count - 1
+    {
+      continue;
+    }
+    let line_number = frame.get_line_number() as u32;
+    let column_number = frame.get_column() as u32;
+    let application = if let Some(source_map_getter) =
+      js_runtime_state.source_map_getter.as_ref()
+    {
+      let mut cache = js_runtime_state.source_map_cache.borrow_mut();
+      apply_source_map(
+        &file_name,
+        line_number,
+        column_number,
+        &mut cache,
+        &***source_map_getter,
+      )
+    } else {
+      SourceMapApplication::Unchanged
+    };
+
+    match application {
+      SourceMapApplication::Unchanged => {
+        write_line_and_col_to_ret_buf(ret_buf, line_number, column_number);
+        return file_name;
+      }
+      SourceMapApplication::LineAndColumn {
+        line_number,
+        column_number,
+      } => {
+        write_line_and_col_to_ret_buf(ret_buf, line_number, column_number);
+        return file_name;
+      }
+      SourceMapApplication::LineAndColumnAndFileName {
+        line_number,
+        column_number,
+        file_name,
+      } => {
+        write_line_and_col_to_ret_buf(ret_buf, line_number, column_number);
+        return file_name;
+      }
+    }
+  }
+
+  unreachable!("No stack frames found on stack at all");
+}
+
 /// Set a callback which formats exception messages as stored in
 /// `JsError::exception_message`. The callback is passed the error value and
 /// should return a string or `null`. If no callback is set or the callback
diff --git a/core/source_map.rs b/core/source_map.rs
index bbb733c..97a8cab 100644
--- a/core/source_map.rs
+++ b/core/source_map.rs
@@ -64,6 +64,8 @@ pub enum SourceMapApplication {
 /// Apply a source map to the passed location. If there is no source map for
 /// this location, or if the location remains unchanged after mapping, the
 /// changed values are returned.
+///
+/// Line and column numbers are 1-based.
 pub fn apply_source_map<G: SourceMapGetter + ?Sized>(
   file_name: &str,
   line_number: u32,


Test patch:
diff --git a/core/runtime/tests/ops.rs b/core/runtime/tests/ops.rs
index e361925..2c1e526 100644
--- a/core/runtime/tests/ops.rs
+++ b/core/runtime/tests/ops.rs
@@ -501,6 +501,22 @@ fn test_dispatch_stack_zero_copy_bufs() {
   assert_eq!(dispatch_count.load(Ordering::Relaxed), 1);
 }
 
+#[test]
+fn test_call_site() {
+  let (mut runtime, _) = setup(Mode::Async);
+  runtime
+    .execute_script_static(
+      "file:///filename.js",
+      r#"
+      const cs = Deno.core.currentUserCallSite();
+      assert(cs.fileName === "file:///filename.js");
+      assert(cs.lineNumber === 2);
+      assert(cs.columnNumber === 28);
+    "#,
+    )
+    .unwrap();
+}
+
 /// Test that long-running ops do not block dynamic imports from loading.
 // https://github.com/denoland/deno/issues/19903
 // https://github.com/denoland/deno/issues/19455