Idea: Concatenate strings in #[doc]

It's often convenient to generate several items (functions/types/impls/...) with macro_rules!. However, substituting parts of documentation comments is awkward:

macro_rules! impl_function {
    ($name:ident, $usage:literal) => {
        /// # Usage:
        ///
        /// ```
        #[doc = $usage]
        /// ```
        pub fn $name() {}
    };
}

impl_function!(foo, " let _ = foo();");

What if we could instead write

macro_rules! impl_function {
    ($name:ident) => {
        /// # Usage:
        ///
        /// ```
        #[doc = concat!(" let _ = ", stringify!($name), "();")]
        /// ```
        pub fn $name() {}
    };
}

impl_function!(foo);

What do you think? Would this be difficult to implement?

https://docs.rs/doc-comment/0.3.3/doc_comment/macro.doc_comment.html

This doc_comment! macro is the common workaround, that allows you to write

doc_comment! {
    concat!("words", stringify!(foo), "words"),
    fn foo() {}
}

This is actually almost possible today! There's an open issue about this in the rust repository: https://github.com/rust-lang/rust/issues/52607

It turns out that the way rust expands macros, if concat!() is used as an argument to another proc-macro, it will be passed successfully into #[doc = ...], whereas using it directly will trigger the error.

I've worked around this in my projects with the following macro:

/// Declares an item with a doc attribute computed by some macro expression.
/// This allows documentation to be dynamically generated based on input.
/// Necessary to work around https://github.com/rust-lang/rust/issues/52607.
macro_rules! calculated_doc {
    (
        $(
            #[doc = $doc:expr]
            $thing:item
        )*
    ) => (
        $(
            #[doc = $doc]
            $thing
        )*
    );
}

This can be expanded/modified to support arbitrary other doc comments! One way would be to mark the specific one to expand using a special symbol, such as @, and to rely on the fact that item includes arbitrary meta attributes as well, like so:


/// Declares an item with a doc attribute computed by some macro expression.
/// This allows documentation to be dynamically generated based on input.
/// Necessary to work around https://github.com/rust-lang/rust/issues/52607.
macro_rules! calculated_doc {
    (
        $(
            $(#[$m:meta])*
            @#[doc = $doc:expr]
            $thing:item
        )*
    ) => (
        $(
            $(#[$m])*
            #[doc = $doc]
            $thing
        )*
    );
}

This could then be used like this:

macro_rules! impl_function {
    ($name:ident) => {
        calculated_doc! {
            /// # Usage:
            ///
            /// ```
            @#[doc = concat!(" let _ = ", stringify!($name), "();")]
            /// ```
            pub fn $name() {}
        }
    };
}

Hopefully that helps - depending on what you're doing, it might make sense to just use an inner macro particular to your macro - something like this:


macro_rules! impl_function {
    ($name:ident) => {
        impl_function!($name, concat!(" let _ = ", stringify!($name), "();"));
    };
    ($name:ident, $usage:expr) => {
        /// # Usage:
        ///
        /// ```
        #[doc = $usage]
        /// ```
        pub fn $name() {}
    };
}
impl_function!(hi);

This whole thing is definitely a bit hacky, but as there are published crates which inadvertently use this (or purposefully use this), I don't think it'll be removed anytime soon.

1 Like