riptc 0.1.7

Rust implementation of the InertiaJS protocol compatible with `riptc` for generating strong TypeScript bindings.
//! This module defines all of the common structures we want to pull out of the user's code
//! and their corresponding access methods. Basically, every type in here represents some "unit"
//! in axum or inertia that we care about and wish to track for code emission.

use bon::Builder;
use matchit::InsertError;
use rustc_errors::DiagCtxtHandle;
use rustc_hir::{HirId, def_id::DefId};
use rustc_middle::ty::Ty as MTy;
use rustc_span::{Span, Symbol};

#[derive(Debug, Builder, Clone, Copy)]
pub struct InertiaProp<'tcx> {
    /// Since inertia props are defined in a call chain, we need to track the chain as a whole.
    /// This contains the id of the thing that the `.prop` method was called on, which will be either
    /// another `.prop` or a receiver
    pub(super) rx_id: HirId,
    /// The ID of either an axum route or middleware handler that this prop was defined in
    pub(super) axum_handler: DefId,
    pub(super) id: HirId,
    /// Lit str of the prop name
    name: Symbol,
    /// The span where the prop is defined in the route handler, containing the _entire_ `.prop` method call
    span: Span,
    /// The type of the prop after full resolution
    ty: MTy<'tcx>,
    /// True if this prop is a flash prop
    flash_prop: bool,
    /// True if this prop is lazy, indicating that its final type must be nullable
    lazy: bool,
    /// True if this prop is deferred, indicating that its final type must be nullable
    deferred: bool,
}

impl<'tcx> InertiaProp<'tcx> {
    pub fn name(&self) -> &str {
        self.name.as_str()
    }

    pub fn id(&self) -> HirId {
        self.id
    }

    pub fn lazy(&self) -> bool {
        self.lazy
    }

    pub fn deferred(&self) -> bool {
        self.deferred
    }

    pub fn flash_prop(&self) -> bool {
        self.flash_prop
    }

    pub fn span(&self) -> Span {
        self.span
    }

    /// Emits a fatal error indicating that the prop chain has been broken and the analysis cannot be done statically
    pub(super) fn emit_fatal_chain_broken(self, dcx: DiagCtxtHandle) -> ! {
        dcx.span_fatal(
            self.span,
            "prop chain broken apart so analysis cannot be done confidently",
        )
    }

    pub(super) fn emit_err_diverged(self, dcx: DiagCtxtHandle, component: &str) {
        let name = self.name();
        dcx.struct_span_err(
            self.span,
            format!("prop `{name}` in `{component}` is not consistent across all branches"),
        )
        .emit();
    }

    pub fn ty(&self) -> MTy<'tcx> {
        self.ty
    }
}

/// A route in axum. These are collected by their definition in a router via the `.route` method.
/// This contains the method + path of the route, as well as extractors we've analyzed for json bodies,
/// query params, and the path type
#[derive(Debug, Clone, Builder)]
pub struct AxumRoute<'tcx> {
    pub(super) id: DefId,
    http_method: HttpMethod,
    route_path: Symbol,
    /// span of the method call that we derived this route handler from, not the function itself
    span: rustc_span::Span,

    /// The type of the path which corresponds to the parsed matchit route.
    /// For instance, /user/{id} with a path extractor of i32 would contain `i32` here.
    /// There can only be one single path extractor, so the `Vec` defined here is actually an unraveling
    /// of a tuple input for the path
    path_tys: Vec<MTy<'tcx>>,

    /// The JSON body type defined via the `Json` extractor
    json_body_ty: Option<MTy<'tcx>>,

    /// The query param type defined via the `Query extractor`
    query_ty: Option<MTy<'tcx>>,

    inertia: bool,
}

impl<'tcx> AxumRoute<'tcx> {
    pub fn inertia(&self) -> bool {
        self.inertia
    }

    pub fn id(&self) -> DefId {
        self.id
    }

    pub fn route_path_symbol(&self) -> Symbol {
        self.route_path
    }

