riptc 0.1.7

Rust implementation of the InertiaJS protocol compatible with `riptc` for generating strong TypeScript bindings.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
//! This module defines conversions of rust hir types into swc ast types

use either::Either;
use rustc_middle::ty::{AdtDef, GenericArg, GenericArgsRef, Ty, TyCtxt, TyKind};
use rustc_type_ir::AliasTyKind;
use swc_atoms::Atom;
use swc_common::DUMMY_SP;
use swc_ecma_ast::{
    TsArrayType, TsEntityName, TsKeywordType, TsKeywordTypeKind, TsQualifiedName, TsTupleElement,
    TsTupleType, TsType, TsTypeElement, TsTypeLit,
};

use crate::{
    callbacks::ConfigNamespacedPathExt,
    codegen::RiptCodegen,
    namespace::{EntityName, NamespacedPath},
    swc_utils,
};

pub fn convert_ty<'tcx>(
    cg: &RiptCodegen<'tcx, '_>,
    origin_span: &rustc_span::Span,
    ty: Ty<'tcx>,
    // track how deeply recused we are inside of a `convert_adt`. adts cannot always
    // be computed, and sometimes all they do is peel out the inner type such as a result.
    // we need to track the recursion depth because we only want to peel back once at the
    // top level to accomodate for special library features that `ript` does
    adt_left_recusion_depth: usize,
) -> swc_ecma_ast::TsType {
    match ty.kind() {
        TyKind::Bool => swc_ecma_ast::TsType::TsKeywordType(TsKeywordType {
            span: DUMMY_SP,
            kind: TsKeywordTypeKind::TsBooleanKeyword,
        }),
        // TODO: probably not allow u128 because i think that overflows,
        // or make a bignum?
        TyKind::Int(_) | TyKind::Uint(_) | TyKind::Float(_) => {
            swc_ecma_ast::TsType::TsKeywordType(TsKeywordType {
                span: DUMMY_SP,
                kind: TsKeywordTypeKind::TsNumberKeyword,
            })
        }
        TyKind::Ref(_, ty, _) => convert_ty(cg, origin_span, *ty, adt_left_recusion_depth),
        TyKind::Tuple(tys) if tys.is_empty() => swc_utils::null_type(),
        // TODO(@lazkindness): possibly user defined unless we want to just not allow aliases,
        // which is possibly ideal
        TyKind::Tuple(tys) => swc_ecma_ast::TsType::TsTupleType(TsTupleType {
            span: DUMMY_SP,
            elem_types: tys
                .iter()
                .map(|ty| TsTupleElement {
                    span: DUMMY_SP,
                    label: None,
                    ty: Box::new(convert_ty(cg, origin_span, ty, adt_left_recusion_depth)),
                })
                .collect(),
        }),
        TyKind::Array(ty, _) => swc_ecma_ast::TsType::TsArrayType(TsArrayType {
            span: DUMMY_SP,
            elem_type: Box::new(convert_ty(cg, origin_span, *ty, adt_left_recusion_depth)),
        }),
        TyKind::Adt(def, args) => {
            let mut block_emit = false;
            // when we receive an adt, we want to convert the adt and then track the underlying adt definition
            // so that we can emit it globally...
            match convert_adt(
                cg,
                *def,
                origin_span,
                args,
                adt_left_recusion_depth,
                &mut block_emit,
            ) {
                // convert adt could immediately resolve without peeling any generic types out,
                // so we can track it and return it
                Either::Right((path, ts_type)) => {
                    // ...but we just want to return a path to it if its user defined so we're not inlining it wherever
                    // this was called.
                    if path.is_user_defined() && !block_emit {
                        cg.insert_ts_type(path.clone(), ts_type.clone());
                        entity_name_into_ts_type(path.into())
                    } else {
                        ts_type
                    }
                }
                // convert adt just peeled a generic out, so we need to recurse on ourselves and try again
                Either::Left(ty) => convert_ty(cg, origin_span, ty, adt_left_recusion_depth + 1),
            }
        }
        TyKind::Str => swc_ecma_ast::TsType::TsKeywordType(TsKeywordType {
            span: DUMMY_SP,
            kind: TsKeywordTypeKind::TsStringKeyword,
        }),
        TyKind::Alias(AliasTyKind::Projection, alias_ty) => {
            let ty = alias_ty.self_ty();
            convert_ty(cg, origin_span, ty, adt_left_recusion_depth)
        }
        _ => unimplemented!("type: {ty:#?}"),
    }
}

