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}