Skip to main content

rkyv_js_codegen/
extractor.rs

1//! Rust source extraction: parses files with `syn` and adds every type
2//! marked with a recognized derive (default `rkyv::Archive`) to the [`CodeGenerator`].
3//!
4//! ## Marker detection
5//!
6//! A derive path marks a type for extraction iff:
7//!
8//! - it is the exact multi-segment marker path (`rkyv::Archive`, leading `::` allowed), or
9//! - it is a single-segment ident resolving to a marker path through the file's `use` imports (including renames), or
10//! - it is a bare ident and a glob import (`use rkyv::*`) brings a marker path into scope, or
11//! - it matches a path registered via [`add_marker_path`](CodeGenerator::add_marker_path).
12//!
13//! ## Use-item analysis
14//!
15//! `use` trees are flattened into a local-name → fully-qualified-path map:
16//!
17//! - `use std::collections::BTreeMap` maps `BTreeMap` to `std::collections::BTreeMap`
18//! - `use rkyv::Archive as Rkyv` maps `Rkyv` to `rkyv::Archive`
19//! - `type HashMap<K, V> = std::collections::HashMap<K, V, S>` maps `HashMap` to `std::collections::HashMap` 
20//!   (the alias *path* only; extra RHS arguments surface as trailing type arguments at the use site)
21//!
22//! ## Remote proxies
23//!
24//! A type with `#[rkyv(remote = T)]` is a serialization proxy: it emits no top-level export.
25//! Instead, the proxy itself is auto-registered as a with-wrapper whose template is the proxy's own codec expression,
26//! so fields annotated `#[rkyv(with = ProxyDef)]` resolve to it (rkyv 0.8 semantics).
27
28use std::collections::HashMap;
29use std::fs;
30use std::io;
31use std::path::{Path, PathBuf};
32
33use quote::ToTokens;
34use syn::ext::IdentExt;
35use syn::spanned::Spanned;
36use syn::{
37    Attribute, Fields, GenericArgument, PathArguments, Type, TypeArray, TypePath, TypeTuple,
38    UseTree,
39};
40use walkdir::WalkDir;
41
42use crate::error::{Diagnostic, DiagnosticKind, Error, SourceLocation};
43use crate::expr::{CodecExpr, codec};
44use crate::generator::{CodeGenerator, EnumVariant, OnUnknown, TypeKind};
45use crate::registry::WithWrapper;
46
47/// Per-file context built from `use` items and type aliases.
48struct SourceContext {
49    /// Maps local name → fully-qualified path.
50    imports: HashMap<String, String>,
51    /// Glob import prefixes (`use rkyv::*` → `"rkyv"`).
52    globs: Vec<String>,
53    /// The file being parsed, if known.
54    file: Option<PathBuf>,
55}
56
57impl SourceContext {
58    fn location(&self, span: proc_macro2::Span) -> SourceLocation {
59        let start = span.start();
60        SourceLocation {
61            file: self.file.clone(),
62            line: start.line,
63            column: start.column + 1,
64        }
65    }
66}
67
68/// Recursively flatten a `UseTree` into import entries and glob prefixes.
69fn collect_imports(
70    tree: &UseTree,
71    prefix: &[String],
72    imports: &mut HashMap<String, String>,
73    globs: &mut Vec<String>,
74) {
75    match tree {
76        UseTree::Path(p) => {
77            let mut new_prefix = prefix.to_vec();
78            new_prefix.push(p.ident.to_string());
79            collect_imports(&p.tree, &new_prefix, imports, globs);
80        }
81        UseTree::Name(n) => {
82            let name = n.ident.to_string();
83            let full_path = make_full_path(prefix, &name);
84            imports.insert(name, full_path);
85        }
86        UseTree::Rename(r) => {
87            let canonical = r.ident.to_string();
88            let alias = r.rename.to_string();
89            let full_path = make_full_path(prefix, &canonical);
90            imports.insert(alias, full_path);
91        }
92        UseTree::Glob(_) => {
93            if !prefix.is_empty() {
94                globs.push(prefix.join("::"));
95            }
96        }
97        UseTree::Group(g) => {
98            for item in &g.items {
99                collect_imports(item, prefix, imports, globs);
100            }
101        }
102    }
103}
104
105fn make_full_path(prefix: &[String], name: &str) -> String {
106    if prefix.is_empty() {
107        name.to_string()
108    } else {
109        format!("{}::{}", prefix.join("::"), name)
110    }
111}
112
113fn path_segments(path: &syn::Path) -> Vec<String> {
114    path.segments.iter().map(|s| s.ident.to_string()).collect()
115}
116
117/// Build a `SourceContext` from all `use` items and type aliases in a file.
118fn build_source_context(file: &syn::File, source_file: Option<PathBuf>) -> SourceContext {
119    let mut imports = HashMap::new();
120    let mut globs = Vec::new();
121
122    for item in &file.items {
123        match item {
124            syn::Item::Use(item_use) => {
125                collect_imports(&item_use.tree, &[], &mut imports, &mut globs);
126            }
127            // `type Foo<..> = some::path::Bar<..>` maps `Foo` to
128            // `some::path::Bar`. Only the path is resolved; generic
129            // parameters on either side are handled at the use site.
130            syn::Item::Type(item_type) => {
131                if let Type::Path(TypePath { path, .. }) = &*item_type.ty
132                    && path.segments.len() > 1
133                {
134                    imports.insert(
135                        item_type.ident.to_string(),
136                        path_segments(path).join("::"),
137                    );
138                }
139            }
140            _ => {}
141        }
142    }
143
144    SourceContext {
145        imports,
146        globs,
147        file: source_file,
148    }
149}
150
151/// Type-level `#[rkyv(...)]` attributes the extractor understands.
152#[derive(Default)]
153struct RkyvTypeAttrs {
154    /// `#[rkyv(remote = T)]`
155    remote: Option<syn::Type>,
156    /// `#[rkyv(archived = Name)]`
157    archived: Option<String>,
158}
159
160/// Consume the rest of an unrecognized nested meta so parsing can continue.
161fn skip_nested_meta_value(meta: &syn::meta::ParseNestedMeta) -> syn::Result<()> {
162    if meta.input.peek(syn::Token![=]) {
163        let _eq: syn::Token![=] = meta.input.parse()?;
164        while !meta.input.is_empty() && !meta.input.peek(syn::Token![,]) {
165            let _tt: proc_macro2::TokenTree = meta.input.parse()?;
166        }
167    } else if meta.input.peek(syn::token::Paren) {
168        let content;
169        syn::parenthesized!(content in meta.input);
170        let _rest: proc_macro2::TokenStream = content.parse()?;
171    }
172    Ok(())
173}
174
175fn parse_rkyv_type_attrs(attrs: &[Attribute]) -> RkyvTypeAttrs {
176    let mut parsed = RkyvTypeAttrs::default();
177    for attr in attrs {
178        if !attr.path().is_ident("rkyv") {
179            continue;
180        }
181        let _ = attr.parse_nested_meta(|meta| {
182            if meta.path.is_ident("remote") {
183                parsed.remote = Some(meta.value()?.parse()?);
184            } else if meta.path.is_ident("archived") {
185                let path: syn::Path = meta.value()?.parse()?;
186                if let Some(last) = path.segments.last() {
187                    parsed.archived = Some(last.ident.to_string());
188                }
189            } else {
190                skip_nested_meta_value(&meta)?;
191            }
192            Ok(())
193        });
194    }
195    parsed
196}
197
198/// The `W` of a field-level `#[rkyv(with = W)]`, if present.
199fn parse_rkyv_field_with(attrs: &[Attribute]) -> Option<syn::Type> {
200    let mut with: Option<syn::Type> = None;
201    for attr in attrs {
202        if !attr.path().is_ident("rkyv") {
203            continue;
204        }
205        let _ = attr.parse_nested_meta(|meta| {
206            if meta.path.is_ident("with") {
207                if with.is_none() {
208                    with = Some(meta.value()?.parse()?);
209                } else {
210                    skip_nested_meta_value(&meta)?;
211                }
212            } else {
213                skip_nested_meta_value(&meta)?;
214            }
215            Ok(())
216        });
217    }
218    with
219}
220
221/// Check whether one of the derive paths marks the type for extraction.
222fn has_marker_derive(attrs: &[Attribute], ctx: &SourceContext, codegen: &CodeGenerator) -> bool {
223    let markers = &codegen.marker_paths;
224    for attr in attrs {
225        if !attr.path().is_ident("derive") {
226            continue;
227        }
228        let Ok(nested) = attr.parse_args_with(
229            syn::punctuated::Punctuated::<syn::Path, syn::Token![,]>::parse_terminated,
230        ) else {
231            continue;
232        };
233        for path in nested {
234            let segments = path_segments(&path);
235            if segments.len() == 1 {
236                let ident = &segments[0];
237                // A user marker registered as a bare name.
238                if markers.contains(ident) {
239                    return true;
240                }
241                // Resolve through imports (incl. renames).
242                if ctx.imports.get(ident).is_some_and(|fq| markers.contains(fq)) {
243                    return true;
244                }
245                // Resolve through glob imports.
246                if ctx
247                    .globs
248                    .iter()
249                    .any(|glob| markers.contains(&format!("{glob}::{ident}")))
250                {
251                    return true;
252                }
253            } else {
254                let joined = segments.join("::");
255                if markers.contains(&joined) {
256                    return true;
257                }
258            }
259        }
260    }
261    false
262}
263
264fn type_to_string(ty: &Type) -> String {
265    ty.to_token_stream().to_string()
266}
267
268/// Convert a syn type to a codec expression.
269///
270/// Errors carry only the [`DiagnosticKind`]; the calling field attaches
271/// `referenced_by` and location provenance.
272fn type_to_expr(
273    ty: &Type,
274    codegen: &CodeGenerator,
275    ctx: &SourceContext,
276) -> Result<CodecExpr, DiagnosticKind> {
277    match ty {
278        Type::Path(TypePath { qself: None, path }) => {
279            let segment = path.segments.last().expect("type paths are non-empty");
280            let raw_ident = segment.ident.to_string();
281
282            // Multi-segment paths are already fully qualified; single-segment
283            // idents resolve through the file's imports.
284            let full_path = if path.segments.len() > 1 {
285                path_segments(path).join("::")
286            } else {
287                ctx.imports
288                    .get(&raw_ident)
289                    .cloned()
290                    .unwrap_or_else(|| raw_ident.clone())
291            };
292
293            match full_path.as_str() {
294                "u8" => Ok(codec::u8()),
295                "i8" => Ok(codec::i8()),
296                "u16" => Ok(codec::u16()),
297                "i16" => Ok(codec::i16()),
298                "u32" => Ok(codec::u32()),
299                "i32" => Ok(codec::i32()),
300                "u64" => Ok(codec::u64()),
301                "i64" => Ok(codec::i64()),
302                "f32" => Ok(codec::f32()),
303                "f64" => Ok(codec::f64()),
304                "bool" => Ok(codec::bool_()),
305                "char" => Ok(codec::char_()),
306                "String" | "std::string::String" => Ok(codec::string()),
307                "Vec" | "std::vec::Vec" => {
308                    let inner = single_generic_arg(segment, ty)?;
309                    Ok(codec::vec(type_to_expr(inner, codegen, ctx)?))
310                }
311                "Option" | "std::option::Option" => {
312                    let inner = single_generic_arg(segment, ty)?;
313                    Ok(codec::option(type_to_expr(inner, codegen, ctx)?))
314                }
315                "Box" | "std::boxed::Box" => {
316                    let inner = single_generic_arg(segment, ty)?;
317                    Ok(codec::boxed(type_to_expr(inner, codegen, ctx)?))
318                }
319                _ => {
320                    if let Some(external) = codegen.registry.get_type(&full_path) {
321                        let raw_args = collect_type_args(segment);
322                        // Trailing arguments (hashers, allocators) are discarded,
323                        // so never try to resolve them to codecs.
324                        let keep = if external.allows_trailing() && raw_args.len() > external.arity()
325                        {
326                            external.arity()
327                        } else {
328                            raw_args.len()
329                        };
330                        let args = raw_args[..keep]
331                            .iter()
332                            .map(|arg| type_to_expr(arg, codegen, ctx))
333                            .collect::<Result<Vec<_>, _>>()?;
334                        external.instantiate(args).map_err(|kind| match kind {
335                            DiagnosticKind::GenericArity {
336                                expected, found, ..
337                            } => DiagnosticKind::GenericArity {
338                                rust_path: full_path.clone(),
339                                expected,
340                                found,
341                            },
342                            other => other,
343                        })
344                    } else if path.segments.len() == 1 && full_path == raw_ident {
345                        // A bare local ident: a reference to another generated type,
346                        // validated at generate time.
347                        Ok(codec::named(raw_ident))
348                    } else {
349                        Err(DiagnosticKind::UnknownType {
350                            suggestion: codegen.registry.suggest_type(&full_path),
351                            rust_path: full_path,
352                        })
353                    }
354                }
355            }
356        }
357        Type::Array(TypeArray { elem, len, .. }) => {
358            let elem_expr = type_to_expr(elem, codegen, ctx)?;
359            if let syn::Expr::Lit(syn::ExprLit {
360                lit: syn::Lit::Int(lit_int),
361                ..
362            }) = len
363                && let Ok(len_val) = lit_int.base10_parse::<u64>()
364            {
365                Ok(codec::array(elem_expr, len_val))
366            } else {
367                Err(DiagnosticKind::UnsupportedFieldType {
368                    rust_type: type_to_string(ty),
369                })
370            }
371        }
372        Type::Tuple(TypeTuple { elems, .. }) => {
373            let elem_exprs = elems
374                .iter()
375                .map(|elem| type_to_expr(elem, codegen, ctx))
376                .collect::<Result<Vec<_>, _>>()?;
377            Ok(codec::tuple(elem_exprs))
378        }
379        Type::Reference(reference) => {
380            if let Type::Path(TypePath { path, .. }) = &*reference.elem
381                && path.is_ident("str")
382            {
383                return Ok(codec::string());
384            }
385            type_to_expr(&reference.elem, codegen, ctx)
386        }
387        Type::Paren(paren) => type_to_expr(&paren.elem, codegen, ctx),
388        Type::Group(group) => type_to_expr(&group.elem, codegen, ctx),
389        other => Err(DiagnosticKind::UnsupportedFieldType {
390            rust_type: type_to_string(other),
391        }),
392    }
393}
394
395fn single_generic_arg<'a>(
396    segment: &'a syn::PathSegment,
397    whole: &Type,
398) -> Result<&'a Type, DiagnosticKind> {
399    if let PathArguments::AngleBracketed(args) = &segment.arguments
400        && let Some(GenericArgument::Type(ty)) = args.args.first()
401    {
402        return Ok(ty);
403    }
404    Err(DiagnosticKind::UnsupportedFieldType {
405        rust_type: type_to_string(whole),
406    })
407}
408
409/// Collect the type arguments of a path segment.
410///
411/// - `[T; N]` array arguments are unwrapped to `T` (SmallVec/TinyVec-style parameters).
412/// - Lifetimes and const generics are skipped.
413fn collect_type_args(segment: &syn::PathSegment) -> Vec<&Type> {
414    let PathArguments::AngleBracketed(args) = &segment.arguments else {
415        return vec![];
416    };
417
418    let mut type_args = Vec::new();
419    for arg in &args.args {
420        if let GenericArgument::Type(ty) = arg {
421            if let Type::Array(TypeArray { elem, .. }) = ty {
422                type_args.push(elem.as_ref());
423            } else {
424                type_args.push(ty);
425            }
426        }
427    }
428    type_args
429}
430
431/// Resolve the `W` of `#[rkyv(with = W)]` to a registry lookup key.
432fn resolve_wrapper_path(ty: &syn::Type, ctx: &SourceContext) -> Option<String> {
433    let Type::Path(TypePath { qself: None, path }) = ty else {
434        return None;
435    };
436    let segments = path_segments(path);
437    if segments.len() == 1 {
438        Some(
439            ctx.imports
440                .get(&segments[0])
441                .cloned()
442                .unwrap_or_else(|| segments[0].clone()),
443        )
444    } else {
445        Some(segments.join("::"))
446    }
447}
448
449/// Resolve a field to its codec expression.
450///
451/// `Ok(None)` means the field is omitted (a `Skip` wrapper).
452fn field_expr(
453    field: &syn::Field,
454    context: &str,
455    codegen: &CodeGenerator,
456    ctx: &SourceContext,
457) -> Result<Option<CodecExpr>, Diagnostic> {
458    if let Some(with_type) = parse_rkyv_field_with(&field.attrs) {
459        let location = ctx.location(with_type.span());
460        let resolved = resolve_wrapper_path(&with_type, ctx);
461        let wrapper = resolved
462            .as_deref()
463            .and_then(|path| lookup_wrapper(codegen, ctx, path));
464        let Some(wrapper) = wrapper else {
465            return Err(Diagnostic::new(DiagnosticKind::UnknownWithWrapper {
466                wrapper_path: resolved.unwrap_or_else(|| type_to_string(&with_type)),
467            })
468            .referenced_by(context)
469            .at(Some(location)));
470        };
471        let underlying = if wrapper.needs_underlying() {
472            let expr = type_to_expr(&field.ty, codegen, ctx).map_err(|kind| {
473                Diagnostic::new(kind)
474                    .referenced_by(context)
475                    .at(Some(ctx.location(field.ty.span())))
476            })?;
477            Some(expr)
478        } else {
479            None
480        };
481        return Ok(wrapper.apply(underlying));
482    }
483
484    type_to_expr(&field.ty, codegen, ctx)
485        .map(Some)
486        .map_err(|kind| {
487            Diagnostic::new(kind)
488                .referenced_by(context)
489                .at(Some(ctx.location(field.ty.span())))
490        })
491}
492
493/// Look up a with-wrapper by resolved path, trying glob prefixes for bare idents
494/// (`use rkyv::with::*` + `with = AsBox`).
495fn lookup_wrapper(
496    codegen: &CodeGenerator,
497    ctx: &SourceContext,
498    path: &str,
499) -> Option<WithWrapper> {
500    if let Some(wrapper) = codegen.registry.get_wrapper(path) {
501        return Some(wrapper.clone());
502    }
503    if !path.contains("::") {
504        for glob in &ctx.globs {
505            if let Some(wrapper) = codegen.registry.get_wrapper(&format!("{glob}::{path}")) {
506                return Some(wrapper.clone());
507            }
508        }
509    }
510    None
511}
512
513/// A named field's identifier, with any raw-identifier escape dropped:
514/// `r#type` is the field `type`, and `r#` is not valid in a JavaScript key.
515fn field_name(field: &syn::Field) -> String {
516    field
517        .ident
518        .as_ref()
519        .expect("named fields have idents")
520        .unraw()
521        .to_string()
522}
523
524/// A struct's extracted shape: named fields form a record codec;
525/// unnamed (tuple-struct) fields are positional.
526enum StructShape {
527    Record(Vec<(String, CodecExpr)>),
528    Tuple(Vec<CodecExpr>),
529}
530
531fn extract_struct_shape(
532    type_name: &str,
533    fields: &Fields,
534    codegen: &CodeGenerator,
535    ctx: &SourceContext,
536) -> Result<StructShape, Vec<Diagnostic>> {
537    let mut diagnostics = Vec::new();
538
539    let shape = match fields {
540        Fields::Named(named) => {
541            let mut out = Vec::new();
542            for field in &named.named {
543                let field_name = field_name(field);
544                let context = format!("{type_name}.{field_name}");
545                match field_expr(field, &context, codegen, ctx) {
546                    Ok(Some(expr)) => out.push((field_name, expr)),
547                    Ok(None) => {}
548                    Err(diagnostic) => diagnostics.push(diagnostic),
549                }
550            }
551            StructShape::Record(out)
552        }
553        Fields::Unnamed(unnamed) => {
554            let mut out = Vec::new();
555            for (index, field) in unnamed.unnamed.iter().enumerate() {
556                let context = format!("{type_name}.{index}");
557                match field_expr(field, &context, codegen, ctx) {
558                    Ok(Some(expr)) => out.push(expr),
559                    Ok(None) => {}
560                    Err(diagnostic) => diagnostics.push(diagnostic),
561                }
562            }
563            StructShape::Tuple(out)
564        }
565        Fields::Unit => StructShape::Record(Vec::new()),
566    };
567
568    if diagnostics.is_empty() {
569        Ok(shape)
570    } else {
571        Err(diagnostics)
572    }
573}
574
575/// The codec expression for a tuple struct: archived exactly like a tuple of its fields,
576/// so `struct Pair(A, B)` aliases `r.tuple(A, B)`. A single-field (newtype) struct is transparent — the inner codec — and
577/// a zero-field one degenerates to `r.unit`, both matching rkyv's layout.
578fn tuple_struct_expr(mut exprs: Vec<CodecExpr>) -> CodecExpr {
579    match exprs.len() {
580        0 => CodecExpr::runtime("unit"),
581        1 => exprs.pop().expect("len checked"),
582        _ => CodecExpr::call(CodecExpr::runtime("tuple"), exprs),
583    }
584}
585
586fn extract_enum_variants(
587    type_name: &str,
588    variants: &syn::punctuated::Punctuated<syn::Variant, syn::token::Comma>,
589    codegen: &CodeGenerator,
590    ctx: &SourceContext,
591) -> Result<Vec<EnumVariant>, Vec<Diagnostic>> {
592    let mut out = Vec::new();
593    let mut diagnostics = Vec::new();
594
595    for variant in variants {
596        let variant_name = variant.ident.unraw().to_string();
597        match &variant.fields {
598            Fields::Unit => out.push(EnumVariant::Unit(variant_name)),
599            Fields::Unnamed(unnamed) => {
600                let mut exprs = Vec::new();
601                for (index, field) in unnamed.unnamed.iter().enumerate() {
602                    let context = format!("{type_name}::{variant_name}.{index}");
603                    match field_expr(field, &context, codegen, ctx) {
604                        Ok(Some(expr)) => exprs.push(expr),
605                        Ok(None) => {}
606                        Err(diagnostic) => diagnostics.push(diagnostic),
607                    }
608                }
609                let variant = if unnamed.unnamed.len() == 1 {
610                    // A newtype variant decodes as the bare inner value;
611                    // a fully skipped one degenerates to a unit variant.
612                    match exprs.pop() {
613                        Some(expr) => EnumVariant::Newtype(variant_name, expr),
614                        None => EnumVariant::Unit(variant_name),
615                    }
616                } else {
617                    EnumVariant::Tuple(variant_name, exprs)
618                };
619                out.push(variant);
620            }
621            Fields::Named(named) => {
622                let mut fields = Vec::new();
623                for field in &named.named {
624                    let field_name = field_name(field);
625                    let context = format!("{type_name}::{variant_name}.{field_name}");
626                    match field_expr(field, &context, codegen, ctx) {
627                        Ok(Some(expr)) => fields.push((field_name, expr)),
628                        Ok(None) => {}
629                        Err(diagnostic) => diagnostics.push(diagnostic),
630                    }
631                }
632                out.push(EnumVariant::Struct(variant_name, fields));
633            }
634        }
635    }
636
637    if diagnostics.is_empty() {
638        Ok(out)
639    } else {
640        Err(diagnostics)
641    }
642}
643
644/// The inline codec expression for a struct (used for remote proxies).
645fn struct_expr(fields: Vec<(String, CodecExpr)>) -> CodecExpr {
646    CodecExpr::call(
647        CodecExpr::runtime("struct"),
648        [CodecExpr::object(fields)],
649    )
650}
651
652/// The inline codec expression for an enum (used for remote proxies).
653fn enum_expr(variants: Vec<EnumVariant>) -> CodecExpr {
654    let entries = variants.into_iter().map(|variant| match variant {
655        EnumVariant::Unit(name) => (name, CodecExpr::raw("null")),
656        EnumVariant::Newtype(name, expr) => (name, expr),
657        EnumVariant::Tuple(name, exprs) => (name, CodecExpr::array(exprs)),
658        EnumVariant::Struct(name, fields) => (name, CodecExpr::object(fields)),
659    });
660    CodecExpr::call(
661        CodecExpr::runtime("taggedEnum"),
662        [CodecExpr::object(entries)],
663    )
664}
665
666/// A top-level struct or enum item.
667enum TypeItem<'a> {
668    Struct(&'a syn::ItemStruct),
669    Enum(&'a syn::ItemEnum),
670}
671
672impl TypeItem<'_> {
673    fn attrs(&self) -> &[Attribute] {
674        match self {
675            TypeItem::Struct(item) => &item.attrs,
676            TypeItem::Enum(item) => &item.attrs,
677        }
678    }
679
680    fn ident(&self) -> &syn::Ident {
681        match self {
682            TypeItem::Struct(item) => &item.ident,
683            TypeItem::Enum(item) => &item.ident,
684        }
685    }
686}
687
688fn type_items(file: &syn::File) -> Vec<TypeItem<'_>> {
689    file.items
690        .iter()
691        .filter_map(|item| match item {
692            syn::Item::Struct(s) => Some(TypeItem::Struct(s)),
693            syn::Item::Enum(e) => Some(TypeItem::Enum(e)),
694            _ => None,
695        })
696        .collect()
697}
698
699fn parse_source(
700    codegen: &mut CodeGenerator,
701    source: &str,
702    file: Option<PathBuf>,
703) -> Result<(), Error> {
704    let parsed = syn::parse_file(source).map_err(|source| Error::Parse {
705        file: file.clone(),
706        source,
707    })?;
708    let ctx = build_source_context(&parsed, file);
709    let items = type_items(&parsed);
710
711    // Pass 1: remote proxies. `#[rkyv(remote = T)]` types register themselves as with-wrappers and emit no top-level export.
712    // Running this pass first makes proxy usage order-independent within a file.
713    for item in &items {
714        if !has_marker_derive(item.attrs(), &ctx, codegen) {
715            continue;
716        }
717        let attrs = parse_rkyv_type_attrs(item.attrs());
718        if attrs.remote.is_none() {
719            continue;
720        }
721        let name = item.ident().to_string();
722        let built = match item {
723            TypeItem::Struct(s) => {
724                extract_struct_shape(&name, &s.fields, codegen, &ctx).map(|shape| match shape {
725                    StructShape::Record(fields) => struct_expr(fields),
726                    StructShape::Tuple(exprs) => tuple_struct_expr(exprs),
727                })
728            }
729            TypeItem::Enum(e) => {
730                extract_enum_variants(&name, &e.variants, codegen, &ctx).map(enum_expr)
731            }
732        };
733        match built {
734            Ok(expr) => {
735                if let Some(fq_path) = ctx.imports.get(&name) {
736                    codegen
737                        .registry
738                        .register_wrapper(fq_path.clone(), WithWrapper::replace(expr.clone()));
739                }
740                codegen
741                    .registry
742                    .register_wrapper(name, WithWrapper::replace(expr));
743            }
744            Err(diagnostics) => match codegen.on_unknown {
745                OnUnknown::Error => codegen.add_diagnostics.extend(diagnostics),
746                OnUnknown::SkipContainingType => {
747                    for diagnostic in diagnostics {
748                        eprintln!(
749                            "cargo:warning=rkyv-js-codegen: skipping remote proxy `{name}`: \
750                             {diagnostic}"
751                        );
752                    }
753                }
754            },
755        }
756    }
757
758    // Pass 2: regular types.
759    for item in &items {
760        if !has_marker_derive(item.attrs(), &ctx, codegen) {
761            continue;
762        }
763        let attrs = parse_rkyv_type_attrs(item.attrs());
764        if attrs.remote.is_some() {
765            continue;
766        }
767        let name = item.ident().to_string();
768        let location = Some(ctx.location(item.ident().span()));
769        let extracted = match item {
770            TypeItem::Struct(s) => {
771                extract_struct_shape(&name, &s.fields, codegen, &ctx).map(|shape| match shape {
772                    StructShape::Record(fields) => TypeKind::Struct(fields),
773                    StructShape::Tuple(exprs) => TypeKind::Alias(tuple_struct_expr(exprs)),
774                })
775            }
776            TypeItem::Enum(e) => {
777                extract_enum_variants(&name, &e.variants, codegen, &ctx).map(TypeKind::Enum)
778            }
779        };
780        match extracted {
781            Ok(kind) => codegen.add_type(name.clone(), kind, location),
782            Err(diagnostics) => codegen.add_failed_type(name.clone(), diagnostics, location),
783        }
784        if let Some(archived) = attrs.archived {
785            codegen.set_archived_name(name, archived);
786        }
787    }
788
789    Ok(())
790}
791
792impl CodeGenerator {
793    /// Parse a Rust source file and extract every type with a marker derive.
794    ///
795    /// # Example
796    ///
797    /// ```no_run
798    /// use rkyv_js_codegen::CodeGenerator;
799    ///
800    /// fn main() -> Result<(), rkyv_js_codegen::Error> {
801    ///     CodeGenerator::new()
802    ///         .add_source_file("src/lib.rs")?
803    ///         .write_to_file("generated/bindings.ts")?;
804    ///     Ok(())
805    /// }
806    /// ```
807    pub fn add_source_file(&mut self, path: impl AsRef<Path>) -> Result<&mut Self, Error> {
808        let path = path.as_ref();
809        let source = fs::read_to_string(path)?;
810        parse_source(self, &source, Some(path.to_path_buf()))?;
811        Ok(self)
812    }
813
814    /// Parse Rust source from a string and extract every type with a marker derive.
815    pub fn add_source_str(&mut self, source: &str) -> Result<&mut Self, Error> {
816        parse_source(self, source, None)?;
817        Ok(self)
818    }
819
820    /// Recursively scan a directory for `.rs` files and extract every type with a marker derive.
821    /// Files are processed in path order.
822    pub fn add_source_dir(&mut self, path: impl AsRef<Path>) -> Result<&mut Self, Error> {
823        let mut files: Vec<PathBuf> = Vec::new();
824        for entry in WalkDir::new(path) {
825            let entry = entry.map_err(io::Error::other)?;
826            let entry_path = entry.path();
827            if entry_path.extension().is_some_and(|ext| ext == "rs") {
828                files.push(entry_path.to_path_buf());
829            }
830        }
831        files.sort();
832        for file in files {
833            self.add_source_file(&file)?;
834        }
835        Ok(self)
836    }
837}
838
839#[cfg(test)]
840mod tests {
841    use super::*;
842    use crate::error::DiagnosticKind;
843    use crate::registry::ExternalType;
844
845    fn generate(source: &str) -> String {
846        let mut codegen = CodeGenerator::new();
847        codegen.add_source_str(source).unwrap();
848        codegen.generate().unwrap()
849    }
850
851    fn generate_diagnostics(source: &str) -> Vec<Diagnostic> {
852        let mut codegen = CodeGenerator::new();
853        codegen.add_source_str(source).unwrap();
854        match codegen.generate() {
855            Err(Error::Codegen(diagnostics)) => diagnostics,
856            other => panic!("expected codegen diagnostics, got {other:?}"),
857        }
858    }
859
860    #[test]
861    fn extracts_simple_struct() {
862        let code = generate(
863            r#"
864            use rkyv::Archive;
865            #[derive(Archive)]
866            struct Point { x: f64, y: f64 }
867        "#,
868        );
869        assert!(code.contains("export const ArchivedPoint = r.struct({\n  x: r.f64,\n  y: r.f64,\n});"));
870        assert!(code.contains("export type Point = r.Infer<typeof ArchivedPoint>;"));
871    }
872
873    #[test]
874    fn extracts_containers_and_str() {
875        let code = generate(
876            r#"
877            use rkyv::Archive;
878            #[derive(Archive)]
879            struct Person {
880                name: String,
881                nickname: &'static str,
882                age: u32,
883                scores: Vec<u32>,
884                email: Option<String>,
885                boxed: Box<u64>,
886                arr: [u16; 4],
887                tup: (u8, String, f64),
888                unit: (),
889            }
890        "#,
891        );
892        assert!(code.contains("name: r.string,"));
893        assert!(code.contains("nickname: r.string,"));
894        assert!(code.contains("scores: r.vec(r.u32),"));
895        assert!(code.contains("email: r.option(r.string),"));
896        assert!(code.contains("boxed: r.box(r.u64),"));
897        assert!(code.contains("arr: r.array(r.u16, 4),"));
898        assert!(code.contains("tup: r.tuple(r.u8, r.string, r.f64),"));
899        assert!(code.contains("unit: r.unit,"));
900    }
901
902    #[test]
903    fn extracts_enum_with_new_variant_shapes() {
904        let code = generate(
905            r#"
906            use rkyv::Archive;
907            #[derive(Archive)]
908            enum Message {
909                Quit,
910                Move { x: i32, y: i32 },
911                Write(String),
912                ChangeColor(u8, u8, u8),
913            }
914        "#,
915        );
916        assert!(code.contains(
917            "export const ArchivedMessage = r.taggedEnum({\n\
918             \x20 Quit: null,\n\
919             \x20 Move: { x: r.i32, y: r.i32 },\n\
920             \x20 Write: r.string,\n\
921             \x20 ChangeColor: [r.u8, r.u8, r.u8],\n\
922             });"
923        ));
924    }
925
926    #[test]
927    fn extracts_tuple_struct() {
928        let code = generate(
929            r#"
930            use rkyv::Archive;
931            #[derive(Archive)]
932            struct Pair(u32, String);
933        "#,
934        );
935        assert!(code.contains("export const ArchivedPair = r.tuple(r.u32, r.string);"));
936    }
937
938    #[test]
939    fn cross_references_resolve_to_archived_names() {
940        let code = generate(
941            r#"
942            use rkyv::Archive;
943            #[derive(Archive)]
944            struct Inner { value: u32 }
945            #[derive(Archive)]
946            struct Outer { inner: Inner, items: Vec<Inner> }
947        "#,
948        );
949        assert!(code.contains("inner: ArchivedInner,"));
950        assert!(code.contains("items: r.vec(ArchivedInner),"));
951        let inner_pos = code.find("export const ArchivedInner").unwrap();
952        let outer_pos = code.find("export const ArchivedOuter").unwrap();
953        assert!(inner_pos < outer_pos);
954    }
955
956    #[test]
957    fn marker_via_plain_import() {
958        let code = generate(
959            r#"
960            use rkyv::Archive;
961            #[derive(Debug)]
962            struct NotExported { x: i32 }
963            #[derive(Debug, Archive)]
964            struct Exported { y: i32 }
965        "#,
966        );
967        assert!(!code.contains("ArchivedNotExported"));
968        assert!(code.contains("ArchivedExported"));
969    }
970
971    #[test]
972    fn marker_via_qualified_path() {
973        let code = generate(
974            r#"
975            #[derive(rkyv::Archive)]
976            struct QualifiedPath { value: u32 }
977            #[derive(::rkyv::Archive)]
978            struct LeadingColons { value: u32 }
979        "#,
980        );
981        assert!(code.contains("ArchivedQualifiedPath"));
982        assert!(code.contains("ArchivedLeadingColons"));
983    }
984
985    #[test]
986    fn marker_via_rename() {
987        let code = generate(
988            r#"
989            use rkyv::Archive as Rkyv;
990            #[derive(Rkyv)]
991            struct AliasedMarker { value: i32 }
992        "#,
993        );
994        assert!(code.contains("ArchivedAliasedMarker"));
995    }
996
997    #[test]
998    fn marker_via_glob_import() {
999        let code = generate(
1000            r#"
1001            use rkyv::*;
1002            #[derive(Archive)]
1003            struct GlobMarked { value: i32 }
1004        "#,
1005        );
1006        assert!(code.contains("ArchivedGlobMarked"));
1007    }
1008
1009    #[test]
1010    fn marker_via_add_marker_path() {
1011        let mut codegen = CodeGenerator::new();
1012        codegen.add_marker_path("my_macros::TS");
1013        codegen
1014            .add_source_str(
1015                r#"
1016                use my_macros::TS;
1017                #[derive(TS)]
1018                struct Custom { value: u8 }
1019                #[derive(my_macros::TS)]
1020                struct CustomQualified { value: u8 }
1021            "#,
1022            )
1023            .unwrap();
1024        let code = codegen.generate().unwrap();
1025        assert!(code.contains("ArchivedCustom"));
1026        assert!(code.contains("ArchivedCustomQualified"));
1027    }
1028
1029    #[test]
1030    fn foreign_archive_paths_do_not_match() {
1031        // The old `ends_with("::Archive")` over-match must be gone.
1032        let mut codegen = CodeGenerator::new();
1033        codegen
1034            .add_source_str(
1035                r#"
1036                #[derive(some_alias::Archive)]
1037                struct NotOurs { id: u64 }
1038                #[derive(deeply::nested::module::Archive)]
1039                struct AlsoNotOurs { data: String }
1040            "#,
1041            )
1042            .unwrap();
1043        let code = codegen.generate().unwrap();
1044        assert!(!code.contains("ArchivedNotOurs"));
1045        assert!(!code.contains("ArchivedAlsoNotOurs"));
1046    }
1047
1048    #[test]
1049    fn bare_marker_without_import_does_not_match() {
1050        let code = generate(
1051            r#"
1052            #[derive(Archive)]
1053            struct NotDetected { a: i32 }
1054            #[derive(Rkyv)]
1055            struct AlsoNotDetected { b: i32 }
1056        "#,
1057        );
1058        assert!(!code.contains("ArchivedNotDetected"));
1059        assert!(!code.contains("ArchivedAlsoNotDetected"));
1060    }
1061
1062    #[test]
1063    fn builtin_leaf_types() {
1064        let code = generate(
1065            r#"
1066            use rkyv::Archive;
1067            use uuid::Uuid;
1068            use bytes::Bytes;
1069            use smol_str::SmolStr;
1070            #[derive(Archive)]
1071            struct Record { id: Uuid, payload: Bytes, key: SmolStr }
1072        "#,
1073        );
1074        assert!(code.contains("import { bytes } from 'rkyv-js/lib/bytes';"));
1075        assert!(code.contains("import { uuid } from 'rkyv-js/lib/uuid';"));
1076        assert!(code.contains("id: uuid,"));
1077        assert!(code.contains("payload: bytes,"));
1078        assert!(code.contains("key: r.string,"));
1079    }
1080
1081    #[test]
1082    fn builtin_vec_like_types() {
1083        let code = generate(
1084            r#"
1085            use rkyv::Archive;
1086            use thin_vec::ThinVec;
1087            use arrayvec::ArrayVec;
1088            use smallvec::SmallVec;
1089            use tinyvec::TinyVec;
1090            use std::collections::VecDeque;
1091            #[derive(Archive)]
1092            struct Data {
1093                thin: ThinVec<u32>,
1094                array_vec: ArrayVec<u8, 64>,
1095                small: SmallVec<[u32; 4]>,
1096                tiny: TinyVec<[String; 8]>,
1097                deque: VecDeque<u32>,
1098            }
1099        "#,
1100        );
1101        assert!(code.contains("thin: r.vec(r.u32),"));
1102        assert!(code.contains("array_vec: r.vec(r.u8),"));
1103        assert!(code.contains("small: r.vec(r.u32),"));
1104        assert!(code.contains("tiny: r.vec(r.string),"));
1105        assert!(code.contains("deque: r.vec(r.u32),"));
1106    }
1107
1108    #[test]
1109    fn builtin_maps_and_sets() {
1110        let code = generate(
1111            r#"
1112            use rkyv::Archive;
1113            use std::collections::{HashMap, HashSet, BTreeMap, BTreeSet};
1114            use indexmap::{IndexMap, IndexSet};
1115            #[derive(Archive)]
1116            struct Collections {
1117                hm: HashMap<String, u32>,
1118                hs: HashSet<String>,
1119                bm: BTreeMap<String, u64>,
1120                bs: BTreeSet<i64>,
1121                im: IndexMap<String, u32>,
1122                is: IndexSet<String>,
1123            }
1124        "#,
1125        );
1126        assert!(code.contains("import { btreeMap, btreeSet } from 'rkyv-js/lib/btreemap';"));
1127        assert!(code.contains("import { hashMap, hashSet } from 'rkyv-js/lib/hashmap';"));
1128        assert!(code.contains("import { indexMap, indexSet } from 'rkyv-js/lib/indexmap';"));
1129        assert!(code.contains("hm: hashMap(r.string, r.u32),"));
1130        assert!(code.contains("hs: hashSet(r.string),"));
1131        assert!(code.contains("bm: btreeMap(r.string, r.u64),"));
1132        assert!(code.contains("bs: btreeSet(r.i64),"));
1133        assert!(code.contains("im: indexMap(r.string, r.u32),"));
1134        assert!(code.contains("is: indexSet(r.string),"));
1135    }
1136
1137    #[test]
1138    fn builtin_pointer_types() {
1139        let code = generate(
1140            r#"
1141            use rkyv::Archive;
1142            use std::rc::{Rc, Weak};
1143            #[derive(Archive)]
1144            struct Shared { data: Rc<String>, weak_ref: Weak<u32>, arc: triomphe::Arc<String> }
1145        "#,
1146        );
1147        assert!(code.contains("data: r.rc(r.string),"));
1148        assert!(code.contains("weak_ref: r.weak(r.u32),"));
1149        assert!(code.contains("arc: r.rc(r.string),"));
1150    }
1151
1152    #[test]
1153    fn renamed_imports_resolve() {
1154        let code = generate(
1155            r#"
1156            use rkyv::Archive;
1157            use std::collections::{HashMap as Map, BTreeSet as SortedSet};
1158            use uuid::Uuid as Id;
1159            #[derive(Archive)]
1160            struct Data { map: Map<String, u32>, set: SortedSet<String>, id: Id }
1161        "#,
1162        );
1163        assert!(code.contains("map: hashMap(r.string, r.u32),"));
1164        assert!(code.contains("set: btreeSet(r.string),"));
1165        assert!(code.contains("id: uuid,"));
1166    }
1167
1168    #[test]
1169    fn generic_type_alias_resolves_to_registry_path() {
1170        // The pinned-hasher alias pattern: generic parameters on the LEFT,
1171        // a trailing hasher argument on the RIGHT. Only the path resolves;
1172        // the use site supplies exactly K and V.
1173        let code = generate(
1174            r#"
1175            use rkyv::Archive;
1176            pub type FixedState = std::hash::BuildHasherDefault<Hasher13>;
1177            pub type HashMap<K, V> = std::collections::HashMap<K, V, FixedState>;
1178            #[derive(Archive)]
1179            struct Data { m: HashMap<String, u32> }
1180        "#,
1181        );
1182        assert!(code.contains("m: hashMap(r.string, r.u32),"));
1183    }
1184
1185    #[test]
1186    fn inline_trailing_hasher_is_allowed() {
1187        let code = generate(
1188            r#"
1189            use rkyv::Archive;
1190            pub type State = std::hash::BuildHasherDefault<Hasher13>;
1191            #[derive(Archive)]
1192            struct Data { m: std::collections::HashMap<String, u32, State> }
1193        "#,
1194        );
1195        assert!(code.contains("m: hashMap(r.string, r.u32),"));
1196    }
1197
1198    #[test]
1199    fn generic_arity_too_few_args() {
1200        let diagnostics = generate_diagnostics(
1201            r#"
1202            use rkyv::Archive;
1203            use std::collections::HashMap;
1204            #[derive(Archive)]
1205            struct Data { m: HashMap<String> }
1206        "#,
1207        );
1208        assert_eq!(diagnostics.len(), 1);
1209        assert!(matches!(
1210            &diagnostics[0].kind,
1211            DiagnosticKind::GenericArity { rust_path, expected: 2, found: 1 }
1212                if rust_path == "std::collections::HashMap"
1213        ));
1214        assert_eq!(diagnostics[0].referenced_by.as_deref(), Some("Data.m"));
1215    }
1216
1217    #[test]
1218    fn generic_arity_too_many_args_without_trailing() {
1219        let diagnostics = generate_diagnostics(
1220            r#"
1221            use rkyv::Archive;
1222            #[derive(Archive)]
1223            struct Data { m: std::collections::BTreeMap<String, u32, Extra> }
1224        "#,
1225        );
1226        assert!(matches!(
1227            &diagnostics[0].kind,
1228            DiagnosticKind::GenericArity { rust_path, expected: 2, found: 3 }
1229                if rust_path == "std::collections::BTreeMap"
1230        ));
1231    }
1232
1233    #[test]
1234    fn unknown_type_reports_path_and_suggestion() {
1235        let diagnostics = generate_diagnostics(
1236            r#"
1237            use rkyv::Archive;
1238            #[derive(Archive)]
1239            struct Event { id: my_uuid::Uuid, at: chrono::NaiveDate }
1240        "#,
1241        );
1242        assert_eq!(diagnostics.len(), 2);
1243        assert!(diagnostics.iter().any(|diagnostic| matches!(
1244            &diagnostic.kind,
1245            DiagnosticKind::UnknownType { rust_path, suggestion: Some(suggestion) }
1246                if rust_path == "my_uuid::Uuid" && suggestion == "uuid::Uuid"
1247        )));
1248        assert!(diagnostics.iter().any(|diagnostic| matches!(
1249            &diagnostic.kind,
1250            DiagnosticKind::UnknownType { rust_path, suggestion: None }
1251                if rust_path == "chrono::NaiveDate"
1252        )));
1253    }
1254
1255    #[test]
1256    fn unknown_imported_type_is_not_a_dangling_ref() {
1257        // A single-segment ident that resolves via imports to an unregistered path must be an UnknownType error,
1258        // not a silent `ArchivedNaiveDate` type reference.
1259        let diagnostics = generate_diagnostics(
1260            r#"
1261            use rkyv::Archive;
1262            use chrono::NaiveDate;
1263            #[derive(Archive)]
1264            struct Event { at: NaiveDate }
1265        "#,
1266        );
1267        assert!(matches!(
1268            &diagnostics[0].kind,
1269            DiagnosticKind::UnknownType { rust_path, .. } if rust_path == "chrono::NaiveDate"
1270        ));
1271    }
1272
1273    #[test]
1274    fn diagnostics_carry_source_locations() {
1275        let diagnostics = generate_diagnostics(
1276            r#"
1277            use rkyv::Archive;
1278            #[derive(Archive)]
1279            struct Event { at: chrono::NaiveDate }
1280        "#,
1281        );
1282        let location = diagnostics[0].location.as_ref().unwrap();
1283        assert_eq!(location.file, None);
1284        assert_eq!(location.line, 4);
1285        assert!(location.column > 1);
1286    }
1287
1288    #[test]
1289    fn undeclared_local_ref_is_unresolved() {
1290        let diagnostics = generate_diagnostics(
1291            r#"
1292            use rkyv::Archive;
1293            #[derive(Archive)]
1294            struct Outer { inner: NeverDeclared }
1295        "#,
1296        );
1297        assert!(matches!(
1298            &diagnostics[0].kind,
1299            DiagnosticKind::UnresolvedTypeRef { name } if name == "NeverDeclared"
1300        ));
1301        assert_eq!(diagnostics[0].referenced_by.as_deref(), Some("Outer.inner"));
1302    }
1303
1304    #[test]
1305    fn duplicate_types_across_sources() {
1306        let mut codegen = CodeGenerator::new();
1307        codegen
1308            .add_source_str("use rkyv::Archive; #[derive(Archive)] struct Point { x: f64 }")
1309            .unwrap();
1310        codegen
1311            .add_source_str("use rkyv::Archive; #[derive(Archive)] struct Point { y: f64 }")
1312            .unwrap();
1313        let Err(Error::Codegen(diagnostics)) = codegen.generate() else {
1314            panic!("expected duplicate diagnostic");
1315        };
1316        assert!(matches!(
1317            &diagnostics[0].kind,
1318            DiagnosticKind::DuplicateType { name } if name == "Point"
1319        ));
1320    }
1321
1322    #[test]
1323    fn parse_errors_propagate() {
1324        let mut codegen = CodeGenerator::new();
1325        let error = codegen.add_source_str("struct {").unwrap_err();
1326        assert!(matches!(error, Error::Parse { file: None, .. }));
1327    }
1328
1329    #[test]
1330    fn skip_mode_omits_unknown_and_dependents() {
1331        let mut codegen = CodeGenerator::new();
1332        codegen.on_unknown_type(OnUnknown::SkipContainingType);
1333        codegen
1334            .add_source_str(
1335                r#"
1336                use rkyv::Archive;
1337                #[derive(Archive)]
1338                struct Fine { x: u32 }
1339                #[derive(Archive)]
1340                struct Broken { at: chrono::NaiveDate }
1341                #[derive(Archive)]
1342                struct UsesBroken { broken: Broken }
1343                #[derive(Archive)]
1344                struct UsesUsesBroken { nested: UsesBroken }
1345            "#,
1346            )
1347            .unwrap();
1348        let code = codegen.generate().unwrap();
1349        assert!(code.contains("ArchivedFine"));
1350        assert!(!code.contains("ArchivedBroken"));
1351        assert!(!code.contains("ArchivedUsesBroken"));
1352        assert!(!code.contains("ArchivedUsesUsesBroken"));
1353    }
1354
1355    #[test]
1356    fn with_asbox_boxes_the_underlying_codec() {
1357        let code = generate(
1358            r#"
1359            use rkyv::Archive;
1360            use rkyv::with::AsBox;
1361            #[derive(Archive)]
1362            struct Data {
1363                #[rkyv(with = AsBox)]
1364                big: String,
1365            }
1366        "#,
1367        );
1368        assert!(code.contains("big: r.box(r.string),"));
1369    }
1370
1371    #[test]
1372    fn with_inline_is_identity() {
1373        let code = generate(
1374            r#"
1375            use rkyv::Archive;
1376            #[derive(Archive)]
1377            struct Data {
1378                #[rkyv(with = rkyv::with::Inline)]
1379                value: u32,
1380                #[rkyv(with = rkyv::with::InlineAsBox)]
1381                other: u64,
1382            }
1383        "#,
1384        );
1385        assert!(code.contains("value: r.u32,"));
1386        assert!(code.contains("other: r.box(r.u64),"));
1387    }
1388
1389    #[test]
1390    fn with_skip_omits_the_field() {
1391        let code = generate(
1392            r#"
1393            use rkyv::Archive;
1394            use rkyv::with::Skip;
1395            #[derive(Archive)]
1396            struct Data {
1397                kept: u32,
1398                #[rkyv(with = Skip)]
1399                dropped: String,
1400            }
1401        "#,
1402        );
1403        assert!(code.contains("kept: r.u32,"));
1404        assert!(!code.contains("dropped"));
1405    }
1406
1407    #[test]
1408    fn with_unknown_wrapper_is_a_diagnostic() {
1409        let diagnostics = generate_diagnostics(
1410            r#"
1411            use rkyv::Archive;
1412            #[derive(Archive)]
1413            struct Data {
1414                #[rkyv(with = Mystery)]
1415                value: u32,
1416            }
1417        "#,
1418        );
1419        assert!(matches!(
1420            &diagnostics[0].kind,
1421            DiagnosticKind::UnknownWithWrapper { wrapper_path } if wrapper_path == "Mystery"
1422        ));
1423        assert_eq!(diagnostics[0].referenced_by.as_deref(), Some("Data.value"));
1424    }
1425
1426    #[test]
1427    fn with_replace_never_resolves_the_field_type() {
1428        // remote::Coord is not registered; a replace wrapper must not care.
1429        let mut codegen = CodeGenerator::new();
1430        codegen.register_with(
1431            "AsJson",
1432            WithWrapper::replace(CodecExpr::import_from("./coord.ts", "Coord")),
1433        );
1434        codegen
1435            .add_source_str(
1436                r#"
1437                use rkyv::Archive;
1438                #[derive(Archive)]
1439                struct RemoteEvent {
1440                    name: String,
1441                    #[rkyv(with = AsJson)]
1442                    location: remote::Coord,
1443                    priority: u32,
1444                }
1445            "#,
1446            )
1447            .unwrap();
1448        let code = codegen.generate().unwrap();
1449        assert!(code.contains("import { Coord } from './coord.ts';"));
1450        assert!(code.contains("location: Coord,"));
1451    }
1452
1453    #[test]
1454    fn with_wrapper_resolves_through_glob_imports() {
1455        let code = generate(
1456            r#"
1457            use rkyv::Archive;
1458            use rkyv::with::*;
1459            #[derive(Archive)]
1460            struct Data {
1461                #[rkyv(with = AsBox)]
1462                big: String,
1463            }
1464        "#,
1465        );
1466        assert!(code.contains("big: r.box(r.string),"));
1467    }
1468
1469    #[test]
1470    fn remote_proxy_registers_itself_as_wrapper() {
1471        let code = generate(
1472            r#"
1473            use rkyv::Archive;
1474            #[derive(Archive)]
1475            #[rkyv(remote = chrono::NaiveDate)]
1476            struct NaiveDateDef {
1477                year: i32,
1478                ordinal: u32,
1479            }
1480            #[derive(Archive)]
1481            struct Event {
1482                name: String,
1483                #[rkyv(with = NaiveDateDef)]
1484                date: chrono::NaiveDate,
1485            }
1486        "#,
1487        );
1488        // The proxy emits no top-level export...
1489        assert!(!code.contains("ArchivedNaiveDateDef"));
1490        // ...and the consuming field gets the proxy's own struct codec.
1491        assert!(code.contains("date: r.struct({ year: r.i32, ordinal: r.u32 }),"));
1492    }
1493
1494    #[test]
1495    fn remote_proxy_is_order_independent_within_a_file() {
1496        let code = generate(
1497            r#"
1498            use rkyv::Archive;
1499            #[derive(Archive)]
1500            struct Event {
1501                #[rkyv(with = CoordDef)]
1502                location: remote::Coord,
1503            }
1504            #[derive(Archive)]
1505            #[rkyv(remote = remote::Coord)]
1506            struct CoordDef { x: f32, y: f32 }
1507        "#,
1508        );
1509        assert!(code.contains("location: r.struct({ x: r.f32, y: r.f32 }),"));
1510        assert!(!code.contains("ArchivedCoordDef"));
1511    }
1512
1513    #[test]
1514    fn remote_field_without_with_is_unknown() {
1515        // rkyv 0.8 consumes remote proxies via #[rkyv(with = ProxyDef)];
1516        // a bare field of the remote type does not resolve.
1517        let diagnostics = generate_diagnostics(
1518            r#"
1519            use rkyv::Archive;
1520            #[derive(Archive)]
1521            #[rkyv(remote = chrono::NaiveDate)]
1522            struct NaiveDateDef { year: i32, ordinal: u32 }
1523            #[derive(Archive)]
1524            struct Event { date: chrono::NaiveDate }
1525        "#,
1526        );
1527        assert!(matches!(
1528            &diagnostics[0].kind,
1529            DiagnosticKind::UnknownType { rust_path, .. } if rust_path == "chrono::NaiveDate"
1530        ));
1531    }
1532
1533    #[test]
1534    fn archived_rename_attribute() {
1535        let code = generate(
1536            r#"
1537            use rkyv::Archive;
1538            #[derive(Archive)]
1539            #[rkyv(compare(PartialEq), archived = CustomPoint, derive(Debug))]
1540            struct Point { x: f64, y: f64 }
1541            #[derive(Archive)]
1542            struct Line { start: Point, end: Point }
1543        "#,
1544        );
1545        assert!(code.contains("export const CustomPoint = r.struct({"));
1546        assert!(code.contains("export type Point = r.Infer<typeof CustomPoint>;"));
1547        assert!(code.contains("start: CustomPoint,"));
1548        assert!(!code.contains("ArchivedPoint"));
1549    }
1550
1551    #[test]
1552    fn snake_case_sources_generate_camel_case_bindings() {
1553        let mut codegen = CodeGenerator::new();
1554        codegen.set_field_casing(crate::Casing::Camel);
1555        codegen
1556            .add_source_str(
1557                r#"
1558            use rkyv::Archive;
1559            #[derive(Archive)]
1560            struct Event { created_at: u64, http_status: u16, tag_ids: Vec<u32> }
1561            #[derive(Archive)]
1562            enum Message { Text { sent_at: u64 }, Ping }
1563        "#,
1564            )
1565            .unwrap();
1566        let code = codegen.generate().unwrap();
1567        assert!(code.contains("createdAt: r.u64,"));
1568        assert!(code.contains("httpStatus: r.u16,"));
1569        assert!(code.contains("tagIds: r.vec(r.u32),"));
1570        assert!(code.contains("Text: { sentAt: r.u64 },"));
1571        assert!(code.contains("Ping: null,"));
1572    }
1573
1574    #[test]
1575    fn raw_identifiers_drop_the_escape() {
1576        // `r#` is not valid in a JavaScript object key.
1577        let code = generate(
1578            r#"
1579            use rkyv::Archive;
1580            #[derive(Archive)]
1581            struct Node { r#type: u32, r#match: bool }
1582            #[derive(Archive)]
1583            enum Kind { r#Struct, Other }
1584        "#,
1585        );
1586        assert!(code.contains("type: r.u32,"));
1587        assert!(code.contains("match: r.bool,"));
1588        assert!(code.contains("Struct: null,"));
1589        assert!(!code.contains("r#"));
1590    }
1591
1592    #[test]
1593    fn custom_external_type() {
1594        let mut codegen = CodeGenerator::new();
1595        codegen.register_external(
1596            "my_crate::CustomVec",
1597            ExternalType::generic1(|t| {
1598                CodecExpr::call(CodecExpr::import_from("my-package/codecs", "customVec"), [t])
1599            }),
1600        );
1601        codegen
1602            .add_source_str(
1603                r#"
1604                use rkyv::Archive;
1605                use my_crate::CustomVec;
1606                #[derive(Archive)]
1607                struct MyData { custom: CustomVec<u32> }
1608            "#,
1609            )
1610            .unwrap();
1611        let code = codegen.generate().unwrap();
1612        assert!(code.contains("import { customVec } from 'my-package/codecs';"));
1613        assert!(code.contains("custom: customVec(r.u32),"));
1614    }
1615
1616    #[test]
1617    fn unregister_external_removes_builtin() {
1618        let mut codegen = CodeGenerator::new();
1619        codegen.unregister_external("uuid::Uuid");
1620        codegen
1621            .add_source_str(
1622                r#"
1623                use rkyv::Archive;
1624                use uuid::Uuid;
1625                #[derive(Archive)]
1626                struct Record { id: Uuid }
1627            "#,
1628            )
1629            .unwrap();
1630        let Err(Error::Codegen(diagnostics)) = codegen.generate() else {
1631            panic!("expected unknown type diagnostic");
1632        };
1633        assert!(matches!(
1634            &diagnostics[0].kind,
1635            DiagnosticKind::UnknownType { rust_path, .. } if rust_path == "uuid::Uuid"
1636        ));
1637    }
1638}