/// Given an ADT (any data type that is not a primitive, not necessarily always a user defined
/// type, could be a third party lib or something like an `Option` which is still a `T`), convert
/// that. This returns either `Left`, indicating that all the ADT is is a container which needs to be
/// peeled back and `convert_ty` needs to recurse on itself again, or `Right` which indicates that we
/// do know what the type is, and we can produce a fully qualified path + a resolved type name
fn convert_adt<'tcx>(
    cg: &RiptCodegen<'tcx, '_>,
    def: AdtDef<'tcx>,
    origin_span: &rustc_span::Span,
    args: &'tcx rustc_middle::ty::List<GenericArg<'tcx>>,
    left_recursion_depth: usize,
    block_emit: &mut bool,
) -> Either<Ty<'tcx>, (NamespacedPath, TsType)> {
    let adt_span = cg.tcx.def_span(def.did());

    let path = NamespacedPath::new_for_adt(cg.tcx, &def);

    // workaround for now, but if a type is boxed we treat it as a potentially recursing type and
    // _always_ reference it by name.
    if path == NamespacedPath::alloc_box() {
        *block_emit = true;

        let mut args = canonicalize_args(CanonicalizeType::Vec, args);
        let arg = args.next().unwrap().expect_ty();
        match arg.kind() {
            TyKind::Slice(ty) => {
                let TyKind::Adt(def, _args) = ty.kind() else {
                    cg.tcx
                        .dcx()
                        .span_fatal(adt_span, "cannot box a type that is not an adt or array");
                };

                let path = NamespacedPath::new_for_adt(cg.tcx, &def);
                let ty = entity_name_into_ts_type(path.clone().into());

                return Either::Right((
                    path,
                    swc_ecma_ast::TsType::TsArrayType(TsArrayType {
                        span: DUMMY_SP,
                        elem_type: Box::new(ty),
                    }),
                ));
            }
            TyKind::Adt(def, _args) => {
                let path = NamespacedPath::new_for_adt(cg.tcx, &def);
                // if we have a boxed adt, we need to convert the adt and then return the type
                // of the adt
                return Either::Right((path.clone(), entity_name_into_ts_type(path.into())));
            }
            _ => {
                cg.tcx
                    .dcx()
                    .span_fatal(adt_span, "cannot box a type that is not an adt or array");
            }
        }
    }

    if path == NamespacedPath::std_string() {
        return Either::Right((
            path,
            swc_ecma_ast::TsType::TsKeywordType(TsKeywordType {
                span: DUMMY_SP,
                kind: TsKeywordTypeKind::TsStringKeyword,
            }),
        ));
    }

    if path == NamespacedPath::std_vec() {
        let mut args = canonicalize_args(CanonicalizeType::Vec, args);
        return Either::Right((
            path,
            swc_ecma_ast::TsType::TsArrayType(TsArrayType {
                span: DUMMY_SP,
                elem_type: Box::new(convert_ty(
                    cg,
                    &adt_span,
                    args.next().unwrap().expect_ty(),
                    left_recursion_depth,
                )),
            }),
        ));
    }

    // sometimes, under certain cacheing conditions, a reference to std hashmap
    // will shortcut straight into hashbrown hashmap, effectively peeling back the
    // re-export. i need to figure out more about why this is actually happening, but regardless
    // we need to be resilient to both and keep our eyes out for similar things that can happen
    // for other types.
    if path == NamespacedPath::hashbrown_hashmap() || path == NamespacedPath::std_hashmap() {
        let mut gen_args = canonicalize_args(CanonicalizeType::HashMap, args);
        let key_ty = convert_ty(
            cg,
            &adt_span,
            gen_args.next().unwrap().expect_ty(),
            left_recursion_depth,
        );
        let value_ty = convert_ty(
            cg,
            &adt_span,
            gen_args.next().unwrap().expect_ty(),
            left_recursion_depth,
        );

        let index_sig = swc_utils::index_signature("k", key_ty, value_ty);

        return Either::Right((path, index_sig));
    }

    if path == NamespacedPath::std_option() {
        let mut args = canonicalize_args(CanonicalizeType::Option, args);
        return Either::Right((
            path,
            swc_utils::union_type(
                vec![
                    convert_ty(
                        cg,
                        &adt_span,
                        args.next().unwrap().expect_ty(),
                        left_recursion_depth,
                    ),
                    swc_ecma_ast::TsType::TsKeywordType(TsKeywordType {
                        span: DUMMY_SP,
                        kind: TsKeywordTypeKind::TsNullKeyword,
                    }),
                ]
                .into_iter(),
            ),
        ));
    }

    /*
    this is quite hacky and not great and should be moved into another place, but more or less
    what this does is act as a direct compatibility layer with the inertia crate's `PropControlFlow`,
    which will consume results and make error fail the page, but allow options to act as they are
    and make the prop nullable. if we encounter an option as the inner type, we need to peel back
    the option layer, ignore the error type, and just get out the inner `T`.
    */
    if path == NamespacedPath::result() {
        if left_recursion_depth != 0 {
            cg.tcx.dcx().span_fatal(
                *origin_span,
                "do not use a `Result` as the inner type of your prop",
            );
        }

        // since we're peeling back the result, `args[0]` is going to be the `T` type of `T, E`
        // in the result and we can discard the error type since inertia runtime will handle feeding that
        // into middleware
        let ty = args[0].expect_ty();

        // in this case, we want to return `Either::Left` because we cannot actually determine the inner
        // type in this adt function, all we can reasonably do is extract `T` from the result, and then tell
        // `convert_ty` to recurse on itself again because we don't know for certain that `T` is an adt and not,
        // say, an `&str`.
        return Either::Left(ty);
    }

    // as far as ts is concerned, these are both just strings under the hood. potentially, we could augment this
    // in such a way that date parsing is automated but afaik most people don't want this nor do this so i think
    // interpolating dates as strings is fine. it _might_ be worth producing an alias and emitting these under chrono /
    // uuid paths in the output, but we'll see if this is enough of an issue to actually worry about
    if path == NamespacedPath::chrono_naive_date_time()
            || NamespacedPath::chrono_naive_date() == path
            || NamespacedPath::chrono_naive_time() == path
            || path == NamespacedPath::uuid()
            // this one actually can be generated fine, its just a giant unit enum, but
            // it's _huge_ and we don't need to bloat the output with this
            || path == NamespacedPath::chrono_tz_timezones()
    {
        return Either::Right((
            path,
            swc_utils::ts_keyword(TsKeywordTypeKind::TsStringKeyword),
        ));
    }

    // check if the path has an override specified by the user in `ript.toml` for
    // types that may have a custom `Serialize` impl instead of a `derive`, meaning we
    // cannot deterministically evaluate this type.
    if let Some(ts_keyword_type) = path.override_for_namespaced_path() {
        return Either::Right((
            path,
            swc_utils::ts_keyword(config_ts_keyword_type_to_swc_ts_keyword_type(
                ts_keyword_type,
            )),
        ));
    };

    let dcx = cg.tcx.dcx();
    let span = cg.tcx.def_span(def.did());

    // See [`Self::smells_like_a_newtype`]
    if let Some(ty) = smells_like_a_newtype(cg.tcx, def, args) {
        return Either::Right((path, convert_ty(cg, &span, ty, left_recursion_depth)));
    }

    if !args.is_empty() {
        // TODO(@lazkindness): generics in TS!!
        dcx.struct_span_err(
                span,
                "`ript` user defined types with generics are not currently supported; you must add an override to this type",
            )
            .emit();
    }

    if def.is_union() {
        dcx.span_fatal(
            span,
            "union types are not supported and likely never will be",
        )
    }

    if def.is_enum() {
        return Either::Right(convert_enum(cg, def, path, left_recursion_depth));
    }

    // Newtype, aka `SomeStruct(T)`. For this, we just peel the wrapper back and extract `T`
    // because that's how serde treats newtypes
    if def.all_fields().count() == 1 && def.all_fields().next().unwrap().ident(cg.tcx).is_numeric()
    {
        let field_def = def.all_fields().next().unwrap();
        let field_ty = field_def.ty(cg.tcx, args);

        return Either::Right((
            path,
            convert_ty(
                cg,
                &cg.tcx.def_span(field_def.did),
                field_ty,
                left_recursion_depth,
            ),
        ));
    }

    let members = def
        .all_fields()
        // first convert all the fields
        .filter_map(|field_def| {
            convert_field_def(cg, origin_span, field_def, args, left_recursion_depth)
        })
        // then the next pass will split out either flattened fields / regular fields, which are both
        // regular named struct fields
        .map(|member| match member {
            Either::Left(members) => Either::Left(members),
            Either::Right((Some(name), type_ann)) => Either::Left(vec![
                swc_utils::object_member_type_element()
                    .key(name)
                    .type_ann(type_ann)
                    .optional(false)
                    .build(),
            ]),
            // or we will collect unnamed fields which correspond to a tuple
            Either::Right((None, type_ann)) => Either::Right(type_ann),
        });

    // then, finally, we homoginize the type elements into either all named, standard fields or a tuple
    // grouping
    let homogenized_type_elements = collect_homogeneous(members.into_iter()).unwrap_or_else(|_| {
        cg.tcx.dcx().span_fatal(
            *origin_span,
            "cannot mix named and unnamed fields in a struct",
        );
    });

    match homogenized_type_elements {
        Either::Left(members) => Either::Right((
            path,
            swc_utils::object_type_from_elements(members.into_iter().flatten()),
        )),
        Either::Right(type_ann) => Either::Right((
            path,
            swc_utils::tuple_type_from_elements(type_ann.into_iter()),
        )),
    }
}

