Skip to content
File

Blob: src/rust/jsg-macros/resource.rs

rust748 lines
1// Copyright (c) 2026 Cloudflare, Inc.
2// Licensed under the Apache 2.0 license found in the LICENSE file or at:
3// https://opensource.org/licenses/Apache-2.0
4 
5//! Code generation for `#[jsg_resource]` on structs and impl blocks.
6//!
7//! - On a **struct**: emits `jsg::Type`, `jsg::ToJS`, `jsg::FromJS`,
8//! `jsg::Traced`, and `jsg::GarbageCollected` implementations.
9//! - On an **impl block**: emits the `jsg::Resource` trait with method, static
10//! method, static constant, and constructor registrations.
11 
12use proc_macro::TokenStream;
13use quote::quote;
14use syn::FnArg;
15use syn::ItemImpl;
16 
17use crate::trace::generate_trace_statements;
18use crate::utils::error;
19use crate::utils::extract_name_attribute;
20use crate::utils::extract_named_fields;
21use crate::utils::has_custom_trace_flag;
22use crate::utils::is_attr;
23use crate::utils::is_lock_ref;
24use crate::utils::snake_to_camel;
25 
26// Compile-time mirror of `jsg::PropertyKind` used to group annotated methods
27// and emit the correct token streams. Cannot reuse the runtime type directly
28// because proc-macro crates cannot link against CXX-bridge runtime crates.
29#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
30enum PropertyKind {
31 Prototype,
32 Instance,
33 Inspect,
34}
35 
36/// Entry point called from `lib.rs` for `#[jsg_resource]` on a struct.
37pub fn generate_resource_struct(attr: TokenStream, input: &syn::DeriveInput) -> TokenStream {
38 // Check for `#[jsg_resource(custom_trace)]` before consuming `attr`.
39 let custom_trace = has_custom_trace_flag(&attr);
40 
41 // Clone the name before mutating `input` so we can borrow freely later.
42 let name = input.ident.clone();
43 
44 let class_name = if attr.is_empty() {
45 name.to_string()
46 } else {
47 extract_name_attribute(attr).unwrap_or_else(|| name.to_string())
48 };
49 
50 let fields = match extract_named_fields(input, "jsg_resource") {
51 Ok(fields) => fields,
52 Err(err) => return err,
53 };
54 
55 let trace_statements = generate_trace_statements(&fields);
56 let name_str = name.to_string();
57 
58 let traced_impl = if custom_trace {
59 // `custom_trace` suppresses the generated `Traced` impl — the user will write their own.
60 quote! {}
61 } else {
62 quote! {
63 #[automatically_derived]
64 impl jsg::Traced for #name {
65 fn trace(&self, visitor: &mut jsg::GcVisitor) {
66 // Suppress unused warning when there are no traceable fields.
67 let _ = visitor;
68 #(#trace_statements)*
69 }
70 }
71 }
72 };
73 
74 let gc_impl = quote! {
75 #[automatically_derived]
76 impl jsg::GarbageCollected for #name {
77 fn memory_name(&self) -> &'static ::std::ffi::CStr {
78 // from_bytes_with_nul on a concat!(name, "\0") literal is a
79 // compile-time constant expression — the compiler folds the
80 // unwrap and emits a direct pointer into the read-only data
81 // segment. The C++ side constructs a kj::StringPtr directly
82 // from data()+size() with no allocation.
83 ::std::ffi::CStr::from_bytes_with_nul(concat!(#name_str, "\0").as_bytes())
84 .unwrap()
85 }
86 }
87 };
88 
89 quote! {
90 #input
91 
92 #[automatically_derived]
93 impl jsg::Type for #name {
94 fn class_name() -> &'static str { #class_name }
95 
96 fn is_exact(value: &jsg::v8::Local<jsg::v8::Value>) -> bool {
97 value.is_object()
98 }
99 }
100 
101 #[automatically_derived]
102 impl jsg::ToJS for #name {
103 fn to_js<'a, 'b>(self, lock: &'a mut jsg::Lock) -> jsg::v8::Local<'b, jsg::v8::Value>
104 where
105 'b: 'a,
106 {
107 let r = jsg::Rc::new(self);
108 r.to_js(lock)
109 }
110 }
111 
112 #[automatically_derived]
113 impl jsg::FromJS for #name {
114 type ResultType = jsg::Rc<Self>;
115 
116 fn from_js(
117 lock: &mut jsg::Lock,
118 value: jsg::v8::Local<jsg::v8::Value>,
119 ) -> Result<Self::ResultType, jsg::Error> {
120 <jsg::Rc<Self> as jsg::FromJS>::from_js(lock, value)
121 }
122 }
123 
124 #traced_impl
125 #gc_impl
126 }
127 .into()
128}
129 
130/// Scans `impl_block` for `#[jsg_method]`-annotated functions and returns a
131/// `Member::Method` / `Member::StaticMethod` token stream for each.
132fn collect_method_registrations(impl_block: &ItemImpl) -> Vec<quote::__private::TokenStream> {
133 impl_block
134 .items
135 .iter()
136 .filter_map(|item| {
137 let syn::ImplItem::Fn(method) = item else {
138 return None;
139 };
140 let attr = method.attrs.iter().find(|a| is_attr(a, "jsg_method"))?;
141 
142 let rust_method_name = &method.sig.ident;
143 let js_name = attr
144 .meta
145 .require_list()
146 .ok()
147 .map(|list| list.tokens.clone().into())
148 .and_then(extract_name_attribute)
149 .unwrap_or_else(|| snake_to_camel(&rust_method_name.to_string()));
150 let callback_name =
151 syn::Ident::new(&format!("{rust_method_name}_callback"), rust_method_name.span());
152 
153 let has_self = method
154 .sig
155 .inputs
156 .iter()
157 .any(|arg| matches!(arg, FnArg::Receiver(_)));
158 
159 Some(if has_self {
160 quote! { jsg::Member::Method { name: #js_name.to_owned(), callback: Self::#callback_name } }
161 } else {
162 quote! { jsg::Member::StaticMethod { name: #js_name.to_owned(), callback: Self::#callback_name } }
163 })
164 })
165 .collect()
166}
167 
168/// Scans `impl_block` for `#[jsg_static_constant]`-annotated consts and returns
169/// a `Member::StaticConstant` token stream for each.
170fn collect_constant_registrations(impl_block: &ItemImpl) -> Vec<quote::__private::TokenStream> {
171 impl_block
172 .items
173 .iter()
174 .filter_map(|item| {
175 let syn::ImplItem::Const(constant) = item else {
176 return None;
177 };
178 let attr = constant
179 .attrs
180 .iter()
181 .find(|a| is_attr(a, "jsg_static_constant"))?;
182 
183 let rust_name = &constant.ident;
184 let js_name = attr
185 .meta
186 .require_list()
187 .ok()
188 .map(|list| list.tokens.clone().into())
189 .and_then(extract_name_attribute)
190 .unwrap_or_else(|| rust_name.to_string());
191 
192 Some(quote! {
193 jsg::Member::StaticConstant {
194 name: #js_name.to_owned(),
195 value: jsg::ConstantValue::from(Self::#rust_name),
196 }
197 })
198 })
199 .collect()
200}
201 
202/// Entry point called from `lib.rs` for `#[jsg_resource]` on an impl block.
203pub fn generate_resource_impl(impl_block: &ItemImpl) -> TokenStream {
204 let self_ty = &impl_block.self_ty;
205 
206 if !matches!(&**self_ty, syn::Type::Path(_)) {
207 return error(
208 self_ty,
209 "#[jsg_resource] impl blocks must use a simple path type (e.g., `impl MyResource`)",
210 );
211 }
212 
213 let method_registrations = collect_method_registrations(impl_block);
214 let property_registrations = collect_property_registrations(impl_block);
215 let constant_registrations = collect_constant_registrations(impl_block);
216 
217 let constructor_registration = generate_constructor_registration(impl_block, self_ty);
218 let constructor_vec: Vec<_> = constructor_registration.into_iter().collect();
219 
220 quote! {
221 #impl_block
222 
223 #[automatically_derived]
224 impl jsg::Resource for #self_ty {
225 fn members() -> Vec<jsg::Member>
226 where
227 Self: Sized,
228 {
229 vec![
230 #(#constructor_vec,)*
231 #(#method_registrations,)*
232 #(#property_registrations,)*
233 #(#constant_registrations,)*
234 ]
235 }
236 }
237 }
238 .into()
239}
240 
241// ---------------------------------------------------------------------------
242// Constructor helpers
243// ---------------------------------------------------------------------------
244 
245/// Validates that a `#[jsg_constructor]` method has the right shape.
246///
247/// Returns a `compile_error!` token stream if the method has a `self` receiver
248/// or does not return `Self`; returns `None` if the method is valid.
249fn validate_constructor(method: &syn::ImplItemFn) -> Option<quote::__private::TokenStream> {
250 let has_self = method
251 .sig
252 .inputs
253 .iter()
254 .any(|arg| matches!(arg, FnArg::Receiver(_)));
255 if has_self {
256 return Some(quote! {
257 compile_error!("#[jsg_constructor] must be a static method (no self receiver)");
258 });
259 }
260 
261 let returns_self = matches!(&method.sig.output,
262 syn::ReturnType::Type(_, ty) if matches!(&**ty,
263 syn::Type::Path(p) if p.path.is_ident("Self")
264 )
265 );
266 if !returns_self {
267 return Some(quote! {
268 compile_error!("#[jsg_constructor] must return Self");
269 });
270 }
271 
272 None
273}
274 
275/// Extracts constructor argument unwrap statements and argument expressions.
276fn extract_constructor_params(
277 method: &syn::ImplItemFn,
278) -> (
279 bool,
280 Vec<quote::__private::TokenStream>,
281 Vec<quote::__private::TokenStream>,
282) {
283 let params: Vec<_> = method
284 .sig
285 .inputs
286 .iter()
287 .filter_map(|arg| {
288 if let FnArg::Typed(pat_type) = arg {
289 Some((*pat_type.ty).clone())
290 } else {
291 None
292 }
293 })
294 .collect();
295 
296 let has_lock_param = params.first().is_some_and(is_lock_ref);
297 let js_arg_offset = usize::from(has_lock_param);
298 
299 let (unwraps, arg_exprs) = params
300 .iter()
301 .enumerate()
302 .skip(js_arg_offset)
303 .map(|(i, ty)| {
304 let js_index = i - js_arg_offset;
305 let var = syn::Ident::new(&format!("arg{js_index}"), method.sig.ident.span());
306 let unwrap = quote! {
307 let #var = match <#ty as jsg::FromJS>::from_js(&mut lock, args.get(#js_index)) {
308 Ok(v) => v,
309 Err(e) => {
310 lock.throw_exception(&e);
311 return;
312 }
313 };
314 };
315 (unwrap, quote! { #var })
316 })
317 .unzip();
318 
319 (has_lock_param, unwraps, arg_exprs)
320}
321 
322/// Scans an impl block for a `#[jsg_constructor]` attribute and generates the
323/// constructor callback registration. Returns `None` if no constructor is defined.
324/// Validates that a `#[jsg_constructor]` method has the right shape and returns
325/// a compile-error token stream if it doesn't.
326fn generate_constructor_registration(
327 impl_block: &ItemImpl,
328 self_ty: &syn::Type,
329) -> Option<quote::__private::TokenStream> {
330 let constructors: Vec<_> = impl_block
331 .items
332 .iter()
333 .filter_map(|item| match item {
334 syn::ImplItem::Fn(m) if m.attrs.iter().any(|a| is_attr(a, "jsg_constructor")) => {
335 Some(m)
336 }
337 _ => None,
338 })
339 .collect();
340 
341 if constructors.len() > 1 {
342 return Some(quote! {
343 compile_error!("only one #[jsg_constructor] is allowed per impl block");
344 });
345 }
346 
347 constructors
348 .into_iter()
349 .map(|method| {
350 if let Some(err) = validate_constructor(method) {
351 return err;
352 }
353 
354 let rust_method_name = &method.sig.ident;
355 let callback_name = syn::Ident::new(
356 &format!("{rust_method_name}_constructor_callback"),
357 rust_method_name.span(),
358 );
359 
360 let (has_lock_param, unwraps, arg_exprs) = extract_constructor_params(method);
361 let lock_arg = if has_lock_param {
362 quote! { &mut lock, }
363 } else {
364 quote! {}
365 };
366 
367 quote! {
368 jsg::Member::Constructor {
369 callback: {
370 unsafe extern "C" fn #callback_name(
371 info: *mut jsg::v8::ffi::FunctionCallbackInfo,
372 ) {
373 let mut lock = unsafe { jsg::Lock::from_args(info) };
374 jsg::catch_panic(&mut lock, || {
375 // SAFETY: info is a valid V8 FunctionCallbackInfo from the constructor call.
376 let mut args = unsafe { jsg::v8::FunctionCallbackInfo::from_ffi(info) };
377 let mut lock = unsafe { jsg::Lock::from_args(info) };
378 
379 #(#unwraps)*
380 
381 let resource = #self_ty::#rust_method_name(#lock_arg #(#arg_exprs),*);
382 let rc = jsg::Rc::new(resource);
383 rc.attach_to_this(&mut args);
384 });
385 }
386 #callback_name
387 },
388 }
389 }
390 })
391 .next()
392}
393 
394// ---------------------------------------------------------------------------
395// Property helpers
396// ---------------------------------------------------------------------------
397 
398/// Emits one `Member::Property { .. }` token stream for a single property group,
399/// or an `Err` compile-error stream if the group has no getter.
400fn emit_property_group(
401 js_name: &str,
402 kind: PropertyKind,
403 getter: Option<syn::Ident>,
404 setter: Option<syn::Ident>,
405) -> Result<quote::__private::TokenStream, quote::__private::TokenStream> {
406 let Some(getter_name) = getter else {
407 return Err(quote! {
408 compile_error!(concat!("no getter found for property \"", #js_name, "\""))
409 });
410 };
411 
412 let getter_cb = syn::Ident::new(&format!("{getter_name}_callback"), getter_name.span());
413 let kind_tokens = match kind {
414 PropertyKind::Prototype => quote! { jsg::PropertyKind::Prototype },
415 PropertyKind::Instance => quote! { jsg::PropertyKind::Instance },
416 PropertyKind::Inspect => quote! { jsg::PropertyKind::Inspect },
417 };
418 let setter_tokens = if let Some(setter_name) = setter {
419 let setter_cb = syn::Ident::new(&format!("{setter_name}_callback"), setter_name.span());
420 quote! { Some(Self::#setter_cb) }
421 } else {
422 quote! { None }
423 };
424 
425 Ok(quote! {
426 jsg::Member::Property {
427 name: #js_name.to_owned(),
428 kind: #kind_tokens,
429 getter_callback: Self::#getter_cb,
430 setter_callback: #setter_tokens,
431 }
432 })
433}
434 
435/// Parses the argument list of `#[jsg_property([placement,] [name = "..."] [, readonly])]`.
436///
437/// `placement` is optional; when omitted it defaults to `Prototype`.
438/// Returns `(PropertyKind, Option<js_name_override>, is_readonly)`.
439fn parse_jsg_property_args(
440 tokens: TokenStream,
441) -> Result<(PropertyKind, Option<String>, bool), quote::__private::TokenStream> {
442 use syn::parse::Parser as _;
443 let metas = syn::punctuated::Punctuated::<syn::Meta, syn::Token![,]>::parse_terminated
444 .parse(tokens)
445 .map_err(|e| e.to_compile_error())?;
446 
447 let mut kind: Option<PropertyKind> = None;
448 let mut name: Option<String> = None;
449 let mut readonly = false;
450 
451 for meta in &metas {
452 match meta {
453 syn::Meta::Path(p) if p.is_ident("instance") => {
454 if kind.is_some() {
455 return Err(syn::Error::new_spanned(
456 p,
457 "conflicting placement: specify either `instance` or `prototype`, not both",
458 )
459 .to_compile_error());
460 }
461 kind = Some(PropertyKind::Instance);
462 }
463 syn::Meta::Path(p) if p.is_ident("prototype") => {
464 if kind.is_some() {
465 return Err(syn::Error::new_spanned(
466 p,
467 "conflicting placement: specify either `instance` or `prototype`, not both",
468 )
469 .to_compile_error());
470 }
471 kind = Some(PropertyKind::Prototype);
472 }
473 syn::Meta::Path(p) if p.is_ident("readonly") => {
474 readonly = true;
475 }
476 syn::Meta::NameValue(nv) if nv.path.is_ident("name") => {
477 if let syn::Expr::Lit(syn::ExprLit {
478 lit: syn::Lit::Str(s),
479 ..
480 }) = &nv.value
481 {
482 name = Some(s.value());
483 } else {
484 return Err(syn::Error::new_spanned(
485 &nv.value,
486 "expected a string literal for `name`",
487 )
488 .to_compile_error());
489 }
490 }
491 _ => {
492 return Err(syn::Error::new_spanned(
493 meta,
494 "unknown argument; expected `instance`, `prototype`, `readonly`, or `name = \"...\"`",
495 )
496 .to_compile_error());
497 }
498 }
499 }
500 
501 // Default to `Prototype` when no explicit placement is given.
502 let kind = kind.unwrap_or(PropertyKind::Prototype);
503 
504 Ok((kind, name, readonly))
505}
506 
507struct PropMethod {
508 rust_name: syn::Ident,
509 is_setter: bool,
510 is_readonly: bool,
511}
512 
513/// Ordered list of property groups, preserving source-code declaration order.
514/// Each entry is `((js_name, kind), methods)`.
515type PropGroups = Vec<((String, PropertyKind), Vec<PropMethod>)>;
516 
517/// Derive the JS property name from a Rust method name: strip `get_`/`set_` prefix then
518/// convert `snake_case` -> `camelCase`.
519fn derive_js_name(rust_name: &str) -> String {
520 let stripped = rust_name
521 .strip_prefix("get_")
522 .or_else(|| rust_name.strip_prefix("set_"))
523 .unwrap_or(rust_name);
524 snake_to_camel(stripped)
525}
526 
527/// Find the group for `key` in `groups`, or append a new empty one and return it.
528/// Preserves insertion order so that property registration matches source-code order.
529fn prop_groups_find_or_insert(
530 groups: &mut PropGroups,
531 key: (String, PropertyKind),
532) -> &mut Vec<PropMethod> {
533 if let Some(pos) = groups.iter().position(|(k, _)| k == &key) {
534 return &mut groups[pos].1;
535 }
536 groups.push((key, Vec::new()));
537 &mut groups.last_mut().expect("just pushed").1
538}
539 
540/// Phase 1: scan `impl_block` for `#[jsg_property]` / `#[jsg_inspect_property]` annotations
541/// and group the annotated methods by `(js_name, PropertyKind)`.
542fn scan_property_annotations(
543 impl_block: &ItemImpl,
544) -> Result<PropGroups, quote::__private::TokenStream> {
545 let mut groups: PropGroups = Vec::new();
546 
547 for item in &impl_block.items {
548 let syn::ImplItem::Fn(method) = item else {
549 continue;
550 };
551 let rust_method_name = method.sig.ident.clone();
552 let rust_name_str = rust_method_name.to_string();
553 let is_setter = rust_name_str.starts_with("set_");
554 
555 if let Some(attr) = method.attrs.iter().find(|a| is_attr(a, "jsg_property")) {
556 if !rust_name_str.starts_with("get_") && !rust_name_str.starts_with("set_") {
557 return Err(syn::Error::new(
558 rust_method_name.span(),
559 "#[jsg_property] methods must be named with a `get_` prefix (getter) \
560 or `set_` prefix (setter)",
561 )
562 .to_compile_error());
563 }
564 
565 let tokens: TokenStream = attr
566 .meta
567 .require_list()
568 .map(|list| list.tokens.clone().into())
569 .unwrap_or_default();
570 let (kind, js_name_opt, is_readonly) = parse_jsg_property_args(tokens)?;
571 let js_name = js_name_opt.unwrap_or_else(|| derive_js_name(&rust_name_str));
572 prop_groups_find_or_insert(&mut groups, (js_name, kind)).push(PropMethod {
573 rust_name: rust_method_name,
574 is_setter,
575 is_readonly,
576 });
577 continue;
578 }
579 
580 if let Some(attr) = method
581 .attrs
582 .iter()
583 .find(|a| is_attr(a, "jsg_inspect_property"))
584 {
585 let attr_tokens: Option<TokenStream> = attr
586 .meta
587 .require_list()
588 .ok()
589 .map(|list| list.tokens.clone().into());
590 let js_name = attr_tokens
591 .and_then(extract_name_attribute)
592 .unwrap_or_else(|| derive_js_name(&rust_name_str));
593 prop_groups_find_or_insert(&mut groups, (js_name, PropertyKind::Inspect)).push(
594 PropMethod {
595 rust_name: rust_method_name,
596 is_setter,
597 is_readonly: false,
598 },
599 );
600 }
601 }
602 Ok(groups)
603}
604 
605/// Phase 2 (per group): validate constraints and emit one `Member::Property`.
606fn validate_and_emit_property(
607 js_name: &str,
608 kind: PropertyKind,
609 methods: Vec<PropMethod>,
610) -> Result<quote::__private::TokenStream, quote::__private::TokenStream> {
611 let has_readonly_getter = methods.iter().any(|m| m.is_readonly && !m.is_setter);
612 for m in &methods {
613 if m.is_readonly && m.is_setter {
614 return Err(syn::Error::new(
615 m.rust_name.span(),
616 "`readonly` attribute cannot be used on a setter method",
617 )
618 .to_compile_error());
619 }
620 }
621 let setters: Vec<_> = methods.iter().filter(|m| m.is_setter).collect();
622 if has_readonly_getter && !setters.is_empty() {
623 return Err(syn::Error::new(
624 setters[0].rust_name.span(),
625 "read-only property cannot have a setter; remove the setter or drop the `readonly` attribute",
626 )
627 .to_compile_error());
628 }
629 if kind == PropertyKind::Inspect {
630 for m in &methods {
631 if m.is_setter {
632 return Err(syn::Error::new(
633 m.rust_name.span(),
634 "#[jsg_inspect_property] methods must be getters; inspect properties are always read-only",
635 )
636 .to_compile_error());
637 }
638 }
639 }
640 
641 let mut getter: Option<syn::Ident> = None;
642 let mut setter: Option<syn::Ident> = None;
643 for m in methods {
644 if m.is_setter {
645 if setter.replace(m.rust_name).is_some() {
646 return Err(
647 quote! { compile_error!(concat!("duplicate setter for property \"", #js_name, "\"")) },
648 );
649 }
650 } else if getter.replace(m.rust_name).is_some() {
651 return Err(
652 quote! { compile_error!(concat!("duplicate getter for property \"", #js_name, "\"")) },
653 );
654 }
655 }
656 emit_property_group(js_name, kind, getter, setter)
657}
658 
659/// Scans an impl block for `#[jsg_property]` and `#[jsg_inspect_property]` annotations
660/// and returns a `Member::Property` token stream for each property group.
661fn collect_property_registrations(impl_block: &ItemImpl) -> Vec<quote::__private::TokenStream> {
662 let groups = match scan_property_annotations(impl_block) {
663 Ok(g) => g,
664 Err(e) => return vec![e],
665 };
666 let mut registrations = Vec::new();
667 for ((js_name, kind), methods) in groups {
668 match validate_and_emit_property(&js_name, kind, methods) {
669 Ok(ts) => registrations.push(ts),
670 Err(ts) => {
671 registrations.push(ts);
672 return registrations;
673 }
674 }
675 }
676 registrations
677}
678 
679#[cfg(test)]
680mod tests {
681 use syn::parse_quote;
682 
683 use super::*;
684 
685 #[test]
686 fn validate_constructor_valid() {
687 // A valid constructor: static (no self), returns Self.
688 let method: syn::ImplItemFn = parse_quote! {
689 fn constructor(name: String) -> Self { todo!() }
690 };
691 assert!(validate_constructor(&method).is_none());
692 }
693 
694 #[test]
695 fn validate_constructor_rejects_self_receiver() {
696 // Instance method — must not have &self.
697 let method: syn::ImplItemFn = parse_quote! {
698 fn constructor(&self) -> Self { todo!() }
699 };
700 assert!(validate_constructor(&method).is_some());
701 }
702 
703 #[test]
704 fn validate_constructor_rejects_non_self_return() {
705 // Returns String, not Self.
706 let method: syn::ImplItemFn = parse_quote! {
707 fn constructor() -> String { todo!() }
708 };
709 assert!(validate_constructor(&method).is_some());
710 }
711 
712 #[test]
713 fn extract_constructor_params_no_lock() {
714 // Plain constructor — no Lock param, two JS args.
715 let method: syn::ImplItemFn = parse_quote! {
716 fn constructor(name: String, value: u32) -> Self { todo!() }
717 };
718 let (has_lock, unwraps, arg_exprs) = extract_constructor_params(&method);
719 assert!(!has_lock);
720 assert_eq!(unwraps.len(), 2);
721 assert_eq!(arg_exprs.len(), 2);
722 }
723 
724 #[test]
725 fn extract_constructor_params_with_lock() {
726 // First param is `&mut jsg::Lock` — skipped from JS args.
727 let method: syn::ImplItemFn = parse_quote! {
728 fn constructor(lock: &mut jsg::Lock, name: String) -> Self { todo!() }
729 };
730 let (has_lock, unwraps, arg_exprs) = extract_constructor_params(&method);
731 assert!(has_lock);
732 // Only one JS arg (name); lock is not counted.
733 assert_eq!(unwraps.len(), 1);
734 assert_eq!(arg_exprs.len(), 1);
735 }
736 
737 #[test]
738 fn extract_constructor_params_no_args() {
739 let method: syn::ImplItemFn = parse_quote! {
740 fn constructor() -> Self { todo!() }
741 };
742 let (has_lock, unwraps, arg_exprs) = extract_constructor_params(&method);
743 assert!(!has_lock);
744 assert!(unwraps.is_empty());
745 assert!(arg_exprs.is_empty());
746 }
747}