Imagine if you could hover over derive macro helper attributes and get documentation for them in your editor. For example, hover over rename_all in #[serde(rename_all = "camelCase")] and it describes what the attribute does
Turns out, it's already possible to do this, but I haven't seen anyone do it. That's why I'm making this post, to hopefully inform more macro authors that this is a thing you can do, and make derive macros in Rust friendlier to use.
Showcase
For example, in here:
#[derive(Clone, Debug, Deserialize)]
#[serde(deny_unknown_fields, rename = "Zee")]
pub struct EditorConfig {
#[serde(default)]
pub theme: String,
pub modes: Vec<ModeConfig>,
#[serde(default)]
pub trim_trailing_whitespace_on_save: bool,
}
I should be able to hover on deny_unknown_fields to get documentation about Serde's deny_unknown_fields attribute:
Always error during deserialization when encountering unknown fields. When this attribute is not present, by default unknown fields are ignored for self-describing formats like JSON.
Note: this attribute is not supported in combination with flatten, neither on the outer struct nor on the flattened field.
How to do it
If the derive(Serialize) macro expanded to this additional line:
use serde::docs::deny_unknown_fields as _;
And deny_unknown_fields had some documentation attached to it:
/// Always error during deseriali...
mod deny_unknown_fields {}
Then when I have this line:
#[serde(deny_unknown_fields, rename = "Zee")]
^^^^^^^^^^^^^^^^^^^
I can take span of the deny_unknown_fields identifier token, and attach it to the import:
use serde::docs::deny_unknown_fields as _;
^^^^^^^^^^^^^^^^^^^
Using 4 lines of code:
// This is the part that the user hovers on
let deny_unknown_fields_span: Span;
// Create this identifier:
// use foo::attr_docs::deny_unknown_fields
// ^^^^^^^^^^^^^^^^^^^
let mut deny_unknown_fields = TokenTree::Ident(Ident::new("deny_unknown_fields"));
// "Link" these 2 locations together by manipulating the span
deny_unknown_fields.set_span(deny_unknown_fields_span);
// Output the import in the macro expansion
let output = quote! { use serde::docs::#deny_unknown_fields as _; };
Now, hovering over the deny_unknown_fields in #[serde(deny_unknown_fields)] will be the same as hovering over deny_unknown_fields in serde::docs::deny_unknown_fields and will show the documentation
Full example
A self-contained example with 3 crates.
Open
crate hierarchy
$ cargo tree
bar
└── foo
└── foo_macros
crate bar
Cargo.toml
[package]
name = "bar"
version = "0.1.0"
edition = "2024"
[dependencies]
foo = { path = "../foo" }
src/lib.rs
#[derive(foo::Foo)]
#[foo(deny_unknown_fields)]
struct Type;
crate foo
Cargo.toml
[package]
name = "foo"
version = "0.1.0"
edition = "2024"
[dependencies]
foo_macros = { path = "../foo_macros" }
src/lib.rs
pub trait Foo {}
pub use foo_macros::Foo;
#[doc(hidden)]
pub mod attr_docs {
/// Documentation comments
pub mod deny_unknown_fields {}
}
crate foo_macros
Cargo.toml
[package]
name = "foo_macros"
version = "0.1.0"
edition = "2024"
[lib]
proc-macro = true
[dependencies]
quote = "1.0.42"
src/lib.rs
use proc_macro::{TokenStream, TokenTree};
use quote::{quote, quote_spanned};
#[proc_macro_derive(Foo, attributes(foo))]
pub fn derive_foo(input: TokenStream) -> TokenStream {
// Step 1: get the span for the thing we want to document
// #[foo(deny_unknown_fields)]
// ^^^^^^^^^^^^^^^^^^^ span of this identifier
let deny_unknown_fields_span = if let Some(TokenTree::Group(brackets)) =
input.into_iter().nth(1)
&& let Some(TokenTree::Group(parens)) = brackets.stream().into_iter().nth(1)
&& let Some(TokenTree::Ident(ident)) = parens.stream().into_iter().next()
{
ident.span()
} else {
unreachable!()
};
// Step 2: import something from our crate which contains documentation attached to it,
// and set our span to be the same as it
let deny_unknown_fields =
quote_spanned! { deny_unknown_fields_span.into() => deny_unknown_fields };
quote! {
use ::foo::attr_docs::#deny_unknown_fields as _;
impl ::foo::Foo for Type {}
}
.into()
}