Skip to main content

alef_codegen/generators/
structs.rs

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