riptc 0.1.2

Rust implementation of the InertiaJS protocol compatible with `riptc` for generating strong TypeScript bindings.
//! This module defines the logic for interpolating item paths
//! and turning them into a structured namespace that can be used
//! for emission

use std::collections::HashMap;

use itertools::Itertools;
use rustc_span::Symbol;

use rustc_hir::def_id::DefId;
use rustc_middle::ty::{AdtDef, TyCtxt};

use crate::callbacks::{ConfigStrExt, config};

/// Tree node for collecting namespace.
/// Each node can have many child namespaces and multiple items per namespace
#[derive(Debug)]
pub struct NamespaceNode<T> {
    pub children: HashMap<Symbol, NamespaceNode<T>>,
    pub items: Vec<(Symbol, T)>,
}

// cannot derive or else `T` must be `Default` which it won't be
impl<T> Default for NamespaceNode<T> {
    fn default() -> Self {
        Self {
            children: Default::default(),
            items: Default::default(),
        }
    }
}

#[bon::bon]
impl<T> NamespaceNode<T> {
    /// Track a new adt in the tree
    #[builder]
    pub fn insert(
        &mut self,
        root_path: NamespacedPath,
        item: T,
        #[builder(default, with = || true)] allow_duplicates: bool,
    ) {
        let mut segments = root_path.segments().to_vec();
        if config().omit_crate_name() && segments.len() > 1 {
            segments = segments.into_iter().skip(1).collect_vec();
        }

        match segments.split_last() {
            None => {
                // no-op
            }
            Some((last_segment, [])) => {
                // goes into the root
                // only insert if we don't already have this Symbol
                if allow_duplicates || !self.items.iter().any(|(sym, _)| sym == last_segment) {
                    self.items.push((*last_segment, item));
                }
            }
            Some((last_segment, prefix)) => {
                let mut node = self;
                for segment in prefix {
                    node = node.children.entry(*segment).or_default();
                }
                // only insert if there's no existing Symbol matching `last_segment`
                if allow_duplicates || !node.items.iter().any(|(sym, _)| sym == last_segment) {
                    node.items.push((*last_segment, item));
                }
            }
        }
    }
}

/// A path to a type that is suitable for TypeScript emission.
///
/// This is a best-effort to convert Rust path qualifications (i.e. crate::mod::Ident)
/// into the resulting typescript. Currently, these are just types wrapped with namespaces,
/// but eventually this should probably split out multiple files.
#[derive(Eq, PartialEq, Debug, Clone, Hash)]
pub struct NamespacedPath(Vec<Symbol>);

impl NamespacedPath {
    pub fn from_mod_syntax(s: &str) -> Self {
        let segments = s.split("::").map(Symbol::intern).collect::<Vec<_>>();

        Self(segments)
    }

    pub fn with_extra_segment(mut self, extra: impl Into<Symbol>) -> Self {
        self.0.push(extra.into());
        self
    }

    /// Returns the name of the item at the end of the path
    pub fn item_name(&self) -> &Symbol {
        self.0
            .last()
            .unwrap_or_else(|| panic!("NamespacedPath is empty: {self:?}"))
    }

    #[allow(dead_code)]
    pub fn with_different_item_name(mut self, alt: impl Into<Symbol>) -> Self {
        self.0.pop();
        self.0.push(alt.into());
        self
    }

    /// Create a new `TypePath` for an adt
    pub fn new_for_adt(tcx: TyCtxt<'_>, adt: &AdtDef<'_>) -> Self {
        Self::new(tcx, adt.did())
    }

    /// Create a new `TypePath` for a def id
    pub fn new(tcx: TyCtxt<'_>, did: DefId) -> Self {
        let path = tcx.def_path(did);
        let crate_name = tcx.crate_name(path.krate);
        let mut symbols = vec![Symbol::intern(crate_name.as_str())];

        symbols.extend(
            path.data
                .iter()
                .map(|d| Symbol::intern(d.to_string().as_str())),
        );

        Self(symbols)
    }

    /// Path of an std string
    pub fn std_string() -> Self {
        Self(vec![
            Symbol::intern("alloc"),
            Symbol::intern("string"),
            Symbol::intern("String"),
        ])
    }

    /// Path of an std vec
    pub fn std_vec() -> Self {
        Self(vec![
            Symbol::intern("alloc"),
            Symbol::intern("vec"),
            Symbol::intern("Vec"),
        ])
    }

