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