// TODO(@lazkindness): make this not insane
#[allow(clippy::type_complexity)]
/// Converts an adt field def. This returns none if the field is skipped.
/// Two outcomes exist from this function:
/// 1. You have a set of type elements representing a flattened adt
/// 2. You have a single type element representing a field, which may or may not have a name depending on if its a tuple
///    variant or not.
fn convert_field_def<'tcx>(
    cg: &RiptCodegen<'tcx, '_>,
    origin_span: &rustc_span::Span,
    field_def: &rustc_middle::ty::FieldDef,
    args: GenericArgsRef<'tcx>,
    left_recursion_depth: usize,
) -> Option<Either<Vec<TsTypeElement>, (Option<Atom>, TsType)>> {
    let did = field_def.did;
    let field_ty = field_def.ty(cg.tcx, args);

    let serde_attrs = crate::serde::serde_attrs_for_field_def(cg.tcx, did);
    if serde_attrs.skip() {
        return None;
    }

    if serde_attrs.flatten() {
        let span = cg.tcx.def_span(did);
        if serde_attrs.default() {
            cg.tcx
                .dcx()
                .span_fatal(span, "flattened types cannot be default");
        }

        let TyKind::Adt(def, args) = field_ty.kind() else {
            cg.tcx
                .dcx()
                .span_fatal(span, "flattened types must be adts");
        };

        let ts_adt = match convert_adt(
            cg,
            *def,
            origin_span,
            args,
            left_recursion_depth,
            &mut false,
        ) {
            Either::Left(_ty) => {
                cg.tcx.dcx().span_fatal(
                    cg.tcx.def_span(def.did()),
                    "unpeelable type found located in flattened adt",
                );
            }
            Either::Right((_namespaced_path, ts_adt)) => ts_adt,
        };

        // now that we've got our ts adt, lets extract all of the fields from it and then
        // just produce those as a batch

        let TsType::TsTypeLit(TsTypeLit { members, .. }) = ts_adt else {
            cg.tcx.dcx().span_fatal(
                cg.tcx.def_span(def.did()),
                "flattened adt must be a is invalid",
            );
        };

        // left = we have a batch of members
        Some(Either::Left(members))
    } else {
        let field_def_span = cg.tcx.def_span(field_def.did);
        let type_ann = convert_ty(cg, &field_def_span, field_ty, left_recursion_depth);

        let field_name = field_def.ident(cg.tcx);

        Some(Either::Right(if field_name.is_numeric() {
            (None, type_ann)
        } else {
            let field_name = serde_attrs.rename_field(field_name.as_str());
            let type_ann = if serde_attrs.default() {
                swc_utils::into_null_union_type(type_ann)
            } else {
                type_ann
            };
            (Some(field_name.into()), type_ann)
        }))
    }
}

