Repository: denoland/deno_core
Pull request #1065 (2025-01-29 17:49:17)

feat(op2): fake async, fix raw fields for WebIDL converter, add more implementations for ToV8 and FromV8

codecov-commenter: ## [Codecov](https://app.codecov.io/gh/denoland/deno_core/pull/1065?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=denoland) Report
Attention: Patch coverage is `46.03175%` with `68 lines` in your changes missing coverage. Please review.
> Project coverage is 81.46%. Comparing base [(`0c7f83e`)](https://app.codecov.io/gh/denoland/deno_core/commit/0c7f83ea8daabe8f541fc93ae97529dc446f5f24?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=denoland) to head [(`69792fe`)](https://app.codecov.io/gh/denoland/deno_core/commit/69792fece163b4e521667a6e21aa6c28baed3eeb?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=denoland).
> Report is 249 commits behind head on main.

| [Files with missing lines](https://app.codecov.io/gh/denoland/deno_core/pull/1065?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=denoland) | Patch % | Lines |
|---|---|---|
| [core/convert.rs](https://app.codecov.io/gh/denoland/deno_core/pull/1065?src=pr&el=tree&filepath=core%2Fconvert.rs&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=denoland#diff-Y29yZS9jb252ZXJ0LnJz) | 0.00% | [56 Missing :warning: ](https://app.codecov.io/gh/denoland/deno_core/pull/1065?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=denoland) |
| [ops/op2/mod.rs](https://app.codecov.io/gh/denoland/deno_core/pull/1065?src=pr&el=tree&filepath=ops%2Fop2%2Fmod.rs&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=denoland#diff-b3BzL29wMi9tb2QucnM=) | 53.84% | [6 Missing :warning: ](https://app.codecov.io/gh/denoland/deno_core/pull/1065?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=denoland) |
| [testing/checkin/runner/ops\_async.rs](https://app.codecov.io/gh/denoland/deno_core/pull/1065?src=pr&el=tree&filepath=testing%2Fcheckin%2Frunner%2Fops_async.rs&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=denoland#diff-dGVzdGluZy9jaGVja2luL3J1bm5lci9vcHNfYXN5bmMucnM=) | 25.00% | [3 Missing :warning: ](https://app.codecov.io/gh/denoland/deno_core/pull/1065?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=denoland) |
| [ops/op2/dispatch\_async.rs](https://app.codecov.io/gh/denoland/deno_core/pull/1065?src=pr&el=tree&filepath=ops%2Fop2%2Fdispatch_async.rs&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=denoland#diff-b3BzL29wMi9kaXNwYXRjaF9hc3luYy5ycw==) | 71.42% | [2 Missing :warning: ](https://app.codecov.io/gh/denoland/deno_core/pull/1065?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=denoland) |
| [ops/op2/signature.rs](https://app.codecov.io/gh/denoland/deno_core/pull/1065?src=pr&el=tree&filepath=ops%2Fop2%2Fsignature.rs&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=denoland#diff-b3BzL29wMi9zaWduYXR1cmUucnM=) | 90.00% | [1 Missing :warning: ](https://app.codecov.io/gh/denoland/deno_core/pull/1065?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=denoland) |

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##             main    #1065      +/-   ##
==========================================
+ Coverage   81.43%   81.46%   +0.02%     
==========================================
  Files          97      101       +4     
  Lines       23877    27951    +4074     
==========================================
+ Hits        19445    22769    +3324     
- Misses       4432     5182     +750     
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/denoland/deno_core/pull/1065?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=denoland).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=denoland).


Patch:
diff --git a/core/convert.rs b/core/convert.rs
index 2b7ed55..729a4cb 100644
--- a/core/convert.rs
+++ b/core/convert.rs
@@ -276,3 +276,128 @@ impl<'a> FromV8<'a> for bool {
       .map_err(|_| JsErrorBox::type_error("Expected boolean"))
   }
 }
+
+impl<'a> FromV8<'a> for String {
+  type Error = Infallible;
+  #[inline]
+  fn from_v8(
+    scope: &mut v8::HandleScope<'a>,
+    value: v8::Local<'a, v8::Value>,
+  ) -> Result<String, Self::Error> {
+    Ok(value.to_rust_string_lossy(scope))
+  }
+}
+impl<'a> ToV8<'a> for String {
+  type Error = Infallible;
+  #[inline]
+  fn to_v8(
+    self,
+    scope: &mut v8::HandleScope<'a>,
+  ) -> Result<v8::Local<'a, v8::Value>, Self::Error> {
+    Ok(v8::String::new(scope, &self).unwrap().into()) // TODO
+  }
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
+/// A wrapper type for `Option<T>` that (de)serializes `None` as `null`
+#[repr(transparent)]
+pub struct OptionNull<T>(pub Option<T>);
+
+impl<T> From<Option<T>> for OptionNull<T> {
+  fn from(option: Option<T>) -> Self {
+    Self(option)
+  }
+}
+
+impl<T> From<OptionNull<T>> for Option<T> {
+  fn from(value: OptionNull<T>) -> Self {
+    value.0
+  }
+}
+
+impl<'a, T> ToV8<'a> for OptionNull<T>
+where
+  T: ToV8<'a>,
+{
+  type Error = T::Error;
+
+  fn to_v8(
+    self,
+    scope: &mut v8::HandleScope<'a>,
+  ) -> Result<v8::Local<'a, v8::Value>, Self::Error> {
+    match self.0 {
+      Some(value) => value.to_v8(scope),
+      None => Ok(v8::null(scope).into()),
+    }
+  }
+}
+
+impl<'a, T> FromV8<'a> for OptionNull<T>
+where
+  T: FromV8<'a>,
+{
+  type Error = T::Error;
+
+  fn from_v8(
+    scope: &mut v8::HandleScope<'a>,
+    value: v8::Local<'a, v8::Value>,
+  ) -> Result<Self, Self::Error> {
+    if value.is_null() {
+      Ok(OptionNull(None))
+    } else {
+      T::from_v8(scope, value).map(|v| OptionNull(Some(v)))
+    }
+  }
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
+/// A wrapper type for `Option<T>` that (de)serializes `None` as `undefined`
+#[repr(transparent)]
+pub struct OptionUndefined<T>(pub Option<T>);
+
+impl<T> From<Option<T>> for OptionUndefined<T> {
+  fn from(option: Option<T>) -> Self {
+    Self(option)
+  }
+}
+
+impl<T> From<OptionUndefined<T>> for Option<T> {
+  fn from(value: OptionUndefined<T>) -> Self {
+    value.0
+  }
+}
+
+impl<'a, T> ToV8<'a> for OptionUndefined<T>
+where
+  T: ToV8<'a>,
+{
+  type Error = T::Error;
+
+  fn to_v8(
+    self,
+    scope: &mut v8::HandleScope<'a>,
+  ) -> Result<v8::Local<'a, v8::Value>, Self::Error> {
+    match self.0 {
+      Some(value) => value.to_v8(scope),
+      None => Ok(v8::undefined(scope).into()),
+    }
+  }
+}
+
+impl<'a, T> FromV8<'a> for OptionUndefined<T>
+where
+  T: FromV8<'a>,
+{
+  type Error = T::Error;
+
+  fn from_v8(
+    scope: &mut v8::HandleScope<'a>,
+    value: v8::Local<'a, v8::Value>,
+  ) -> Result<Self, Self::Error> {
+    if value.is_undefined() {
+      Ok(OptionUndefined(None))
+    } else {
+      T::from_v8(scope, value).map(|v| OptionUndefined(Some(v)))
+    }
+  }
+}
diff --git a/ops/op2/config.rs b/ops/op2/config.rs
index 9199ed9..7e772be 100644
--- a/ops/op2/config.rs
+++ b/ops/op2/config.rs
@@ -18,6 +18,7 @@ pub(crate) struct MacroConfig {
   pub fast_alternatives: Vec<String>,
   /// Marks an async function (either `async fn` or `fn -> impl Future`).
   pub r#async: bool,
+  pub fake_async: bool,
   /// Marks a lazy async function (async must also be true).
   pub async_lazy: bool,
   /// Marks a deferred async function (async must also be true).
@@ -114,6 +115,8 @@ impl MacroConfig {
         config.nofast = true;
       } else if flag == "async" || flag == "async_method" {
         config.r#async = true;
+      } else if flag == "async(fake)" || flag == "async_method(fake)" {
+        config.r#fake_async = true;
       } else if flag == "async(lazy)" {
         config.r#async = true;
         config.async_lazy = true;
diff --git a/ops/op2/dispatch_async.rs b/ops/op2/dispatch_async.rs
index b7d8014..dc1ed2e 100644
--- a/ops/op2/dispatch_async.rs
+++ b/ops/op2/dispatch_async.rs
@@ -28,15 +28,21 @@ pub(crate) fn map_async_return_type(
 ) -> Result<(TokenStream, TokenStream, TokenStream), V8MappingError> {
   let return_value = return_value_v8_value(generator_state, ret_val.arg())?;
   let (mapper, return_value_immediate) = match ret_val {
-    RetVal::Future(r) | RetVal::ResultFuture(r) => (
+    RetVal::Infallible(r, true)
+    | RetVal::Future(r)
+    | RetVal::ResultFuture(r) => (
       quote!(map_async_op_infallible),
       return_value_infallible(generator_state, r)?,
     ),
-    RetVal::FutureResult(r) | RetVal::ResultFutureResult(r) => (
+    RetVal::Result(r, true)
+    | RetVal::FutureResult(r)
+    | RetVal::ResultFutureResult(r) => (
       quote!(map_async_op_fallible),
       return_value_result(generator_state, r)?,
     ),
-    RetVal::Infallible(_) | RetVal::Result(_) => return Err("an async return"),
+    RetVal::Infallible(_, false) | RetVal::Result(_, false) => {
+      return Err("an async return")
+    }
   };
   Ok((return_value, mapper, return_value_immediate))
 }
diff --git a/ops/op2/dispatch_slow.rs b/ops/op2/dispatch_slow.rs
index f545899..8467d6a 100644
--- a/ops/op2/dispatch_slow.rs
+++ b/ops/op2/dispatch_slow.rs
@@ -755,7 +755,13 @@ pub fn from_arg(
         throw_type_error(generator_state, format!("expected {}", &ty));
       let ty =
         syn::parse_str::<syn::Path>(ty).expect("Failed to reparse state type");
-      if matches!(ret_val, RetVal::Future(_) | RetVal::FutureResult(_)) {
+      if matches!(
+        ret_val,
+        RetVal::Future(_)
+          | RetVal::FutureResult(_)
+          | RetVal::Infallible(.., true)
+          | RetVal::Result(.., true)
+      ) {
         let tokens = quote! {
           let Some(mut #arg_ident) = deno_core::_ops::try_unwrap_cppgc_object::<#ty>(&mut #scope, #arg_ident) else {
             #throw_exception;
@@ -782,7 +788,13 @@ pub fn from_arg(
       let ty =
         syn::parse_str::<syn::Path>(ty).expect("Failed to reparse state type");
       let scope = &generator_state.scope;
-      if matches!(ret_val, RetVal::Future(_) | RetVal::FutureResult(_)) {
+      if matches!(
+        ret_val,
+        RetVal::Future(_)
+          | RetVal::FutureResult(_)
+          | RetVal::Infallible(.., true)
+          | RetVal::Result(.., true)
+      ) {
         let tokens = quote! {
           let #arg_ident = if #arg_ident.is_null_or_undefined() {
             None
@@ -1002,6 +1014,11 @@ pub fn call(
       #moves
       #call.await
     })
+  } else if matches!(
+    ret_val,
+    RetVal::Infallible(.., true) | RetVal::Result(.., true)
+  ) {
+    return quote!(std::future::ready(#call));
   } else {
     call
   }
@@ -1012,10 +1029,12 @@ pub fn return_value(
   ret_type: &RetVal,
 ) -> Result<TokenStream, V8MappingError> {
   match ret_type {
-    RetVal::Infallible(ret_type) => {
+    RetVal::Infallible(ret_type, ..) => {
       return_value_infallible(generator_state, ret_type)
     }
-    RetVal::Result(ret_type) => return_value_result(generator_state, ret_type),
+    RetVal::Result(ret_type, ..) => {
+      return_value_result(generator_state, ret_type)
+    }
     _ => todo!(),
   }
 }
diff --git a/ops/op2/mod.rs b/ops/op2/mod.rs
index c4e2c73..b1ead2f 100644
--- a/ops/op2/mod.rs
+++ b/ops/op2/mod.rs
@@ -124,7 +124,8 @@ pub(crate) fn generate_op2(
   } else if config.static_member {
     func.sig.ident = format_ident!("__static_{}", func.sig.ident);
   }
-  let signature = parse_signature(func.attrs, func.sig.clone())?;
+  let signature =
+    parse_signature(config.fake_async, func.attrs, func.sig.clone())?;
   if let Some(ident) = signature.lifetime.as_ref().map(|s| format_ident!("{s}"))
   {
     op_fn.sig.generics.params.push(syn::GenericParam::Lifetime(
@@ -217,7 +218,7 @@ pub(crate) fn generate_op2(
   let is_reentrant = config.reentrant;
   let no_side_effect = config.no_side_effects;
 
-  match (is_async, config.r#async) {
+  match (is_async, config.r#async || config.fake_async) {
     (true, false) => return Err(Op2Error::ShouldBeAsync),
     (false, true) => return Err(Op2Error::ShouldNotBeAsync),
     _ => {}
@@ -490,7 +491,7 @@ mod tests {
         let function = format!("fn op_test({} x: {}) {{}}", attr, ty);
         let function =
           syn::parse_str::<ItemFn>(&function).expect("Failed to parse type");
-        let sig = parse_signature(vec![], function.sig.clone())
+        let sig = parse_signature(false, vec![], function.sig.clone())
           .expect("Failed to parse signature");
         println!("Parsed signature: {sig:?}");
         generate_op2(
@@ -548,8 +549,9 @@ mod tests {
         let function = format!("{} fn op_test() -> {} {{}}", attr, ty);
         let function =
           syn::parse_str::<ItemFn>(&function).expect("Failed to parse type");
-        let sig = parse_signature(function.attrs.clone(), function.sig.clone())
-          .expect("Failed to parse signature");
+        let sig =
+          parse_signature(false, function.attrs.clone(), function.sig.clone())
+            .expect("Failed to parse signature");
         println!("Parsed signature: {sig:?}");
         generate_op2(
           MacroConfig {
@@ -564,9 +566,12 @@ mod tests {
           let function = format!("{} async fn op_test() -> {} {{}}", attr, ty);
           let function =
             syn::parse_str::<ItemFn>(&function).expect("Failed to parse type");
-          let sig =
-            parse_signature(function.attrs.clone(), function.sig.clone())
-              .expect("Failed to parse signature");
+          let sig = parse_signature(
+            false,
+            function.attrs.clone(),
+            function.sig.clone(),
+          )
+          .expect("Failed to parse signature");
           println!("Parsed signature: {sig:?}");
           generate_op2(
             MacroConfig {
diff --git a/ops/op2/signature.rs b/ops/op2/signature.rs
index 13cfc34..780681f 100644
--- a/ops/op2/signature.rs
+++ b/ops/op2/signature.rs
@@ -654,9 +654,9 @@ impl ParsedTypeContainer {
 #[derive(Clone, Debug, Eq, PartialEq)]
 pub enum RetVal {
   /// An op that can never fail.
-  Infallible(Arg),
+  Infallible(Arg, bool),
   /// An op returning Result<Something, ...>
-  Result(Arg),
+  Result(Arg, bool),
   /// An op returning a future, either `async fn() -> Something` or `fn() -> impl Future<Output = Something>`.
   Future(Arg),
   /// An op returning a future with a result, either `async fn() -> Result<Something, ...>`
@@ -675,7 +675,12 @@ impl RetVal {
     use RetVal::*;
     matches!(
       self,
-      Future(..) | FutureResult(..) | ResultFuture(..) | ResultFutureResult(..)
+      Future(..)
+        | FutureResult(..)
+        | ResultFuture(..)
+        | ResultFutureResult(..)
+        | Infallible(.., true)
+        | Result(.., true)
     )
   }
 
@@ -683,7 +688,7 @@ impl RetVal {
   pub fn unwrap_result(&self) -> Option<RetVal> {
     use RetVal::*;
     Some(match self {
-      Result(arg) => Infallible(arg.clone()),
+      Result(arg, false) => Infallible(arg.clone(), false),
       ResultFuture(arg) => Future(arg.clone()),
       ResultFutureResult(arg) => FutureResult(arg.clone()),
       _ => return None,
@@ -693,8 +698,8 @@ impl RetVal {
   pub fn arg(&self) -> &Arg {
     use RetVal::*;
     match self {
-      Infallible(arg)
-      | Result(arg)
+      Infallible(arg, ..)
+      | Result(arg, ..)
       | Future(arg)
       | FutureResult(arg)
       | ResultFuture(arg)
@@ -999,6 +1004,7 @@ fn parse_metadata(
 }
 
 pub fn parse_signature(
+  is_fake_async: bool,
   attributes: Vec<Attribute>,
   signature: Signature,
 ) -> Result<ParsedSignature, SignatureError> {
@@ -1020,6 +1026,7 @@ pub fn parse_signature(
   }
   let ret_val = parse_return(
     signature.asyncness.is_some(),
+    is_fake_async,
     parse_attributes(&attributes).map_err(RetError::AttributeError)?,
     &signature.output,
   )?;
@@ -1879,9 +1886,10 @@ mod tests {
       .unwrap_or_else(|_| panic!("Failed to parse {op} as a ItemFn"));
 
     let attrs = item_fn.attrs;
-    let sig = parse_signature(attrs, item_fn.sig).unwrap_or_else(|err| {
-      panic!("Failed to successfully parse signature from {op} ({err:?})")
-    });
+    let sig =
+      parse_signature(false, attrs, item_fn.sig).unwrap_or_else(|err| {
+        panic!("Failed to successfully parse signature from {op} ({err:?})")
+      });
     println!("Raw parsed signatures = {sig:?}");
 
     let mut generics_res = vec![];
@@ -1931,7 +1939,7 @@ mod tests {
         let item_fn = parse_str::<ItemFn>(op)
           .unwrap_or_else(|_| panic!("Failed to parse {op} as a ItemFn"));
         let attrs = item_fn.attrs;
-        let error = parse_signature(attrs, item_fn.sig)
+        let error = parse_signature(false, attrs, item_fn.sig)
           .expect_err("Expected function to fail to parse");
         assert_eq!(format!("{error:?}"), format!("{:?}", $error));
       }
@@ -1940,119 +1948,119 @@ mod tests {
 
   test!(
     fn op_state_and_number(opstate: &mut OpState, a: u32) -> ();
-    (Ref(Mut, OpState), Numeric(u32, None)) -> Infallible(Void)
+    (Ref(Mut, OpState), Numeric(u32, None)) -> Infallible(Void, false)
   );
   test!(
     fn op_slices(#[buffer] r#in: &[u8], #[buffer] out: &mut [u8]);
-    (Buffer(Slice(Ref, u8), Default, TypedArray), Buffer(Slice(Mut, u8), Default, TypedArray)) -> Infallible(Void)
+    (Buffer(Slice(Ref, u8), Default, TypedArray), Buffer(Slice(Mut, u8), Default, TypedArray)) -> Infallible(Void, false)
   );
   test!(
     fn op_pointers(#[buffer] r#in: *const u8, #[buffer] out: *mut u8);
-    (Buffer(Ptr(Ref, u8), Default, TypedArray), Buffer(Ptr(Mut, u8), Default, TypedArray)) -> Infallible(Void)
+    (Buffer(Ptr(Ref, u8), Default, TypedArray), Buffer(Ptr(Mut, u8), Default, TypedArray)) -> Infallible(Void, false)
   );
   test!(
     fn op_arraybuffer(#[arraybuffer] r#in: &[u8]);
-    (Buffer(Slice(Ref, u8), Default, ArrayBuffer)) -> Infallible(Void)
+    (Buffer(Slice(Ref, u8), Default, ArrayBuffer)) -> Infallible(Void, false)
   );
   test!(
     #[serde] fn op_serde(#[serde] input: package::SerdeInputType) -> Result<package::SerdeReturnType, Error>;
-    (SerdeV8(package::SerdeInputType)) -> Result(SerdeV8(package::SerdeReturnType))
+    (SerdeV8(package::SerdeInputType)) -> Result(SerdeV8(package::SerdeReturnType), false)
   );
   // Note the turbofish syntax here because of macro constraints
   test!(
     #[serde] fn op_serde_option(#[serde] maybe: Option<package::SerdeInputType>) -> Result<Option<package::SerdeReturnType>, Error>;
-    (SerdeV8(Option::<package::SerdeInputType>)) -> Result(SerdeV8(Option::<package::SerdeReturnType>))
+    (SerdeV8(Option::<package::SerdeInputType>)) -> Result(SerdeV8(Option::<package::SerdeReturnType>), false)
   );
   test!(
     #[serde] fn op_serde_tuple(#[serde] input: (A, B)) -> (A, B);
-    (SerdeV8((A, B))) -> Infallible(SerdeV8((A, B)))
+    (SerdeV8((A, B))) -> Infallible(SerdeV8((A, B)), false)
   );
   test!(
     fn op_local(input: v8::Local<v8::String>) -> Result<v8::Local<v8::String>, Error>;
-    (V8Local(String)) -> Result(V8Local(String))
+    (V8Local(String)) -> Result(V8Local(String), false)
   );
   test!(
     fn op_resource(#[smi] rid: ResourceId, #[buffer] buffer: &[u8]);
-    (Numeric(__SMI__, None), Buffer(Slice(Ref, u8), Default, TypedArray)) ->  Infallible(Void)
+    (Numeric(__SMI__, None), Buffer(Slice(Ref, u8), Default, TypedArray)) ->  Infallible(Void, false)
   );
   test!(
     #[smi] fn op_resource2(#[smi] rid: ResourceId) -> Result<ResourceId, Error>;
-    (Numeric(__SMI__, None)) -> Result(Numeric(__SMI__, None))
+    (Numeric(__SMI__, None)) -> Result(Numeric(__SMI__, None), false)
   );
   test!(
     fn op_option_numeric_result(state: &mut OpState) -> Result<Option<u32>, JsErrorBox>;
-    (Ref(Mut, OpState)) -> Result(OptionNumeric(u32, None))
+    (Ref(Mut, OpState)) -> Result(OptionNumeric(u32, None), false)
   );
   test!(
     #[smi] fn op_option_numeric_smi_result(#[smi] a: Option<u32>) -> Result<Option<u32>, JsErrorBox>;
-    (OptionNumeric(__SMI__, None)) -> Result(OptionNumeric(__SMI__, None))
+    (OptionNumeric(__SMI__, None)) -> Result(OptionNumeric(__SMI__, None), false)
   );
   test!(
     fn op_ffi_read_f64(state: &mut OpState, ptr: *mut c_void, #[bigint] offset: isize) -> Result<f64, JsErrorBox>;
-    (Ref(Mut, OpState), External(Ptr(Mut)), Numeric(isize, None)) -> Result(Numeric(f64, None))
+    (Ref(Mut, OpState), External(Ptr(Mut)), Numeric(isize, None)) -> Result(Numeric(f64, None), false)
   );
   test!(
     #[number] fn op_64_bit_number(#[number] offset: isize) -> Result<u64, JsErrorBox>;
-    (Numeric(isize, Number)) -> Result(Numeric(u64, Number))
+    (Numeric(isize, Number)) -> Result(Numeric(u64, Number), false)
   );
   test!(
     fn op_ptr_out(ptr: *const c_void) -> *mut c_void;
-    (External(Ptr(Ref))) -> Infallible(External(Ptr(Mut)))
+    (External(Ptr(Ref))) -> Infallible(External(Ptr(Mut)), false)
   );
   test!(
     fn op_print(#[string] msg: &str, is_err: bool) -> Result<(), Error>;
-    (String(RefStr), Numeric(bool, None)) -> Result(Void)
+    (String(RefStr), Numeric(bool, None)) -> Result(Void, false)
   );
   test!(
     #[string] fn op_lots_of_strings(#[string] s: String, #[string] s2: Option<String>, #[string] s3: Cow<str>, #[string(onebyte)] s4: Cow<[u8]>) -> String;
-    (String(String), OptionString(String), String(CowStr), String(CowByte)) -> Infallible(String(String))
+    (String(String), OptionString(String), String(CowStr), String(CowByte)) -> Infallible(String(String), false)
   );
   test!(
     #[string] fn op_lots_of_option_strings(#[string] s: Option<String>, #[string] s2: Option<&str>, #[string] s3: Option<Cow<str>>) -> Option<String>;
-    (OptionString(String), OptionString(RefStr), OptionString(CowStr)) -> Infallible(OptionString(String))
+    (OptionString(String), OptionString(RefStr), OptionString(CowStr)) -> Infallible(OptionString(String), false)
   );
   test!(
     fn op_scope<'s>(#[string] msg: &'s str);
-    <'s> (String(RefStr)) -> Infallible(Void)
+    <'s> (String(RefStr)) -> Infallible(Void, false)
   );
   test!(
     fn op_scope_and_generics<'s, AB, BC>(#[string] msg: &'s str) where AB: some::Trait, BC: OtherTrait;
-    <'s, AB: some::Trait, BC: OtherTrait> (String(RefStr)) -> Infallible(Void)
+    <'s, AB: some::Trait, BC: OtherTrait> (String(RefStr)) -> Infallible(Void, false)
   );
   test!(
     fn op_generics_static<'s, AB, BC>(#[string] msg: &'s str) where AB: some::Trait + 'static, BC: OtherTrait;
-    <'s, AB: some::Trait + 'static, BC: OtherTrait> (String(RefStr)) -> Infallible(Void)
+    <'s, AB: some::Trait + 'static, BC: OtherTrait> (String(RefStr)) -> Infallible(Void, false)
   );
   test!(
     fn op_v8_types(s: &mut v8::String, sopt: Option<&mut v8::String>, s2: v8::Local<v8::String>, #[global] s3: v8::Global<v8::String>);
-    (V8Ref(Mut, String), OptionV8Ref(Mut, String), V8Local(String), V8Global(String)) -> Infallible(Void)
+    (V8Ref(Mut, String), OptionV8Ref(Mut, String), V8Local(String), V8Global(String)) -> Infallible(Void, false)
   );
   test!(
     fn op_v8_scope<'s>(scope: &mut v8::HandleScope<'s>);
-    <'s> (Ref(Mut, HandleScope)) -> Infallible(Void)
+    <'s> (Ref(Mut, HandleScope)) -> Infallible(Void, false)
   );
   test!(
     fn op_state_rc(state: Rc<RefCell<OpState>>);
-    (RcRefCell(OpState)) -> Infallible(Void)
+    (RcRefCell(OpState)) -> Infallible(Void, false)
   );
   test!(
     fn op_state_ref(state: &OpState);
-    (Ref(Ref, OpState)) -> Infallible(Void)
+    (Ref(Ref, OpState)) -> Infallible(Void, false)
   );
   test!(
     fn op_state_attr(#[state] something: &Something, #[state] another: Option<&Something>);
-    (State(Ref, Something), OptionState(Ref, Something)) -> Infallible(Void)
+    (State(Ref, Something), OptionState(Ref, Something)) -> Infallible(Void, false)
   );
   test!(
     #[buffer] fn op_buffers(#[buffer(copy)] a: Vec<u8>, #[buffer(copy)] b: Box<[u8]>, #[buffer(copy)] c: bytes::Bytes,
       #[buffer] d: V8Slice<u8>, #[buffer] e: JsBuffer, #[buffer(detach)] f: JsBuffer) -> Vec<u8>;
     (Buffer(Vec(u8), Copy, TypedArray), Buffer(BoxSlice(u8), Copy, TypedArray),
       Buffer(Bytes, Copy, TypedArray), Buffer(V8Slice(u8), Default, TypedArray),
-      Buffer(JsBuffer, Default, TypedArray), Buffer(JsBuffer, Detach, TypedArray)) -> Infallible(Buffer(Vec(u8), Default, TypedArray))
+      Buffer(JsBuffer, Default, TypedArray), Buffer(JsBuffer, Detach, TypedArray)) -> Infallible(Buffer(Vec(u8), Default, TypedArray), false)
   );
   test!(
     #[buffer] fn op_return_bytesmut() -> bytes::BytesMut;
-    () -> Infallible(Buffer(BytesMut, Default, TypedArray))
+    () -> Infallible(Buffer(BytesMut, Default, TypedArray), false)
   );
   test!(
     async fn op_async_void();
@@ -2072,19 +2080,19 @@ mod tests {
   );
   test!(
     fn op_js_runtime_state_ref(state: &JsRuntimeState);
-    (Ref(Ref, JsRuntimeState)) -> Infallible(Void)
+    (Ref(Ref, JsRuntimeState)) -> Infallible(Void, false)
   );
   test!(
     fn op_js_runtime_state_mut(state: &mut JsRuntimeState);
-    (Ref(Mut, JsRuntimeState)) -> Infallible(Void)
+    (Ref(Mut, JsRuntimeState)) -> Infallible(Void, false)
   );
   test!(
     fn op_js_runtime_state_rc(state: Rc<JsRuntimeState>);
-    (Rc(JsRuntimeState)) -> Infallible(Void)
+    (Rc(JsRuntimeState)) -> Infallible(Void, false)
   );
   test!(
     fn op_isolate(isolate: *mut v8::Isolate);
-    (Special(Isolate)) -> Infallible(Void)
+    (Special(Isolate)) -> Infallible(Void, false)
   );
   test!(
     #[serde]
diff --git a/ops/op2/signature_retval.rs b/ops/op2/signature_retval.rs
index 7dbf753..85fb2ec 100644
--- a/ops/op2/signature_retval.rs
+++ b/ops/op2/signature_retval.rs
@@ -67,17 +67,24 @@ fn unwrap_return(ty: &Type) -> Result<UnwrappedReturn, RetError> {
 
 pub(crate) fn parse_return(
   is_async: bool,
+  is_fake_async: bool,
   attrs: Attributes,
   rt: &ReturnType,
 ) -> Result<RetVal, RetError> {
   use UnwrappedReturn::*;
 
   let res = match rt {
-    ReturnType::Default => RetVal::Infallible(Arg::Void),
+    ReturnType::Default => RetVal::Infallible(Arg::Void, is_fake_async),
     ReturnType::Type(_, rt) => match unwrap_return(rt)? {
-      Type(ty) => RetVal::Infallible(parse_type(Position::RetVal, attrs, &ty)?),
+      Type(ty) => RetVal::Infallible(
+        parse_type(Position::RetVal, attrs, &ty)?,
+        is_fake_async,
+      ),
       Result(ty) => match unwrap_return(&ty)? {
-        Type(ty) => RetVal::Result(parse_type(Position::RetVal, attrs, &ty)?),
+        Type(ty) => RetVal::Result(
+          parse_type(Position::RetVal, attrs, &ty)?,
+          is_fake_async,
+        ),
         Future(ty) => match unwrap_return(&ty)? {
           Type(ty) => {
             RetVal::ResultFuture(parse_type(Position::RetVal, attrs, &ty)?)
@@ -119,8 +126,8 @@ pub(crate) fn parse_return(
   // If the signature was async, wrap this return value in one level of future.
   if is_async {
     let res = match res {
-      RetVal::Infallible(t) => RetVal::Future(t),
-      RetVal::Result(t) => RetVal::FutureResult(t),
+      RetVal::Infallible(t, ..) => RetVal::Future(t),
+      RetVal::Result(t, ..) => RetVal::FutureResult(t),
       _ => {
         return Err(RetError::InvalidType(ArgError::InvalidType(
           stringify_token(rt),
@@ -145,10 +152,10 @@ mod tests {
     use RetVal::*;
 
     for (expected, input) in [
-      (Infallible(Void), "()"),
-      (Result(Void), "Result<()>"),
-      (Result(Void), "Result<(), ()>"),
-      (Result(Void), "Result<(), (),>"),
+      (Infallible(Void, false), "()"),
+      (Result(Void, false), "Result<()>"),
+      (Result(Void, false), "Result<(), ()>"),
+      (Result(Void, false), "Result<(), (),>"),
       (Future(Void), "impl Future<Output = ()>"),
       (FutureResult(Void), "impl Future<Output = Result<()>>"),
       (ResultFuture(Void), "Result<impl Future<Output = ()>>"),
@@ -159,7 +166,7 @@ mod tests {
     ] {
       let rt = parse_str::<ReturnType>(&format!("-> {input}"))
         .expect("Failed to parse");
-      let actual = parse_return(false, Attributes::default(), &rt)
+      let actual = parse_return(false, false, Attributes::default(), &rt)
         .expect("Failed to parse return");
       assert_eq!(expected, actual);
     }
diff --git a/ops/webidl/dictionary.rs b/ops/webidl/dictionary.rs
index 20cbf51..83a5d8e 100644
--- a/ops/webidl/dictionary.rs
+++ b/ops/webidl/dictionary.rs
@@ -7,6 +7,7 @@ use proc_macro2::TokenStream;
 use quote::format_ident;
 use quote::quote;
 use quote::ToTokens;
+use syn::ext::IdentExt;
 use syn::parse::Parse;
 use syn::parse::ParseStream;
 use syn::punctuated::Punctuated;
@@ -53,7 +54,7 @@ pub fn get_body(
   let v8_static_strings = fields
     .iter()
     .map(|field| {
-      let name = field.get_name();
+      let name = field.get_js_name();
       let new_ident = format_ident!("__v8_static_{name}");
       let name_str = name.to_string();
       quote!(#new_ident = #name_str)
@@ -62,7 +63,7 @@ pub fn get_body(
   let v8_lazy_strings = fields
     .iter()
     .map(|field| {
-      let name = field.get_name();
+      let name = field.get_js_name();
       let v8_eternal_name = format_ident!("__v8_{name}_eternal");
       quote! {
         static #v8_eternal_name: ::deno_core::v8::Eternal<::deno_core::v8::String> = ::deno_core::v8::Eternal::empty();
@@ -71,7 +72,7 @@ pub fn get_body(
     .collect::<Vec<_>>();
 
   let fields = fields.into_iter().map(|field| {
-    let name = field.get_name();
+    let name = field.get_js_name();
     let string_name = name.to_string();
     let original_name = field.name;
     let v8_static_name = format_ident!("__v8_static_{name}");
@@ -200,7 +201,7 @@ struct DictionaryField {
 }
 
 impl DictionaryField {
-  fn get_name(&self) -> Ident {
+  fn get_js_name(&self) -> Ident {
     Ident::new(
       &self
         .rename
@@ -270,9 +271,14 @@ impl TryFrom<Field> for DictionaryField {
       }
     }
 
+    let name = value.ident.unwrap();
+    if rename.is_none() {
+      rename = Some(stringcase::camel_case(&name.unraw().to_string()));
+    }
+
     Ok(Self {
       span,
-      name: value.ident.unwrap(),
+      name,
       rename,
       default_value,
       converter_options,


Test patch:
diff --git a/testing/checkin/runner/extensions.rs b/testing/checkin/runner/extensions.rs
index 3bad797..94b046c 100644
--- a/testing/checkin/runner/extensions.rs
+++ b/testing/checkin/runner/extensions.rs
@@ -35,6 +35,7 @@ deno_core::extension!(
     ops_async::op_async_make_cppgc_resource,
     ops_async::op_async_get_cppgc_resource,
     ops_async::op_async_never_resolves,
+    ops_async::op_async_fake,
     ops_error::op_async_throw_error_eager,
     ops_error::op_async_throw_error_lazy,
     ops_error::op_async_throw_error_deferred,
diff --git a/testing/checkin/runner/ops_async.rs b/testing/checkin/runner/ops_async.rs
index 77702fb..a8f7dd6 100644
--- a/testing/checkin/runner/ops_async.rs
+++ b/testing/checkin/runner/ops_async.rs
@@ -1,18 +1,18 @@
 // Copyright 2018-2025 the Deno authors. MIT license.
 
+use super::Output;
+use super::TestData;
 use deno_core::op2;
 use deno_core::v8;
 use deno_core::GarbageCollected;
 use deno_core::OpState;
 use deno_core::V8TaskSpawner;
+use deno_error::JsErrorBox;
 use futures::future::poll_fn;
 use std::cell::RefCell;
 use std::future::Future;
 use std::rc::Rc;
 
-use super::Output;
-use super::TestData;
-
 #[op2]
 pub fn op_task_submit(
   state: &mut OpState,
@@ -87,3 +87,8 @@ pub async fn op_async_get_cppgc_resource(
 pub fn op_async_never_resolves() -> impl Future<Output = ()> {
   futures::future::pending::<()>()
 }
+
+#[op2(async(fake))]
+pub fn op_async_fake() -> Result<u32, JsErrorBox> {
+  Ok(1)
+}
diff --git a/testing/checkin/runtime/async.ts b/testing/checkin/runtime/async.ts
index 207e875..7693ff8 100644
--- a/testing/checkin/runtime/async.ts
+++ b/testing/checkin/runtime/async.ts
@@ -9,6 +9,7 @@ const {
   op_stats_dump,
   op_stats_delete,
   op_async_never_resolves,
+  op_async_fake,
 } = Deno
   .core
   .ops;
@@ -38,6 +39,10 @@ export function asyncNeverResolves() {
 
 let nextStats = 0;
 
+export function fakeAsync() {
+  return op_async_fake();
+}
+
 export class Stats {
   constructor(public name: string) {
     op_stats_capture(this.name);