#![allow(unused)]
use std::cell::RefCell;
use rustc_hash::FxHashMap;
use syn::{ExprPath, ExprReference, Ident, Token, parse_quote, punctuated::Punctuated};
use super::{ToCode, input::QuoteVar};
#[cfg(all(feature = "swc", not(feature = "oxc")))]
use swc_macros_common::call_site;
#[cfg(any(not(feature = "swc"), feature = "oxc"))]
fn call_site() -> proc_macro2::Span {
proc_macro2::Span::call_site()
}
#[derive(Debug)]
pub(crate) struct Ctx {
pub(crate) vars: FxHashMap<VarPos, Vars>,
}
impl Ctx {
pub fn var(&self, ty: VarPos, var_name: &str) -> Option<&VarData> {
self.vars.get(&ty)?.get(var_name)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum VarPos {
Ident,
Expr,
Pat,
AssignTarget,
Str,
TsType,
}
#[derive(Debug)]
pub struct VarData {
pos: VarPos,
is_counting: bool,
clone: RefCell<usize>,
ident: syn::Ident,
}
impl VarData {
pub fn get_expr(&self) -> syn::Expr {
if self.is_counting {
*self.clone.borrow_mut() += 1;
return self.expr_for_var_ref();
}
let use_clone = {
let mut b = self.clone.borrow_mut();
let val = *b;
if val > 0 {
*b -= 1;
val != 1
} else {
false
}
};
if use_clone {
let var_ref_expr = self.expr_for_var_ref();
#[cfg(all(feature = "swc", not(feature = "oxc")))]
{
parse_quote!(macroforge_ts::swc_core::quote::ImplicitClone::clone_quote_var(&#var_ref_expr))
}
#[cfg(any(not(feature = "swc"), feature = "oxc"))]
{
var_ref_expr
}
} else {
self.expr_for_var_ref()
}
}
fn expr_for_var_ref(&self) -> syn::Expr {
syn::Expr::Path(ExprPath {
attrs: Default::default(),
qself: Default::default(),
path: self.ident.clone().into(),
})
}
}
pub type Vars = FxHashMap<String, VarData>;
pub(super) fn prepare_vars(
src: &dyn ToCode,
vars: Punctuated<QuoteVar, Token![,]>,
) -> syn::Result<(Vec<syn::Stmt>, FxHashMap<VarPos, Vars>)> {
let mut stmts = Vec::new();
let mut init_map = FxHashMap::<_, Vars>::default();
for var in vars {
let value = var.value;
let ident = var.name.clone();
let ident_str = ident.to_string();
let pos = match &var.ty {
Some(syn::Type::Path(syn::TypePath {
qself: None,
path:
syn::Path {
leading_colon: None,
segments,
},
})) => {
let segment = segments.first().unwrap();
match segment.ident.to_string().as_str() {
"Ident" => VarPos::Ident,
"Expr" => VarPos::Expr,
"Pat" => VarPos::Pat,
"Str" => VarPos::Str,
"AssignTarget" => VarPos::AssignTarget,
"TsType" => VarPos::TsType,
ty => {
return Err(syn::Error::new_spanned(
&segment.ident,
format!(
"invalid variable type `{ty}`, expected one of: \
Ident, Expr, Pat, Str, AssignTarget, TsType"
),
));
}
}
}
None => VarPos::Ident,
Some(ty) => {
return Err(syn::Error::new_spanned(
ty,
"variable type must be a simple identifier like Ident, Expr, Pat, etc.",
));
}
};
let var_ident = syn::Ident::new(&format!("quote_var_{ident}"), ident.span());
let old = init_map.entry(pos).or_default().insert(
ident_str.clone(),
VarData {
pos,
is_counting: true,
clone: Default::default(),
ident: var_ident.clone(),
},
);
if old.is_some() {
return Err(syn::Error::new_spanned(
&ident,
format!("duplicate variable name: `{ident_str}`"),
));
}
let type_name = Ident::new(
match pos {
VarPos::Ident => "Ident",
VarPos::Expr => "Expr",
VarPos::Pat => "Pat",
VarPos::AssignTarget => "AssignTarget",
VarPos::Str => "Str",
VarPos::TsType => "TsType",
},
call_site(),
);
#[cfg(all(feature = "swc", not(feature = "oxc")))]
stmts.push(parse_quote! {
let #var_ident: macroforge_ts::swc_core::ecma::ast::#type_name = #value;
});
#[cfg(any(not(feature = "swc"), feature = "oxc"))]
stmts.push(parse_quote! {
let #var_ident = #value;
});
}
let mut cx = Ctx { vars: init_map };
src.to_code(&cx);
cx.vars
.iter_mut()
.for_each(|(_, v)| v.iter_mut().for_each(|(_, v)| v.is_counting = false));
Ok((stmts, cx.vars))
}
#[cfg(test)]
mod tests {
use super::*;
use quote::ToTokens;
#[test]
fn test_var_pos_equality() {
assert_eq!(VarPos::Ident, VarPos::Ident);
assert_eq!(VarPos::Expr, VarPos::Expr);
assert_eq!(VarPos::Pat, VarPos::Pat);
assert_eq!(VarPos::AssignTarget, VarPos::AssignTarget);
assert_eq!(VarPos::Str, VarPos::Str);
assert_eq!(VarPos::TsType, VarPos::TsType);
}
#[test]
fn test_var_pos_inequality() {
assert_ne!(VarPos::Ident, VarPos::Expr);
assert_ne!(VarPos::Pat, VarPos::TsType);
assert_ne!(VarPos::Str, VarPos::AssignTarget);
}
#[test]
fn test_var_pos_ordering() {
let mut positions = [VarPos::TsType, VarPos::Ident, VarPos::Expr];
positions.sort();
}
#[test]
fn test_var_pos_hash() {
let mut map = FxHashMap::default();
map.insert(VarPos::Ident, "ident");
map.insert(VarPos::Expr, "expr");
assert_eq!(map.get(&VarPos::Ident), Some(&"ident"));
assert_eq!(map.get(&VarPos::Expr), Some(&"expr"));
assert_eq!(map.get(&VarPos::Pat), None);
}
#[test]
fn test_var_pos_clone() {
let pos = VarPos::Ident;
let cloned = pos;
assert_eq!(pos, cloned);
}
#[test]
fn test_var_pos_debug() {
let debug_str = format!("{:?}", VarPos::Ident);
assert!(debug_str.contains("Ident"));
}
#[test]
fn test_ctx_empty() {
let ctx = Ctx {
vars: FxHashMap::default(),
};
assert!(ctx.var(VarPos::Ident, "foo").is_none());
}
#[test]
fn test_ctx_var_lookup() {
let mut vars = FxHashMap::default();
let mut inner_map: Vars = FxHashMap::default();
inner_map.insert(
"foo".to_string(),
VarData {
pos: VarPos::Ident,
is_counting: false,
clone: RefCell::new(0),
ident: syn::Ident::new("quote_var_foo", proc_macro2::Span::call_site()),
},
);
vars.insert(VarPos::Ident, inner_map);
let ctx = Ctx { vars };
assert!(ctx.var(VarPos::Ident, "foo").is_some());
assert!(ctx.var(VarPos::Ident, "bar").is_none());
assert!(ctx.var(VarPos::Expr, "foo").is_none());
}
#[test]
fn test_ctx_multiple_var_positions() {
let mut vars = FxHashMap::default();
let mut ident_map: Vars = FxHashMap::default();
ident_map.insert(
"x".to_string(),
VarData {
pos: VarPos::Ident,
is_counting: false,
clone: RefCell::new(0),
ident: syn::Ident::new("quote_var_x", proc_macro2::Span::call_site()),
},
);
vars.insert(VarPos::Ident, ident_map);
let mut expr_map: Vars = FxHashMap::default();
expr_map.insert(
"y".to_string(),
VarData {
pos: VarPos::Expr,
is_counting: false,
clone: RefCell::new(0),
ident: syn::Ident::new("quote_var_y", proc_macro2::Span::call_site()),
},
);
vars.insert(VarPos::Expr, expr_map);
let ctx = Ctx { vars };
assert!(ctx.var(VarPos::Ident, "x").is_some());
assert!(ctx.var(VarPos::Expr, "y").is_some());
assert!(ctx.var(VarPos::Ident, "y").is_none());
assert!(ctx.var(VarPos::Expr, "x").is_none());
}
#[test]
fn test_var_data_get_expr_first_use_no_clone() {
let var_data = VarData {
pos: VarPos::Ident,
is_counting: false,
clone: RefCell::new(0),
ident: syn::Ident::new("quote_var_test", proc_macro2::Span::call_site()),
};
let expr = var_data.get_expr();
let expr_str = expr.to_token_stream().to_string();
assert_eq!(expr_str, "quote_var_test");
}
#[test]
fn test_var_data_get_expr_with_clone() {
let var_data = VarData {
pos: VarPos::Ident,
is_counting: false,
clone: RefCell::new(2), ident: syn::Ident::new("quote_var_test", proc_macro2::Span::call_site()),
};
let expr1 = var_data.get_expr();
let expr1_str = expr1.to_token_stream().to_string();
#[cfg(feature = "swc")]
assert!(expr1_str.contains("clone_quote_var"));
#[cfg(not(feature = "swc"))]
assert_eq!(expr1_str, "quote_var_test");
let expr2 = var_data.get_expr();
let expr2_str = expr2.to_token_stream().to_string();
assert_eq!(expr2_str, "quote_var_test");
let expr3 = var_data.get_expr();
let expr3_str = expr3.to_token_stream().to_string();
assert_eq!(expr3_str, "quote_var_test");
}
#[test]
fn test_var_data_counting_mode() {
let var_data = VarData {
pos: VarPos::Ident,
is_counting: true,
clone: RefCell::new(0),
ident: syn::Ident::new("quote_var_test", proc_macro2::Span::call_site()),
};
var_data.get_expr();
assert_eq!(*var_data.clone.borrow(), 1);
var_data.get_expr();
assert_eq!(*var_data.clone.borrow(), 2);
var_data.get_expr();
assert_eq!(*var_data.clone.borrow(), 3);
}
#[test]
fn test_var_data_expr_for_var_ref() {
let var_data = VarData {
pos: VarPos::Expr,
is_counting: false,
clone: RefCell::new(0),
ident: syn::Ident::new("my_var", proc_macro2::Span::call_site()),
};
let expr = var_data.expr_for_var_ref();
if let syn::Expr::Path(path) = expr {
assert_eq!(path.path.segments.len(), 1);
assert_eq!(path.path.segments[0].ident.to_string(), "my_var");
} else {
panic!("Expected ExprPath");
}
}
#[test]
fn test_var_data_debug() {
let var_data = VarData {
pos: VarPos::Ident,
is_counting: false,
clone: RefCell::new(0),
ident: syn::Ident::new("test", proc_macro2::Span::call_site()),
};
let debug_str = format!("{:?}", var_data);
assert!(debug_str.contains("VarData"));
}
#[test]
fn test_var_data_clone_count_boundary() {
let var_data = VarData {
pos: VarPos::Ident,
is_counting: false,
clone: RefCell::new(1),
ident: syn::Ident::new("test", proc_macro2::Span::call_site()),
};
let expr1 = var_data.get_expr();
let expr1_str = expr1.to_token_stream().to_string();
assert_eq!(expr1_str, "test");
}
#[test]
fn test_empty_vars_map() {
let vars: Vars = FxHashMap::default();
assert!(vars.is_empty());
assert!(!vars.contains_key("anything"));
}
#[test]
fn test_vars_map_multiple_entries() {
let mut vars: Vars = FxHashMap::default();
vars.insert(
"a".to_string(),
VarData {
pos: VarPos::Ident,
is_counting: false,
clone: RefCell::new(0),
ident: syn::Ident::new("a", proc_macro2::Span::call_site()),
},
);
vars.insert(
"b".to_string(),
VarData {
pos: VarPos::Ident,
is_counting: false,
clone: RefCell::new(0),
ident: syn::Ident::new("b", proc_macro2::Span::call_site()),
},
);
assert_eq!(vars.len(), 2);
assert!(vars.contains_key("a"));
assert!(vars.contains_key("b"));
assert!(!vars.contains_key("c"));
}
}