mod analysis;
mod structures;
pub use structures::*;
use std::collections::{HashMap, HashSet};
use either::Either;
use itertools::Itertools;
use rustc_ast::LitKind;
use rustc_hir::{
self, Expr, ExprKind, HirId,
def_id::DefId,
definitions::DefPath,
intravisit::{Visitor, nested_filter::NestedFilter},
};
use rustc_middle::ty::{AdtDef, GenericArg, Ty as MTy, TyCtxt, TyKind as MTyKind};
use rustc_span::{Span, Symbol, source_map::Spanned};
use crate::namespace::NamespacedPath;
pub struct RiptAnalyzer<'tcx> {
tcx: TyCtxt<'tcx>,
matchit_router: parking_lot::RwLock<matchit::Router<()>>,
axum_routes: HashMap<DefId, structures::AxumRoute<'tcx>>,
axum_map_response_fns: HashSet<DefId>,
bridge_marked_items: HashMap<DefId, BridgeMarkedItem<'tcx>>,
inertia_prop_destinations: HashMap<HirId, Either<HirId, structures::InertiaPropDestination>>,
inertia_props: HashMap<HirId, structures::InertiaProp<'tcx>>,
}
impl<'tcx> RiptAnalyzer<'tcx> {
pub fn new(tcx: TyCtxt<'tcx>) -> Self {
Self {
tcx,
matchit_router: parking_lot::RwLock::new(matchit::Router::new()),
axum_routes: Default::default(),
axum_map_response_fns: Default::default(),
inertia_prop_destinations: Default::default(),
inertia_props: Default::default(),
bridge_marked_items: Default::default(),
}
}
pub fn inertia_share_props(&self) -> impl Iterator<Item = &structures::InertiaProp<'tcx>> {
self.inertia_props.iter().filter_map(|(id, p)| {
let dest = self.inertia_prop_destination(*id);
matches!(dest, InertiaPropDestination::Share).then(|| p)
})
}
pub fn namespaced_path(&self, id: DefId) -> NamespacedPath {
NamespacedPath::new(self.tcx, id)
}
pub fn inertia_flash_props(&self) -> impl Iterator<Item = &structures::InertiaProp<'tcx>> {
self.inertia_props.values().filter(|p| p.flash_prop())
}
fn try_method_call(
&self,
ex: Expr<'tcx>,
) -> Option<(Expr<'tcx>, impl Iterator<Item = Expr<'tcx>>, Span)> {
match ex.kind {
ExprKind::MethodCall(_path_seg, rx, args, span) => {
Some((*rx, args.iter().copied(), span))
}
_ => None,
}
}
fn try_assert_type_dependent_def_path(&self, hir_id: HirId, expecting: &str) -> Option<()> {
let dp = self
.tcx
.typeck(hir_id.owner)
.type_dependent_def_id(hir_id)?;
let dp = self.tcx.def_path(dp);
(self.fmt_def_path(&dp) == expecting).then_some(())
}
fn def_path_and<T>(&self, dp: &DefPath, expecting: &str, and: T) -> Option<T> {
(self.fmt_def_path(dp) == expecting).then_some(and)
}
fn get_def_path_and<T>(
&self,
id: DefId,
expecting: &str,
and: impl FnOnce() -> Option<T>,
) -> Option<T> {
let dp = self.tcx.def_path(id);
(self.def_path_and(&dp, expecting, and)?)()
}
fn fmt_def_path(&self, dp: &DefPath) -> String {
let dp = format!(
"{}{}",
self.tcx.crate_name(dp.krate),
dp.to_string_no_crate_verbose()
);
dp
}
pub fn bridge_marked_items(&self) -> impl Iterator<Item = &structures::BridgeMarkedItem<'tcx>> {
self.bridge_marked_items.values()
}
pub fn axum_routes(&self) -> impl Iterator<Item = &structures::AxumRoute<'tcx>> {
self.axum_routes.values().filter(|r| !r.inertia())
}
pub fn inertia_axum_routes(&self) -> impl Iterator<Item = &structures::AxumRoute<'tcx>> {
self.axum_routes.values().filter(|r| r.inertia())
}
pub fn axum_route_path(&self, id: DefId) -> Vec<AxumRoutePathSegment<'tcx>> {
self.axum_routes
.get(&id)
.expect("expected axum route")
.route_path(self)
.collect_vec()
}
pub fn component_for_inertia_prop(&self, id: HirId) -> Option<Symbol> {
let prop = self
.inertia_props
.get(&id)
.expect("inertia prop not found in prop destinations");
let dest = self.inertia_prop_destination(prop.id);
match dest {
InertiaPropDestination::Render(component) => Some(component),
_ => None,
}
}
#[track_caller]
fn inertia_prop_destination(&self, id: HirId) -> structures::InertiaPropDestination {
let prop = self
.inertia_props
.get(&id)
.expect("inertia prop not found in prop destinations");
let mut current = prop.rx_id;
while let Some(Either::Left(rx_id)) = self.inertia_prop_destinations.get(¤t) {
current = *rx_id;
}
let Some(Either::Right(destination)) = self.inertia_prop_destinations.get(¤t) else {
prop.emit_fatal_chain_broken(self.tcx.dcx())
};
*destination
}
fn fn_params(&self, id: DefId) -> impl Iterator<Item = &MTy<'tcx>> {
self.tcx
.fn_sig(id)
.skip_binder()
.inputs()
.iter()
.map(|i| i.skip_binder())
}
fn typeof_expr(&self, ex: Expr<'tcx>) -> MTy<'tcx> {
let typeck_result = self.tcx.typeck(ex.hir_id.owner.def_id);
typeck_result.expr_ty(&ex)
}
pub fn inertia_props_for_axum_route(
&self,
route: DefId,
) -> impl Iterator<Item = &structures::InertiaProp<'tcx>> {
let props_by_component = self
.inertia_props
.iter()
.filter(|(_id, p)| p.axum_handler == route)
.filter(|(_id, p)| !p.flash_prop())
.filter_map(|(id, p)| match self.inertia_prop_destination(*id) {
InertiaPropDestination::Render(component) => Some((component, p)),
_ => None,
})
.into_group_map();
let mut all_prop_names: Option<HashSet<&str>> = None;
for (component, props) in &props_by_component {
let props_by_name: HashMap<_, _> = props.iter().map(|p| (p.name(), p)).collect();
let names_in_this_group: HashSet<_> = props_by_name.keys().copied().collect();
match all_prop_names.as_ref() {
Some(known) => {
props_by_name
.keys()
.copied()
.collect::<HashSet<_>>()
.difference(known)
.for_each(|divergent| {
let prop = props_by_name.get(divergent).unwrap();
prop.emit_err_diverged(self.tcx.dcx(), component.as_str());
});
}
None => {
all_prop_names = Some(names_in_this_group);
}
}
}
props_by_component.into_values().flatten()
}
}
pub trait MTyAnalysisExt<'tcx> {
fn peel_tuple(self) -> Vec<MTy<'tcx>>;
fn extract_adt(self) -> Option<(AdtDef<'tcx>, impl Iterator<Item = GenericArg<'tcx>> + 'tcx)>;
}
impl<'tcx> MTyAnalysisExt<'tcx> for MTy<'tcx> {
fn peel_tuple(self) -> Vec<MTy<'tcx>> {
let MTyKind::Tuple(tys) = self.kind() else {
return vec![self];
};
tys.iter().collect()
}
fn extract_adt(self) -> Option<(AdtDef<'tcx>, impl Iterator<Item = GenericArg<'tcx>> + 'tcx)> {
let MTyKind::Adt(adt, genargs) = self.kind() else {
return None;
};
Some((*adt, genargs.into_iter()))
}
}
pub trait ExprAnalysisExt<'tcx> {
fn extract_lit_str(self) -> Option<Symbol>;
fn extract_call_with_single_arg(self, ra: &RiptAnalyzer<'tcx>)
-> Option<(DefPath, Expr<'tcx>)>;
fn extract_qpath_did(self, ra: &RiptAnalyzer<'tcx>) -> Option<DefId>;
}
impl<'tcx> ExprAnalysisExt<'tcx> for &Expr<'tcx> {
fn extract_lit_str(self) -> Option<Symbol> {
let rustc_hir::ExprKind::Lit(Spanned {
node: LitKind::Str(sym, _cooked_or_raw),
..
}) = self.kind
else {
return None;
};
Some(*sym)
}
fn extract_call_with_single_arg(
self,
ra: &RiptAnalyzer<'tcx>,
) -> Option<(DefPath, Expr<'tcx>)> {
let rustc_hir::ExprKind::Call(ex, &[first_arg]) = self.kind else {
return None;
};
let rustc_hir::ExprKind::Path(qpath) = ex.kind else {
return None;
};
let typeck_results = ra.tcx.typeck(first_arg.hir_id.owner);
let qpath_res = typeck_results.qpath_res(&qpath, first_arg.hir_id);
let dp = ra.tcx.def_path(qpath_res.def_id());
Some((dp, first_arg))
}
fn extract_qpath_did(self, ra: &RiptAnalyzer<'tcx>) -> Option<DefId> {
let rustc_hir::ExprKind::Path(qpath) = self.kind else {
return None;
};
let typeck_results = ra.tcx.typeck(self.hir_id.owner);
let qpath_res = typeck_results.qpath_res(&qpath, self.hir_id);
Some(qpath_res.def_id())
}
}
impl<'tcx> Visitor<'tcx> for RiptAnalyzer<'tcx> {
type NestedFilter = VisitorFilter;
fn visit_item(&mut self, i: &'tcx rustc_hir::Item<'tcx>) -> Self::Result {
if let Some(bmi) = analysis::try_bridge_marked_item(self, i) {
self.bridge_marked_items.insert(bmi.id, bmi);
}
rustc_hir::intravisit::walk_item(self, i)
}
fn visit_expr(&mut self, ex: &'tcx Expr<'tcx>) -> Self::Result {
if let Some((dest_id, prop_dest)) = analysis::try_inertia_prop_dest_chain_definer(self, *ex)
{
self.inertia_prop_destinations
.insert(dest_id, Either::Right(prop_dest));
}
if let Some(prop) = analysis::try_inertia_prop_from_chain(self, *ex) {
self.inertia_prop_destinations
.insert(ex.hir_id, Either::Left(prop.rx_id));
self.inertia_props.insert(ex.hir_id, prop);
}
if let Some(prop) = analysis::try_inertia_flash_prop_from_chain(self, *ex) {
self.inertia_prop_destinations
.insert(ex.hir_id, Either::Left(prop.rx_id));
self.inertia_props.insert(ex.hir_id, prop);
}
if let Some(route) = analysis::try_axum_route_from_route_method_call(self, *ex) {
self.axum_routes.insert(route.id, route);
}
if let Some(mw) = analysis::try_axum_share_middleware_registration(self, *ex) {
self.axum_map_response_fns.insert(mw);
}
rustc_hir::intravisit::walk_expr(self, ex)
}
fn maybe_tcx(&mut self) -> Self::MaybeTyCtxt {
self.tcx
}
}
#[doc(hidden)]
pub struct VisitorFilter;
impl<'tcx> NestedFilter<'tcx> for VisitorFilter {
type MaybeTyCtxt = TyCtxt<'tcx>;
const INTER: bool = true;
const INTRA: bool = true;
}