fn convert_enum<'tcx>(
    cg: &RiptCodegen<'tcx, '_>,
    def: AdtDef<'tcx>,
    path: NamespacedPath,
    result_depth: usize,
) -> (NamespacedPath, TsType) {
    let variants = def.variants();
    // if all variants are units (i.e. no fields), then produce a simple union of literal strings:
    // e.g. enum Color { Red, Green, Blue } => "Red" | "Green" | "Blue"
    let all_unit = variants.iter().all(|v| v.fields.is_empty());

    if all_unit {
        let string_literal_types: Vec<TsType> = variants
            .iter()
            .filter_map(|variant_def| {
                let serde_attrs =
                    crate::serde::serde_attrs_for_field_def(cg.tcx, variant_def.def_id);

                if serde_attrs.skip() {
                    return None;
                }

                if serde_attrs.default() {
                    cg.tcx.dcx().span_fatal(
                        cg.tcx.def_span(variant_def.def_id),
                        "default variant cannot be used in a union",
                    );
                }

                let variant_name = variant_def.ident(cg.tcx);
                let variant_name = serde_attrs.rename_field(variant_name.as_str());

                Some(TsType::TsLitType(swc_ecma_ast::TsLitType {
                    span: DUMMY_SP,
                    lit: swc_utils::lit_str(variant_name).into(),
                }))
            })
            .collect();
        (
            path,
            swc_utils::union_type(string_literal_types.into_iter()),
        )
    } else {
        // if there are fields then produce a discriminated union of object types where
        // each variant becomes an object with a `type` field set to the variant name,
        // plus either struct fields or numeric tuple fields.
        // struct fields become this:
        //    { type: "VariantName", fieldName: fieldType, ... }
        // tuple tuple fields become this:
        //    { type: "VariantName", 0: T, 1: U, ... }
        // and unit fields become this: { type: "VariantName" }
        let mut variant_types = Vec::new();
        for variant_def in variants {
            let serde_attrs = crate::serde::serde_attrs_for_field_def(cg.tcx, variant_def.def_id);
            if serde_attrs.skip() {
                continue;
            }

            if serde_attrs.default() {
                cg.tcx.dcx().span_fatal(
                    cg.tcx.def_span(variant_def.def_id),
                    "default variant cannot be used in a union",
                );
            }

            let members = variant_def
                .fields
                .iter()
                .filter_map(|field_def| {
                    convert_field_def(
                        cg,
                        &cg.tcx.def_span(variant_def.def_id),
                        field_def,
                        Default::default(),
                        result_depth,
                    )
                })
                .map(|member| match member {
                    Either::Left(members) => Either::Left(members),
                    Either::Right((Some(name), type_ann)) => Either::Left(vec![
                        swc_utils::object_member_type_element()
                            .key(name)
                            .type_ann(type_ann)
                            .optional(false)
                            .build(),
                    ]),
                    Either::Right((None, type_ann)) => Either::Right(type_ann),
                });

            // then, finally, we homoginize the type elements into either all named, standard fields or a tuple
            // grouping
            let homogenized_type_elements = collect_homogeneous(members.into_iter())
                .unwrap_or_else(|_| {
                    cg.tcx.dcx().span_fatal(
                        cg.tcx.def_span(variant_def.def_id),
                        "cannot mix named and unnamed fields in a struct",
                    );
                });

            // either a struct or a tuple type derived from the variant fields
            let variant_fields_ty = match homogenized_type_elements {
                Either::Left(members) if members.is_empty() => None,
                Either::Right(members) if members.is_empty() => None,
                Either::Left(members) => Some(swc_utils::object_type_from_elements(
                    members.into_iter().flatten(),
                )),
                // if there's just one tuple variant, this type can be peeled back
                Either::Right(mut type_ann) if type_ann.len() == 1 => Some(type_ann.remove(0)),
                Either::Right(type_ann) => {
                    Some(swc_utils::tuple_type_from_elements(type_ann.into_iter()))
                }
            };

            let variant_name = variant_def.ident(cg.tcx);

            // the tag name specifies the name of the field that marks the variant.
            // if there is no tag, or the variant is untagged, then all of the cotent of the variant
            // will live in an object keyed under this tag name. if there is a tag, then the variant
            // name will be keyed under this tag and the content will be flattened into the type, _or_
            // it will also be keyed under its own object if there's a content name
            let tag_name = serde_attrs.tag_name();

            let mut variant_obj_type = vec![];
            if let Some(tag_name) = tag_name {
                // if we have a tag name, then we need to add the tag to the object
                // regardless of the content disposition
                variant_obj_type.push(
                    swc_utils::object_member_type_element()
                        .key(tag_name)
                        .type_ann(swc_utils::lit_str_type(variant_name.as_str()))
                        .optional(false)
                        .build(),
                );
            }

            if let Some(variant_fields_ty) = variant_fields_ty {
                let content_name = serde_attrs
                    .content_name()
                    .or(tag_name)
                    .unwrap_or(variant_name.as_str());
                variant_obj_type.push(
                    swc_utils::object_member_type_element()
                        .key(content_name)
                        .type_ann(variant_fields_ty)
                        .optional(false)
                        .build(),
                );
            }

            variant_types.push(swc_utils::object_type_from_elements(
                variant_obj_type.into_iter(),
            ));
        }
        (path, swc_utils::union_type(variant_types.into_iter()))
    }
}

