1use std::cell::RefCell;
16use std::collections::{HashMap, HashSet};
17
18use proc_macro2::TokenStream;
19use quote::quote;
20
21use crate::context::GlobalContext;
22use crate::ir::{self, TypeKind, TypeRef};
23use crate::parse::scope::ScopeId;
24use crate::util::diagnostics::DiagnosticCollector;
25
26pub const JS_SYS_RESERVED: &[&str] = &[
29 "Array",
30 "ArrayBuffer",
31 "ArrayTuple",
32 "AsyncGenerator",
33 "AsyncIterator",
34 "BigInt",
35 "BigInt64Array",
36 "BigUint64Array",
37 "Boolean",
38 "DataView",
39 "Date",
40 "Error",
41 "EvalError",
42 "Float32Array",
43 "Float64Array",
44 "Function",
45 "Generator",
46 "Global",
47 "Int16Array",
48 "Int32Array",
49 "Int8Array",
50 "Iterator",
51 "IteratorNext",
52 "JsOption",
53 "JsString",
54 "Map",
55 "Number",
56 "Object",
57 "Promise",
58 "Proxy",
59 "RangeError",
60 "ReferenceError",
61 "RegExp",
62 "Set",
63 "SharedArrayBuffer",
64 "Symbol",
65 "SyntaxError",
66 "TypeError",
67 "Uint16Array",
68 "Uint32Array",
69 "Uint8Array",
70 "Uint8ClampedArray",
71 "Undefined",
72 "UriError",
73 "WeakMap",
74 "WeakRef",
75 "WeakSet",
76];
77
78#[derive(Clone, Copy, Debug, PartialEq, Eq)]
80pub enum Direction {
81 Argument,
83 Return,
85}
86
87#[derive(Clone, Copy, Debug, PartialEq, Eq)]
93pub struct TypePosition {
94 pub direction: Direction,
95 pub inner: bool,
98}
99
100impl TypePosition {
101 pub const ARGUMENT: Self = Self {
103 direction: Direction::Argument,
104 inner: false,
105 };
106 pub const RETURN: Self = Self {
108 direction: Direction::Return,
109 inner: false,
110 };
111
112 pub fn to_inner(self) -> Self {
115 Self {
116 direction: self.direction,
117 inner: true,
118 }
119 }
120
121 pub fn is_argument(self) -> bool {
122 matches!(self.direction, Direction::Argument)
123 }
124}
125
126pub struct CodegenContext<'a> {
131 pub gctx: &'a GlobalContext,
133 pub local_types: HashSet<String>,
135 pub renamed_locals: HashMap<String, String>,
139 pub root_scope: ScopeId,
141 pub file_scopes: Vec<ScopeId>,
143 pub external_uses: RefCell<HashMap<String, String>>,
145 pub diagnostics: RefCell<DiagnosticCollector>,
147}
148
149impl<'a> CodegenContext<'a> {
150 pub fn from_module(module: &ir::Module, gctx: &'a GlobalContext) -> Self {
152 let mut ctx = CodegenContext {
153 gctx,
154 local_types: HashSet::new(),
155 renamed_locals: HashMap::new(),
156 root_scope: module.builtin_scope,
157 file_scopes: module.file_scopes.clone(),
158 external_uses: RefCell::new(HashMap::new()),
159 diagnostics: RefCell::new(DiagnosticCollector::new()),
160 };
161 for &type_id in &module.types {
162 let decl = gctx.get_type(type_id);
163 ctx.collect_declaration(&decl.kind);
164 }
165 ctx.resolve_collisions();
166 ctx
167 }
168
169 pub fn empty(gctx: &'a GlobalContext, root_scope: ScopeId) -> Self {
171 CodegenContext {
172 gctx,
173 local_types: HashSet::new(),
174 renamed_locals: HashMap::new(),
175 root_scope,
176 file_scopes: vec![],
177 external_uses: RefCell::new(HashMap::new()),
178 diagnostics: RefCell::new(DiagnosticCollector::new()),
179 }
180 }
181
182 fn register_external(&self, local_name: &str, rust_path: &str) {
185 self.external_uses
186 .borrow_mut()
187 .insert(local_name.to_string(), rust_path.to_string());
188 }
189
190 pub fn external_use_tokens(&self) -> TokenStream {
192 let uses = self.external_uses.borrow();
193 let mut entries: Vec<_> = uses.iter().collect();
194 entries.sort_by_key(|(name, _)| (*name).clone());
195
196 let stmts: Vec<TokenStream> = entries
197 .into_iter()
198 .map(|(local_name, rust_path)| {
199 let local_ident = make_ident(local_name);
200 let path: TokenStream = rust_path.parse().unwrap_or_else(|_| {
202 quote! { JsValue }
204 });
205 if rust_path == "JsValue" || rust_path.ends_with("::JsValue") {
206 quote! { #[allow(dead_code)] use JsValue as #local_ident; }
208 } else {
209 quote! { #[allow(dead_code)] use #path as #local_ident; }
210 }
211 })
212 .collect();
213
214 quote! { #(#stmts)* }
215 }
216
217 pub fn resolve_external(
219 &self,
220 type_name: &str,
221 from_module: &str,
222 ) -> Option<crate::external_map::RustPath> {
223 self.gctx.external_map.resolve(type_name, from_module)
224 }
225
226 pub fn resolve_alias(&self, name: &str, scope: ScopeId) -> Option<&ir::TypeRef> {
235 let mut visited = HashSet::new();
236 self.resolve_alias_impl(name, scope, &mut visited)
237 }
238
239 fn resolve_alias_impl<'b>(
240 &'b self,
241 name: &str,
242 scope: ScopeId,
243 visited: &mut HashSet<String>,
244 ) -> Option<&'b ir::TypeRef> {
245 if !visited.insert(name.to_string()) {
246 return None; }
248 if let Some(type_id) = self.gctx.scopes.resolve(scope, name) {
249 let decl = self.gctx.get_type(type_id);
250 if let TypeKind::TypeAlias(ref alias) = decl.kind {
251 if let ir::TypeRef::Named(ref inner_name) = alias.target {
253 if let Some(resolved) = self.resolve_alias_impl(inner_name, scope, visited) {
254 return Some(resolved);
255 }
256 }
257 return Some(&alias.target);
258 }
259 }
260 None
261 }
262
263 pub fn error(&self, message: impl Into<String>) {
265 self.diagnostics.borrow_mut().error(message);
266 }
267
268 pub fn warn(&self, message: impl Into<String>) {
270 self.diagnostics.borrow_mut().warn(message);
271 }
272
273 pub fn take_diagnostics(&self) -> DiagnosticCollector {
275 self.diagnostics.take()
276 }
277
278 fn resolve_collisions(&mut self) {
281 let reserved: HashSet<&str> = JS_SYS_RESERVED.iter().copied().collect();
282
283 for name in &reserved {
284 if self.local_types.contains(*name) {
285 let mut renamed = format!("{name}_");
286 let mut i = 2;
287 while self.local_types.contains(&renamed) || reserved.contains(renamed.as_str()) {
288 renamed = format!("{name}_{i}");
289 i += 1;
290 }
291 self.renamed_locals.insert(name.to_string(), renamed);
292 }
293 }
294 }
295
296 fn collect_declaration(&mut self, kind: &ir::TypeKind) {
297 match kind {
298 ir::TypeKind::Class(c) => {
299 self.local_types.insert(c.name.clone());
300 }
301 ir::TypeKind::Interface(i) => {
302 self.local_types.insert(i.name.clone());
303 }
304 ir::TypeKind::StringEnum(e) => {
305 self.local_types.insert(e.name.clone());
306 }
307 ir::TypeKind::NumericEnum(e) => {
308 self.local_types.insert(e.name.clone());
309 }
310 ir::TypeKind::TypeAlias(_) => {
311 }
314 ir::TypeKind::Namespace(ns) => {
315 for inner in &ns.declarations {
316 self.collect_declaration(&inner.kind);
317 }
318 }
319 ir::TypeKind::Function(_) | ir::TypeKind::Variable(_) => {}
320 }
321 }
322}
323
324pub fn to_syn_type(
336 ty: &TypeRef,
337 pos: TypePosition,
338 ctx: Option<&CodegenContext<'_>>,
339 scope: ScopeId,
340) -> TokenStream {
341 if pos.inner {
343 match ty {
344 TypeRef::Boolean | TypeRef::BooleanLiteral(_) => return quote! { Boolean },
345 TypeRef::Number | TypeRef::NumberLiteral(_) => return quote! { Number },
346 TypeRef::String | TypeRef::StringLiteral(_) => return quote! { JsString },
347 TypeRef::Void | TypeRef::Undefined => return quote! { Undefined },
348 TypeRef::Nullable(inner) => {
349 let inner_ty = to_syn_type(inner, pos, ctx, scope);
350 return quote! { JsOption<#inner_ty> };
351 }
352 _ => {}
353 }
354 }
355
356 let borrow = pos.is_argument() && !pos.inner;
360
361 match ty {
362 TypeRef::Boolean => quote! { bool },
364 TypeRef::Number => quote! { f64 },
365 TypeRef::String => {
366 if borrow {
367 quote! { &str }
368 } else {
369 quote! { String }
370 }
371 }
372 TypeRef::BigInt => maybe_ref(quote! { BigInt }, borrow),
373 TypeRef::Void => quote! { () },
374 TypeRef::Undefined => maybe_ref(quote! { Undefined }, borrow),
375 TypeRef::Null => maybe_ref(quote! { JsValue }, borrow),
376 TypeRef::Any => maybe_ref(quote! { JsValue }, borrow),
377 TypeRef::Unknown => maybe_ref(quote! { JsValue }, borrow),
378 TypeRef::Object => maybe_ref(quote! { Object }, borrow),
379 TypeRef::Symbol => maybe_ref(quote! { JsValue }, borrow),
380
381 TypeRef::Int8Array => maybe_ref(quote! { Int8Array }, borrow),
383 TypeRef::Uint8Array => maybe_ref(quote! { Uint8Array }, borrow),
384 TypeRef::Uint8ClampedArray => maybe_ref(quote! { Uint8ClampedArray }, borrow),
385 TypeRef::Int16Array => maybe_ref(quote! { Int16Array }, borrow),
386 TypeRef::Uint16Array => maybe_ref(quote! { Uint16Array }, borrow),
387 TypeRef::Int32Array => maybe_ref(quote! { Int32Array }, borrow),
388 TypeRef::Uint32Array => maybe_ref(quote! { Uint32Array }, borrow),
389 TypeRef::Float32Array => maybe_ref(quote! { Float32Array }, borrow),
390 TypeRef::Float64Array => maybe_ref(quote! { Float64Array }, borrow),
391 TypeRef::BigInt64Array => maybe_ref(quote! { BigInt64Array }, borrow),
392 TypeRef::BigUint64Array => maybe_ref(quote! { BigUint64Array }, borrow),
393 TypeRef::ArrayBuffer => maybe_ref(quote! { ArrayBuffer }, borrow),
394 TypeRef::ArrayBufferView => maybe_ref(quote! { Object }, borrow),
395 TypeRef::DataView => maybe_ref(quote! { DataView }, borrow),
396
397 TypeRef::Promise(inner) => maybe_ref(
399 generic_container(quote! { Promise }, inner, pos, ctx, scope),
400 borrow,
401 ),
402 TypeRef::Array(inner) => maybe_ref(
403 generic_container(quote! { Array }, inner, pos, ctx, scope),
404 borrow,
405 ),
406 TypeRef::Record(_k, v) => maybe_ref(
407 generic_container(quote! { Object }, v, pos, ctx, scope),
408 borrow,
409 ),
410 TypeRef::Map(k, v) => {
411 let inner_pos = pos.to_inner();
412 let k_arg = to_syn_type(k, inner_pos, ctx, scope);
413 let v_arg = to_syn_type(v, inner_pos, ctx, scope);
414 let base = if is_jsvalue_arg(&k_arg) && is_jsvalue_arg(&v_arg) {
415 quote! { Map }
416 } else {
417 quote! { Map<#k_arg, #v_arg> }
418 };
419 maybe_ref(base, borrow)
420 }
421 TypeRef::Set(inner) => maybe_ref(
422 generic_container(quote! { Set }, inner, pos, ctx, scope),
423 borrow,
424 ),
425
426 TypeRef::Nullable(inner) => {
428 if pos.inner {
429 let inner_ty = to_syn_type(inner, pos, ctx, scope);
430 quote! { JsOption<#inner_ty> }
431 } else {
432 let inner_ty = to_syn_type(inner, pos, ctx, scope);
433 quote! { Option<#inner_ty> }
434 }
435 }
436 TypeRef::Union(_) => maybe_ref(quote! { JsValue }, borrow),
437 TypeRef::Intersection(_) => maybe_ref(quote! { JsValue }, borrow),
438 TypeRef::Tuple(elems) => {
439 let base = if elems.is_empty() {
440 quote! { Array }
441 } else {
442 let inner_pos = pos.to_inner();
443 let elem_types: Vec<TokenStream> = elems
444 .iter()
445 .map(|e| to_syn_type(e, inner_pos, ctx, scope))
446 .collect();
447 quote! { ArrayTuple<(#(#elem_types),*)> }
448 };
449 maybe_ref(base, borrow)
450 }
451 TypeRef::Function(sig) => {
452 let inner_pos = pos.to_inner();
453 let params: Vec<TokenStream> = sig
454 .params
455 .iter()
456 .take(8)
457 .map(|p| to_syn_type(&p.type_ref, inner_pos, ctx, scope))
458 .collect();
459 let ret = to_syn_type(&sig.return_type, inner_pos, ctx, scope);
460 let base = if params.iter().all(is_jsvalue_arg) && is_jsvalue_arg(&ret) {
461 quote! { Function }
462 } else {
463 quote! { Function<fn(#(#params),*) -> #ret> }
464 };
465 maybe_ref(base, borrow)
466 }
467
468 TypeRef::StringLiteral(_) => {
470 if borrow {
471 quote! { &str }
472 } else {
473 quote! { String }
474 }
475 }
476 TypeRef::NumberLiteral(_) => quote! { f64 },
477 TypeRef::BooleanLiteral(_) => quote! { bool },
478
479 TypeRef::Named(name) => {
481 if let Some(c) = ctx {
483 if let Some(target) = c.resolve_alias(name, scope) {
484 let target = target.clone();
485 return to_syn_type(&target, pos, ctx, scope);
486 }
487 }
488 maybe_ref(named_type_to_rust(name, ctx), borrow)
489 }
490 TypeRef::GenericInstantiation(name, _args) => {
491 if let Some(c) = ctx {
494 c.warn(format!(
495 "generic type arguments on `{name}<...>` are not yet emitted, using bare `{name}`"
496 ));
497 }
498 maybe_ref(named_type_to_rust(name, ctx), borrow)
499 }
500
501 TypeRef::Date => maybe_ref(quote! { Date }, borrow),
503 TypeRef::RegExp => maybe_ref(quote! { RegExp }, borrow),
504 TypeRef::Error => maybe_ref(quote! { Error }, borrow),
505
506 TypeRef::Unresolved(desc) => {
508 if let Some(cgctx) = ctx {
509 cgctx.warn(format!("unresolved type `{desc}`, falling back to JsValue"));
510 }
511 maybe_ref(quote! { JsValue }, borrow)
512 }
513 }
514}
515
516fn maybe_ref(ty: TokenStream, borrow: bool) -> TokenStream {
521 if borrow {
522 quote! { &#ty }
523 } else {
524 ty
525 }
526}
527
528fn generic_container(
530 base: TokenStream,
531 inner: &TypeRef,
532 pos: TypePosition,
533 ctx: Option<&CodegenContext<'_>>,
534 scope: ScopeId,
535) -> TokenStream {
536 let arg = to_syn_type(inner, pos.to_inner(), ctx, scope);
537 if is_jsvalue_arg(&arg) {
538 base
539 } else {
540 quote! { #base<#arg> }
541 }
542}
543
544fn is_jsvalue_arg(tokens: &TokenStream) -> bool {
547 let s = tokens.to_string();
548 s == "JsValue"
549}
550
551fn emit_type_name(name: &str, ctx: &CodegenContext<'_>) -> TokenStream {
561 let resolved = ctx.file_scopes.iter().find_map(|&scope| {
563 if name.contains('.') {
564 ctx.gctx.resolve_path(scope, name)
565 } else {
566 ctx.gctx.scopes.resolve(scope, name)
567 }
568 });
569
570 let ident_name = name.rsplit('.').next().unwrap_or(name);
572
573 if let Some(type_id) = resolved {
575 if matches!(&ctx.gctx.get_type(type_id).kind, TypeKind::Namespace(_)) {
576 return quote! { JsValue };
577 }
578 }
579
580 if ctx.local_types.contains(ident_name) {
582 if let Some(renamed) = ctx.renamed_locals.get(ident_name) {
583 let ident = make_ident(renamed);
584 return quote! { #ident };
585 }
586 let ident = make_ident(ident_name);
587 return quote! { #ident };
588 }
589
590 if let Some(rust_path) = ctx.gctx.external_map.resolve_type(ident_name) {
592 ctx.register_external(ident_name, &rust_path.path);
593 let ident = make_ident(ident_name);
594 return quote! { #ident };
595 }
596
597 if resolved.is_some() {
600 ctx.error(format!(
601 "Non-local type `{name}` resolved but has no external mapping. \
602 Use --external to map this type."
603 ));
604 ctx.register_external(ident_name, "JsValue");
605 let ident = make_ident(ident_name);
606 return quote! { #ident };
607 }
608
609 if JS_SYS_RESERVED.contains(&ident_name) {
612 let ident = make_ident(ident_name);
613 return quote! { #ident };
614 }
615
616 ctx.error(format!(
618 "Unresolved type `{name}`. Use --external to map this type."
619 ));
620 ctx.register_external(ident_name, "JsValue");
621 let ident = make_ident(ident_name);
622 quote! { #ident }
623}
624
625fn named_type_to_rust(name: &str, ctx: Option<&CodegenContext<'_>>) -> TokenStream {
627 match ctx {
628 Some(ctx) => emit_type_name(name, ctx),
629 None => quote! { JsValue },
630 }
631}
632
633pub(crate) fn make_ident(name: &str) -> syn::Ident {
635 let sanitized: String = name
637 .chars()
638 .filter(|c| c.is_alphanumeric() || *c == '_')
639 .collect();
640 let sanitized = if sanitized.is_empty() {
641 "__unknown__".to_string()
642 } else if sanitized.starts_with(|c: char| c.is_ascii_digit()) {
643 format!("_{sanitized}")
644 } else {
645 sanitized
646 };
647 if let Ok(ident) = syn::parse_str::<syn::Ident>(&sanitized) {
649 return ident;
650 }
651 match sanitized.as_str() {
653 "self" | "Self" | "super" | "crate" => {
654 syn::Ident::new(&format!("{sanitized}_"), proc_macro2::Span::call_site())
655 }
656 _ => syn::Ident::new_raw(&sanitized, proc_macro2::Span::call_site()),
658 }
659}
660
661pub fn to_return_type(
664 ty: &TypeRef,
665 catch: bool,
666 ctx: Option<&CodegenContext<'_>>,
667 scope: ScopeId,
668) -> TokenStream {
669 let inner = to_syn_type(ty, TypePosition::RETURN, ctx, scope);
670 if catch {
671 quote! { Result<#inner, JsValue> }
672 } else {
673 inner
674 }
675}
676
677#[cfg(test)]
678mod tests {
679 use super::*;
680
681 use crate::parse::scope::ScopeId;
682
683 fn arg_type(ty: &TypeRef) -> String {
685 to_syn_type(ty, TypePosition::ARGUMENT, None, ScopeId(0)).to_string()
687 }
688
689 fn ret_type(ty: &TypeRef) -> String {
690 to_syn_type(ty, TypePosition::RETURN, None, ScopeId(0)).to_string()
691 }
692
693 fn inner_type(ty: &TypeRef) -> String {
694 to_syn_type(ty, TypePosition::RETURN.to_inner(), None, ScopeId(0)).to_string()
695 }
696
697 #[test]
698 fn test_string_positions() {
699 assert_eq!(arg_type(&TypeRef::String), "& str");
700 assert_eq!(ret_type(&TypeRef::String), "String");
701 }
702
703 #[test]
704 fn test_string_inner_position() {
705 assert_eq!(inner_type(&TypeRef::String), "JsString");
707 }
708
709 #[test]
710 fn test_number_inner_position() {
711 assert_eq!(inner_type(&TypeRef::Number), "Number");
713 }
714
715 #[test]
716 fn test_boolean_inner_position() {
717 assert_eq!(inner_type(&TypeRef::Boolean), "Boolean");
719 }
720
721 #[test]
722 fn test_void_inner_position() {
723 assert_eq!(inner_type(&TypeRef::Void), "Undefined");
725 }
726
727 #[test]
728 fn test_nullable() {
729 let ty = TypeRef::Nullable(Box::new(TypeRef::String));
730 let result = ret_type(&ty);
732 assert_eq!(result, "Option < String >");
733 }
734
735 #[test]
736 fn test_promise_with_named_type_unresolved() {
737 let ty = TypeRef::Promise(Box::new(TypeRef::Named("Foo".into())));
739 assert_eq!(ret_type(&ty), "Promise");
740 }
741
742 #[test]
743 fn test_nullable_inner() {
744 let ty = TypeRef::Nullable(Box::new(TypeRef::String));
746 let result = inner_type(&ty);
747 assert_eq!(result, "JsOption < JsString >");
748 }
749
750 #[test]
751 fn test_promise_with_string() {
752 let ty = TypeRef::Promise(Box::new(TypeRef::String));
753 let result = ret_type(&ty);
754 assert_eq!(result, "Promise < JsString >");
755 }
756
757 #[test]
758 fn test_promise_with_any_elides_generic() {
759 let ty = TypeRef::Promise(Box::new(TypeRef::Any));
760 let result = ret_type(&ty);
761 assert_eq!(result, "Promise");
762 }
763
764 #[test]
765 fn test_promise_with_void() {
766 let ty = TypeRef::Promise(Box::new(TypeRef::Void));
767 let result = ret_type(&ty);
768 assert_eq!(result, "Promise < Undefined >");
769 }
770
771 #[test]
772 fn test_nullable_named_type_unresolved() {
773 let ty = TypeRef::Nullable(Box::new(TypeRef::Named("Foo".into())));
775 assert_eq!(arg_type(&ty), "Option < & JsValue >");
776 assert_eq!(ret_type(&ty), "Option < JsValue >");
777 }
778
779 #[test]
780 fn test_promise_with_arraybuffer() {
781 let ty = TypeRef::Promise(Box::new(TypeRef::ArrayBuffer));
782 let result = ret_type(&ty);
783 assert_eq!(result, "Promise < ArrayBuffer >");
784 }
785
786 #[test]
787 fn test_array_with_type() {
788 let ty = TypeRef::Array(Box::new(TypeRef::Number));
789 let result = ret_type(&ty);
790 assert_eq!(result, "Array < Number >");
791 }
792
793 #[test]
794 fn test_array_with_any_elides() {
795 let ty = TypeRef::Array(Box::new(TypeRef::Any));
796 let result = ret_type(&ty);
797 assert_eq!(result, "Array");
798 }
799
800 #[test]
801 fn test_set_with_type() {
802 let ty = TypeRef::Set(Box::new(TypeRef::String));
803 let result = ret_type(&ty);
804 assert_eq!(result, "Set < JsString >");
805 }
806
807 #[test]
808 fn test_map_with_types() {
809 let ty = TypeRef::Map(Box::new(TypeRef::String), Box::new(TypeRef::Number));
810 let result = ret_type(&ty);
811 assert_eq!(result, "Map < JsString , Number >");
812 }
813
814 #[test]
815 fn test_record_erases_key() {
816 let ty = TypeRef::Record(Box::new(TypeRef::String), Box::new(TypeRef::Number));
817 let result = ret_type(&ty);
818 assert_eq!(result, "Object < Number >");
819 }
820
821 #[test]
822 fn test_promise_nullable_inner() {
823 let ty = TypeRef::Promise(Box::new(TypeRef::Nullable(Box::new(TypeRef::String))));
825 let result = ret_type(&ty);
826 assert_eq!(result, "Promise < JsOption < JsString > >");
827 }
828
829 #[test]
830 fn test_function_typed() {
831 let sig = ir::FunctionSig {
832 params: vec![ir::Param {
833 name: "x".into(),
834 type_ref: TypeRef::Number,
835 optional: false,
836 variadic: false,
837 }],
838 return_type: Box::new(TypeRef::Boolean),
839 };
840 let ty = TypeRef::Function(sig);
841 let result = ret_type(&ty);
842 assert_eq!(result, "Function < fn (Number) -> Boolean >");
843 }
844
845 #[test]
846 fn test_function_untyped() {
847 let sig = ir::FunctionSig {
848 params: vec![ir::Param {
849 name: "x".into(),
850 type_ref: TypeRef::Any,
851 optional: false,
852 variadic: false,
853 }],
854 return_type: Box::new(TypeRef::Any),
855 };
856 let ty = TypeRef::Function(sig);
857 let result = ret_type(&ty);
858 assert_eq!(result, "Function");
859 }
860
861 #[test]
862 fn test_named_unresolved_without_ctx() {
863 let ty = TypeRef::Named("Request".into());
865 assert_eq!(ret_type(&ty), "JsValue");
866 }
867
868 #[test]
869 fn test_named_unknown_without_ctx() {
870 let ty = TypeRef::Named("MyCustomType".into());
871 assert_eq!(ret_type(&ty), "JsValue");
872 }
873
874 #[test]
875 fn test_return_with_catch() {
876 let ty = TypeRef::Promise(Box::new(TypeRef::Void));
877 let result = to_return_type(&ty, true, None, ScopeId(0)).to_string();
878 assert_eq!(result, "Result < Promise < Undefined > , JsValue >");
879 }
880
881 #[test]
882 fn test_union_erases() {
883 let ty = TypeRef::Union(vec![TypeRef::String, TypeRef::Number]);
884 assert_eq!(arg_type(&ty), "& JsValue");
886 assert_eq!(ret_type(&ty), "JsValue");
887 }
888
889 fn test_gctx() -> (GlobalContext, ScopeId) {
890 let mut gctx = GlobalContext::new();
891 let scope = gctx.create_root_scope();
892 (gctx, scope)
893 }
894
895 #[test]
896 fn test_local_type_overrides_web_sys() {
897 let (gctx, scope) = test_gctx();
898 let mut ctx = CodegenContext::empty(&gctx, scope);
899 ctx.local_types.insert("Response".into());
900 let ty = TypeRef::Named("Response".into());
901 let result = to_syn_type(&ty, TypePosition::RETURN, Some(&ctx), scope).to_string();
902 assert_eq!(result, "Response");
903 }
904
905 #[test]
906 fn test_union_alias_resolves_to_jsvalue() {
907 let (mut gctx, scope) = test_gctx();
909 let alias_id = gctx.insert_type(crate::ir::TypeDeclaration {
910 kind: crate::ir::TypeKind::TypeAlias(crate::ir::TypeAliasDecl {
911 name: "BodyInit".to_string(),
912 type_params: vec![],
913 target: TypeRef::Union(vec![TypeRef::String, TypeRef::ArrayBuffer]),
914 from_module: None,
915 }),
916 module_context: crate::ir::ModuleContext::Global,
917 doc: None,
918 scope_id: scope,
919 exported: false,
920 });
921 gctx.scopes.insert(scope, "BodyInit".to_string(), alias_id);
922
923 let ctx = CodegenContext::empty(&gctx, scope);
924 let ty = TypeRef::Named("BodyInit".into());
925 let result = to_syn_type(&ty, TypePosition::RETURN, Some(&ctx), scope).to_string();
926 assert_eq!(result, "JsValue");
927 }
928
929 #[test]
930 fn test_unresolved_with_ctx_registers_jsvalue_alias() {
931 let (gctx, scope) = test_gctx();
932 let ctx = CodegenContext::empty(&gctx, scope);
933 let ty = TypeRef::Named("Response".into());
934 let result = to_syn_type(&ty, TypePosition::RETURN, Some(&ctx), scope).to_string();
935 assert_eq!(result, "Response");
937 let uses = ctx.external_uses.borrow();
939 assert_eq!(uses.get("Response"), Some(&"JsValue".to_string()));
940 }
941
942 #[test]
943 fn test_local_type_in_promise() {
944 let (gctx, scope) = test_gctx();
945 let mut ctx = CodegenContext::empty(&gctx, scope);
946 ctx.local_types.insert("MyThing".into());
947 let ty = TypeRef::Promise(Box::new(TypeRef::Named("MyThing".into())));
948 let result = to_syn_type(&ty, TypePosition::RETURN, Some(&ctx), scope).to_string();
949 assert_eq!(result, "Promise < MyThing >");
950 }
951
952 #[test]
955 fn test_to_inner_preserves_direction() {
956 let pos = TypePosition::ARGUMENT.to_inner();
957 assert!(pos.is_argument());
958 assert!(pos.inner);
959
960 let pos = TypePosition::RETURN.to_inner();
961 assert!(!pos.is_argument());
962 assert!(pos.inner);
963 }
964
965 #[test]
966 fn test_inner_position_named_type_unresolved() {
967 let ty = TypeRef::Named("Response".into());
969 assert_eq!(inner_type(&ty), "JsValue");
970 assert_eq!(ret_type(&ty), "JsValue");
971 }
972
973 #[test]
974 fn test_inner_position_typed_array_unchanged() {
975 let ty = TypeRef::Uint8Array;
977 assert_eq!(inner_type(&ty), "Uint8Array");
978 assert_eq!(ret_type(&ty), "Uint8Array");
979 }
980
981 #[test]
982 fn test_tuple_generates_array_tuple() {
983 let ty = TypeRef::Tuple(vec![
985 TypeRef::Array(Box::new(TypeRef::Named("ImportSpecifier".into()))),
986 TypeRef::Array(Box::new(TypeRef::Named("ExportSpecifier".into()))),
987 TypeRef::Boolean,
988 TypeRef::Boolean,
989 ]);
990 let result = ret_type(&ty);
991 assert_eq!(result, "ArrayTuple < (Array , Array , Boolean , Boolean) >");
992 }
993
994 #[test]
995 fn test_empty_tuple_is_bare_array() {
996 let ty = TypeRef::Tuple(vec![]);
997 assert_eq!(ret_type(&ty), "Array");
998 }
999
1000 #[test]
1001 fn test_type_position_all_variants() {
1002 let ty = TypeRef::String;
1004 assert_eq!(
1005 to_syn_type(&ty, TypePosition::ARGUMENT, None, ScopeId(0)).to_string(),
1006 "& str"
1007 );
1008 assert_eq!(
1009 to_syn_type(&ty, TypePosition::RETURN, None, ScopeId(0)).to_string(),
1010 "String"
1011 );
1012 assert_eq!(
1014 to_syn_type(&ty, TypePosition::RETURN.to_inner(), None, ScopeId(0)).to_string(),
1015 "JsString"
1016 );
1017 assert_eq!(
1019 to_syn_type(&ty, TypePosition::ARGUMENT.to_inner(), None, ScopeId(0)).to_string(),
1020 "JsString"
1021 );
1022 }
1023}