Skip to main content

make_noop/
lib.rs

1//! Procedural macros that turn functions, methods, or every method in an
2//! `impl` block into no-ops, and that strip a struct down to a unit struct.
3//!
4//! The intended use is to toggle the no-op'ing on a compile-time condition:
5//! pair any of these macros with [`cfg_attr`] so a feature flag (or any `cfg`)
6//! decides whether the real implementation is compiled or quietly replaced with
7//! a stub — without `#[cfg]` blocks duplicating each item or a runtime branch.
8//!
9//! [`cfg_attr`]: https://doc.rust-lang.org/reference/conditional-compilation.html#the-cfg_attr-attribute
10//!
11//! ```ignore
12//! use make_noop::{make_noop, make_unit, noop_returns};
13//!
14//! // No-op under the `dry-run` feature; the real body otherwise.
15//! #[cfg_attr(feature = "dry-run", make_noop)]
16//! fn launch_missiles(hardware: &Hardware) {
17//!     hardware.arm();
18//!     hardware.fire();
19//! }
20//!
21//! // Report success without touching the network under `dry-run`.
22//! #[cfg_attr(feature = "dry-run", noop_returns(Ok(())))]
23//! fn upload(client: &Client, blob: &[u8]) -> Result<(), Error> {
24//!     client.put(blob)
25//! }
26//!
27//! // Collapse to `struct Telemetry;` under `dry-run`.
28//! #[cfg_attr(feature = "dry-run", make_unit)]
29//! struct Telemetry {
30//!     events: Vec<Event>,
31//! }
32//! ```
33//!
34//! Each function's body is replaced with one that does nothing; functions
35//! returning a value return `Default::default()` unless tagged with
36//! [`macro@noop_returns`]. The macros also apply unconditionally (without
37//! `cfg_attr`) for stubbing in tests and prototypes.
38
39use proc_macro::TokenStream;
40use proc_macro2::Span;
41use quote::quote;
42use syn::{Attribute, Block, Expr, Fields, ImplItem, Item, parse_macro_input, parse_quote};
43
44/// Replaces the body of the tagged function(s) with a no-op.
45///
46/// Can be applied to:
47/// - a free function,
48/// - a single method inside an `impl` block, or
49/// - an entire `impl` block (every method is made a no-op).
50///
51/// Functions that return a value return `Default::default()` (so the return
52/// type must implement [`Default`]). To return a custom expression instead, tag
53/// the function or method with [`macro@noop_returns`]. Within an `impl` block,
54/// individual methods may carry their own `#[noop_returns(EXPR)]`.
55#[proc_macro_attribute]
56pub fn make_noop(attr: TokenStream, item: TokenStream) -> TokenStream {
57    if !attr.is_empty() {
58        return syn::Error::new(
59            Span::call_site(),
60            "`make_noop` takes no arguments; use `#[noop_returns(EXPR)]` on a \
61             function or method to customize the return value",
62        )
63        .to_compile_error()
64        .into();
65    }
66
67    let input = parse_macro_input!(item as Item);
68
69    let result: syn::Result<proc_macro2::TokenStream> = match input {
70        // A free function, or a single method tagged directly inside an impl
71        // block (methods with a `self` receiver parse as `Item::Fn`).
72        Item::Fn(mut func) => {
73            *func.block = noop_block(&func.sig.output, None);
74            Ok(quote! { #func })
75        }
76        // An entire impl block: make every method a no-op, honoring any
77        // per-method `#[noop_returns(EXPR)]` override.
78        Item::Impl(mut item_impl) => (|| {
79            for impl_item in &mut item_impl.items {
80                if let ImplItem::Fn(method) = impl_item {
81                    let override_expr = take_noop_returns(&mut method.attrs)?;
82                    method.block = noop_block(&method.sig.output, override_expr.as_ref());
83                }
84            }
85            Ok(quote! { #item_impl })
86        })(),
87        // Anything else is invalid.
88        other => Err(syn::Error::new_spanned(
89            &other,
90            "`make_noop` can only be applied to a function, a method, or an impl block",
91        )),
92    };
93
94    match result {
95        Ok(tokens) => tokens.into(),
96        Err(err) => err.to_compile_error().into(),
97    }
98}
99
100/// Makes the tagged function/method a no-op that returns the given expression.
101///
102/// `#[noop_returns(EXPR)]` replaces the function or method body with `{ EXPR }`.
103/// It can be applied to:
104/// - a free function or a single method, whose body becomes `{ EXPR }`, or
105/// - an entire `impl` block, where every method returns `EXPR`, except for
106///   methods carrying their own `#[noop_returns(OTHER)]`, which return `OTHER`.
107///
108/// Applying it to anything else is an error.
109///
110/// Inside a `#[make_noop]` impl block this attribute overrides the default
111/// `Default::default()` return for that method; used anywhere else it stands on
112/// its own.
113#[proc_macro_attribute]
114pub fn noop_returns(attr: TokenStream, item: TokenStream) -> TokenStream {
115    let return_expr = parse_macro_input!(attr as Expr);
116    let input = parse_macro_input!(item as Item);
117
118    match input {
119        Item::Fn(mut func) => {
120            *func.block = noop_block(&func.sig.output, Some(&return_expr));
121            quote! { #func }.into()
122        }
123        // An entire impl block: every method returns `return_expr`, unless it
124        // carries its own `#[noop_returns(EXPR)]`, which takes precedence.
125        Item::Impl(mut item_impl) => {
126            let result: syn::Result<proc_macro2::TokenStream> = (|| {
127                for impl_item in &mut item_impl.items {
128                    if let ImplItem::Fn(method) = impl_item {
129                        let override_expr = take_noop_returns(&mut method.attrs)?;
130                        let expr = override_expr.as_ref().unwrap_or(&return_expr);
131                        method.block = noop_block(&method.sig.output, Some(expr));
132                    }
133                }
134                Ok(quote! { #item_impl })
135            })();
136            match result {
137                Ok(tokens) => tokens.into(),
138                Err(err) => err.to_compile_error().into(),
139            }
140        }
141        other => {
142            let err = syn::Error::new_spanned(
143                &other,
144                "`noop_returns` can only be applied to a function, a method, or an impl block",
145            )
146            .to_compile_error();
147            quote! { #err #other }.into()
148        }
149    }
150}
151
152/// Strips a struct's fields, turning it into a unit struct.
153///
154/// Applied to a `struct` with named fields (`struct Foo { .. }`) or a tuple
155/// struct (`struct Foo(..)`), it discards every field, leaving `struct Foo;`.
156/// Attributes, visibility, and generics are preserved; an already-unit struct
157/// is left unchanged.
158///
159/// Applying it to anything other than a struct is an error.
160///
161/// ```ignore
162/// use make_noop::make_unit;
163///
164/// #[make_unit]
165/// struct Config {
166///     verbose: bool,
167///     retries: u32,
168/// }
169/// // Expands to `struct Config;`
170/// ```
171#[proc_macro_attribute]
172pub fn make_unit(attr: TokenStream, item: TokenStream) -> TokenStream {
173    if !attr.is_empty() {
174        return syn::Error::new(Span::call_site(), "`make_unit` takes no arguments")
175            .to_compile_error()
176            .into();
177    }
178
179    let input = parse_macro_input!(item as Item);
180
181    match input {
182        Item::Struct(mut item_struct) => {
183            item_struct.fields = Fields::Unit;
184            item_struct.semi_token = Some(Default::default());
185            quote! { #item_struct }.into()
186        }
187        other => syn::Error::new_spanned(&other, "`make_unit` can only be applied to a struct")
188            .to_compile_error()
189            .into(),
190    }
191}
192
193/// Removes a `#[noop_returns(EXPR)]` attribute from `attrs`, if present, and
194/// returns the parsed expression. Errors if the attribute is malformed.
195fn take_noop_returns(attrs: &mut Vec<Attribute>) -> syn::Result<Option<Expr>> {
196    attrs
197        .extract_if(.., |attr| attr.path().is_ident("noop_returns"))
198        .last()
199        .map(|attr| attr.parse_args::<Expr>())
200        .transpose()
201}
202
203/// Builds the replacement body. The original body is discarded.
204///
205/// A function with no return value (no `->`) becomes an empty `{}` — a void
206/// function must not return a value, even if a `return_expr` was requested.
207/// Otherwise the body yields the result: `{ EXPR }` when a `return_expr` is
208/// given, else `{ Default::default() }` (so the return type must implement
209/// [`Default`]).
210fn noop_block(output: &syn::ReturnType, return_expr: Option<&Expr>) -> Block {
211    match output {
212        syn::ReturnType::Default => parse_quote!({}),
213        syn::ReturnType::Type(..) => match return_expr {
214            Some(expr) => parse_quote!({ #expr }),
215            None => parse_quote!({ ::core::default::Default::default() }),
216        },
217    }
218}