make-noop 0.1.0

Attribute macros that replace function, method, and impl-block bodies with no-ops, with customizable return values.
Documentation
//! Procedural macros that turn functions, methods, or every method in an
//! `impl` block into no-ops, and that strip a struct down to a unit struct.
//!
//! The intended use is to toggle the no-op'ing on a compile-time condition:
//! pair any of these macros with [`cfg_attr`] so a feature flag (or any `cfg`)
//! decides whether the real implementation is compiled or quietly replaced with
//! a stub — without `#[cfg]` blocks duplicating each item or a runtime branch.
//!
//! [`cfg_attr`]: https://doc.rust-lang.org/reference/conditional-compilation.html#the-cfg_attr-attribute
//!
//! ```ignore
//! use make_noop::{make_noop, make_unit, noop_returns};
//!
//! // No-op under the `dry-run` feature; the real body otherwise.
//! #[cfg_attr(feature = "dry-run", make_noop)]
//! fn launch_missiles(hardware: &Hardware) {
//!     hardware.arm();
//!     hardware.fire();
//! }
//!
//! // Report success without touching the network under `dry-run`.
//! #[cfg_attr(feature = "dry-run", noop_returns(Ok(())))]
//! fn upload(client: &Client, blob: &[u8]) -> Result<(), Error> {
//!     client.put(blob)
//! }
//!
//! // Collapse to `struct Telemetry;` under `dry-run`.
//! #[cfg_attr(feature = "dry-run", make_unit)]
//! struct Telemetry {
//!     events: Vec<Event>,
//! }
//! ```
//!
//! Each function's body is replaced with one that does nothing; functions
//! returning a value return `Default::default()` unless tagged with
//! [`macro@noop_returns`]. The macros also apply unconditionally (without
//! `cfg_attr`) for stubbing in tests and prototypes.

use proc_macro::TokenStream;
use proc_macro2::Span;
use quote::quote;
use syn::{Attribute, Block, Expr, Fields, ImplItem, Item, parse_macro_input, parse_quote};

