riptc 0.1.7

Rust implementation of the InertiaJS protocol compatible with `riptc` for generating strong TypeScript bindings.
use itertools::Itertools;
use rustc_hir::{HirId, Impl, ItemKind, def_id::DefId};

use super::{
    BridgeMarkedItem, BridgeMarkedItemKind, ExprAnalysisExt, MTyAnalysisExt,
    structures::{self, AxumRoute, HttpMethod, InertiaProp, InertiaPropDestination},
};
use rustc_middle::ty::{Ty as MTy, TyKind as MTyKind};

pub fn try_bridge_marked_item<'tcx>(
    ra: &super::RiptAnalyzer<'tcx>,
    item: &rustc_hir::Item<'tcx>,
) -> Option<structures::BridgeMarkedItem<'tcx>> {
    match item.kind {
        ItemKind::Impl(
            ipl @ Impl {
                of_trait: Some(trait_ref),
                ..
            },
        ) => {
            let path_res = trait_ref.path.res;
            let dp = ra.tcx.def_path(path_res.def_id());
            ra.def_path_and(&dp, "riptc_bridge::Riptc", Some(()))?;

            let ty = ipl.self_ty;
            let span = ty.span;

            let ty = ra.tcx.type_of(ty.hir_id.owner);

            Some(
                BridgeMarkedItem::builder()
                    .id(ipl.self_ty.hir_id.owner.to_def_id())
                    .kind(BridgeMarkedItemKind::Ty(ty.skip_binder()))
                    .span(span)
                    .build(),
            )
        }
        _ => None,
    }
}

/// This tries to find definitions of share middleware, which is basically and function
/// that is defined with `route_layer` + `map_response`. We don't really care about the contents
/// of the middleware since we can't do anything useful with it like extracting body types as
/// that happens in routes, but we do want to know about it so it can act as a valid container
/// id for prop definitions.
pub fn try_axum_share_middleware_registration<'tcx>(
    ra: &super::RiptAnalyzer<'tcx>,
    ex: rustc_hir::Expr<'tcx>,
) -> Option<DefId> {
    let (_rx, mut args, _span) = ra.try_method_call(ex)?;
    ra.try_assert_type_dependent_def_path(ex.hir_id, "axum::routing::{impl#3}::route_layer")?;

    let map_response_expr = args.next()?;
    let (dp, middleware_arg) = map_response_expr.extract_call_with_single_arg(ra)?;

    let _ = ra.def_path_and(&dp, "axum::middleware::map_response::map_response", || ())?;
    let middleware_id = middleware_arg.extract_qpath_did(ra)?;

    Some(middleware_id)
}