    pub fn path_tys(&self) -> impl Iterator<Item = MTy<'tcx>> {
        self.path_tys.iter().copied()
    }

    // pub fn emit_warn_prop_type_name_collision(&self, dcx: DiagCtxtHandle) {
    //     dcx.struct_span_warn(
    //         self.span,
    //         format!("prop type name has a collision, consider renaming your route"),
    //     )
    //     .emit();
    // }

    pub(super) fn route_path<'a>(
        &'a self,
        ra: &super::RiptAnalyzer<'tcx>,
    ) -> impl Iterator<Item = AxumRoutePathSegment<'tcx>> + 'tcx {
        let mut matchit_router = ra.matchit_router.write();

        let path_str = self.route_path.as_str();
        if path_str.contains("*") {
            unimplemented!("wildcard routes are not currently supported");
        }

        match matchit_router.insert(path_str, ()) {
            Ok(_) => {}
            // TODO(@lazkindness): we could probably just emit these as errors and return an empty iter
            Err(InsertError::Conflict { .. })
                if ra
                    .axum_routes
                    .values()
                    .any(|r| r.http_method == self.http_method) =>
            {
                ra.tcx.dcx().span_warn(self.span, "duplicate route path")
            }
            Err(e) => ra
                .tcx
                .dcx()
                .span_fatal(self.span, format!("unable to parse route path: {e}")),
        }

        let extracted_match = matchit_router
            .at(path_str)
            .expect("always valid as we inserted it right above");

        if extracted_match.params.is_empty() && !self.path_tys.is_empty() {
            // TODO(@lazkindness): we could probably just emit these as errors and return an empty iter
            ra.tcx.dcx().span_fatal(
                self.span,
                "path definition has no parameters, but there is a path extractor on the route",
            )
        }

        let mut segments = vec![];
        let mut current_part_head = path_str;

        for (i, (param_name, param_def_slice)) in extracted_match.params.iter().enumerate() {
            // SAFETY: this split will always be valid because the insertion would fail if it wasn't
            let (this_head, this_tail) =
                unsafe { Option::unwrap_unchecked(current_part_head.split_once(param_def_slice)) };

            let ty = match self.path_tys.get(i) {
                Some(ty) => ty,
                None => {
                    ra.tcx.dcx().span_warn(
                        self.span,
                        format!("path param `{param_name}` does not have a type"),
                    );

                    continue;
                }
            };

            segments.push(AxumRoutePathSegment::Static(Symbol::intern(this_head)));
            segments.push(AxumRoutePathSegment::Dynamic(
                Symbol::intern(param_name),
                *ty,
            ));
            current_part_head = this_tail;
        }

        segments.push(AxumRoutePathSegment::Static(Symbol::intern(
            current_part_head,
        )));

        segments.into_iter()
    }

    pub fn span(&self) -> Span {
        self.span
    }

    // TODO(@lazkindness): support multiple data tys
    /// Returns the data ty of this route, enforcing that only one (either query or json) are present and not both.
    pub fn data_ty(&self, ra: &super::RiptAnalyzer<'tcx>) -> Option<MTy<'tcx>> {
        match (self.query_ty, self.json_body_ty) {
            (Some(_qt), Some(_jt)) => {
                ra.tcx.dcx().span_err(
                    self.span,
                    "cannot have both query and json extractors at the same time",
                );
                None
            }
            (Some(t), None) | (None, Some(t)) => Some(t),
            (None, None) => None,
        }
    }

    pub fn http_method(&self) -> HttpMethod {
        self.http_method
    }
}

#[derive(Clone, Copy, Debug)]
pub enum AxumRoutePathSegment<'tcx> {
    /// A static segment, such as `/user`
    Static(Symbol),
    /// A dynamic segment, such as `{id}`
    Dynamic(Symbol, MTy<'tcx>),
}

/// An HTTP method that comes from an `axum::routing` function call
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
pub enum HttpMethod {
    Get,
    Post,
    Put,
    Patch,
    Delete,
}

impl HttpMethod {
    pub fn static_str(&self) -> &'static str {
        match self {
            HttpMethod::Get => "get",
            HttpMethod::Post => "post",
            HttpMethod::Put => "put",
            HttpMethod::Patch => "patch",
            HttpMethod::Delete => "delete",
        }
    }
}

/// The destination of a prop after the route resolves
#[derive(Debug, Clone, Copy)]
pub enum InertiaPropDestination {
    /// The prop is being rendered to a page, indicated by the name of the component
    Render(Symbol),
    /// The prop is being flashed as part of a call to `.back`, and as such should effectively
    /// become a share prop with the caveat that it cannot be partially reloaded
    Back,
    /// This prop is a share prop which is not flashed, nor is it part of a particular page.
    /// This prop simply belongs to the global and can be partially reloaded.
    Share,
}

#[derive(Debug, Clone, Copy, Builder)]
pub struct BridgeMarkedItem<'tcx> {
    pub(super) id: DefId,
    pub(super) span: Span,
    pub(super) kind: BridgeMarkedItemKind<'tcx>,
}

impl<'tcx> BridgeMarkedItem<'tcx> {
    pub fn kind(&self) -> &BridgeMarkedItemKind<'tcx> {
        &self.kind
    }

    pub fn span(&self) -> Span {
        self.span
    }
}

#[derive(Debug, Clone, Copy)]
pub enum BridgeMarkedItemKind<'tcx> {
    Ty(MTy<'tcx>),
    // Const(ConstValue<'tcx>),
}