enum CanonicalizeType {
    Vec,
    HashMap,
    Option,
}

/// Some types, such as vec, contain two generic arguments: the type of the elements and the allocator.
/// The allocator is irrelevant for ts codegen, so we can just drop it and move on
fn canonicalize_args<'tcx>(
    canonicalize_type: CanonicalizeType,
    args: &'tcx [GenericArg<'tcx>],
) -> Box<dyn Iterator<Item = &'tcx GenericArg<'tcx>> + 'tcx> {
    match canonicalize_type {
        CanonicalizeType::Vec => Box::new(args.iter().rev().skip(1)),
        CanonicalizeType::HashMap => {
            // std::collections::HashMap<K, V, S, A=Global> => keep K, V, skip S and A
            // S is the hasher which we may want to support eventually, but not at all
            // necessary right now
            Box::new(args.iter().take(2))
        }
        CanonicalizeType::Option => Box::new(args.iter()),
    }
}

/// `riptc` has to do something incredibly difficult, which is interop with serde.
/// For derives, this is easy because the behavior of derives with serde attributes are 100%
/// deterministic. When types have _custom derives_, this gets a lot more difficult because it's
/// impossible to know the actual resulting type from analysis alone, it's fundementally a runtime
/// concept. Eventually I want to add a way to have users add an attribute over types with custom impls
/// on serialize (and we can easily detect this by seeing that the trait is implemented, but the derive
/// is missing). Until then, there are some common "shapes" that types will have that we can target.
///
/// This method checks if an adt has exactly two fields and one of them is a phantom data. In this case,
/// we select the field that is not phantom and then use that as the target for serialization.
fn smells_like_a_newtype<'tcx>(
    tcx: TyCtxt<'tcx>,
    def: AdtDef,
    args: GenericArgsRef<'tcx>,
) -> Option<Ty<'tcx>> {
    let mut found_phantom = false;
    let mut found_inner = None;

    for (i, field_def) in def.all_fields().enumerate() {
        if i > 1 {
            return None;
        }

        let field_ty = field_def.ty(tcx, args);
        if field_ty.is_phantom_data() {
            found_phantom = true;
        } else {
            found_inner = Some(field_ty);
        }
    }

    (found_phantom && found_inner.is_some())
        .then_some(found_inner)
        .flatten()
}