/// Try to resolve a full axum route from a method call to `.route`.
/// This looks first for the route string itself, then seeks inward for the method on `axum::routing`,
/// then seeks inward once more for the def id of the axum route after which we can resolve the sig.
pub fn try_axum_route_from_route_method_call<'tcx>(
    ra: &super::RiptAnalyzer<'tcx>,
    ex: rustc_hir::Expr<'tcx>,
) -> Option<structures::AxumRoute<'tcx>> {
    let (_rx, mut args, span) = ra.try_method_call(ex)?;
    ra.try_assert_type_dependent_def_path(ex.hir_id, "axum::routing::{impl#3}::route")?;

    // first arg to an axum `.route` must be a lit str of the path, this effectively prevents non-static strs
    // from being tracked
    let route_path = args.next()?.extract_lit_str()?;

    // the next arg is going to be some function on `axum::routing::<method>` that will inform us of:
    // 1. the method itself
    // 2. contain a function as its only argument that we can derive the route handler itself from
    let (http_method_path, http_method_arg) = args.next()?.extract_call_with_single_arg(ra)?;
    let http_method = ra
        .def_path_and(
            &http_method_path,
            "axum::routing::method_routing::get",
            HttpMethod::Get,
        )
        .or(ra.def_path_and(
            &http_method_path,
            "axum::routing::method_routing::post",
            HttpMethod::Post,
        ))
        .or(ra.def_path_and(
            &http_method_path,
            "axum::routing::method_routing::put",
            HttpMethod::Put,
        ))
        .or(ra.def_path_and(
            &http_method_path,
            "axum::routing::method_routing::patch",
            HttpMethod::Patch,
        ))
        .or(ra.def_path_and(
            &http_method_path,
            "axum::routing::method_routing::delete",
            HttpMethod::Delete,
        ))?;

    let route_handler_path = http_method_arg.extract_qpath_did(ra)?;
    let route_handler_params = ra.fn_params(route_handler_path).collect_vec();

    let is_inertia_route = route_handler_params
        .iter()
        .filter_map(|p| p.extract_adt())
        .any(|(adt, _)| {
            ra.get_def_path_and(adt.did(), "ript::inertia::Inertia", || Some(true))
                .unwrap_or(false)
        });

    let path_tys = route_handler_params
        .iter()
        .filter_map(|p| p.extract_adt())
        .filter_map(|(adt, mut genargs)| {
            ra.get_def_path_and(adt.did(), "axum::extract::path::Path", || {
                Some(genargs.next()?.expect_ty().peel_tuple())
            })
        })
        .flatten()
        .collect_vec();

    let json_body_ty = route_handler_params
        .iter()
        .filter_map(|p| p.extract_adt())
        .find_map(|(adt, mut genargs)| {
            ra.get_def_path_and(adt.did(), "axum::json::Json", || {
                genargs.next().map(|a| a.expect_ty())
            })
        });

    let query_ty = route_handler_params
        .iter()
        .filter_map(|p| p.extract_adt())
        .find_map(|(adt, mut genargs)| {
            ra.get_def_path_and(adt.did(), "axum::extract::query::Query", || {
                genargs.next().map(|a| a.expect_ty())
            })
            .or_else(|| {
                ra.get_def_path_and(adt.did(), "axum_extra::extract::query::Query", || {
                    genargs.next().map(|a| a.expect_ty())
                })
            })
        });

    Some(
        AxumRoute::builder()
            .id(route_handler_path)
            .http_method(http_method)
            .span(span)
            .route_path(route_path)
            .path_tys(path_tys)
            .maybe_json_body_ty(json_body_ty)
            .maybe_query_ty(query_ty)
            .inertia(is_inertia_route)
            .build(),
    )
}

/// Try to extract an inertia prop from a chain definition
pub fn try_inertia_prop_from_chain<'tcx>(
    ra: &super::RiptAnalyzer<'tcx>,
    ex: rustc_hir::Expr<'tcx>,
) -> Option<structures::InertiaProp<'tcx>> {
    let (rx, mut args, span) = ra.try_method_call(ex)?;
    ra.try_assert_type_dependent_def_path(ex.hir_id, "ript::inertia::{impl#4}::prop")?;

    let prop_name = args.next()?.extract_lit_str()?;
    let prop_value_closure = args.next()?;

    let lazy = prop_value_closure
        .extract_call_with_single_arg(ra)
        .and_then(|(path, _call)| ra.def_path_and(&path, "ript::prop::lazy", true))
        .unwrap_or(false);

    let deferred = prop_value_closure
        .extract_call_with_single_arg(ra)
        .and_then(|(path, _call)| ra.def_path_and(&path, "ript::prop::defer", true))
        .unwrap_or(false);

    let prop_value_closure_ty = ra.typeof_expr(prop_value_closure);

    let ty = resolve_final_prop_ty(ra, prop_value_closure_ty);

    Some(
        InertiaProp::builder()
            .ty(ty)
            .span(span)
            .axum_handler(ex.hir_id.owner.to_def_id())
            .name(prop_name)
            .rx_id(rx.hir_id)
            .id(ex.hir_id)
            .lazy(lazy)
            .deferred(deferred)
            .flash_prop(false)
            .build(),
    )
}

