riptc 0.1.2

Rust implementation of the InertiaJS protocol compatible with `riptc` for generating strong TypeScript bindings.
use std::{borrow::Cow, str::FromStr, sync::atomic::AtomicPtr};

use heck::ToLowerCamelCase;
use itertools::Itertools;
use ript_config::RiptDotToml;
use rustc_driver::{Callbacks, Compilation};
use rustc_errors::DiagCtxtHandle;
use rustc_span::source_map::SourceMap;

use crate::{
    analyzer::RiptAnalyzer,
    codegen::{RiptCodegen, new_swc_emitter},
    namespace::NamespacedPath,
    swc_utils,
};

/// When compiling a crate that's not the primary package, we don't need any of our special callbacks so
/// lets optimize a bit and use no callbacks at all
pub struct DefaultCallbacks;

impl Callbacks for DefaultCallbacks {}

/// The root callbacks of kind inertia ts that drive everything.
/// Here, we hook into the post analysis phase and then analyze all items and output the ts we need
pub struct KindInertiaTsCallbacks;

impl Callbacks for KindInertiaTsCallbacks {
    fn after_analysis(
        &mut self,
        compiler: &rustc_interface::interface::Compiler,
        tcx: rustc_middle::ty::TyCtxt<'_>,
    ) -> rustc_driver::Compilation {
        let config_ = load_config_files(tcx.dcx(), compiler.sess.source_map());

        // store the config in a static so that we can access it later
        CONFIG.store(
            Box::into_raw(Box::new(config_)),
            std::sync::atomic::Ordering::SeqCst,
        );

        let mut ra = RiptAnalyzer::new(tcx);
        tcx.hir_walk_toplevel_module(&mut ra);

        let mut w = match config().outfile_writer() {
            Ok(w) => w,
            Err(e) => {
                tcx.dcx().err(format!("failed to open ts writer: {e}"));
                return Compilation::Stop;
            }
        };

        let comments = swc_utils::get_comments();

        let swc_emitter = match new_swc_emitter().w(&mut w).comments(&comments).call() {
            Ok(w) => w,
            Err(e) => {
                tcx.dcx().err(format!("failed to create swc emitter: {e}"));
                return Compilation::Stop;
            }
        };

        let cg = RiptCodegen::builder()
            .analyzer(ra)
            .tcx(tcx)
            .swc_emitter(swc_emitter)
            .build();

        if let Err(e) = cg.emit_all() {
            tcx.dcx().err(format!("failed to emit ts: {e}"));
        };

        Compilation::Stop
    }
}
/// Loads the config and all its referenced files into the source map, returning the prepared config.
/// Loading through the source map in this way is very advantageous to us because we can trigger rebuilds
/// when the config file changes.
fn load_config_files(dcx: DiagCtxtHandle<'_>, source_map: &SourceMap) -> RiptDotToml {
    let config_path = match RiptDotToml::provision() {
        Ok(p) => p,
        Err(e) => {
            dcx.fatal(format!("failed to provision config file: {e}"));
        }
    };

    let f = match source_map.load_file(&config_path) {
        Ok(f) => f,
        Err(e) => {
            dcx.fatal(format!("failed to load config file: {e}"));
        }
    };

    match RiptDotToml::from_str(f.src.as_deref().map(|f| f.as_str()).unwrap_or_default()) {
        Ok(c) => c,
        Err(e) => {
            dcx.fatal(format!("failed to parse config file: {e}"));
        }
    }
}

static CONFIG: AtomicPtr<RiptDotToml> = AtomicPtr::new(std::ptr::null_mut());

/// Returns the current config
pub fn config() -> &'static RiptDotToml {
    unsafe { &*CONFIG.load(std::sync::atomic::Ordering::SeqCst) }
}

// Extension on the common types that derive parameters from the config

pub trait ConfigStrExt {
    /// Returns the namespace name, camelizing it according to the config's rules
    fn namespace_name(&self) -> Cow<'_, str>;
}

impl ConfigStrExt for str {
    fn namespace_name(&self) -> Cow<'_, str> {
        if config().camelize_namespaces() {
            Cow::Owned(self.to_lower_camel_case())
        } else {
            Cow::Borrowed(self)
        }
    }
}

pub trait ConfigNamespacedPathExt {
    /// Given a namespaced path, see if there is an override for it.
    /// Overrides are user-defined types that are defined in the `ript.toml` that
    /// indicate a hard coded value for a type which we may not get right, such
    /// as types with a custom `Serialize` / `Deserialize` impl that we can't evaluate
    /// determinstically
    fn override_for_namespaced_path(&self) -> Option<ript_config::TsKeywordType>;
}

impl ConfigNamespacedPathExt for NamespacedPath {
    fn override_for_namespaced_path(&self) -> Option<ript_config::TsKeywordType> {
        let overrides = config().overrides();
        let self_as_key = self.clone().rev().join("::");
        overrides.get(&self_as_key).copied()
    }
}