use quote::format_ident;
use super::detector::Candidate;
pub(crate) struct Rewrite {
pub(crate) helper: syn::ItemFn,
pub(crate) rewritten_fn: syn::ItemFn,
pub(crate) helper_ident: String,
}
pub(crate) fn rewrite(
func: &syn::ItemFn,
cand: &Candidate,
existing_idents: &[String],
) -> Option<Rewrite> {
let (i, j) = cand.stmt_range;
let stmts = &func.block.stmts;
if i >= j || j > stmts.len() {
return None;
}
let helper_ident = fresh_helper_ident(&cand.fn_ident, existing_idents);
let helper_id = format_ident!("{}", helper_ident);
let params = build_params(&cand.inputs);
let (ret_ty, tail_expr, body_stmts) = build_helper_body(func, cand, &stmts[i..j])?;
let helper_block = make_block(body_stmts, tail_expr);
let helper: syn::ItemFn = syn::parse_quote! {
fn #helper_id ( #params ) -> #ret_ty #helper_block
};
let call_args = build_call_args(&cand.inputs);
let call_expr: syn::Expr = syn::parse_quote!( #helper_id ( #(#call_args),* ) );
let mut new_stmts: Vec<syn::Stmt> = Vec::with_capacity(stmts.len());
new_stmts.extend_from_slice(&stmts[0..i]);
if cand.tail_is_output {
new_stmts.push(syn::Stmt::Expr(call_expr, None));
} else {
let bind = build_output_binding(&cand.outputs, &call_expr)?;
new_stmts.push(bind);
new_stmts.extend_from_slice(&stmts[j..]);
}
let mut rewritten_fn = func.clone();
rewritten_fn.block = Box::new(syn::Block {
brace_token: func.block.brace_token,
stmts: new_stmts,
});
Some(Rewrite {
helper,
rewritten_fn,
helper_ident,
})
}
fn build_helper_body(
func: &syn::ItemFn,
cand: &Candidate,
run: &[syn::Stmt],
) -> Option<(syn::Type, syn::Expr, Vec<syn::Stmt>)> {
if cand.tail_is_output {
let ret_ty = match &func.sig.output {
syn::ReturnType::Type(_, ty) => (**ty).clone(),
syn::ReturnType::Default => return None,
};
let (last, leading) = run.split_last()?;
let tail_expr = match last {
syn::Stmt::Expr(expr, None) => expr.clone(),
_ => return None,
};
Some((ret_ty, tail_expr, leading.to_vec()))
} else {
let (ret_ty, tail_expr) = build_output_return(&cand.outputs)?;
Some((ret_ty, tail_expr, run.to_vec()))
}
}
fn build_output_return(outputs: &[(String, syn::Type)]) -> Option<(syn::Type, syn::Expr)> {
match outputs.len() {
0 => None, 1 => {
let (name, ty) = &outputs[0];
let id = format_ident!("{}", name);
let expr: syn::Expr = syn::parse_quote!(#id);
Some((ty.clone(), expr))
}
_ => {
let types: Vec<&syn::Type> = outputs.iter().map(|(_, t)| t).collect();
let names: Vec<syn::Ident> = outputs
.iter()
.map(|(n, _)| format_ident!("{}", n))
.collect();
let ret_ty: syn::Type = syn::parse_quote!( ( #(#types),* ) );
let tail: syn::Expr = syn::parse_quote!( ( #(#names),* ) );
Some((ret_ty, tail))
}
}
}
fn build_output_binding(
outputs: &[(String, syn::Type)],
call_expr: &syn::Expr,
) -> Option<syn::Stmt> {
match outputs.len() {
0 => None,
1 => {
let id = format_ident!("{}", outputs[0].0);
Some(syn::parse_quote!( let #id = #call_expr; ))
}
_ => {
let names: Vec<syn::Ident> = outputs
.iter()
.map(|(n, _)| format_ident!("{}", n))
.collect();
Some(syn::parse_quote!( let ( #(#names),* ) = #call_expr; ))
}
}
}
fn build_params(
inputs: &[(String, syn::Type)],
) -> syn::punctuated::Punctuated<syn::FnArg, syn::token::Comma> {
let mut params = syn::punctuated::Punctuated::new();
for (name, ty) in inputs {
let id = format_ident!("{}", name);
let arg: syn::FnArg = syn::parse_quote!( #id: #ty );
params.push(arg);
}
params
}
fn build_call_args(inputs: &[(String, syn::Type)]) -> Vec<syn::Expr> {
inputs
.iter()
.map(|(name, _)| {
let id = format_ident!("{}", name);
syn::parse_quote!(#id)
})
.collect()
}
fn make_block(mut stmts: Vec<syn::Stmt>, tail: syn::Expr) -> syn::Block {
stmts.push(syn::Stmt::Expr(tail, None));
syn::Block {
brace_token: Default::default(),
stmts,
}
}
fn fresh_helper_ident(orig: &str, existing: &[String]) -> String {
let base = format!("{orig}_extracted");
if !existing.iter().any(|e| e == &base) {
return base;
}
let mut n = 2usize;
loop {
let candidate = format!("{base}_{n}");
if !existing.iter().any(|e| e == &candidate) {
return candidate;
}
n += 1;
}
}
#[cfg(test)]
mod tests {
use super::*;
fn parse_fn(src: &str) -> syn::ItemFn {
syn::parse_str(src).expect("parse fn")
}
fn render(item: &syn::ItemFn) -> String {
prettyplease::unparse(&syn::File {
shebang: None,
attrs: Vec::new(),
items: vec![syn::Item::Fn(item.clone())],
})
}
#[test]
fn fresh_ident_avoids_collisions() {
let existing = vec!["f_extracted".to_string()];
assert_eq!(fresh_helper_ident("f", &existing), "f_extracted_2");
assert_eq!(fresh_helper_ident("g", &existing), "g_extracted");
}
#[test]
fn rewrites_single_output_run() {
let func = parse_fn("fn calc(a: u32, b: u32) -> u32 { let s = a + b; let t = s * 2; t }");
let cand = Candidate {
fn_ident: "calc".to_string(),
stmt_range: (0, 1),
inputs: vec![
("a".to_string(), syn::parse_str("u32").expect("ty")),
("b".to_string(), syn::parse_str("u32").expect("ty")),
],
outputs: vec![("s".to_string(), syn::parse_str("u32").expect("ty"))],
tail_is_output: false,
};
let rw = rewrite(&func, &cand, &["calc".to_string()]).expect("rewrite");
let helper_src = render(&rw.helper);
assert!(helper_src.contains("fn calc_extracted"));
assert!(helper_src.contains("-> u32"));
let caller_src = render(&rw.rewritten_fn);
assert!(caller_src.contains("calc_extracted(a, b)"));
}
#[test]
fn rewrites_two_output_tuple() {
let func = parse_fn("fn g(a: i32, b: i32) -> i32 { let p = a + b; let q = a - b; p * q }");
let cand = Candidate {
fn_ident: "g".to_string(),
stmt_range: (0, 2),
inputs: vec![
("a".to_string(), syn::parse_str("i32").expect("ty")),
("b".to_string(), syn::parse_str("i32").expect("ty")),
],
outputs: vec![
("p".to_string(), syn::parse_str("i32").expect("ty")),
("q".to_string(), syn::parse_str("i32").expect("ty")),
],
tail_is_output: false,
};
let rw = rewrite(&func, &cand, &["g".to_string()]).expect("rewrite");
let helper_src = render(&rw.helper);
assert!(helper_src.contains("-> (i32, i32)"));
let caller_src = render(&rw.rewritten_fn);
assert!(caller_src.contains("let (p, q) = g_extracted(a, b)"));
}
}