pub fn try_inertia_flash_prop_from_chain<'tcx>(
    ra: &super::RiptAnalyzer<'tcx>,
    ex: rustc_hir::Expr<'tcx>,
) -> Option<structures::InertiaProp<'tcx>> {
    let (rx, mut args, span) = ra.try_method_call(ex)?;
    ra.try_assert_type_dependent_def_path(ex.hir_id, "ript::inertia::{impl#4}::flash_prop")?;

    let prop_name = args.next()?.extract_lit_str()?;
    let prop_value = args.next()?;

    let ty = ra.typeof_expr(prop_value);

    Some(
        InertiaProp::builder()
            .ty(ty)
            .span(span)
            .axum_handler(ex.hir_id.owner.to_def_id())
            .name(prop_name)
            .rx_id(rx.hir_id)
            .id(ex.hir_id)
            .flash_prop(true)
            .lazy(false)
            .deferred(false)
            .build(),
    )
}

/// Props in a chain are defined like such:
/// ```rs
/// .prop("propName", maybeAttrOrMaybeNot(async || { }))
/// ```
///
/// This function looks at the second argument, peels back the attr
/// and then inspects the async closure to find its return type and figure
/// out what the actual type of the prop is.
fn resolve_final_prop_ty<'tcx>(
    ra: &super::RiptAnalyzer<'tcx>,
    // second arg, first is the name of the prop itself
    prop_second_arg: MTy<'tcx>,
) -> MTy<'tcx> {
    // here, we need to be resilient to the type being either a closure directly,
    // or a closure wrapped in something but either way we want to just get the inner
    // closure.
    //
    // we return the genargs in all branches because at the end of the day an async closure
    // just becomes desugared to an internal struct with generics that specify its params,
    // return type, captures, etc
    let closure_genargs = match prop_second_arg.kind() {
        // in `ript`, the prop type is `Prop<F>` where `F` will always be the async closure
        // so if its an adt, we can just pull out the generics which is what a closure lowers to
        MTyKind::Adt(_adt_def, genargs) => match genargs.type_at(0).kind() {
            MTyKind::CoroutineClosure(_def_id, genargs) => genargs,
            _ => unreachable!("code would not compile prior to this point"),
        },
        MTyKind::CoroutineClosure(_def_id, genargs) => genargs,
        _ => unreachable!("code would not compile prior to this point"),
    };

    // the second generic argument of the corouting is the binder of the future.
    // internally, this is basically a tuple that specifies the shape of the future, and the third element
    // is the output type which is all that we actually care about; we just need to know the final type
    // of the expression.
    let closure_return_ty_future = closure_genargs
        .type_at(1)
        .fn_sig(ra.tcx)
        .output()
        .skip_binder();
    let mut closure_return_ty_future_walker = closure_return_ty_future.walk();
    let closure_return_ty_future_output_ty = closure_return_ty_future_walker
        .nth(2)
        .map(|output| output.expect_ty())
        .expect("code would not compile prior to this point");

    closure_return_ty_future_output_ty
}

/// From a chain definer like `.render` or `.share`, try and resolve that into a prop destination.
/// Prop destinations are very important as they delegate where a prop should go; as props have different
/// behavior depending on if they're local to a page, global + reloadable, or global + only readable
pub fn try_inertia_prop_dest_chain_definer<'tcx>(
    ra: &super::RiptAnalyzer<'tcx>,
    ex: rustc_hir::Expr<'tcx>,
) -> Option<(HirId, structures::InertiaPropDestination)> {
    // TODO(@lazkindness): track span here
    let (ex, _args, _span) = ra.try_method_call(ex)?;

    ra.try_assert_type_dependent_def_path(ex.hir_id, "ript::inertia::{impl#1}::back")
        .map(|_| InertiaPropDestination::Back)
        .or(ra
            .try_assert_type_dependent_def_path(ex.hir_id, "ript::inertia::{impl#1}::share")
            .map(|_| InertiaPropDestination::Share))
        .or(ra
            .try_assert_type_dependent_def_path(ex.hir_id, "ript::inertia::{impl#1}::render")
            .and_then(|_| {
                let (_ex, mut args, _span) = ra.try_method_call(ex)?;
                Some(InertiaPropDestination::Render({
                    args.next()?.extract_lit_str()?
                }))
            }))
        .map(|dest| (ex.hir_id, dest))
}