Skip to main content

alef_codegen/generators/
structs.rs

1use crate::builder::StructBuilder;
2use crate::generators::RustBindingConfig;
3use crate::shared::binding_fields;
4use crate::type_mapper::TypeMapper;
5use alef_core::ir::{CoreWrapper, TypeDef, TypeRef};
6
7/// Check if a type's fields can all be safely defaulted.
8/// Primitives, strings, collections, Options, and Duration all have Default impls.
9/// Named types (custom structs) only have Default if explicitly marked with `has_default=true`.
10/// If any field is a Named type without `has_default`, returning true would generate
11/// code that calls `Default::default()` on a type that doesn't implement it.
12pub fn can_generate_default_impl(typ: &TypeDef, known_default_types: &std::collections::HashSet<&str>) -> bool {
13    for field in binding_fields(&typ.fields) {
14        if field.cfg.is_some() {
15            continue; // Skip cfg-gated fields
16        }
17        if !field_type_has_default(&field.ty, known_default_types) {
18            return false;
19        }
20    }
21    true
22}
23
24/// Check if a specific TypeRef can be safely defaulted.
25fn field_type_has_default(ty: &TypeRef, known_default_types: &std::collections::HashSet<&str>) -> bool {
26    match ty {
27        TypeRef::Primitive(_)
28        | TypeRef::String
29        | TypeRef::Char
30        | TypeRef::Bytes
31        | TypeRef::Path
32        | TypeRef::Unit
33        | TypeRef::Duration
34        | TypeRef::Json => true,
35        // Optional<T> defaults to None regardless of T
36        TypeRef::Optional(inner) => field_type_has_default(inner, known_default_types),
37        // Vec<T> defaults to empty vec regardless of T
38        TypeRef::Vec(inner) => field_type_has_default(inner, known_default_types),
39        // Map<K, V> defaults to empty map regardless of K/V
40        TypeRef::Map(k, v) => {
41            field_type_has_default(k, known_default_types) && field_type_has_default(v, known_default_types)
42        }
43        // Named types only have Default if marked with has_default=true
44        TypeRef::Named(name) => known_default_types.contains(name.as_str()),
45    }
46}
47
48/// Check if any two field names are similar enough to trigger clippy::similar_names.
49/// This detects patterns like "sub_symbol" and "sup_symbol" (differ by 1-2 chars).
50fn has_similar_names(names: &[&String]) -> bool {
51    for (i, &name1) in names.iter().enumerate() {
52        for &name2 in &names[i + 1..] {
53            // Simple heuristic: if names differ by <= 2 characters and have same length, flag it
54            if name1.len() == name2.len() && diff_count(name1, name2) <= 2 {
55                return true;
56            }
57        }
58    }
59    false
60}
61
62/// Count how many characters differ between two strings of equal length.
63fn diff_count(s1: &str, s2: &str) -> usize {
64    s1.chars().zip(s2.chars()).filter(|(c1, c2)| c1 != c2).count()
65}
66
67/// Check if a TypeRef references an opaque type, including through Optional and Vec wrappers.
68/// Opaque types use Arc<T> which doesn't implement Serialize/Deserialize, so any struct with
69/// such a field cannot derive those traits.
70pub fn field_references_opaque_type(ty: &TypeRef, opaque_names: &[String]) -> bool {
71    match ty {
72        TypeRef::Named(name) => opaque_names.contains(name),
73        TypeRef::Optional(inner) | TypeRef::Vec(inner) => field_references_opaque_type(inner, opaque_names),
74        TypeRef::Map(k, v) => {
75            field_references_opaque_type(k, opaque_names) || field_references_opaque_type(v, opaque_names)
76        }
77        _ => false,
78    }
79}
80
81/// Generate a struct definition using the builder, with a per-field attribute callback.
82///
83/// `extra_field_attrs` is called for each field and returns additional `#[...]` attributes to
84/// prepend (beyond `cfg.field_attrs`). Pass `|_| vec![]` to use the default behaviour.
85pub fn gen_struct_with_per_field_attrs(
86    typ: &TypeDef,
87    mapper: &dyn TypeMapper,
88    cfg: &RustBindingConfig,
89    extra_field_attrs: impl Fn(&alef_core::ir::FieldDef) -> Vec<String>,
90) -> String {
91    let mut sb = StructBuilder::new(&typ.name);
92    for attr in cfg.struct_attrs {
93        sb.add_attr(attr);
94    }
95
96    // Check if struct has similar field names (e.g., sub_symbol and sup_symbol)
97    let field_names: Vec<_> = binding_fields(&typ.fields)
98        .filter(|f| f.cfg.is_none())
99        .map(|f| &f.name)
100        .collect();
101    if has_similar_names(&field_names) {
102        sb.add_attr("allow(clippy::similar_names)");
103    }
104
105    for d in cfg.struct_derives {
106        sb.add_derive(d);
107    }
108    // Track which fields are opaque so we can conditionally skip derives and add #[serde(skip)].
109    let opaque_fields: Vec<&str> = typ
110        .fields
111        .iter()
112        .filter(|f| !f.binding_excluded)
113        .filter(|f| {
114            f.cfg.is_none()
115                && field_references_opaque_type(&f.ty, cfg.opaque_type_names)
116                && !field_references_opaque_type(&f.ty, cfg.serializable_opaque_type_names)
117        })
118        .map(|f| f.name.as_str())
119        .collect();
120    // Always derive Default/Serialize/Deserialize. Opaque fields get #[serde(skip)]
121    // so they use Default::default() during deserialization. This is needed for the
122    // serde recovery path where binding types round-trip through JSON.
123    sb.add_derive("Default");
124    sb.add_derive("serde::Serialize");
125    sb.add_derive("serde::Deserialize");
126    let has_serde = true;
127    for field in binding_fields(&typ.fields) {
128        let force_optional = cfg.option_duration_on_defaults
129            && typ.has_default
130            && !field.optional
131            && matches!(field.ty, TypeRef::Duration);
132        let ty = if (field.optional || force_optional) && !matches!(field.ty, TypeRef::Optional(_)) {
133            mapper.optional(&mapper.map_type(&field.ty))
134        } else {
135            // field.ty is already Optional(T) — mapped type is already Option<T>, don't double-wrap
136            mapper.map_type(&field.ty)
137        };
138        let mut attrs: Vec<String> = cfg.field_attrs.iter().map(|a| a.to_string()).collect();
139        attrs.extend(extra_field_attrs(field));
140        // Add #[serde(skip)] for opaque fields or sanitized fields when the struct derives serde.
141        // Cow-backed strings are lossless String bindings, so they must remain serializable.
142        // Cfg-gated trait-bridge fields are detected via their type referencing an opaque
143        // wrapper (e.g. PyVisitorRef in opaque_type_names) — those get serde(skip) because
144        // their wrapper types typically don't impl serde. Regular cfg-gated fields with
145        // serializable types (e.g. HtmlMetadata) remain serializable.
146        let skip_sanitized_field = field.sanitized && field.core_wrapper != CoreWrapper::Cow;
147        let skip_cfg_bridge_field = field.cfg.is_some()
148            && cfg.never_skip_cfg_field_names.contains(&field.name)
149            && field_references_opaque_type(&field.ty, cfg.opaque_type_names);
150        if has_serde && (opaque_fields.contains(&field.name.as_str()) || skip_sanitized_field || skip_cfg_bridge_field)
151        {
152            attrs.push("serde(skip)".to_string());
153        }
154        sb.add_field_with_doc(&field.name, &ty, attrs, &field.doc);
155    }
156    sb.build()
157}
158
159/// Generate a struct definition using the builder, with per-field attribute and name override callbacks.
160///
161/// This is the most flexible variant.  Use it when the target language may need to escape
162/// reserved keywords in field names (e.g. Python's `class` → `class_`).
163///
164/// * `extra_field_attrs` — called per field, returns additional `#[…]` attribute strings to
165///   append **after** `cfg.field_attrs`.  Return an empty vec for the default behaviour.
166/// * `field_name_override` — called per field, returns `Some(escaped_name)` when the Rust
167///   binding struct field name should differ from `field.name` (e.g. for keyword escaping),
168///   or `None` to keep the original name.
169///
170/// When a field name is overridden the caller is responsible for adding the appropriate
171/// language attribute (e.g. `pyo3(get, name = "original")`) via `extra_field_attrs`.
172/// `cfg.field_attrs` is **still** applied for non-renamed fields; for renamed fields the
173/// caller should replace the default field attrs entirely by returning them from
174/// `extra_field_attrs` and passing a modified `cfg` with empty `field_attrs`.
175pub fn gen_struct_with_rename(
176    typ: &TypeDef,
177    mapper: &dyn TypeMapper,
178    cfg: &RustBindingConfig,
179    extra_field_attrs: impl Fn(&alef_core::ir::FieldDef) -> Vec<String>,
180    field_name_override: impl Fn(&alef_core::ir::FieldDef) -> Option<String>,
181) -> String {
182    let mut sb = StructBuilder::new(&typ.name);
183    for attr in cfg.struct_attrs {
184        sb.add_attr(attr);
185    }
186
187    let field_names: Vec<_> = binding_fields(&typ.fields)
188        .filter(|f| f.cfg.is_none())
189        .map(|f| &f.name)
190        .collect();
191    if has_similar_names(&field_names) {
192        sb.add_attr("allow(clippy::similar_names)");
193    }
194
195    for d in cfg.struct_derives {
196        sb.add_derive(d);
197    }
198    let opaque_fields: Vec<&str> = typ
199        .fields
200        .iter()
201        .filter(|f| !f.binding_excluded)
202        .filter(|f| {
203            f.cfg.is_none()
204                && field_references_opaque_type(&f.ty, cfg.opaque_type_names)
205                && !field_references_opaque_type(&f.ty, cfg.serializable_opaque_type_names)
206        })
207        .map(|f| f.name.as_str())
208        .collect();
209    sb.add_derive("Default");
210    sb.add_derive("serde::Serialize");
211    sb.add_derive("serde::Deserialize");
212    let has_serde = true;
213    for field in binding_fields(&typ.fields) {
214        let force_optional = cfg.option_duration_on_defaults
215            && typ.has_default
216            && !field.optional
217            && matches!(field.ty, TypeRef::Duration);
218        let ty = if (field.optional || force_optional) && !matches!(field.ty, TypeRef::Optional(_)) {
219            mapper.optional(&mapper.map_type(&field.ty))
220        } else {
221            mapper.map_type(&field.ty)
222        };
223        let name_override = field_name_override(field);
224        let extra_attrs = extra_field_attrs(field);
225        // When the field name is overridden (keyword-escaped), skip cfg.field_attrs so the
226        // caller's extra_field_attrs callback can supply the full replacement attr set
227        // (e.g. `pyo3(get, name = "class")` instead of the default `pyo3(get)`).
228        let mut attrs: Vec<String> = if name_override.is_some() && !extra_attrs.is_empty() {
229            extra_attrs
230        } else {
231            let mut a: Vec<String> = cfg.field_attrs.iter().map(|a| a.to_string()).collect();
232            a.extend(extra_attrs);
233            a
234        };
235        // Add #[serde(skip)] for opaque/sanitized fields and cfg-gated trait-bridge fields.
236        // Trait-bridge fields are detected via their type referencing an opaque wrapper
237        // (e.g. PyVisitorRef in opaque_type_names) so regular cfg-gated fields like
238        // `metadata: HtmlMetadata` remain serializable for JSON round-trip.
239        let skip_sanitized_field = field.sanitized && field.core_wrapper != CoreWrapper::Cow;
240        let skip_cfg_bridge_field = field.cfg.is_some()
241            && cfg.never_skip_cfg_field_names.contains(&field.name)
242            && field_references_opaque_type(&field.ty, cfg.opaque_type_names);
243        if has_serde && (opaque_fields.contains(&field.name.as_str()) || skip_sanitized_field || skip_cfg_bridge_field)
244        {
245            attrs.push("serde(skip)".to_string());
246        }
247        // Mirror per-field `#[serde(rename = "...")]` from the core type so the binding
248        // struct's JSON wire format matches the core's. Only when no caller-supplied
249        // serde rename is already present (caller may emit one for keyword-escapes).
250        if has_serde
251            && !attrs.iter().any(|a| a.starts_with("serde(rename"))
252            && !attrs.iter().any(|a| a == "serde(skip)")
253        {
254            if let Some(rename) = &field.serde_rename {
255                attrs.push(format!("serde(rename = \"{rename}\")"));
256            }
257        }
258        let emit_name = name_override.unwrap_or_else(|| field.name.clone());
259        sb.add_field_with_doc(&emit_name, &ty, attrs, &field.doc);
260    }
261    sb.build()
262}
263
264/// Generate a struct definition using the builder.
265pub fn gen_struct(typ: &TypeDef, mapper: &dyn TypeMapper, cfg: &RustBindingConfig) -> String {
266    let mut sb = StructBuilder::new(&typ.name);
267    for attr in cfg.struct_attrs {
268        sb.add_attr(attr);
269    }
270
271    // Check if struct has similar field names (e.g., sub_symbol and sup_symbol)
272    let field_names: Vec<_> = binding_fields(&typ.fields)
273        .filter(|f| f.cfg.is_none())
274        .map(|f| &f.name)
275        .collect();
276    if has_similar_names(&field_names) {
277        sb.add_attr("allow(clippy::similar_names)");
278    }
279
280    for d in cfg.struct_derives {
281        sb.add_derive(d);
282    }
283    let _opaque_fields: Vec<&str> = typ
284        .fields
285        .iter()
286        .filter(|f| !f.binding_excluded)
287        .filter(|f| {
288            f.cfg.is_none()
289                && field_references_opaque_type(&f.ty, cfg.opaque_type_names)
290                && !field_references_opaque_type(&f.ty, cfg.serializable_opaque_type_names)
291        })
292        .map(|f| f.name.as_str())
293        .collect();
294    sb.add_derive("Default");
295    sb.add_derive("serde::Serialize");
296    sb.add_derive("serde::Deserialize");
297    let _has_serde = true;
298    for field in binding_fields(&typ.fields) {
299        // Skip cfg-gated fields — they depend on features that may not be enabled
300        // for this binding crate. Including them would require the binding struct to
301        // handle conditional compilation which struct literal initializers can't express.
302        if field.cfg.is_some() && !cfg.never_skip_cfg_field_names.contains(&field.name) {
303            continue;
304        }
305        // When option_duration_on_defaults is set, wrap non-optional Duration fields in
306        // Option<u64> for has_default types so the binding constructor can accept None
307        // and the From conversion falls back to the core type's Default.
308        let force_optional = cfg.option_duration_on_defaults
309            && typ.has_default
310            && !field.optional
311            && matches!(field.ty, TypeRef::Duration);
312        let ty = if (field.optional || force_optional) && !matches!(field.ty, TypeRef::Optional(_)) {
313            mapper.optional(&mapper.map_type(&field.ty))
314        } else {
315            // field.ty is already Optional(T) — mapped type is already Option<T>, don't double-wrap
316            mapper.map_type(&field.ty)
317        };
318        let mut attrs: Vec<String> = cfg.field_attrs.iter().map(|a| a.to_string()).collect();
319        // Mirror per-field `#[serde(rename = "...")]` from the core type so the binding
320        // struct's JSON wire format matches the core's.
321        if let Some(rename) = &field.serde_rename {
322            if !attrs.iter().any(|a| a.starts_with("serde(rename")) {
323                attrs.push(format!("serde(rename = \"{rename}\")"));
324            }
325        }
326        sb.add_field_with_doc(&field.name, &ty, attrs, &field.doc);
327    }
328    sb.build()
329}
330
331/// Generate a `Default` impl for a non-opaque binding struct with `has_default`.
332/// All fields use their type's Default::default().
333/// Optional fields use None instead of Default::default().
334/// This enables the struct to be used with `unwrap_or_default()` in config constructors.
335///
336/// WARNING: This assumes all field types implement Default. If a Named field type
337/// doesn't implement Default, this impl will fail to compile. Callers should verify
338/// that the struct's fields can be safely defaulted before calling this function.
339pub fn gen_struct_default_impl(typ: &TypeDef, name_prefix: &str) -> String {
340    let full_name = format!("{}{}", name_prefix, typ.name);
341    let fields: Vec<_> = typ
342        .fields
343        .iter()
344        .filter(|field| !field.binding_excluded)
345        .filter_map(|field| {
346            if field.cfg.is_some() {
347                return None;
348            }
349            let default_val = match &field.ty {
350                TypeRef::Optional(_) => "None".to_string(),
351                _ => "Default::default()".to_string(),
352            };
353            Some(minijinja::context! {
354                name => field.name.clone(),
355                default_val => default_val
356            })
357        })
358        .collect();
359
360    crate::template_env::render(
361        "structs/default_impl.jinja",
362        minijinja::context! {
363            full_name => full_name,
364            fields => fields
365        },
366    )
367}
368
369/// Check if any method on a type takes `&mut self`, meaning the opaque wrapper
370/// must use `Arc<Mutex<T>>` instead of `Arc<T>` to allow interior mutability.
371pub fn type_needs_mutex(typ: &TypeDef) -> bool {
372    typ.methods
373        .iter()
374        .any(|m| m.receiver == Some(alef_core::ir::ReceiverKind::RefMut))
375}
376
377/// Check if a type wrapping `Arc<Mutex<T>>` should use `tokio::sync::Mutex` instead
378/// of `std::sync::Mutex` because every `&mut self` method is `async`.
379///
380/// `std::sync::MutexGuard` is `!Send`, so holding a guard across `.await` makes the
381/// surrounding future `!Send`, which fails to compile in PyO3 / NAPI-RS bindings that
382/// require `Send` futures. `tokio::sync::MutexGuard` IS `Send`, so swapping the lock
383/// type fixes the entire async-locking story for these structs.
384///
385/// The condition is tight: every method that takes `&mut self` MUST be async. If even
386/// one sync method takes `&mut self`, switching to `tokio::sync::Mutex` would break
387/// it (since `tokio::sync::Mutex::lock()` returns a `Future` and cannot be awaited
388/// from sync context). In that mixed case we keep `std::sync::Mutex`.
389pub fn type_needs_tokio_mutex(typ: &TypeDef) -> bool {
390    use alef_core::ir::ReceiverKind;
391    if !type_needs_mutex(typ) {
392        return false;
393    }
394    let refmut_methods = typ.methods.iter().filter(|m| m.receiver == Some(ReceiverKind::RefMut));
395    let mut any = false;
396    for m in refmut_methods {
397        any = true;
398        if !m.is_async {
399            return false;
400        }
401    }
402    any
403}
404
405/// Generate an opaque wrapper struct with `inner: Arc<core::Type>`.
406/// For trait types, uses `Arc<dyn Type + Send + Sync>`.
407/// For types with `&mut self` methods, uses `Arc<Mutex<core::Type>>`.
408///
409/// Special case: if ALL methods on this type are sanitized, the type was created by the
410/// impl-block fallback for a generic core type (e.g. `GraphQLExecutor<Q,M,S>`). Sanitized
411/// methods never access `self.inner` (they emit `gen_unimplemented_body`), so we omit the
412/// `inner` field entirely. This avoids generating `Arc<CoreType>` with missing generic
413/// parameters, which would fail to compile.
414pub fn gen_opaque_struct(typ: &TypeDef, cfg: &RustBindingConfig) -> String {
415    let needs_mutex = type_needs_mutex(typ);
416    // Omit the inner field only when the rust_path contains generic type parameters
417    // (angle brackets), which means the concrete types are unknown at codegen time and
418    // `Arc<CoreType<_, _, _>>` would fail to compile. This typically occurs for types
419    // created from a generic impl block where all methods are sanitized.
420    // We do NOT omit inner solely because all_methods_sanitized is true: even when no
421    // methods delegate to self.inner, the inner field may be required by From impls
422    // generated for non-opaque structs that have this type as a field.
423    let core_path = typ.rust_path.replace('-', "_");
424    let has_unresolvable_generics = core_path.contains('<');
425    let all_methods_sanitized = !typ.methods.is_empty() && typ.methods.iter().all(|m| m.sanitized);
426    let omit_inner = all_methods_sanitized && has_unresolvable_generics;
427
428    let struct_attrs: Vec<_> = cfg.struct_attrs.iter().map(|s| s.to_string()).collect();
429    let has_derives = !cfg.struct_derives.is_empty();
430    let inner_type = if typ.is_trait {
431        format!("Arc<dyn {core_path} + Send + Sync>")
432    } else if needs_mutex {
433        format!("Arc<std::sync::Mutex<{core_path}>>")
434    } else {
435        format!("Arc<{core_path}>")
436    };
437
438    crate::template_env::render(
439        "structs/opaque_struct.jinja",
440        minijinja::context! {
441            struct_name => typ.name.clone(),
442            has_derives => has_derives,
443            struct_attrs => struct_attrs,
444            omit_inner => omit_inner,
445            inner_type => inner_type,
446        },
447    )
448}
449
450/// Generate an opaque wrapper struct with `inner: Arc<core::Type>` and a name prefix.
451/// For types with `&mut self` methods, uses `Arc<Mutex<core::Type>>`.
452///
453/// Special case: if ALL methods on this type are sanitized, omit the `inner` field.
454/// See `gen_opaque_struct` for the rationale.
455pub fn gen_opaque_struct_prefixed(typ: &TypeDef, cfg: &RustBindingConfig, prefix: &str) -> String {
456    let needs_mutex = type_needs_mutex(typ);
457    let core_path = typ.rust_path.replace('-', "_");
458    let has_unresolvable_generics = core_path.contains('<');
459    let all_methods_sanitized = !typ.methods.is_empty() && typ.methods.iter().all(|m| m.sanitized);
460    let omit_inner = all_methods_sanitized && has_unresolvable_generics;
461
462    let struct_attrs: Vec<_> = cfg.struct_attrs.iter().map(|s| s.to_string()).collect();
463    let has_derives = !cfg.struct_derives.is_empty();
464    let struct_name = format!("{prefix}{}", typ.name);
465    let inner_type = if typ.is_trait {
466        format!("Arc<dyn {core_path} + Send + Sync>")
467    } else if needs_mutex {
468        format!("Arc<std::sync::Mutex<{core_path}>>")
469    } else {
470        format!("Arc<{core_path}>")
471    };
472
473    crate::template_env::render(
474        "structs/opaque_struct.jinja",
475        minijinja::context! {
476            struct_name => struct_name,
477            has_derives => has_derives,
478            struct_attrs => struct_attrs,
479            omit_inner => omit_inner,
480            inner_type => inner_type,
481        },
482    )
483}
484
485#[cfg(test)]
486mod tests {
487    use super::{type_needs_mutex, type_needs_tokio_mutex};
488    use alef_core::ir::{MethodDef, ReceiverKind, TypeDef, TypeRef};
489
490    fn method(name: &str, receiver: Option<ReceiverKind>, is_async: bool) -> MethodDef {
491        MethodDef {
492            name: name.into(),
493            params: vec![],
494            return_type: TypeRef::Unit,
495            is_async,
496            is_static: false,
497            error_type: None,
498            doc: String::new(),
499            receiver,
500            sanitized: false,
501            trait_source: None,
502            returns_ref: false,
503            returns_cow: false,
504            return_newtype_wrapper: None,
505            has_default_impl: false,
506            binding_excluded: false,
507            binding_exclusion_reason: None,
508        }
509    }
510
511    fn type_with_methods(name: &str, methods: Vec<MethodDef>) -> TypeDef {
512        TypeDef {
513            name: name.into(),
514            rust_path: format!("my_crate::{name}"),
515            original_rust_path: String::new(),
516            fields: vec![],
517            methods,
518            is_opaque: true,
519            is_clone: false,
520            is_copy: false,
521            is_trait: false,
522            has_default: false,
523            has_stripped_cfg_fields: false,
524            is_return_type: false,
525            serde_rename_all: None,
526            has_serde: false,
527            super_traits: vec![],
528            doc: String::new(),
529            cfg: None,
530            binding_excluded: false,
531            binding_exclusion_reason: None,
532        }
533    }
534
535    #[test]
536    fn tokio_mutex_when_all_refmut_methods_async() {
537        let typ = type_with_methods(
538            "WebSocketConnection",
539            vec![
540                method("send_text", Some(ReceiverKind::RefMut), true),
541                method("receive_text", Some(ReceiverKind::RefMut), true),
542                method("close", None, true),
543            ],
544        );
545        assert!(type_needs_mutex(&typ));
546        assert!(type_needs_tokio_mutex(&typ));
547    }
548
549    #[test]
550    fn no_tokio_mutex_when_any_refmut_is_sync() {
551        let typ = type_with_methods(
552            "Mixed",
553            vec![
554                method("async_op", Some(ReceiverKind::RefMut), true),
555                method("sync_op", Some(ReceiverKind::RefMut), false),
556            ],
557        );
558        assert!(type_needs_mutex(&typ));
559        assert!(!type_needs_tokio_mutex(&typ));
560    }
561
562    #[test]
563    fn no_tokio_mutex_when_no_refmut() {
564        let typ = type_with_methods("ReadOnly", vec![method("get", Some(ReceiverKind::Ref), true)]);
565        assert!(!type_needs_mutex(&typ));
566        assert!(!type_needs_tokio_mutex(&typ));
567    }
568
569    #[test]
570    fn no_tokio_mutex_when_empty_methods() {
571        let typ = type_with_methods("Empty", vec![]);
572        assert!(!type_needs_mutex(&typ));
573        assert!(!type_needs_tokio_mutex(&typ));
574    }
575}