fn config_ts_keyword_type_to_swc_ts_keyword_type(
    config_ts_keyword_type: ript_config::TsKeywordType,
) -> TsKeywordTypeKind {
    use ript_config::TsKeywordType as ConfigTsKeywordType;
    match config_ts_keyword_type {
        ConfigTsKeywordType::String => TsKeywordTypeKind::TsStringKeyword,
        ConfigTsKeywordType::Number => TsKeywordTypeKind::TsNumberKeyword,
    }
}

/// Consume an iterator of `Either<L, R>` and produce either all `L`s or all `R`s preventing a mixture
fn collect_homogeneous<I, L, R>(mut it: I) -> Result<Either<Vec<L>, Vec<R>>, ()>
where
    I: Iterator<Item = Either<L, R>>,
{
    match it.next() {
        None => Ok(Either::Left(vec![])),
        Some(Either::Left(first_l)) => {
            let mut lefts = vec![first_l];
            for e in it {
                match e {
                    Either::Left(l) => lefts.push(l),
                    Either::Right(_) => return Err(()),
                }
            }
            Ok(Either::Left(lefts))
        }
        Some(Either::Right(first_r)) => {
            let mut rights = vec![first_r];
            for e in it {
                match e {
                    Either::Right(r) => rights.push(r),
                    Either::Left(_) => return Err(()),
                }
            }
            Ok(Either::Right(rights))
        }
    }
}

pub fn entity_name_into_ts_type(entity_name: EntityName) -> TsType {
    fn entity_name_into_ts_entity_name(entity_name: EntityName) -> TsEntityName {
        match entity_name {
            EntityName::Ident(s) => TsEntityName::Ident(swc_utils::ident(s.as_str())),
            EntityName::QualifiedEntityName(q) => {
                let left = entity_name_into_ts_entity_name(q.left);
                let right = swc_utils::ident(q.right.as_str());
                TsEntityName::TsQualifiedName(Box::new(TsQualifiedName {
                    span: DUMMY_SP,
                    left,
                    right: right.into(),
                }))
            }
        }
    }

    swc_ecma_ast::TsTypeRef {
        span: DUMMY_SP,
        type_name: entity_name_into_ts_entity_name(entity_name),
        type_params: None,
    }
    .into()
}