inline-fn 0.0.1

Inline the content of a function without duplicating code. Useful when duplicating code is desired but you are too embarrassed to do it manually.
Documentation
#![doc = include_str!("../README.md")]
mod parser;

use std::path::PathBuf;

use anyhow::Context;
use proc_macro::TokenStream;
use syn::{parse::Parse, parse_macro_input};

struct ItemFn {
    fn_name: syn::Ident,
    _comma: Option<syn::Token![,]>,
    relative_path: Option<syn::LitStr>,
}

/// Inline a function from a file.
/// Expands to the function body fetched from the specified file.
/// # Usage
/// * Function from a specific file relative from the workspace root (this is best for performance reasons)
///   `inline_fn!(fn_name, "crates/app/src/file.rs")`
/// * Function from the first file with the matching function name (this will cause a recursive scan in the workspace):
///   `inline_fn!(fn_name)`
///
/// # Examples
///
/// ```ignore
/// fn add_two(a: usize, b: usize) -> usize {
///    a + b
/// }
///
/// fn add_three(a: usize, b: usize, c: usize) -> usize {
///    inline_fn::inline_fn!(add_two) + c // no need to repeat the code
/// }
/// ```
#[proc_macro]
pub fn inline_fn(input: TokenStream) -> TokenStream {
    let input: ItemFn = parse_macro_input!(input);
    let relative_path = input
        .relative_path
        .map(|p| PathBuf::from(p.token().to_string().trim_matches('"')));

    let fn_name = input.fn_name.to_string();
    let workspace_root = std::env::current_dir().expect("Failed to get current directory");

    find_function(fn_name, workspace_root, relative_path)
        .parse()
        .expect("parse")
}

fn find_function(
    fn_name: String,
    workspace_root: PathBuf,
    relative_path: Option<PathBuf>,
) -> String {
    if let Some(source_file) = relative_path {
        match find_in_file(fn_name.clone(), workspace_root.join(source_file.clone())) {
            Ok(Some(c)) => return c.join("\n"),
            Ok(None) => panic!(
                "Function `{}` not found, location searched: `{}`",
                fn_name,
                source_file.display()
            ),
            Err(e) => panic!("{}, location searched: {}", e, source_file.display()),
        }
    }

    // search all codes
    walkdir::WalkDir::new(&workspace_root)
        .into_iter()
        .filter_map(|e| e.ok())
        .filter(|e| e.file_type().is_file())
        .filter(|e| e.path().extension().map(|e| e == "rs").unwrap_or(false))
        .map(|e| find_in_file(fn_name.clone(), e.path().to_path_buf()))
        .filter_map(|e| e.ok())
        .filter(|e| e.is_some())
        .map(|e| {
            e.with_context(|| {
                format!(
                    "Failed to find function `{}` in folder: {}",
                    fn_name,
                    workspace_root.display()
                )
            })
            .unwrap()
        })
        .flatten()
        .collect::<Vec<String>>()
        .join("\n")
}

fn find_in_file(fn_name: String, file: PathBuf) -> anyhow::Result<Option<Vec<String>>> {
    if file.extension().map(|e| e != "rs").unwrap_or(false) {
        return Err(anyhow::anyhow!("File is not a rust file"));
    }

    let sc = std::fs::read_to_string(file.clone())
        .with_context(|| format!("Failed to read file XX: {}", file.display()))?;

    Ok(parser::extract_function(&sc, &fn_name).map(|c| {
        c.split('\n')
            .map(|s| s.to_string())
            .collect::<Vec<String>>()
    }))
}

impl Parse for ItemFn {
    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
        Ok(Self {
            fn_name: input.parse()?,
            _comma: input.parse()?,
            relative_path: input.parse()?,
        })
    }
}