    pub fn hashbrown_hashmap() -> Self {
        Self(vec![
            Symbol::intern("hashbrown"),
            Symbol::intern("map"),
            Symbol::intern("HashMap"),
        ])
    }

    /// Path of an std hashmap
    pub fn std_hashmap() -> Self {
        Self(vec![
            Symbol::intern("std"),
            Symbol::intern("collections"),
            Symbol::intern("hash"),
            Symbol::intern("map"),
            Symbol::intern("HashMap"),
        ])
    }

    /// Path of an std option
    pub fn std_option() -> Self {
        Self(vec![
            Symbol::intern("core"),
            Symbol::intern("option"),
            Symbol::intern("Option"),
        ])
    }

    pub fn result() -> Self {
        Self(vec![
            Symbol::intern("core"),
            Symbol::intern("result"),
            Symbol::intern("Result"),
        ])
    }

    pub fn chrono_naive_date_time() -> Self {
        Self(vec![
            Symbol::intern("chrono"),
            Symbol::intern("naive"),
            Symbol::intern("datetime"),
            Symbol::intern("NaiveDateTime"),
        ])
    }

    pub fn chrono_naive_date() -> Self {
        Self(vec![
            Symbol::intern("chrono"),
            Symbol::intern("naive"),
            Symbol::intern("date"),
            Symbol::intern("NaiveDate"),
        ])
    }

    pub fn chrono_naive_time() -> Self {
        Self(vec![
            Symbol::intern("chrono"),
            Symbol::intern("naive"),
            Symbol::intern("time"),
            Symbol::intern("NaiveTime"),
        ])
    }

    /// `chrono_tz` has a gigantic enum full of all timezones that anyone using chrono will likely have as a dep,
    /// and it may get picked up during out output. Let's just omit it for now. We should probably extend the config
    /// to include a set of skipped namespaces
    pub fn chrono_tz_timezones() -> Self {
        Self(vec![
            Symbol::intern("chrono_tz"),
            Symbol::intern("timezones"),
            Symbol::intern("Tz"),
        ])
    }

    pub fn uuid() -> Self {
        Self(vec![Symbol::intern("uuid"), Symbol::intern("Uuid")])
    }

    pub fn segments(&self) -> &[Symbol] {
        &self.0
    }

    // TODO(@lazkindness): make this better
    /// Returns true if the type path is not one of the standard library types.
    pub fn is_user_defined(&self) -> bool {
        !(self == &Self::std_string()
            || self == &Self::std_vec()
            || self == &Self::hashbrown_hashmap()
            || self == &Self::std_hashmap()
            || self == &Self::std_option()
            || self == &Self::chrono_naive_date_time()
            || self == &Self::chrono_naive_date()
            || self == &Self::chrono_naive_time()
            || self == &Self::uuid())
            || self == &Self::result()
    }
}

#[derive(Clone, Debug)]
pub enum EntityName {
    Ident(Symbol),
    QualifiedEntityName(Box<QualifiedEntityName>),
}

#[derive(Clone, Debug)]
pub struct QualifiedEntityName {
    pub left: EntityName,
    pub right: Symbol,
}

impl From<NamespacedPath> for EntityName {
    fn from(path: NamespacedPath) -> EntityName {
        fn build_entity_name(segments: &[Symbol]) -> EntityName {
            match segments {
                [] => EntityName::Ident(Symbol::intern("")),
                [single] => EntityName::Ident(*single),
                _ => {
                    let (prefix, last) = segments.split_at(segments.len() - 1);
                    let prefix_maybe_camel = prefix
                        .iter()
                        .map(|s| Symbol::intern(s.as_str().namespace_name().as_ref()))
                        .collect::<Vec<_>>();

                    EntityName::QualifiedEntityName(Box::new(QualifiedEntityName {
                        left: build_entity_name(&prefix_maybe_camel),
                        right: Symbol::intern(last[0].as_str()),
                    }))
                }
            }
        }

        let root_path = if config().omit_crate_name() {
            path.0[1..].to_vec()
        } else {
            path.0
        };

        build_entity_name(&root_path)
    }
}

impl Iterator for NamespacedPath {
    type Item = Symbol;

    fn next(&mut self) -> Option<Self::Item> {
        self.0.pop()
    }
}

impl DoubleEndedIterator for NamespacedPath {
    fn next_back(&mut self) -> Option<Self::Item> {
        if self.0.is_empty() {
            return None;
        }
        Some(self.0.remove(0))
    }
}