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