/// Replaces the body of the tagged function(s) with a no-op.
///
/// Can be applied to:
/// - a free function,
/// - a single method inside an `impl` block, or
/// - an entire `impl` block (every method is made a no-op).
///
/// Functions that return a value return `Default::default()` (so the return
/// type must implement [`Default`]). To return a custom expression instead, tag
/// the function or method with [`macro@noop_returns`]. Within an `impl` block,
/// individual methods may carry their own `#[noop_returns(EXPR)]`.
#[proc_macro_attribute]
pub fn make_noop(attr: TokenStream, item: TokenStream) -> TokenStream {
    if !attr.is_empty() {
        return syn::Error::new(
            Span::call_site(),
            "`make_noop` takes no arguments; use `#[noop_returns(EXPR)]` on a \
             function or method to customize the return value",
        )
        .to_compile_error()
        .into();
    }

    let input = parse_macro_input!(item as Item);

    let result: syn::Result<proc_macro2::TokenStream> = match input {
        // A free function, or a single method tagged directly inside an impl
        // block (methods with a `self` receiver parse as `Item::Fn`).
        Item::Fn(mut func) => {
            *func.block = noop_block(&func.sig.output, None);
            Ok(quote! { #func })
        }
        // An entire impl block: make every method a no-op, honoring any
        // per-method `#[noop_returns(EXPR)]` override.
        Item::Impl(mut item_impl) => (|| {
            for impl_item in &mut item_impl.items {
                if let ImplItem::Fn(method) = impl_item {
                    let override_expr = take_noop_returns(&mut method.attrs)?;
                    method.block = noop_block(&method.sig.output, override_expr.as_ref());
                }
            }
            Ok(quote! { #item_impl })
        })(),
        // Anything else is invalid.
        other => Err(syn::Error::new_spanned(
            &other,
            "`make_noop` can only be applied to a function, a method, or an impl block",
        )),
    };

    match result {
        Ok(tokens) => tokens.into(),
        Err(err) => err.to_compile_error().into(),
    }
}

/// Makes the tagged function/method a no-op that returns the given expression.
///
/// `#[noop_returns(EXPR)]` replaces the function or method body with `{ EXPR }`.
/// It can be applied to:
/// - a free function or a single method, whose body becomes `{ EXPR }`, or
/// - an entire `impl` block, where every method returns `EXPR`, except for
///   methods carrying their own `#[noop_returns(OTHER)]`, which return `OTHER`.
///
/// Applying it to anything else is an error.
///
/// Inside a `#[make_noop]` impl block this attribute overrides the default
/// `Default::default()` return for that method; used anywhere else it stands on
/// its own.
#[proc_macro_attribute]
pub fn noop_returns(attr: TokenStream, item: TokenStream) -> TokenStream {
    let return_expr = parse_macro_input!(attr as Expr);
    let input = parse_macro_input!(item as Item);

    match input {
        Item::Fn(mut func) => {
            *func.block = noop_block(&func.sig.output, Some(&return_expr));
            quote! { #func }.into()
        }
        // An entire impl block: every method returns `return_expr`, unless it
        // carries its own `#[noop_returns(EXPR)]`, which takes precedence.
        Item::Impl(mut item_impl) => {
            let result: syn::Result<proc_macro2::TokenStream> = (|| {
                for impl_item in &mut item_impl.items {
                    if let ImplItem::Fn(method) = impl_item {
                        let override_expr = take_noop_returns(&mut method.attrs)?;
                        let expr = override_expr.as_ref().unwrap_or(&return_expr);
                        method.block = noop_block(&method.sig.output, Some(expr));
                    }
                }
                Ok(quote! { #item_impl })
            })();
            match result {
                Ok(tokens) => tokens.into(),
                Err(err) => err.to_compile_error().into(),
            }
        }
        other => {
            let err = syn::Error::new_spanned(
                &other,
                "`noop_returns` can only be applied to a function, a method, or an impl block",
            )
            .to_compile_error();
            quote! { #err #other }.into()
        }
    }
}

/// Strips a struct's fields, turning it into a unit struct.
///
/// Applied to a `struct` with named fields (`struct Foo { .. }`) or a tuple
/// struct (`struct Foo(..)`), it discards every field, leaving `struct Foo;`.
/// Attributes, visibility, and generics are preserved; an already-unit struct
/// is left unchanged.
///
/// Applying it to anything other than a struct is an error.
///
/// ```ignore
/// use make_noop::make_unit;
///
/// #[make_unit]
/// struct Config {
///     verbose: bool,
///     retries: u32,
/// }
/// // Expands to `struct Config;`
/// ```
#[proc_macro_attribute]
pub fn make_unit(attr: TokenStream, item: TokenStream) -> TokenStream {
    if !attr.is_empty() {
        return syn::Error::new(Span::call_site(), "`make_unit` takes no arguments")
            .to_compile_error()
            .into();
    }

    let input = parse_macro_input!(item as Item);

    match input {
        Item::Struct(mut item_struct) => {
            item_struct.fields = Fields::Unit;
            item_struct.semi_token = Some(Default::default());
            quote! { #item_struct }.into()
        }
        other => syn::Error::new_spanned(&other, "`make_unit` can only be applied to a struct")
            .to_compile_error()
            .into(),
    }
}

/// Removes a `#[noop_returns(EXPR)]` attribute from `attrs`, if present, and
/// returns the parsed expression. Errors if the attribute is malformed.
fn take_noop_returns(attrs: &mut Vec<Attribute>) -> syn::Result<Option<Expr>> {
    attrs
        .extract_if(.., |attr| attr.path().is_ident("noop_returns"))
        .last()
        .map(|attr| attr.parse_args::<Expr>())
        .transpose()
}

/// Builds the replacement body. The original body is discarded.
///
/// A function with no return value (no `->`) becomes an empty `{}` — a void
/// function must not return a value, even if a `return_expr` was requested.
/// Otherwise the body yields the result: `{ EXPR }` when a `return_expr` is
/// given, else `{ Default::default() }` (so the return type must implement
/// [`Default`]).
fn noop_block(output: &syn::ReturnType, return_expr: Option<&Expr>) -> Block {
    match output {
        syn::ReturnType::Default => parse_quote!({}),
        syn::ReturnType::Type(..) => match return_expr {
            Some(expr) => parse_quote!({ #expr }),
            None => parse_quote!({ ::core::default::Default::default() }),
        },
    }
}