use proc_macro2::{Span, TokenStream};
use quote::{quote, ToTokens};
use std::collections::HashMap;
use syn::{
visit_mut::VisitMut, Expr, ExprBlock, ExprPath, Ident, Member, Path, PathSegment,
};
use crate::parse::{FieldMode, MethodDef, WithInput};
struct FieldInfo {
mode: FieldMode,
span: Span,
}
struct BodyTransformer<'a> {
receiver: &'a Expr,
methods: HashMap<String, &'a MethodDef>,
field_modes: &'a HashMap<String, FieldInfo>,
}
impl<'a> VisitMut for BodyTransformer<'a> {
fn visit_expr_mut(&mut self, expr: &mut Expr) {
match expr {
Expr::MethodCall(call) => {
if self.is_receiver(&call.receiver) {
let method_name = call.method.to_string();
if let Some(method_def) = self.methods.get(&method_name) {
*expr = self.inline_method(call, method_def);
return;
}
}
syn::visit_mut::visit_expr_mut(self, expr);
}
Expr::Field(field_expr) => {
if self.is_receiver(&field_expr.base) {
if let Member::Named(field_name) = &field_expr.member {
let name_str = field_name.to_string();
if self.field_modes.contains_key(&name_str) {
*expr = self.make_ident_expr(field_name);
return;
}
}
}
syn::visit_mut::visit_expr_mut(self, expr);
}
_ => {
syn::visit_mut::visit_expr_mut(self, expr);
}
}
}
}
impl<'a> BodyTransformer<'a> {
fn is_receiver(&self, expr: &Expr) -> bool {
expr.to_token_stream().to_string() == self.receiver.to_token_stream().to_string()
}
fn make_ident_expr(&self, ident: &Ident) -> Expr {
Expr::Path(ExprPath {
attrs: vec![],
qself: None,
path: Path {
leading_colon: None,
segments: std::iter::once(PathSegment {
ident: ident.clone(),
arguments: syn::PathArguments::None,
})
.collect(),
},
})
}
fn inline_method(&self, call: &syn::ExprMethodCall, method: &MethodDef) -> Expr {
let mut body = method.body.clone();
let mut replacer = ParamReplacer {
replacements: HashMap::new(),
};
for (param, arg) in method.extra_params.iter().zip(call.args.iter()) {
replacer
.replacements
.insert(param.name.to_string(), arg.clone());
}
syn::visit_mut::visit_block_mut(&mut replacer, &mut body);
Expr::Block(ExprBlock {
attrs: vec![],
label: None,
block: body,
})
}
}
struct ParamReplacer {
replacements: HashMap<String, Expr>,
}
impl VisitMut for ParamReplacer {
fn visit_expr_mut(&mut self, expr: &mut Expr) {
if let Expr::Path(path) = expr {
if path.qself.is_none() && path.path.segments.len() == 1 {
let name = path.path.segments[0].ident.to_string();
if let Some(replacement) = self.replacements.get(&name) {
*expr = replacement.clone();
return;
}
}
}
syn::visit_mut::visit_expr_mut(self, expr);
}
}
fn merge_mode(existing: FieldMode, new: FieldMode) -> FieldMode {
match (existing, new) {
(FieldMode::Mut, _) | (_, FieldMode::Mut) => FieldMode::Mut,
(FieldMode::Copy, _) | (_, FieldMode::Copy) => FieldMode::Copy,
_ => FieldMode::Ref,
}
}
pub fn expand(input: WithInput) -> syn::Result<TokenStream> {
let WithInput {
receiver,
methods,
fields,
mut body,
} = input;
let mut field_modes: HashMap<String, FieldInfo> = HashMap::new();
for method in &methods {
for param in &method.field_params {
let name = param.name.to_string();
field_modes
.entry(name)
.and_modify(|info| {
info.mode = merge_mode(info.mode, param.mode);
})
.or_insert(FieldInfo {
mode: param.mode,
span: param.name.span(),
});
}
}
for field in &fields {
let name = field.name.to_string();
field_modes
.entry(name)
.and_modify(|info| {
info.mode = merge_mode(info.mode, field.mode);
})
.or_insert(FieldInfo {
mode: field.mode,
span: field.name.span(),
});
}
let methods_map: HashMap<String, &MethodDef> =
methods.iter().map(|m| (m.name.to_string(), m)).collect();
let mut transformer = BodyTransformer {
receiver: &receiver,
methods: methods_map,
field_modes: &field_modes,
};
syn::visit_mut::visit_block_mut(&mut transformer, &mut body);
let bindings: Vec<TokenStream> = field_modes
.iter()
.map(|(name, info)| {
let ident = Ident::new(name, info.span);
match info.mode {
FieldMode::Ref => quote! { let #ident = &#receiver.#ident; },
FieldMode::Mut => quote! { let #ident = &mut #receiver.#ident; },
FieldMode::Copy => quote! { let #ident = #receiver.#ident; },
}
})
.collect();
let body_stmts = &body.stmts;
Ok(quote! {
{
#(#bindings)*
#(#body_stmts)*
}
})
}