Skip to main content

rustmax_macros/
lib.rs

1//! Procedural macros for the [`rustmax`] crate.
2//!
3//! This crate is an implementation detail of `rustmax`.
4//! Use it through the `rmx` re-exports,
5//! not as a direct dependency.
6//!
7//! [`rustmax`]: https://docs.rs/rustmax
8
9use proc_macro::TokenStream;
10use proc_macro2::{Ident, Literal, Span, TokenStream as TokenStream2, TokenTree};
11use proc_macro_crate::{FoundCrate, crate_name};
12use quote::{quote, quote_spanned};
13
14/// Derive macros that `rustmax` knows how to route, and their origin crate.
15///
16/// Names that collide with a standard library derive are deliberately
17/// absent so that the standard derive keeps winning: `Debug`, `Eq` and
18/// `PartialEq` all exist in `derive_more` but are far more often meant as
19/// the built-in ones. Those are still reachable by qualifying the name,
20/// as in `#[rmx::derive(derive_more::Debug)]`.
21///
22/// `Error` is routed to `thiserror` rather than `derive_more` because that
23/// is what it usually means, even though `thiserror` is the one crate here
24/// whose derive cannot work through a re-export.
25const KNOWN_DERIVES: &[(&str, &str)] = &[
26    ("Args", "clap"),
27    ("Parser", "clap"),
28    ("Subcommand", "clap"),
29    ("ValueEnum", "clap"),
30    ("Add", "derive_more"),
31    ("AddAssign", "derive_more"),
32    ("AsMut", "derive_more"),
33    ("AsRef", "derive_more"),
34    ("Binary", "derive_more"),
35    ("BitAnd", "derive_more"),
36    ("BitAndAssign", "derive_more"),
37    ("BitOr", "derive_more"),
38    ("BitOrAssign", "derive_more"),
39    ("BitXor", "derive_more"),
40    ("BitXorAssign", "derive_more"),
41    ("Constructor", "derive_more"),
42    ("Deref", "derive_more"),
43    ("DerefMut", "derive_more"),
44    ("Display", "derive_more"),
45    ("Div", "derive_more"),
46    ("DivAssign", "derive_more"),
47    ("From", "derive_more"),
48    ("FromStr", "derive_more"),
49    ("Index", "derive_more"),
50    ("IndexMut", "derive_more"),
51    ("Into", "derive_more"),
52    ("IntoIterator", "derive_more"),
53    ("IsVariant", "derive_more"),
54    ("LowerExp", "derive_more"),
55    ("LowerHex", "derive_more"),
56    ("Mul", "derive_more"),
57    ("MulAssign", "derive_more"),
58    ("Neg", "derive_more"),
59    ("Not", "derive_more"),
60    ("Octal", "derive_more"),
61    ("Pointer", "derive_more"),
62    ("Product", "derive_more"),
63    ("Rem", "derive_more"),
64    ("RemAssign", "derive_more"),
65    ("Shl", "derive_more"),
66    ("ShlAssign", "derive_more"),
67    ("Shr", "derive_more"),
68    ("ShrAssign", "derive_more"),
69    ("Sub", "derive_more"),
70    ("SubAssign", "derive_more"),
71    ("Sum", "derive_more"),
72    ("TryFrom", "derive_more"),
73    ("TryInto", "derive_more"),
74    ("TryUnwrap", "derive_more"),
75    ("Unwrap", "derive_more"),
76    ("UpperExp", "derive_more"),
77    ("UpperHex", "derive_more"),
78    ("FromPrimitive", "num_enum"),
79    ("IntoPrimitive", "num_enum"),
80    ("TryFromPrimitive", "num_enum"),
81    ("UnsafeFromPrimitive", "num_enum"),
82    ("Deserialize", "serde"),
83    ("Serialize", "serde"),
84    ("Error", "thiserror"),
85];
86
87/// How the code generated by a derive macro names its own crate.
88///
89/// This is what decides whether a derive can be used through a re-export
90/// at all, and what has to be done to it if so.
91enum Naming {
92    /// Generated code does `extern crate serde as _serde`, overridable
93    /// with a `#[serde(crate = "...")]` string attribute.
94    CrateAttrStr,
95    /// Generated code uses an absolute path, overridable with a
96    /// `#[num_enum(crate = ...)]` path attribute.
97    CrateAttrPath,
98    /// Generated code names the crate relative to the enclosing scope, so
99    /// the crate name only has to be bound there.
100    Scoped,
101    /// Generated code uses an absolute path with no override, so only a
102    /// direct dependency can satisfy it.
103    ExternOnly,
104}
105
106fn naming(krate: &str) -> Naming {
107    match krate {
108        "serde" => Naming::CrateAttrStr,
109        "num_enum" => Naming::CrateAttrPath,
110        "clap" | "derive_more" => Naming::Scoped,
111        "thiserror" => Naming::ExternOnly,
112        _ => unreachable!("no naming strategy for `{krate}`"),
113    }
114}
115
116/// Apply derive macros re-exported by `rustmax`.
117///
118/// Most derive macros generate code that names their own crate, which
119/// fails when the crate is reached through `rustmax` instead of being a
120/// direct dependency. This attribute rewrites the derive paths and injects
121/// whatever each derive crate needs to find its runtime.
122///
123/// ```ignore
124/// #[rmx::derive(Serialize, Deserialize, Clone, Debug)]
125/// #[serde(rename_all = "camelCase")]
126/// struct Config {
127///     some_field: u32,
128/// }
129/// ```
130///
131/// Names it does not recognize are passed through to `#[derive(..)]`
132/// unchanged, so built-in and third-party derives can be listed alongside.
133#[proc_macro_attribute]
134pub fn derive(attr: TokenStream, item: TokenStream) -> TokenStream {
135    let item = TokenStream2::from(item);
136    let requests = match parse_requests(TokenStream2::from(attr)) {
137        Ok(requests) => requests,
138        Err(err) => return prepend(err, item).into(),
139    };
140
141    let rustmax = match rustmax_path() {
142        Ok(path) => path,
143        Err(err) => return prepend(err, item).into(),
144    };
145
146    let mut paths = Vec::new();
147    let mut scoped = Vec::new();
148    let mut crate_attrs = Vec::new();
149    let mut errors = TokenStream2::new();
150
151    for request in requests {
152        let (name, krate, span) = match request {
153            Request::Passthrough(tokens) => {
154                paths.push(tokens);
155                continue;
156            }
157            Request::Bare(name) => match lookup(&name.to_string()) {
158                Some(krate) => {
159                    let span = name.span();
160                    (name, krate, span)
161                }
162                None => {
163                    paths.push(TokenTree::Ident(name).into());
164                    continue;
165                }
166            },
167            Request::Qualified(krate, name) => match lookup_crate(&krate.to_string()) {
168                Some(krate) => {
169                    let span = name.span();
170                    (name, krate, span)
171                }
172                None => {
173                    paths.push(quote!(#krate::#name));
174                    continue;
175                }
176            },
177        };
178
179        match naming(krate) {
180            // A crate-path attribute works whether or not the crate is also
181            // a direct dependency, so always route these through `rustmax`.
182            Naming::CrateAttrStr | Naming::CrateAttrPath => {
183                if !crate_attrs.iter().any(|(k, _)| *k == krate) {
184                    crate_attrs.push((krate, crate_attr(krate, &rustmax)));
185                }
186            }
187            // Binding the crate's name in scope conflicts with a direct
188            // dependency of the same name, so only do it when there is none.
189            Naming::Scoped => match direct_path(krate) {
190                Some(direct) => {
191                    paths.push(quote!(#direct::#name));
192                    continue;
193                }
194                None => {
195                    if !scoped.contains(&krate) {
196                        scoped.push(krate);
197                    }
198                }
199            },
200            Naming::ExternOnly => match direct_path(krate) {
201                Some(direct) => {
202                    paths.push(quote!(#direct::#name));
203                    continue;
204                }
205                None => {
206                    errors.extend(extern_only_error(span, krate));
207                    continue;
208                }
209            },
210        }
211
212        let root = &rustmax.tokens;
213        let module = Ident::new(krate, span);
214        paths.push(quote!(#root::#module::#name));
215    }
216
217    let root = &rustmax.tokens;
218    let scope_imports = scoped.iter().map(|krate| {
219        let module = Ident::new(krate, Span::call_site());
220        quote! {
221            #[allow(unused_imports)]
222            use #root::__macro_scope::#module::*;
223        }
224    });
225    let crate_attrs = crate_attrs.into_iter().map(|(_, attr)| attr);
226    let derive_attr = (!paths.is_empty()).then(|| quote!(#[derive(#(#paths),*)]));
227
228    quote! {
229        #errors
230        #(#scope_imports)*
231        #derive_attr
232        #(#crate_attrs)*
233        #item
234    }
235    .into()
236}
237
238/// The crate-path attribute that tells a derive crate where its runtime is.
239fn crate_attr(krate: &str, rustmax: &RustmaxPath) -> TokenStream2 {
240    let module = Ident::new(krate, Span::call_site());
241    match naming(krate) {
242        Naming::CrateAttrStr => {
243            let path = Literal::string(&format!("{}::{}", rustmax.text, krate));
244            quote!(#[#module(crate = #path)])
245        }
246        Naming::CrateAttrPath => {
247            let root = &rustmax.tokens;
248            quote!(#[#module(crate = #root::#module)])
249        }
250        Naming::Scoped | Naming::ExternOnly => {
251            unreachable!("`{krate}` has no crate-path attribute")
252        }
253    }
254}
255
256fn extern_only_error(span: Span, krate: &str) -> TokenStream2 {
257    let message = format!(
258        "`{krate}` derives only work when `{krate}` is a direct dependency: \
259         the code they generate names `::{krate}` absolutely, which resolves \
260         only against direct dependencies. Add `{krate}` to your Cargo.toml \
261         alongside `rustmax`, or use `derive_more` instead, as in \
262         `#[rmx::derive(Display, derive_more::Error)]`."
263    );
264    error(span, &message)
265}
266
267/// The path by which the crate being compiled can name `rustmax`.
268struct RustmaxPath {
269    tokens: TokenStream2,
270    text: String,
271}
272
273fn rustmax_path() -> Result<RustmaxPath, TokenStream2> {
274    match crate_name("rustmax") {
275        Ok(FoundCrate::Itself) if in_doctest() => Ok(RustmaxPath {
276            tokens: quote!(::rustmax),
277            text: "::rustmax".to_owned(),
278        }),
279        Ok(FoundCrate::Itself) => Ok(RustmaxPath {
280            tokens: quote!(crate),
281            text: "crate".to_owned(),
282        }),
283        Ok(FoundCrate::Name(name)) => {
284            let ident = Ident::new(&name, Span::call_site());
285            Ok(RustmaxPath {
286                tokens: quote!(::#ident),
287                text: format!("::{name}"),
288            })
289        }
290        Err(_) => Err(error(
291            Span::call_site(),
292            "`#[rmx::derive]` requires `rustmax` to be a direct dependency \
293             of this crate",
294        )),
295    }
296}
297
298/// Whether the current compilation is a doctest.
299///
300/// `proc-macro-crate` reports `FoundCrate::Itself` whenever the manifest
301/// being compiled is `rustmax`'s own, but a doctest is a separate crate
302/// that reaches `rustmax` through the extern prelude, so `crate` does not
303/// name it there. `rustdoc` sets this variable for the doctests it builds.
304fn in_doctest() -> bool {
305    std::env::var_os("UNSTABLE_RUSTDOC_TEST_PATH").is_some()
306}
307
308/// The path by which the crate being compiled can name `krate`, if it is a
309/// direct dependency.
310///
311/// This reads the manifest, which does not distinguish `[dependencies]`
312/// from `[dev-dependencies]`, while the extern prelude of a given target
313/// contains only one of the two. A crate that is a normal dependency but
314/// not a dev-dependency is reported here for an integration test that
315/// cannot actually name it.
316fn direct_path(krate: &str) -> Option<TokenStream2> {
317    match crate_name(krate) {
318        Ok(FoundCrate::Itself) => Some(quote!(crate)),
319        Ok(FoundCrate::Name(name)) => {
320            let ident = Ident::new(&name, Span::call_site());
321            Some(quote!(::#ident))
322        }
323        Err(_) => None,
324    }
325}
326
327fn lookup(name: &str) -> Option<&'static str> {
328    KNOWN_DERIVES
329        .iter()
330        .find(|(known, _)| *known == name)
331        .map(|(_, krate)| *krate)
332}
333
334fn lookup_crate(name: &str) -> Option<&'static str> {
335    KNOWN_DERIVES
336        .iter()
337        .find(|(_, krate)| *krate == name)
338        .map(|(_, krate)| *krate)
339}
340
341/// One entry in the attribute's comma-separated list.
342enum Request {
343    /// A bare derive name, as in `Serialize`.
344    Bare(Ident),
345    /// A crate-qualified derive name, as in `derive_more::Debug`.
346    Qualified(Ident, Ident),
347    /// Anything else, passed to `#[derive(..)]` unchanged.
348    Passthrough(TokenStream2),
349}
350
351fn parse_requests(attr: TokenStream2) -> Result<Vec<Request>, TokenStream2> {
352    let mut requests = Vec::new();
353
354    for segment in split_commas(attr) {
355        let idents: Vec<&TokenTree> = segment.iter().collect();
356        match idents.as_slice() {
357            [] => continue,
358            [TokenTree::Ident(name)] => requests.push(Request::Bare(name.clone())),
359            [
360                TokenTree::Ident(krate),
361                TokenTree::Punct(first),
362                TokenTree::Punct(second),
363                TokenTree::Ident(name),
364            ] if first.as_char() == ':' && second.as_char() == ':' => {
365                requests.push(Request::Qualified(krate.clone(), name.clone()));
366            }
367            _ => {
368                let span = segment
369                    .first()
370                    .map(|tree| tree.span())
371                    .unwrap_or_else(Span::call_site);
372                if segment.iter().any(|tree| matches!(tree, TokenTree::Ident(_))) {
373                    requests.push(Request::Passthrough(segment.into_iter().collect()));
374                } else {
375                    return Err(error(
376                        span,
377                        "expected a derive macro name, as in \
378                         `#[rmx::derive(Serialize, Clone)]`",
379                    ));
380                }
381            }
382        }
383    }
384
385    Ok(requests)
386}
387
388fn split_commas(input: TokenStream2) -> Vec<Vec<TokenTree>> {
389    let mut segments = vec![Vec::new()];
390
391    for tree in input {
392        match &tree {
393            TokenTree::Punct(punct) if punct.as_char() == ',' => segments.push(Vec::new()),
394            _ => segments
395                .last_mut()
396                .expect("`segments` is never empty")
397                .push(tree),
398        }
399    }
400
401    segments
402}
403
404fn error(span: Span, message: &str) -> TokenStream2 {
405    quote_spanned!(span=> ::core::compile_error!{#message})
406}
407
408/// Emit `errors` ahead of `item` so that the item still parses and only the
409/// intended diagnostic is reported.
410fn prepend(errors: TokenStream2, item: TokenStream2) -> TokenStream2 {
411    quote! {
412        #errors
413        #item
414    }
415}