rustdoc: Inherit inline attributes for declarative macros

When explicitly re-exporting a declarative macro by name, rustdoc
previously bypassed intermediate re-exports and dropped `#[doc(inline)]`
attributes, causing the macro to be incorrectly stripped if the original
definition was `#[doc(hidden)]`.

This updates `generate_item_with_correct_attrs` to walk the
`reexport_chain` specifically for declarative macros, allowing them to
inherit inline attributes exactly as glob imports do, while preserving
strict visibility rules for standard items.
This commit is contained in:
Shivendra Sharma
2026-04-06 21:48:47 +05:30
parent 9602bda1dd
commit f8d3c27650
3 changed files with 175 additions and 6 deletions
+31 -1
View File
@@ -176,6 +176,35 @@ fn is_glob_import(tcx: TyCtxt<'_>, import_id: LocalDefId) -> bool {
}
}
/// Returns true if `def_id` is a macro and should be inlined.
pub(crate) fn macro_reexport_is_inline(
tcx: TyCtxt<'_>,
import_id: LocalDefId,
def_id: DefId,
) -> bool {
if !matches!(tcx.def_kind(def_id), DefKind::Macro(MacroKinds::BANG)) {
return false;
}
for reexport_def_id in reexport_chain(tcx, import_id, def_id).iter().flat_map(|r| r.id()) {
let is_hidden = tcx.is_doc_hidden(reexport_def_id);
let is_inline = find_attr!(
inline::load_attrs(tcx, reexport_def_id),
Doc(d)
if d.inline.first().is_some_and(|(inline, _)| *inline == DocInline::Inline)
);
// hidden takes absolute priority over inline on the same node
if is_hidden {
return false;
}
if is_inline {
return true;
}
}
false
}
fn generate_item_with_correct_attrs(
cx: &mut DocContext<'_>,
kind: ItemKind,
@@ -201,7 +230,8 @@ fn generate_item_with_correct_attrs(
Doc(d)
if d.inline.first().is_some_and(|(inline, _)| *inline == DocInline::Inline)
) || (is_glob_import(tcx, import_id)
&& (cx.document_hidden() || !tcx.is_doc_hidden(def_id)));
&& (cx.document_hidden() || !tcx.is_doc_hidden(def_id)))
|| macro_reexport_is_inline(tcx, import_id, def_id);
attrs.extend(get_all_import_attributes(cx, import_id, def_id, is_inline));
is_inline = is_inline || import_is_inline;
}
+15 -5
View File
@@ -478,11 +478,21 @@ fn visit_item_inner(
// If there was a private module in the current path then don't bother inlining
// anything as it will probably be stripped anyway.
if is_pub && self.inside_public_path {
let please_inline = find_attr!(
attrs,
Doc(d)
if d.inline.first().is_some_and(|(inline, _)| *inline == DocInline::Inline)
);
let please_inline = if let Some(res_did) = res.opt_def_id()
&& matches!(tcx.def_kind(res_did), DefKind::Macro(MacroKinds::BANG))
{
crate::clean::macro_reexport_is_inline(
tcx,
item.owner_id.def_id,
res_did,
)
} else {
find_attr!(
attrs,
Doc(d)
if d.inline.first().is_some_and(|(inline, _)| *inline == DocInline::Inline)
)
};
let ident = match kind {
hir::UseKind::Single(ident) => Some(ident.name),
hir::UseKind::Glob => None,
+129
View File
@@ -0,0 +1,129 @@
// Regression test for <https://github.com/rust-lang/rust/issues/154694>.
// The goal is to ensure that declarative macros re-exported by name
// inherit the `#[doc(inline)]` attribute from intermediate re-exports,
// matching the behavior of glob re-exports.
#![crate_name = "foo"]
#[macro_use]
mod macros {
#[macro_export]
#[doc(hidden)]
macro_rules! explicit_macro {
() => {};
}
#[macro_export]
#[doc(hidden)]
macro_rules! wild_macro {
() => {};
}
#[macro_export]
#[doc(hidden)]
macro_rules! actually_hidden_macro {
() => {};
}
#[macro_export]
#[doc(hidden)]
macro_rules! actually_hidden_wild_macro {
() => {};
}
#[macro_export]
#[doc(hidden)]
macro_rules! actually_hidden_indirect_macro {
() => {};
}
}
// Standard items (like structs) are provided as control cases to ensure
// macro inlining behavior maintains parity.
#[doc(hidden)]
pub struct HiddenStruct;
#[doc(hidden)]
pub struct IndirectlyHiddenStruct;
pub mod bar {
mod hidden_explicit {
#[doc(inline)]
pub use crate::explicit_macro;
}
mod hidden_wild {
#[doc(inline)]
pub use crate::wild_macro;
}
mod actually_hidden {
// BUG: as demonstrated by the `actually_hidden_struct` module, when both
// `doc(hidden)` and `doc(inline)` are specified, `doc(hidden)`
// should take priority.
#[doc(hidden)]
#[doc(inline)]
pub use crate::actually_hidden_macro;
}
mod actually_hidden_indirect_inner {
#[doc(inline)]
pub use crate::actually_hidden_indirect_macro;
}
mod actually_hidden_indirect {
// BUG: when there is a chain of imports, we should stop looking as soon as soon as we hit
// something with `doc(hidden)`.
#[doc(hidden)]
pub use super::actually_hidden_indirect_inner::actually_hidden_indirect_macro;
}
mod actually_hidden_indirect_struct_inner {
#[doc(inline)]
pub use crate::IndirectlyHiddenStruct;
}
mod actually_hidden_indirect_struct {
#[doc(hidden)]
pub use super::actually_hidden_indirect_struct_inner::IndirectlyHiddenStruct;
}
mod actually_hidden_wild {
#[doc(hidden)]
#[doc(inline)]
pub use crate::actually_hidden_wild_macro;
}
mod actually_hidden_struct {
#[doc(inline)]
#[doc(hidden)]
pub use crate::HiddenStruct;
}
// First, we check that the explicitly named macro inherits the inline attribute
// from `hidden_explicit` and is successfully rendered.
//@ has 'foo/bar/macro.explicit_macro.html'
//@ has 'foo/bar/index.html' '//a[@href="macro.explicit_macro.html"]' 'explicit_macro'
pub use self::hidden_explicit::explicit_macro;
// Next, we ensure that the glob-imported macro continues to render correctly
// as a control case.
//@ has 'foo/bar/macro.wild_macro.html'
//@ has 'foo/bar/index.html' '//a[@href="macro.wild_macro.html"]' 'wild_macro'
pub use self::hidden_wild::*;
//@ !has 'foo/bar/macro.actually_hidden_macro.html'
pub use self::actually_hidden::actually_hidden_macro;
//@ !has 'foo/bar/macro.actually_hidden_wild_macro.html'
pub use self::actually_hidden_wild::*;
//@ !has 'foo/bar/struct.HiddenStruct.html'
pub use self::actually_hidden_struct::HiddenStruct;
//@ !has 'foo/bar/macro.actually_hidden_indirect_macro.html'
pub use self::actually_hidden_indirect::actually_hidden_indirect_macro;
//@ !has 'foo/bar/struct.IndirectlyHiddenStruct.html'
pub use self::actually_hidden_indirect_struct::IndirectlyHiddenStruct;
}