What if hovering on `deny_unknown_fields` showed documentation?

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()
}
13 Likes

This is cool! This should make its way into the derive / proc macro section of the Rust book. I read the whole book and regularly write proc macros for small tasks, but regularly forget this is a thing too :slight_smile: