From da1997edc908237d9ede4498e797b06c6d7d5ea5 Mon Sep 17 00:00:00 2001 From: Guillaume Gomez Date: Thu, 14 Aug 2025 17:51:40 +0200 Subject: [PATCH 1/7] Support new bang macro kinds in rustdoc --- src/librustdoc/clean/inline.rs | 27 +- src/librustdoc/clean/mod.rs | 31 ++- src/librustdoc/clean/types.rs | 24 +- src/librustdoc/fold.rs | 2 +- src/librustdoc/html/render/context.rs | 60 ++++- src/librustdoc/html/render/mod.rs | 49 ++-- src/librustdoc/html/render/print_item.rs | 317 +++++++++++++---------- src/librustdoc/html/render/sidebar.rs | 8 +- src/librustdoc/html/static/js/main.js | 12 +- src/librustdoc/json/conversions.rs | 2 +- src/librustdoc/visit.rs | 2 +- 11 files changed, 330 insertions(+), 204 deletions(-) diff --git a/src/librustdoc/clean/inline.rs b/src/librustdoc/clean/inline.rs index b470af50f68fe..28076a0b7db67 100644 --- a/src/librustdoc/clean/inline.rs +++ b/src/librustdoc/clean/inline.rs @@ -140,13 +140,12 @@ pub(crate) fn try_inline( Res::Def(DefKind::Macro(kinds), did) => { let mac = build_macro(cx, did, name, kinds); - // FIXME: handle attributes and derives that aren't proc macros, and macros with - // multiple kinds let type_kind = match kinds { MacroKinds::BANG => ItemType::Macro, MacroKinds::ATTR => ItemType::ProcAttribute, MacroKinds::DERIVE => ItemType::ProcDerive, - _ => todo!("Handle macros with multiple kinds"), + _ if kinds.contains(MacroKinds::BANG) => ItemType::Macro, + _ => panic!("unsupported macro kind {kinds:?}"), }; record_extern_fqn(cx, did, type_kind); mac @@ -764,13 +763,14 @@ fn build_macro( macro_kinds: MacroKinds, ) -> clean::ItemKind { match CStore::from_tcx(cx.tcx).load_macro_untracked(def_id, cx.tcx) { - // FIXME: handle attributes and derives that aren't proc macros, and macros with multiple - // kinds LoadedMacro::MacroDef { def, .. } => match macro_kinds { - MacroKinds::BANG => clean::MacroItem(clean::Macro { - source: utils::display_macro_source(cx, name, &def), - macro_rules: def.macro_rules, - }), + MacroKinds::BANG => clean::MacroItem( + clean::Macro { + source: utils::display_macro_source(cx, name, &def), + macro_rules: def.macro_rules, + }, + None, + ), MacroKinds::DERIVE => clean::ProcMacroItem(clean::ProcMacro { kind: MacroKind::Derive, helpers: Vec::new(), @@ -779,7 +779,14 @@ fn build_macro( kind: MacroKind::Attr, helpers: Vec::new(), }), - _ => todo!("Handle macros with multiple kinds"), + _ if macro_kinds == (MacroKinds::BANG | MacroKinds::ATTR) => clean::MacroItem( + clean::Macro { + source: utils::display_macro_source(cx, name, &def), + macro_rules: def.macro_rules, + }, + Some(macro_kinds), + ), + _ => panic!("unsupported macro kind {macro_kinds:?}"), }, LoadedMacro::ProcMacro(ext) => { // Proc macros can only have a single kind diff --git a/src/librustdoc/clean/mod.rs b/src/librustdoc/clean/mod.rs index 4a95f21a3a5bd..b8507b52fd5b8 100644 --- a/src/librustdoc/clean/mod.rs +++ b/src/librustdoc/clean/mod.rs @@ -2841,19 +2841,24 @@ fn clean_maybe_renamed_item<'tcx>( generics: clean_generics(generics, cx), fields: variant_data.fields().iter().map(|x| clean_field(x, cx)).collect(), }), - // FIXME: handle attributes and derives that aren't proc macros, and macros with - // multiple kinds - ItemKind::Macro(_, macro_def, MacroKinds::BANG) => MacroItem(Macro { - source: display_macro_source(cx, name, macro_def), - macro_rules: macro_def.macro_rules, - }), - ItemKind::Macro(_, _, MacroKinds::ATTR) => { - clean_proc_macro(item, &mut name, MacroKind::Attr, cx) - } - ItemKind::Macro(_, _, MacroKinds::DERIVE) => { - clean_proc_macro(item, &mut name, MacroKind::Derive, cx) - } - ItemKind::Macro(_, _, _) => todo!("Handle macros with multiple kinds"), + ItemKind::Macro(_, macro_def, kinds) => match kinds { + MacroKinds::BANG => MacroItem( + Macro { + source: display_macro_source(cx, name, macro_def), + macro_rules: macro_def.macro_rules, + }, + None, + ), + MacroKinds::ATTR => clean_proc_macro(item, &mut name, MacroKind::Attr, cx), + MacroKinds::DERIVE => clean_proc_macro(item, &mut name, MacroKind::Derive, cx), + _ => MacroItem( + Macro { + source: display_macro_source(cx, name, macro_def), + macro_rules: macro_def.macro_rules, + }, + Some(kinds), + ), + }, // proc macros can have a name set by attributes ItemKind::Fn { ref sig, generics, body: body_id, .. } => { clean_fn_or_proc_macro(item, sig, generics, body_id, &mut name, cx) diff --git a/src/librustdoc/clean/types.rs b/src/librustdoc/clean/types.rs index 73d0f40275404..5c2f89b2ce95b 100644 --- a/src/librustdoc/clean/types.rs +++ b/src/librustdoc/clean/types.rs @@ -9,7 +9,7 @@ use rustc_abi::{ExternAbi, VariantIdx}; use rustc_data_structures::fx::{FxHashSet, FxIndexMap, FxIndexSet}; use rustc_data_structures::thin_vec::ThinVec; use rustc_hir::attrs::{AttributeKind, DeprecatedSince, Deprecation}; -use rustc_hir::def::{CtorKind, DefKind, Res}; +use rustc_hir::def::{CtorKind, DefKind, MacroKinds, Res}; use rustc_hir::def_id::{CrateNum, DefId, LOCAL_CRATE, LocalDefId}; use rustc_hir::lang_items::LangItem; use rustc_hir::{BodyId, ConstStability, Mutability, Stability, StableSince, find_attr}; @@ -664,6 +664,24 @@ impl Item { find_attr!(&self.attrs.other_attrs, AttributeKind::NonExhaustive(..)) } + pub(crate) fn bang_macro_types(&self) -> Option> { + match self.kind { + ItemKind::MacroItem(_, None) => Some(vec![ItemType::Macro]), + ItemKind::MacroItem(_, Some(kinds)) => Some( + kinds + .iter() + .map(|kind| match kind { + MacroKinds::BANG => ItemType::Macro, + MacroKinds::ATTR => ItemType::ProcAttribute, + MacroKinds::DERIVE => ItemType::ProcDerive, + _ => panic!("unexpected macro kind {kind:?}"), + }) + .collect::>(), + ), + _ => None, + } + } + /// Returns a documentation-level item type from the item. pub(crate) fn type_(&self) -> ItemType { ItemType::from(self) @@ -834,7 +852,7 @@ pub(crate) enum ItemKind { ForeignStaticItem(Static, hir::Safety), /// `type`s from an extern block ForeignTypeItem, - MacroItem(Macro), + MacroItem(Macro, Option), ProcMacroItem(ProcMacro), PrimitiveItem(PrimitiveType), /// A required associated constant in a trait declaration. @@ -889,7 +907,7 @@ impl ItemKind { | ForeignFunctionItem(_, _) | ForeignStaticItem(_, _) | ForeignTypeItem - | MacroItem(_) + | MacroItem(..) | ProcMacroItem(_) | PrimitiveItem(_) | RequiredAssocConstItem(..) diff --git a/src/librustdoc/fold.rs b/src/librustdoc/fold.rs index ee5f260615db5..1d4bdc4f2f047 100644 --- a/src/librustdoc/fold.rs +++ b/src/librustdoc/fold.rs @@ -88,7 +88,7 @@ pub(crate) trait DocFolder: Sized { | ForeignFunctionItem(..) | ForeignStaticItem(..) | ForeignTypeItem - | MacroItem(_) + | MacroItem(..) | ProcMacroItem(_) | PrimitiveItem(_) | RequiredAssocConstItem(..) diff --git a/src/librustdoc/html/render/context.rs b/src/librustdoc/html/render/context.rs index 4c06d0da47013..30f827ad3159e 100644 --- a/src/librustdoc/html/render/context.rs +++ b/src/librustdoc/html/render/context.rs @@ -13,6 +13,7 @@ use rustc_middle::ty::TyCtxt; use rustc_session::Session; use rustc_span::edition::Edition; use rustc_span::{BytePos, FileName, Symbol, sym}; +use serde::ser::SerializeSeq; use tracing::info; use super::print_item::{full_path, print_item, print_item_path}; @@ -166,6 +167,27 @@ impl SharedContext<'_> { } } +struct SidebarItem { + name: String, + is_actually_macro: bool, +} + +impl serde::Serialize for SidebarItem { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + if self.is_actually_macro { + let mut seq = serializer.serialize_seq(Some(2))?; + seq.serialize_element(&self.name)?; + seq.serialize_element(&1)?; + seq.end() + } else { + serializer.serialize_some(&Some(&self.name)) + } + } +} + impl<'tcx> Context<'tcx> { pub(crate) fn tcx(&self) -> TyCtxt<'tcx> { self.shared.tcx @@ -306,7 +328,20 @@ impl<'tcx> Context<'tcx> { } /// Construct a map of items shown in the sidebar to a plain-text summary of their docs. - fn build_sidebar_items(&self, m: &clean::Module) -> BTreeMap> { + fn build_sidebar_items(&self, m: &clean::Module) -> BTreeMap> { + fn build_sidebar_items_inner( + name: Symbol, + type_: ItemType, + map: &mut BTreeMap>, + inserted: &mut FxHashMap>, + is_actually_macro: bool, + ) { + if inserted.entry(type_).or_default().insert(name) { + let type_ = type_.to_string(); + let name = name.to_string(); + map.entry(type_).or_default().push(SidebarItem { name, is_actually_macro }); + } + } // BTreeMap instead of HashMap to get a sorted output let mut map: BTreeMap<_, Vec<_>> = BTreeMap::new(); let mut inserted: FxHashMap> = FxHashMap::default(); @@ -315,23 +350,24 @@ impl<'tcx> Context<'tcx> { if item.is_stripped() { continue; } - - let short = item.type_(); - let myname = match item.name { + let name = match item.name { None => continue, Some(s) => s, }; - if inserted.entry(short).or_default().insert(myname) { - let short = short.to_string(); - let myname = myname.to_string(); - map.entry(short).or_default().push(myname); + + if let Some(types) = item.bang_macro_types() { + for type_ in types { + build_sidebar_items_inner(name, type_, &mut map, &mut inserted, true); + } + } else { + build_sidebar_items_inner(name, item.type_(), &mut map, &mut inserted, false); } } match self.shared.module_sorting { ModuleSorting::Alphabetical => { for items in map.values_mut() { - items.sort(); + items.sort_by(|a, b| a.name.cmp(&b.name)); } } ModuleSorting::DeclarationOrder => {} @@ -864,7 +900,11 @@ impl<'tcx> FormatRenderer<'tcx> for Context<'tcx> { self.shared.fs.write(joint_dst, buf)?; if !self.info.render_redirect_pages { - self.shared.all.borrow_mut().append(full_path(self, item), &item_type); + self.shared.all.borrow_mut().append( + full_path(self, item), + &item_type, + item.bang_macro_types(), + ); } // If the item is a macro, redirect from the old macro URL (with !) // to the new one (without). diff --git a/src/librustdoc/html/render/mod.rs b/src/librustdoc/html/render/mod.rs index 13178ee4e9934..402db67e25302 100644 --- a/src/librustdoc/html/render/mod.rs +++ b/src/librustdoc/html/render/mod.rs @@ -505,33 +505,46 @@ impl AllTypes { } } - fn append(&mut self, item_name: String, item_type: &ItemType) { + fn append( + &mut self, + item_name: String, + item_type: &ItemType, + extra_types: Option>, + ) { let mut url: Vec<_> = item_name.split("::").skip(1).collect(); if let Some(name) = url.pop() { let new_url = format!("{}/{item_type}.{name}.html", url.join("/")); url.push(name); let name = url.join("::"); - match *item_type { - ItemType::Struct => self.structs.insert(ItemEntry::new(new_url, name)), - ItemType::Enum => self.enums.insert(ItemEntry::new(new_url, name)), - ItemType::Union => self.unions.insert(ItemEntry::new(new_url, name)), - ItemType::Primitive => self.primitives.insert(ItemEntry::new(new_url, name)), - ItemType::Trait => self.traits.insert(ItemEntry::new(new_url, name)), - ItemType::Macro => self.macros.insert(ItemEntry::new(new_url, name)), - ItemType::Function => self.functions.insert(ItemEntry::new(new_url, name)), - ItemType::TypeAlias => self.type_aliases.insert(ItemEntry::new(new_url, name)), - ItemType::Static => self.statics.insert(ItemEntry::new(new_url, name)), - ItemType::Constant => self.constants.insert(ItemEntry::new(new_url, name)), - ItemType::ProcAttribute => { - self.attribute_macros.insert(ItemEntry::new(new_url, name)) + if let Some(extra_types) = extra_types { + for extra_type in extra_types { + self.append_with_url(name.clone(), &extra_type, new_url.clone()); } - ItemType::ProcDerive => self.derive_macros.insert(ItemEntry::new(new_url, name)), - ItemType::TraitAlias => self.trait_aliases.insert(ItemEntry::new(new_url, name)), - _ => true, - }; + } else { + self.append_with_url(name, item_type, new_url); + } } } + fn append_with_url(&mut self, name: String, item_type: &ItemType, url: String) { + match *item_type { + ItemType::Struct => self.structs.insert(ItemEntry::new(url, name)), + ItemType::Enum => self.enums.insert(ItemEntry::new(url, name)), + ItemType::Union => self.unions.insert(ItemEntry::new(url, name)), + ItemType::Primitive => self.primitives.insert(ItemEntry::new(url, name)), + ItemType::Trait => self.traits.insert(ItemEntry::new(url, name)), + ItemType::Macro => self.macros.insert(ItemEntry::new(url, name)), + ItemType::Function => self.functions.insert(ItemEntry::new(url, name)), + ItemType::TypeAlias => self.type_aliases.insert(ItemEntry::new(url, name)), + ItemType::Static => self.statics.insert(ItemEntry::new(url, name)), + ItemType::Constant => self.constants.insert(ItemEntry::new(url, name)), + ItemType::ProcAttribute => self.attribute_macros.insert(ItemEntry::new(url, name)), + ItemType::ProcDerive => self.derive_macros.insert(ItemEntry::new(url, name)), + ItemType::TraitAlias => self.trait_aliases.insert(ItemEntry::new(url, name)), + _ => true, + }; + } + fn item_sections(&self) -> FxHashSet { let mut sections = FxHashSet::default(); diff --git a/src/librustdoc/html/render/print_item.rs b/src/librustdoc/html/render/print_item.rs index adfc7481c73ae..7fd35440fd32d 100644 --- a/src/librustdoc/html/render/print_item.rs +++ b/src/librustdoc/html/render/print_item.rs @@ -7,7 +7,7 @@ use rustc_abi::VariantIdx; use rustc_ast::join_path_syms; use rustc_data_structures::fx::{FxHashMap, FxIndexSet}; use rustc_hir as hir; -use rustc_hir::def::CtorKind; +use rustc_hir::def::{CtorKind, MacroKinds}; use rustc_hir::def_id::DefId; use rustc_index::IndexVec; use rustc_middle::ty::{self, TyCtxt}; @@ -212,6 +212,9 @@ pub(super) fn print_item(cx: &Context<'_>, item: &clean::Item) -> impl fmt::Disp let item_vars = ItemVars { typ, name: item.name.as_ref().unwrap().as_str(), + // If `type_` returns `None`, it means it's a bang macro with multiple kinds, but + // since we're generating its documentation page, we can default to the "parent" type, + // ie "bang macro". item_type: &item.type_().to_string(), path_components, stability_since_raw: &stability_since_raw, @@ -236,7 +239,7 @@ pub(super) fn print_item(cx: &Context<'_>, item: &clean::Item) -> impl fmt::Disp clean::TypeAliasItem(t) => { write!(buf, "{}", item_type_alias(cx, item, t)) } - clean::MacroItem(m) => write!(buf, "{}", item_macro(cx, item, m)), + clean::MacroItem(m, kinds) => write!(buf, "{}", item_macro(cx, item, m, *kinds)), clean::ProcMacroItem(m) => { write!(buf, "{}", item_proc_macro(cx, item, m)) } @@ -307,8 +310,21 @@ fn item_module(cx: &Context<'_>, item: &clean::Item, items: &[clean::Item]) -> i fmt::from_fn(|w| { write!(w, "{}", document(cx, item, None, HeadingOffset::H2))?; - let mut not_stripped_items = - items.iter().filter(|i| !i.is_stripped()).enumerate().collect::>(); + let mut not_stripped_items: FxHashMap> = + FxHashMap::default(); + + let mut index = 0; + for item in items.iter().filter(|i| !i.is_stripped()) { + if let Some(types) = item.bang_macro_types() { + for type_ in types { + not_stripped_items.entry(type_).or_default().push((index, item)); + index += 1; + } + } else { + not_stripped_items.entry(item.type_()).or_default().push((index, item)); + index += 1; + } + } // the order of item types in the listing fn reorder(ty: ItemType) -> u8 { @@ -331,11 +347,6 @@ fn item_module(cx: &Context<'_>, item: &clean::Item, items: &[clean::Item]) -> i } fn cmp(i1: &clean::Item, i2: &clean::Item, tcx: TyCtxt<'_>) -> Ordering { - let rty1 = reorder(i1.type_()); - let rty2 = reorder(i2.type_()); - if rty1 != rty2 { - return rty1.cmp(&rty2); - } let is_stable1 = i1.stability(tcx).as_ref().map(|s| s.level.is_stable()).unwrap_or(true); let is_stable2 = @@ -356,8 +367,12 @@ fn item_module(cx: &Context<'_>, item: &clean::Item, items: &[clean::Item]) -> i let tcx = cx.tcx(); match cx.shared.module_sorting { - ModuleSorting::Alphabetical => { - not_stripped_items.sort_by(|(_, i1), (_, i2)| cmp(i1, i2, tcx)); + ModuleSorting::Alphabetical => + { + #[allow(rustc::potential_query_instability)] + for items in not_stripped_items.values_mut() { + items.sort_by(|(_, i1), (_, i2)| cmp(i1, i2, tcx)); + } } ModuleSorting::DeclarationOrder => {} } @@ -380,155 +395,156 @@ fn item_module(cx: &Context<'_>, item: &clean::Item, items: &[clean::Item]) -> i // can be identical even if the elements are different (mostly in imports). // So in case this is an import, we keep everything by adding a "unique id" // (which is the position in the vector). - not_stripped_items.dedup_by_key(|(idx, i)| { - ( - i.item_id, - if i.name.is_some() { Some(full_path(cx, i)) } else { None }, - i.type_(), - if i.is_import() { *idx } else { 0 }, - ) - }); + #[allow(rustc::potential_query_instability)] + for items in not_stripped_items.values_mut() { + items.dedup_by_key(|(idx, i)| { + ( + i.item_id, + if i.name.is_some() { Some(full_path(cx, i)) } else { None }, + i.type_(), + if i.is_import() { *idx } else { 0 }, + ) + }); + } debug!("{not_stripped_items:?}"); - let mut last_section = None; - for (_, myitem) in ¬_stripped_items { - let my_section = item_ty_to_section(myitem.type_()); - if Some(my_section) != last_section { - if last_section.is_some() { - w.write_str(ITEM_TABLE_CLOSE)?; - } - last_section = Some(my_section); - let section_id = my_section.id(); - let tag = - if section_id == "reexports" { REEXPORTS_TABLE_OPEN } else { ITEM_TABLE_OPEN }; - write!( - w, - "{}", - write_section_heading(my_section.name(), &cx.derive_id(section_id), None, tag) - )?; - } + #[allow(rustc::potential_query_instability)] + let mut types = not_stripped_items.keys().copied().collect::>(); + types.sort_unstable_by(|a, b| reorder(*a).cmp(&reorder(*b))); + + for type_ in types { + let my_section = item_ty_to_section(type_); + let section_id = my_section.id(); + let tag = + if section_id == "reexports" { REEXPORTS_TABLE_OPEN } else { ITEM_TABLE_OPEN }; + write!( + w, + "{}", + write_section_heading(my_section.name(), &cx.derive_id(section_id), None, tag) + )?; - match myitem.kind { - clean::ExternCrateItem { ref src } => { - use crate::html::format::print_anchor; + for (_, myitem) in ¬_stripped_items[&type_] { + match myitem.kind { + clean::ExternCrateItem { ref src } => { + use crate::html::format::print_anchor; - match *src { - Some(src) => { - write!( - w, - "
{}extern crate {} as {};", - visibility_print_with_space(myitem, cx), - print_anchor(myitem.item_id.expect_def_id(), src, cx), - EscapeBodyTextWithWbr(myitem.name.unwrap().as_str()) - )?; - } - None => { - write!( - w, - "
{}extern crate {};", - visibility_print_with_space(myitem, cx), - print_anchor( - myitem.item_id.expect_def_id(), - myitem.name.unwrap(), - cx - ) - )?; + match *src { + Some(src) => { + write!( + w, + "
{}extern crate {} as {};", + visibility_print_with_space(myitem, cx), + print_anchor(myitem.item_id.expect_def_id(), src, cx), + EscapeBodyTextWithWbr(myitem.name.unwrap().as_str()) + )?; + } + None => { + write!( + w, + "
{}extern crate {};", + visibility_print_with_space(myitem, cx), + print_anchor( + myitem.item_id.expect_def_id(), + myitem.name.unwrap(), + cx + ) + )?; + } } } - w.write_str("
")?; - } - clean::ImportItem(ref import) => { - let stab_tags = import.source.did.map_or_else(String::new, |import_def_id| { - print_extra_info_tags(tcx, myitem, item, Some(import_def_id)).to_string() - }); - - let id = match import.kind { - clean::ImportKind::Simple(s) => { - format!(" id=\"{}\"", cx.derive_id(format!("reexport.{s}"))) - } - clean::ImportKind::Glob => String::new(), - }; - write!( - w, - "\ - " - )?; - render_attributes_in_code(w, myitem, "", cx); - write!( - w, - "{vis}{imp}{stab_tags}\ - ", - vis = visibility_print_with_space(myitem, cx), - imp = import.print(cx) - )?; - } + clean::ImportItem(ref import) => { + let stab_tags = + import.source.did.map_or_else(String::new, |import_def_id| { + print_extra_info_tags(tcx, myitem, item, Some(import_def_id)) + .to_string() + }); - _ => { - if myitem.name.is_none() { - continue; + let id = match import.kind { + clean::ImportKind::Simple(s) => { + format!(" id=\"{}\"", cx.derive_id(format!("reexport.{s}"))) + } + clean::ImportKind::Glob => String::new(), + }; + write!( + w, + "\ + " + )?; + render_attributes_in_code(w, myitem, "", cx); + write!( + w, + "{vis}{imp}{stab_tags}\ + ", + vis = visibility_print_with_space(myitem, cx), + imp = import.print(cx) + )?; } - let unsafety_flag = match myitem.kind { - clean::FunctionItem(_) | clean::ForeignFunctionItem(..) - if myitem.fn_header(tcx).unwrap().safety - == hir::HeaderSafety::Normal(hir::Safety::Unsafe) => - { - "" - } - clean::ForeignStaticItem(_, hir::Safety::Unsafe) => { - "" + _ => { + if myitem.name.is_none() { + continue; } - _ => "", - }; - - let visibility_and_hidden = match myitem.visibility(tcx) { - Some(ty::Visibility::Restricted(_)) => { - if myitem.is_doc_hidden() { - // Don't separate with a space when there are two of them - " 🔒👻 " - } else { - " 🔒 " + + let unsafety_flag = match myitem.kind { + clean::FunctionItem(_) | clean::ForeignFunctionItem(..) + if myitem.fn_header(tcx).unwrap().safety + == hir::HeaderSafety::Normal(hir::Safety::Unsafe) => + { + "" } - } - _ if myitem.is_doc_hidden() => { - " 👻 " - } - _ => "", - }; + clean::ForeignStaticItem(_, hir::Safety::Unsafe) => { + "" + } + _ => "", + }; - let docs = - MarkdownSummaryLine(&myitem.doc_value(), &myitem.links(cx)).into_string(); - let (docs_before, docs_after) = - if docs.is_empty() { ("", "") } else { ("
", "
") }; - write!( - w, - "
\ - \ - {name}\ - \ - {visibility_and_hidden}\ - {unsafety_flag}\ - {stab_tags}\ -
\ - {docs_before}{docs}{docs_after}", - name = EscapeBodyTextWithWbr(myitem.name.unwrap().as_str()), - visibility_and_hidden = visibility_and_hidden, - stab_tags = print_extra_info_tags(tcx, myitem, item, None), - class = myitem.type_(), - unsafety_flag = unsafety_flag, - href = print_item_path(myitem.type_(), myitem.name.unwrap().as_str()), - title1 = myitem.type_(), - title2 = full_path(cx, myitem), - )?; + let visibility_and_hidden = match myitem.visibility(tcx) { + Some(ty::Visibility::Restricted(_)) => { + if myitem.is_doc_hidden() { + // Don't separate with a space when there are two of them + " 🔒👻 " + } else { + " 🔒 " + } + } + _ if myitem.is_doc_hidden() => { + " 👻 " + } + _ => "", + }; + + let docs = MarkdownSummaryLine(&myitem.doc_value(), &myitem.links(cx)) + .into_string(); + let (docs_before, docs_after) = + if docs.is_empty() { ("", "") } else { ("
", "
") }; + write!( + w, + "
\ + \ + {name}\ + \ + {visibility_and_hidden}\ + {unsafety_flag}\ + {stab_tags}\ +
\ + {docs_before}{docs}{docs_after}", + name = EscapeBodyTextWithWbr(myitem.name.unwrap().as_str()), + visibility_and_hidden = visibility_and_hidden, + stab_tags = print_extra_info_tags(tcx, myitem, item, None), + class = type_, + unsafety_flag = unsafety_flag, + href = print_item_path(myitem.type_(), myitem.name.unwrap().as_str()), + title1 = myitem.type_(), + title2 = full_path(cx, myitem), + )?; + } } } - } - - if last_section.is_some() { w.write_str(ITEM_TABLE_CLOSE)?; } + Ok(()) }) } @@ -1878,8 +1894,13 @@ fn item_variants( }) } -fn item_macro(cx: &Context<'_>, it: &clean::Item, t: &clean::Macro) -> impl fmt::Display { - fmt::from_fn(|w| { +fn item_macro( + cx: &Context<'_>, + it: &clean::Item, + t: &clean::Macro, + kinds: Option, +) -> impl fmt::Display { + fmt::from_fn(move |w| { wrap_item(w, |w| { // FIXME: Also print `#[doc(hidden)]` for `macro_rules!` if it `is_doc_hidden`. render_attributes_in_code(w, it, "", cx); @@ -1888,6 +1909,14 @@ fn item_macro(cx: &Context<'_>, it: &clean::Item, t: &clean::Macro) -> impl fmt: } write!(w, "{}", Escape(&t.source)) })?; + if let Some(kinds) = kinds { + write!( + w, + "

ⓘ This is {} {}

", + kinds.article(), + kinds.descr(), + )?; + } write!(w, "{}", document(cx, it, None, HeadingOffset::H2)) }) } diff --git a/src/librustdoc/html/render/sidebar.rs b/src/librustdoc/html/render/sidebar.rs index b9f5ada417c7f..ad87799a77c85 100644 --- a/src/librustdoc/html/render/sidebar.rs +++ b/src/librustdoc/html/render/sidebar.rs @@ -653,7 +653,13 @@ fn sidebar_module( }) .is_some() }) - .map(|it| item_ty_to_section(it.type_())) + .flat_map(|it| { + if let Some(types) = it.bang_macro_types() { + types.into_iter().map(|type_| item_ty_to_section(type_)).collect::>() + } else { + vec![item_ty_to_section(it.type_())] + } + }) .collect(); sidebar_module_like(item_sections_in_use, ids, module_like) diff --git a/src/librustdoc/html/static/js/main.js b/src/librustdoc/html/static/js/main.js index 3ea9de381eca6..1910257c467e9 100644 --- a/src/librustdoc/html/static/js/main.js +++ b/src/librustdoc/html/static/js/main.js @@ -736,12 +736,20 @@ function preLoadCss(cssUrl) { const ul = document.createElement("ul"); ul.className = "block " + shortty; - for (const name of filtered) { + for (const item of filtered) { + let name = item; + let isMacro = false; + if (Array.isArray(item)) { + name = item[0]; + isMacro = true; + } let path; if (shortty === "mod") { path = `${modpath}${name}/index.html`; - } else { + } else if (!isMacro) { path = `${modpath}${shortty}.${name}.html`; + } else { + path = `${modpath}macro.${name}.html`; } let current_page = document.location.href.toString(); if (current_page.endsWith("/")) { diff --git a/src/librustdoc/json/conversions.rs b/src/librustdoc/json/conversions.rs index 909262d563e9f..b90b8ce145188 100644 --- a/src/librustdoc/json/conversions.rs +++ b/src/librustdoc/json/conversions.rs @@ -308,7 +308,7 @@ fn from_clean_item(item: &clean::Item, renderer: &JsonRenderer<'_>) -> ItemEnum type_: ci.type_.into_json(renderer), const_: ci.kind.into_json(renderer), }, - MacroItem(m) => ItemEnum::Macro(m.source.clone()), + MacroItem(m, _) => ItemEnum::Macro(m.source.clone()), ProcMacroItem(m) => ItemEnum::ProcMacro(m.into_json(renderer)), PrimitiveItem(p) => { ItemEnum::Primitive(Primitive { diff --git a/src/librustdoc/visit.rs b/src/librustdoc/visit.rs index 4d31409afe825..25b2d6bb3de28 100644 --- a/src/librustdoc/visit.rs +++ b/src/librustdoc/visit.rs @@ -41,7 +41,7 @@ pub(crate) trait DocVisitor<'a>: Sized { | ForeignFunctionItem(..) | ForeignStaticItem(..) | ForeignTypeItem - | MacroItem(_) + | MacroItem(..) | ProcMacroItem(_) | PrimitiveItem(_) | RequiredAssocConstItem(..) From add2aaf66702c96f0e158bd5a62b1272a74ad66e Mon Sep 17 00:00:00 2001 From: Guillaume Gomez Date: Fri, 15 Aug 2025 15:19:57 +0200 Subject: [PATCH 2/7] Add tests for attribute bang macros in rustdoc --- tests/rustdoc-gui/attr-macros.goml | 39 +++++++++++++++++++++++ tests/rustdoc-gui/src/test_docs/lib.rs | 1 + tests/rustdoc-gui/src/test_docs/macros.rs | 7 ++++ 3 files changed, 47 insertions(+) create mode 100644 tests/rustdoc-gui/attr-macros.goml diff --git a/tests/rustdoc-gui/attr-macros.goml b/tests/rustdoc-gui/attr-macros.goml new file mode 100644 index 0000000000000..0d16c3e764d9e --- /dev/null +++ b/tests/rustdoc-gui/attr-macros.goml @@ -0,0 +1,39 @@ +// This test ensures that a bang macro which is also an attribute macro is listed correctly in +// the sidebar and in the module. + +go-to: "file://" + |DOC_PATH| + "/test_docs/macro.b.html" +// It should be present twice in the sidebar. +assert-count: ("#rustdoc-modnav a[href='macro.attr_macro.html']", 2) +assert-count: ("//*[@id='rustdoc-modnav']//a[text()='attr_macro']", 2) +// We check that the current item in the sidebar is the correct one. +assert-text: ("#rustdoc-modnav .block.macro .current", "b") + +// We now go to the attribute macro page. +click: "#rustdoc-modnav a[href='macro.attr_macro.html']" +// It should be present twice in the sidebar. +assert-count: ("#rustdoc-modnav a[href='macro.attr_macro.html']", 2) +assert-count: ("//*[@id='rustdoc-modnav']//a[text()='attr_macro']", 2) +// We check that the current item is the "attr_macro". +assert-text: ("#rustdoc-modnav .block.macro .current", "attr_macro") +// Since the item is present twice in the sidebar, we should have two "current" items. +assert-count: ("#rustdoc-modnav .current", 2) +// We check it has the expected information. +assert-text: ("h3.macro-info", "ⓘ This is an attribute/function macro") + +// Now we check it's correctly listed in the crate page. +go-to: "file://" + |DOC_PATH| + "/test_docs/index.html" +// It should be only present twice. +assert-count: ("#main-content a[href='macro.attr_macro.html']", 2) +// First in the "Macros" section. +assert-text: ("#macros + .item-table a[href='macro.attr_macro.html']", "attr_macro") +// Then in the "Attribute Macros" section. +assert-text: ("#attributes + .item-table a[href='macro.attr_macro.html']", "attr_macro") + +// And finally we check it's correctly listed in the "all items" page. +go-to: "file://" + |DOC_PATH| + "/test_docs/all.html" +// It should be only present twice. +assert-count: ("#main-content a[href='macro.attr_macro.html']", 2) +// First in the "Macros" section. +assert-text: ("#macros + .all-items a[href='macro.attr_macro.html']", "attr_macro") +// Then in the "Attribute Macros" section. +assert-text: ("#attributes + .all-items a[href='macro.attr_macro.html']", "attr_macro") diff --git a/tests/rustdoc-gui/src/test_docs/lib.rs b/tests/rustdoc-gui/src/test_docs/lib.rs index c0771583ab658..a9e280b974aa9 100644 --- a/tests/rustdoc-gui/src/test_docs/lib.rs +++ b/tests/rustdoc-gui/src/test_docs/lib.rs @@ -8,6 +8,7 @@ #![feature(rustdoc_internals)] #![feature(doc_cfg)] #![feature(associated_type_defaults)] +#![feature(macro_attr)] /*! Enable the feature some-feature to enjoy diff --git a/tests/rustdoc-gui/src/test_docs/macros.rs b/tests/rustdoc-gui/src/test_docs/macros.rs index 07b2b97926d43..6564088c7be70 100644 --- a/tests/rustdoc-gui/src/test_docs/macros.rs +++ b/tests/rustdoc-gui/src/test_docs/macros.rs @@ -2,3 +2,10 @@ macro_rules! a{ () => {}} #[macro_export] macro_rules! b{ () => {}} + +// An attribute bang macro. +#[macro_export] +macro_rules! attr_macro { + attr() () => {}; + () => {}; +} From 76a832286a3fa865892f2c340dc84e52e93fa975 Mon Sep 17 00:00:00 2001 From: Guillaume Gomez Date: Fri, 15 Aug 2025 17:43:59 +0200 Subject: [PATCH 3/7] Use `ItemKind` placeholders for alternative macro kinds --- src/librustdoc/clean/inline.rs | 109 +++++++++++++------ src/librustdoc/clean/mod.rs | 44 ++++++-- src/librustdoc/clean/types.rs | 34 +++--- src/librustdoc/fold.rs | 2 + src/librustdoc/formats/cache.rs | 6 +- src/librustdoc/formats/item_type.rs | 4 + src/librustdoc/html/render/context.rs | 60 +++++----- src/librustdoc/html/render/mod.rs | 52 ++++----- src/librustdoc/html/render/print_item.rs | 26 ++--- src/librustdoc/html/render/sidebar.rs | 8 +- src/librustdoc/json/conversions.rs | 12 +- src/librustdoc/passes/propagate_stability.rs | 2 + src/librustdoc/passes/stripper.rs | 2 + src/librustdoc/visit.rs | 2 + 14 files changed, 210 insertions(+), 153 deletions(-) diff --git a/src/librustdoc/clean/inline.rs b/src/librustdoc/clean/inline.rs index 28076a0b7db67..23ae1a950c9a1 100644 --- a/src/librustdoc/clean/inline.rs +++ b/src/librustdoc/clean/inline.rs @@ -46,6 +46,27 @@ pub(crate) fn try_inline( attrs: Option<(&[hir::Attribute], Option)>, visited: &mut DefIdSet, ) -> Option> { + fn try_inline_inner( + cx: &mut DocContext<'_>, + kind: clean::ItemKind, + did: DefId, + name: Symbol, + import_def_id: Option, + ) -> clean::Item { + cx.inlined.insert(did.into()); + let mut item = crate::clean::generate_item_with_correct_attrs( + cx, + kind, + did, + name, + import_def_id.as_slice(), + None, + ); + // The visibility needs to reflect the one from the reexport and not from the "source" DefId. + item.inner.inline_stmt_id = import_def_id; + item + } + let did = res.opt_def_id()?; if did.is_local() { return None; @@ -138,7 +159,7 @@ pub(crate) fn try_inline( }) } Res::Def(DefKind::Macro(kinds), did) => { - let mac = build_macro(cx, did, name, kinds); + let (mac, others) = build_macro(cx, did, name, kinds); let type_kind = match kinds { MacroKinds::BANG => ItemType::Macro, @@ -148,23 +169,21 @@ pub(crate) fn try_inline( _ => panic!("unsupported macro kind {kinds:?}"), }; record_extern_fqn(cx, did, type_kind); - mac + let first = try_inline_inner(cx, mac, did, name, import_def_id); + if let Some(others) = others { + for mac_kind in others { + let mut mac = first.clone(); + mac.inner.kind = mac_kind; + ret.push(mac); + } + } + ret.push(first); + return Some(ret); } _ => return None, }; - cx.inlined.insert(did.into()); - let mut item = crate::clean::generate_item_with_correct_attrs( - cx, - kind, - did, - name, - import_def_id.as_slice(), - None, - ); - // The visibility needs to reflect the one from the reexport and not from the "source" DefId. - item.inner.inline_stmt_id = import_def_id; - ret.push(item); + ret.push(try_inline_inner(cx, kind, did, name, import_def_id)); Some(ret) } @@ -761,31 +780,51 @@ fn build_macro( def_id: DefId, name: Symbol, macro_kinds: MacroKinds, -) -> clean::ItemKind { +) -> (clean::ItemKind, Option>) { match CStore::from_tcx(cx.tcx).load_macro_untracked(def_id, cx.tcx) { LoadedMacro::MacroDef { def, .. } => match macro_kinds { - MacroKinds::BANG => clean::MacroItem( - clean::Macro { - source: utils::display_macro_source(cx, name, &def), - macro_rules: def.macro_rules, - }, + MacroKinds::BANG => ( + clean::MacroItem( + clean::Macro { + source: utils::display_macro_source(cx, name, &def), + macro_rules: def.macro_rules, + }, + None, + ), + None, + ), + MacroKinds::DERIVE => ( + clean::ProcMacroItem(clean::ProcMacro { + kind: MacroKind::Derive, + helpers: Vec::new(), + }), None, ), - MacroKinds::DERIVE => clean::ProcMacroItem(clean::ProcMacro { - kind: MacroKind::Derive, - helpers: Vec::new(), - }), - MacroKinds::ATTR => clean::ProcMacroItem(clean::ProcMacro { - kind: MacroKind::Attr, - helpers: Vec::new(), - }), - _ if macro_kinds == (MacroKinds::BANG | MacroKinds::ATTR) => clean::MacroItem( - clean::Macro { - source: utils::display_macro_source(cx, name, &def), - macro_rules: def.macro_rules, - }, - Some(macro_kinds), + MacroKinds::ATTR => ( + clean::ProcMacroItem(clean::ProcMacro { + kind: MacroKind::Attr, + helpers: Vec::new(), + }), + None, ), + _ if macro_kinds.contains(MacroKinds::BANG) => { + let kind = clean::MacroItem( + clean::Macro { + source: utils::display_macro_source(cx, name, &def), + macro_rules: def.macro_rules, + }, + Some(macro_kinds), + ); + let mut ret = vec![]; + for kind in macro_kinds.iter().filter(|kind| *kind != MacroKinds::BANG) { + match kind { + MacroKinds::ATTR => ret.push(clean::AttrMacroItem), + MacroKinds::DERIVE => ret.push(clean::DeriveMacroItem), + _ => panic!("unsupported macro kind {kind:?}"), + } + } + (kind, Some(ret)) + } _ => panic!("unsupported macro kind {macro_kinds:?}"), }, LoadedMacro::ProcMacro(ext) => { @@ -796,7 +835,7 @@ fn build_macro( MacroKinds::DERIVE => MacroKind::Derive, _ => unreachable!(), }; - clean::ProcMacroItem(clean::ProcMacro { kind, helpers: ext.helper_attrs }) + (clean::ProcMacroItem(clean::ProcMacro { kind, helpers: ext.helper_attrs }), None) } } } diff --git a/src/librustdoc/clean/mod.rs b/src/librustdoc/clean/mod.rs index b8507b52fd5b8..2baa418c9b884 100644 --- a/src/librustdoc/clean/mod.rs +++ b/src/librustdoc/clean/mod.rs @@ -2851,13 +2851,43 @@ fn clean_maybe_renamed_item<'tcx>( ), MacroKinds::ATTR => clean_proc_macro(item, &mut name, MacroKind::Attr, cx), MacroKinds::DERIVE => clean_proc_macro(item, &mut name, MacroKind::Derive, cx), - _ => MacroItem( - Macro { - source: display_macro_source(cx, name, macro_def), - macro_rules: macro_def.macro_rules, - }, - Some(kinds), - ), + _ if kinds.contains(MacroKinds::BANG) => { + let kind = MacroItem( + Macro { + source: display_macro_source(cx, name, macro_def), + macro_rules: macro_def.macro_rules, + }, + Some(kinds), + ); + let mac = generate_item_with_correct_attrs( + cx, + kind, + item.owner_id.def_id.to_def_id(), + name, + import_ids, + renamed, + ); + + let mut ret = Vec::with_capacity(3); + for kind in kinds.iter().filter(|kind| *kind != MacroKinds::BANG) { + match kind { + MacroKinds::ATTR => { + let mut attr = mac.clone(); + attr.inner.kind = AttrMacroItem; + ret.push(attr); + } + MacroKinds::DERIVE => { + let mut derive = mac.clone(); + derive.inner.kind = DeriveMacroItem; + ret.push(derive); + } + _ => panic!("unsupported macro kind {kind:?}"), + } + } + ret.push(mac); + return ret; + } + _ => panic!("unsupported macro kind {kinds:?}"), }, // proc macros can have a name set by attributes ItemKind::Fn { ref sig, generics, body: body_id, .. } => { diff --git a/src/librustdoc/clean/types.rs b/src/librustdoc/clean/types.rs index 5c2f89b2ce95b..f8a91d707a944 100644 --- a/src/librustdoc/clean/types.rs +++ b/src/librustdoc/clean/types.rs @@ -664,29 +664,23 @@ impl Item { find_attr!(&self.attrs.other_attrs, AttributeKind::NonExhaustive(..)) } - pub(crate) fn bang_macro_types(&self) -> Option> { - match self.kind { - ItemKind::MacroItem(_, None) => Some(vec![ItemType::Macro]), - ItemKind::MacroItem(_, Some(kinds)) => Some( - kinds - .iter() - .map(|kind| match kind { - MacroKinds::BANG => ItemType::Macro, - MacroKinds::ATTR => ItemType::ProcAttribute, - MacroKinds::DERIVE => ItemType::ProcDerive, - _ => panic!("unexpected macro kind {kind:?}"), - }) - .collect::>(), - ), - _ => None, - } - } - /// Returns a documentation-level item type from the item. pub(crate) fn type_(&self) -> ItemType { ItemType::from(self) } + /// Generates the HTML file name based on the item kind. + pub(crate) fn html_filename(&self) -> String { + let type_ = if self.is_macro_placeholder() { ItemType::Macro } else { self.type_() }; + format!("{type_}.{}.html", self.name.unwrap()) + } + + /// If the current item is a "fake" macro (ie, `AttrMacroItem | ItemKind::DeriveMacroItem` which + /// don't hold any data), it returns `true`. + pub(crate) fn is_macro_placeholder(&self) -> bool { + matches!(self.kind, ItemKind::AttrMacroItem | ItemKind::DeriveMacroItem) + } + pub(crate) fn is_default(&self) -> bool { match self.kind { ItemKind::MethodItem(_, Some(defaultness)) => { @@ -853,6 +847,8 @@ pub(crate) enum ItemKind { /// `type`s from an extern block ForeignTypeItem, MacroItem(Macro, Option), + AttrMacroItem, + DeriveMacroItem, ProcMacroItem(ProcMacro), PrimitiveItem(PrimitiveType), /// A required associated constant in a trait declaration. @@ -908,6 +904,8 @@ impl ItemKind { | ForeignStaticItem(_, _) | ForeignTypeItem | MacroItem(..) + | AttrMacroItem + | DeriveMacroItem | ProcMacroItem(_) | PrimitiveItem(_) | RequiredAssocConstItem(..) diff --git a/src/librustdoc/fold.rs b/src/librustdoc/fold.rs index 1d4bdc4f2f047..8beea6aad11b4 100644 --- a/src/librustdoc/fold.rs +++ b/src/librustdoc/fold.rs @@ -89,6 +89,8 @@ pub(crate) trait DocFolder: Sized { | ForeignStaticItem(..) | ForeignTypeItem | MacroItem(..) + | AttrMacroItem + | DeriveMacroItem | ProcMacroItem(_) | PrimitiveItem(_) | RequiredAssocConstItem(..) diff --git a/src/librustdoc/formats/cache.rs b/src/librustdoc/formats/cache.rs index 5e5592269af60..d89b862bbedb9 100644 --- a/src/librustdoc/formats/cache.rs +++ b/src/librustdoc/formats/cache.rs @@ -378,8 +378,10 @@ impl DocFolder for CacheBuilder<'_, '_> { | clean::RequiredAssocTypeItem(..) | clean::AssocTypeItem(..) | clean::StrippedItem(..) - | clean::KeywordItem - | clean::AttributeItem => { + | clean::AttributeItem + | clean::AttrMacroItem + | clean::DeriveMacroItem + | clean::KeywordItem => { // FIXME: Do these need handling? // The person writing this comment doesn't know. // So would rather leave them to an expert, diff --git a/src/librustdoc/formats/item_type.rs b/src/librustdoc/formats/item_type.rs index ab40c01cb369d..b80bc446e1152 100644 --- a/src/librustdoc/formats/item_type.rs +++ b/src/librustdoc/formats/item_type.rs @@ -128,6 +128,10 @@ impl<'a> From<&'a clean::Item> for ItemType { clean::ForeignFunctionItem(..) => ItemType::Function, // no ForeignFunction clean::ForeignStaticItem(..) => ItemType::Static, // no ForeignStatic clean::MacroItem(..) => ItemType::Macro, + // Is this a good idea? + clean::AttrMacroItem => ItemType::ProcAttribute, + // Is this a good idea? + clean::DeriveMacroItem => ItemType::ProcDerive, clean::PrimitiveItem(..) => ItemType::Primitive, clean::RequiredAssocConstItem(..) | clean::ProvidedAssocConstItem(..) diff --git a/src/librustdoc/html/render/context.rs b/src/librustdoc/html/render/context.rs index 30f827ad3159e..385eeef81b683 100644 --- a/src/librustdoc/html/render/context.rs +++ b/src/librustdoc/html/render/context.rs @@ -16,7 +16,7 @@ use rustc_span::{BytePos, FileName, Symbol, sym}; use serde::ser::SerializeSeq; use tracing::info; -use super::print_item::{full_path, print_item, print_item_path}; +use super::print_item::{full_path, print_item, print_item_path, print_ty_path}; use super::sidebar::{ModuleLike, Sidebar, print_sidebar, sidebar_module_like}; use super::{AllTypes, LinkFromSrc, StylePath, collect_spans_and_sources, scrape_examples_help}; use crate::clean::types::ExternalLocation; @@ -302,7 +302,7 @@ impl<'tcx> Context<'tcx> { for name in &names[..names.len() - 1] { write!(f, "{name}/")?; } - write!(f, "{}", print_item_path(ty, names.last().unwrap().as_str())) + write!(f, "{}", print_ty_path(ty, names.last().unwrap().as_str())) }); match self.shared.redirections { Some(ref redirections) => { @@ -314,7 +314,7 @@ impl<'tcx> Context<'tcx> { let _ = write!( current_path, "{}", - print_item_path(ty, names.last().unwrap().as_str()) + print_ty_path(ty, names.last().unwrap().as_str()) ); redirections.borrow_mut().insert(current_path, path.to_string()); } @@ -329,19 +329,6 @@ impl<'tcx> Context<'tcx> { /// Construct a map of items shown in the sidebar to a plain-text summary of their docs. fn build_sidebar_items(&self, m: &clean::Module) -> BTreeMap> { - fn build_sidebar_items_inner( - name: Symbol, - type_: ItemType, - map: &mut BTreeMap>, - inserted: &mut FxHashMap>, - is_actually_macro: bool, - ) { - if inserted.entry(type_).or_default().insert(name) { - let type_ = type_.to_string(); - let name = name.to_string(); - map.entry(type_).or_default().push(SidebarItem { name, is_actually_macro }); - } - } // BTreeMap instead of HashMap to get a sorted output let mut map: BTreeMap<_, Vec<_>> = BTreeMap::new(); let mut inserted: FxHashMap> = FxHashMap::default(); @@ -355,12 +342,14 @@ impl<'tcx> Context<'tcx> { Some(s) => s, }; - if let Some(types) = item.bang_macro_types() { - for type_ in types { - build_sidebar_items_inner(name, type_, &mut map, &mut inserted, true); - } - } else { - build_sidebar_items_inner(name, item.type_(), &mut map, &mut inserted, false); + let type_ = item.type_(); + + if inserted.entry(type_).or_default().insert(name) { + let type_ = type_.to_string(); + let name = name.to_string(); + map.entry(type_) + .or_default() + .push(SidebarItem { name, is_actually_macro: item.is_macro_placeholder() }); } } @@ -833,7 +822,7 @@ impl<'tcx> FormatRenderer<'tcx> for Context<'tcx> { info!("Recursing into {}", self.dst.display()); - if !item.is_stripped() { + if !item.is_stripped() && !item.is_macro_placeholder() { let buf = self.render_item(item, true); // buf will be empty if the module is stripped and there is no redirect for it if !buf.is_empty() { @@ -889,26 +878,29 @@ impl<'tcx> FormatRenderer<'tcx> for Context<'tcx> { self.info.render_redirect_pages = item.is_stripped(); } + if item.is_macro_placeholder() { + if !self.info.render_redirect_pages { + self.shared.all.borrow_mut().append(full_path(self, item), &item); + } + return Ok(()); + } + let buf = self.render_item(item, false); // buf will be empty if the item is stripped and there is no redirect for it if !buf.is_empty() { - let name = item.name.as_ref().unwrap(); - let item_type = item.type_(); - let file_name = print_item_path(item_type, name.as_str()).to_string(); + if !self.info.render_redirect_pages { + self.shared.all.borrow_mut().append(full_path(self, item), &item); + } + + let file_name = print_item_path(item).to_string(); self.shared.ensure_dir(&self.dst)?; let joint_dst = self.dst.join(&file_name); self.shared.fs.write(joint_dst, buf)?; - - if !self.info.render_redirect_pages { - self.shared.all.borrow_mut().append( - full_path(self, item), - &item_type, - item.bang_macro_types(), - ); - } // If the item is a macro, redirect from the old macro URL (with !) // to the new one (without). + let item_type = item.type_(); if item_type == ItemType::Macro { + let name = item.name.as_ref().unwrap(); let redir_name = format!("{item_type}.{name}!.html"); if let Some(ref redirections) = self.shared.redirections { let crate_name = &self.shared.layout.krate; diff --git a/src/librustdoc/html/render/mod.rs b/src/librustdoc/html/render/mod.rs index 402db67e25302..9f1264442ad39 100644 --- a/src/librustdoc/html/render/mod.rs +++ b/src/librustdoc/html/render/mod.rs @@ -505,46 +505,34 @@ impl AllTypes { } } - fn append( - &mut self, - item_name: String, - item_type: &ItemType, - extra_types: Option>, - ) { + fn append(&mut self, item_name: String, item: &clean::Item) { let mut url: Vec<_> = item_name.split("::").skip(1).collect(); if let Some(name) = url.pop() { - let new_url = format!("{}/{item_type}.{name}.html", url.join("/")); + let new_url = format!("{}/{}", url.join("/"), item.html_filename()); url.push(name); + let item_type = item.type_(); let name = url.join("::"); - if let Some(extra_types) = extra_types { - for extra_type in extra_types { - self.append_with_url(name.clone(), &extra_type, new_url.clone()); + match item_type { + ItemType::Struct => self.structs.insert(ItemEntry::new(new_url, name)), + ItemType::Enum => self.enums.insert(ItemEntry::new(new_url, name)), + ItemType::Union => self.unions.insert(ItemEntry::new(new_url, name)), + ItemType::Primitive => self.primitives.insert(ItemEntry::new(new_url, name)), + ItemType::Trait => self.traits.insert(ItemEntry::new(new_url, name)), + ItemType::Macro => self.macros.insert(ItemEntry::new(new_url, name)), + ItemType::Function => self.functions.insert(ItemEntry::new(new_url, name)), + ItemType::TypeAlias => self.type_aliases.insert(ItemEntry::new(new_url, name)), + ItemType::Static => self.statics.insert(ItemEntry::new(new_url, name)), + ItemType::Constant => self.constants.insert(ItemEntry::new(new_url, name)), + ItemType::ProcAttribute => { + self.attribute_macros.insert(ItemEntry::new(new_url, name)) } - } else { - self.append_with_url(name, item_type, new_url); - } + ItemType::ProcDerive => self.derive_macros.insert(ItemEntry::new(new_url, name)), + ItemType::TraitAlias => self.trait_aliases.insert(ItemEntry::new(new_url, name)), + _ => true, + }; } } - fn append_with_url(&mut self, name: String, item_type: &ItemType, url: String) { - match *item_type { - ItemType::Struct => self.structs.insert(ItemEntry::new(url, name)), - ItemType::Enum => self.enums.insert(ItemEntry::new(url, name)), - ItemType::Union => self.unions.insert(ItemEntry::new(url, name)), - ItemType::Primitive => self.primitives.insert(ItemEntry::new(url, name)), - ItemType::Trait => self.traits.insert(ItemEntry::new(url, name)), - ItemType::Macro => self.macros.insert(ItemEntry::new(url, name)), - ItemType::Function => self.functions.insert(ItemEntry::new(url, name)), - ItemType::TypeAlias => self.type_aliases.insert(ItemEntry::new(url, name)), - ItemType::Static => self.statics.insert(ItemEntry::new(url, name)), - ItemType::Constant => self.constants.insert(ItemEntry::new(url, name)), - ItemType::ProcAttribute => self.attribute_macros.insert(ItemEntry::new(url, name)), - ItemType::ProcDerive => self.derive_macros.insert(ItemEntry::new(url, name)), - ItemType::TraitAlias => self.trait_aliases.insert(ItemEntry::new(url, name)), - _ => true, - }; - } - fn item_sections(&self) -> FxHashSet { let mut sections = FxHashSet::default(); diff --git a/src/librustdoc/html/render/print_item.rs b/src/librustdoc/html/render/print_item.rs index 7fd35440fd32d..c4ee2de5deecd 100644 --- a/src/librustdoc/html/render/print_item.rs +++ b/src/librustdoc/html/render/print_item.rs @@ -313,17 +313,8 @@ fn item_module(cx: &Context<'_>, item: &clean::Item, items: &[clean::Item]) -> i let mut not_stripped_items: FxHashMap> = FxHashMap::default(); - let mut index = 0; - for item in items.iter().filter(|i| !i.is_stripped()) { - if let Some(types) = item.bang_macro_types() { - for type_ in types { - not_stripped_items.entry(type_).or_default().push((index, item)); - index += 1; - } - } else { - not_stripped_items.entry(item.type_()).or_default().push((index, item)); - index += 1; - } + for (index, item) in items.iter().filter(|i| !i.is_stripped()).enumerate() { + not_stripped_items.entry(item.type_()).or_default().push((index, item)); } // the order of item types in the listing @@ -535,7 +526,7 @@ fn item_module(cx: &Context<'_>, item: &clean::Item, items: &[clean::Item]) -> i stab_tags = print_extra_info_tags(tcx, myitem, item, None), class = type_, unsafety_flag = unsafety_flag, - href = print_item_path(myitem.type_(), myitem.name.unwrap().as_str()), + href = print_item_path(myitem), title1 = myitem.type_(), title2 = full_path(cx, myitem), )?; @@ -2293,7 +2284,16 @@ pub(super) fn full_path(cx: &Context<'_>, item: &clean::Item) -> String { s } -pub(super) fn print_item_path(ty: ItemType, name: &str) -> impl Display { +pub(super) fn print_item_path(item: &clean::Item) -> impl Display { + fmt::from_fn(move |f| match item.kind { + clean::ItemKind::ModuleItem(..) => { + write!(f, "{}index.html", ensure_trailing_slash(item.name.unwrap().as_str())) + } + _ => f.write_str(&item.html_filename()), + }) +} + +pub(super) fn print_ty_path(ty: ItemType, name: &str) -> impl Display { fmt::from_fn(move |f| match ty { ItemType::Module => write!(f, "{}index.html", ensure_trailing_slash(name)), _ => write!(f, "{ty}.{name}.html"), diff --git a/src/librustdoc/html/render/sidebar.rs b/src/librustdoc/html/render/sidebar.rs index ad87799a77c85..b9f5ada417c7f 100644 --- a/src/librustdoc/html/render/sidebar.rs +++ b/src/librustdoc/html/render/sidebar.rs @@ -653,13 +653,7 @@ fn sidebar_module( }) .is_some() }) - .flat_map(|it| { - if let Some(types) = it.bang_macro_types() { - types.into_iter().map(|type_| item_ty_to_section(type_)).collect::>() - } else { - vec![item_ty_to_section(it.type_())] - } - }) + .map(|it| item_ty_to_section(it.type_())) .collect(); sidebar_module_like(item_sections_in_use, ids, module_like) diff --git a/src/librustdoc/json/conversions.rs b/src/librustdoc/json/conversions.rs index b90b8ce145188..21d5a4f89a0dc 100644 --- a/src/librustdoc/json/conversions.rs +++ b/src/librustdoc/json/conversions.rs @@ -61,12 +61,12 @@ impl JsonRenderer<'_> { clean::ModuleItem(_) if self.imported_items.contains(&item_id.expect_def_id()) => { - from_clean_item(item, self) + from_clean_item(item, self)? } _ => return None, } } - _ => from_clean_item(item, self), + _ => from_clean_item(item, self)?, }; Some(Item { id, @@ -268,13 +268,13 @@ impl FromClean for AssocItemConstraintKind { } } -fn from_clean_item(item: &clean::Item, renderer: &JsonRenderer<'_>) -> ItemEnum { +fn from_clean_item(item: &clean::Item, renderer: &JsonRenderer<'_>) -> Option { use clean::ItemKind::*; let name = item.name; let is_crate = item.is_crate(); let header = item.fn_header(renderer.tcx); - match &item.inner.kind { + Some(match &item.inner.kind { ModuleItem(m) => { ItemEnum::Module(Module { is_crate, items: renderer.ids(&m.items), is_stripped: false }) } @@ -309,6 +309,8 @@ fn from_clean_item(item: &clean::Item, renderer: &JsonRenderer<'_>) -> ItemEnum const_: ci.kind.into_json(renderer), }, MacroItem(m, _) => ItemEnum::Macro(m.source.clone()), + // They are just placeholders so no need to handle them. + AttrMacroItem | DeriveMacroItem => return None, ProcMacroItem(m) => ItemEnum::ProcMacro(m.into_json(renderer)), PrimitiveItem(p) => { ItemEnum::Primitive(Primitive { @@ -352,7 +354,7 @@ fn from_clean_item(item: &clean::Item, renderer: &JsonRenderer<'_>) -> ItemEnum name: name.as_ref().unwrap().to_string(), rename: src.map(|x| x.to_string()), }, - } + }) } impl FromClean for Struct { diff --git a/src/librustdoc/passes/propagate_stability.rs b/src/librustdoc/passes/propagate_stability.rs index 5139ca301dd3d..dca0b509782d6 100644 --- a/src/librustdoc/passes/propagate_stability.rs +++ b/src/librustdoc/passes/propagate_stability.rs @@ -88,6 +88,8 @@ impl DocFolder for StabilityPropagator<'_, '_> { | ItemKind::ForeignStaticItem(..) | ItemKind::ForeignTypeItem | ItemKind::MacroItem(..) + | ItemKind::AttrMacroItem + | ItemKind::DeriveMacroItem | ItemKind::ProcMacroItem(..) | ItemKind::ConstantItem(..) => { // If any of the item's parents was stabilized later or is still unstable, diff --git a/src/librustdoc/passes/stripper.rs b/src/librustdoc/passes/stripper.rs index 99d22526f85b7..b625c61b9be45 100644 --- a/src/librustdoc/passes/stripper.rs +++ b/src/librustdoc/passes/stripper.rs @@ -64,6 +64,8 @@ impl DocFolder for Stripper<'_, '_> { | clean::UnionItem(..) | clean::TraitAliasItem(..) | clean::MacroItem(..) + | clean::AttrMacroItem + | clean::DeriveMacroItem | clean::ForeignTypeItem => { let item_id = i.item_id; if item_id.is_local() diff --git a/src/librustdoc/visit.rs b/src/librustdoc/visit.rs index 25b2d6bb3de28..2cd39f8d34e62 100644 --- a/src/librustdoc/visit.rs +++ b/src/librustdoc/visit.rs @@ -42,6 +42,8 @@ pub(crate) trait DocVisitor<'a>: Sized { | ForeignStaticItem(..) | ForeignTypeItem | MacroItem(..) + | AttrMacroItem + | DeriveMacroItem | ProcMacroItem(_) | PrimitiveItem(_) | RequiredAssocConstItem(..) From 91806e0ff9ed79a58dcca2de49527c9929a52d42 Mon Sep 17 00:00:00 2001 From: Guillaume Gomez Date: Tue, 9 Sep 2025 17:13:45 +0200 Subject: [PATCH 4/7] Deduplicate "attributes" and "attribute macros" sections IDs --- src/librustdoc/html/static/js/main.js | 2 +- tests/rustdoc-gui/attr-macros.goml | 4 ++-- tests/rustdoc-gui/module-items-font.goml | 9 +++++++++ tests/rustdoc-gui/src/test_docs/macros.rs | 2 +- 4 files changed, 13 insertions(+), 4 deletions(-) diff --git a/src/librustdoc/html/static/js/main.js b/src/librustdoc/html/static/js/main.js index 1910257c467e9..bc32e78e53535 100644 --- a/src/librustdoc/html/static/js/main.js +++ b/src/librustdoc/html/static/js/main.js @@ -799,7 +799,7 @@ function preLoadCss(cssUrl) { block("foreigntype", "foreign-types", "Foreign Types"); block("keyword", "keywords", "Keywords"); block("attribute", "attributes", "Attributes"); - block("attr", "attributes", "Attribute Macros"); + block("attr", "attribute-macros", "Attribute Macros"); block("derive", "derives", "Derive Macros"); block("traitalias", "trait-aliases", "Trait Aliases"); } diff --git a/tests/rustdoc-gui/attr-macros.goml b/tests/rustdoc-gui/attr-macros.goml index 0d16c3e764d9e..cf7337703651f 100644 --- a/tests/rustdoc-gui/attr-macros.goml +++ b/tests/rustdoc-gui/attr-macros.goml @@ -27,7 +27,7 @@ assert-count: ("#main-content a[href='macro.attr_macro.html']", 2) // First in the "Macros" section. assert-text: ("#macros + .item-table a[href='macro.attr_macro.html']", "attr_macro") // Then in the "Attribute Macros" section. -assert-text: ("#attributes + .item-table a[href='macro.attr_macro.html']", "attr_macro") +assert-text: ("#attribute-macros + .item-table a[href='macro.attr_macro.html']", "attr_macro") // And finally we check it's correctly listed in the "all items" page. go-to: "file://" + |DOC_PATH| + "/test_docs/all.html" @@ -36,4 +36,4 @@ assert-count: ("#main-content a[href='macro.attr_macro.html']", 2) // First in the "Macros" section. assert-text: ("#macros + .all-items a[href='macro.attr_macro.html']", "attr_macro") // Then in the "Attribute Macros" section. -assert-text: ("#attributes + .all-items a[href='macro.attr_macro.html']", "attr_macro") +assert-text: ("#attribute-macros + .all-items a[href='macro.attr_macro.html']", "attr_macro") diff --git a/tests/rustdoc-gui/module-items-font.goml b/tests/rustdoc-gui/module-items-font.goml index bed95b378c6ab..822e0adc3b71e 100644 --- a/tests/rustdoc-gui/module-items-font.goml +++ b/tests/rustdoc-gui/module-items-font.goml @@ -74,3 +74,12 @@ assert-css: ( "#attributes + .item-table dd", {"font-family": '"Source Serif 4", NanumBarunGothic, serif'}, ) +// attribute macros +assert-css: ( + "#attribute-macros + .item-table dt a", + {"font-family": '"Fira Sans", Arial, NanumBarunGothic, sans-serif'}, +) +assert-css: ( + "#attribute-macros + .item-table dd", + {"font-family": '"Source Serif 4", NanumBarunGothic, serif'}, +) diff --git a/tests/rustdoc-gui/src/test_docs/macros.rs b/tests/rustdoc-gui/src/test_docs/macros.rs index 6564088c7be70..9d8e8baca9d57 100644 --- a/tests/rustdoc-gui/src/test_docs/macros.rs +++ b/tests/rustdoc-gui/src/test_docs/macros.rs @@ -3,7 +3,7 @@ macro_rules! a{ () => {}} #[macro_export] macro_rules! b{ () => {}} -// An attribute bang macro. +/// An attribute bang macro. #[macro_export] macro_rules! attr_macro { attr() () => {}; From 8bd80caa00f81acdb52897c8b55ef4e18981b756 Mon Sep 17 00:00:00 2001 From: Guillaume Gomez Date: Tue, 9 Sep 2025 17:13:59 +0200 Subject: [PATCH 5/7] Improve code and apply suggestions --- src/librustdoc/clean/inline.rs | 4 ++-- src/librustdoc/clean/mod.rs | 4 ++-- src/librustdoc/clean/types.rs | 6 +++++- src/librustdoc/html/markdown.rs | 4 ++-- src/librustdoc/html/render/context.rs | 9 ++++++--- src/librustdoc/html/render/mod.rs | 2 +- src/librustdoc/html/render/print_item.rs | 14 ++++++++------ src/librustdoc/json/conversions.rs | 24 ++++++++++++++---------- 8 files changed, 40 insertions(+), 27 deletions(-) diff --git a/src/librustdoc/clean/inline.rs b/src/librustdoc/clean/inline.rs index 23ae1a950c9a1..e95b9bd3c33ee 100644 --- a/src/librustdoc/clean/inline.rs +++ b/src/librustdoc/clean/inline.rs @@ -789,7 +789,7 @@ fn build_macro( source: utils::display_macro_source(cx, name, &def), macro_rules: def.macro_rules, }, - None, + MacroKinds::BANG, ), None, ), @@ -813,7 +813,7 @@ fn build_macro( source: utils::display_macro_source(cx, name, &def), macro_rules: def.macro_rules, }, - Some(macro_kinds), + macro_kinds, ); let mut ret = vec![]; for kind in macro_kinds.iter().filter(|kind| *kind != MacroKinds::BANG) { diff --git a/src/librustdoc/clean/mod.rs b/src/librustdoc/clean/mod.rs index 2baa418c9b884..0591c60251ca1 100644 --- a/src/librustdoc/clean/mod.rs +++ b/src/librustdoc/clean/mod.rs @@ -2847,7 +2847,7 @@ fn clean_maybe_renamed_item<'tcx>( source: display_macro_source(cx, name, macro_def), macro_rules: macro_def.macro_rules, }, - None, + MacroKinds::BANG, ), MacroKinds::ATTR => clean_proc_macro(item, &mut name, MacroKind::Attr, cx), MacroKinds::DERIVE => clean_proc_macro(item, &mut name, MacroKind::Derive, cx), @@ -2857,7 +2857,7 @@ fn clean_maybe_renamed_item<'tcx>( source: display_macro_source(cx, name, macro_def), macro_rules: macro_def.macro_rules, }, - Some(kinds), + kinds, ); let mac = generate_item_with_correct_attrs( cx, diff --git a/src/librustdoc/clean/types.rs b/src/librustdoc/clean/types.rs index f8a91d707a944..08c5425fd464c 100644 --- a/src/librustdoc/clean/types.rs +++ b/src/librustdoc/clean/types.rs @@ -846,8 +846,12 @@ pub(crate) enum ItemKind { ForeignStaticItem(Static, hir::Safety), /// `type`s from an extern block ForeignTypeItem, - MacroItem(Macro, Option), + MacroItem(Macro, MacroKinds), + /// This is NOT an attribute proc-macro but a bang macro with support for being used as an + /// attribute macro. AttrMacroItem, + /// This is NOT an attribute proc-macro but a bang macro with support for being used as a + /// derive macro. DeriveMacroItem, ProcMacroItem(ProcMacro), PrimitiveItem(PrimitiveType), diff --git a/src/librustdoc/html/markdown.rs b/src/librustdoc/html/markdown.rs index 752cd2e610a54..5614f3a266bdf 100644 --- a/src/librustdoc/html/markdown.rs +++ b/src/librustdoc/html/markdown.rs @@ -2038,7 +2038,7 @@ fn is_default_id(id: &str) -> bool { | "crate-search" | "crate-search-div" // This is the list of IDs used in HTML generated in Rust (including the ones - // used in tera template files). + // used in askama template files). | "themeStyle" | "settings-menu" | "help-button" @@ -2077,7 +2077,7 @@ fn is_default_id(id: &str) -> bool { | "blanket-implementations-list" | "deref-methods" | "layout" - | "aliased-type" + | "aliased-type", ) } diff --git a/src/librustdoc/html/render/context.rs b/src/librustdoc/html/render/context.rs index 385eeef81b683..ad2bc3612603a 100644 --- a/src/librustdoc/html/render/context.rs +++ b/src/librustdoc/html/render/context.rs @@ -169,7 +169,10 @@ impl SharedContext<'_> { struct SidebarItem { name: String, - is_actually_macro: bool, + /// Bang macros can now be used as attribute/derive macros, making it tricky to correctly + /// handle all their cases at once, which means that even if they are categorized as + /// derive/attribute macros, they should still link to a "macro_rules" URL. + is_macro_rules: bool, } impl serde::Serialize for SidebarItem { @@ -177,7 +180,7 @@ impl serde::Serialize for SidebarItem { where S: serde::Serializer, { - if self.is_actually_macro { + if self.is_macro_rules { let mut seq = serializer.serialize_seq(Some(2))?; seq.serialize_element(&self.name)?; seq.serialize_element(&1)?; @@ -349,7 +352,7 @@ impl<'tcx> Context<'tcx> { let name = name.to_string(); map.entry(type_) .or_default() - .push(SidebarItem { name, is_actually_macro: item.is_macro_placeholder() }); + .push(SidebarItem { name, is_macro_rules: item.is_macro_placeholder() }); } } diff --git a/src/librustdoc/html/render/mod.rs b/src/librustdoc/html/render/mod.rs index 9f1264442ad39..8a3a8db8b7b7a 100644 --- a/src/librustdoc/html/render/mod.rs +++ b/src/librustdoc/html/render/mod.rs @@ -2549,7 +2549,7 @@ impl ItemSection { Self::ForeignTypes => "foreign-types", Self::Keywords => "keywords", Self::Attributes => "attributes", - Self::AttributeMacros => "attributes", + Self::AttributeMacros => "attribute-macros", Self::DeriveMacros => "derives", Self::TraitAliases => "trait-aliases", } diff --git a/src/librustdoc/html/render/print_item.rs b/src/librustdoc/html/render/print_item.rs index c4ee2de5deecd..b72ab699ef978 100644 --- a/src/librustdoc/html/render/print_item.rs +++ b/src/librustdoc/html/render/print_item.rs @@ -406,13 +406,15 @@ fn item_module(cx: &Context<'_>, item: &clean::Item, items: &[clean::Item]) -> i for type_ in types { let my_section = item_ty_to_section(type_); - let section_id = my_section.id(); - let tag = - if section_id == "reexports" { REEXPORTS_TABLE_OPEN } else { ITEM_TABLE_OPEN }; + let tag = if my_section == super::ItemSection::Reexports { + REEXPORTS_TABLE_OPEN + } else { + ITEM_TABLE_OPEN + }; write!( w, "{}", - write_section_heading(my_section.name(), &cx.derive_id(section_id), None, tag) + write_section_heading(my_section.name(), &cx.derive_id(my_section.id()), None, tag) )?; for (_, myitem) in ¬_stripped_items[&type_] { @@ -1889,7 +1891,7 @@ fn item_macro( cx: &Context<'_>, it: &clean::Item, t: &clean::Macro, - kinds: Option, + kinds: MacroKinds, ) -> impl fmt::Display { fmt::from_fn(move |w| { wrap_item(w, |w| { @@ -1900,7 +1902,7 @@ fn item_macro( } write!(w, "{}", Escape(&t.source)) })?; - if let Some(kinds) = kinds { + if kinds != MacroKinds::BANG { write!( w, "

ⓘ This is {} {}

", diff --git a/src/librustdoc/json/conversions.rs b/src/librustdoc/json/conversions.rs index 21d5a4f89a0dc..8bb72e54f65e3 100644 --- a/src/librustdoc/json/conversions.rs +++ b/src/librustdoc/json/conversions.rs @@ -52,7 +52,12 @@ impl JsonRenderer<'_> { let clean::ItemInner { name, item_id, .. } = *item.inner; let id = self.id_from_item(item); let inner = match item.kind { - clean::KeywordItem | clean::AttributeItem => return None, + clean::KeywordItem + | clean::AttributeItem + // Placeholder so no need to handle it. + | clean::AttrMacroItem + // Placeholder so no need to handle it. + | clean::DeriveMacroItem => return None, clean::StrippedItem(ref inner) => { match &**inner { // We document stripped modules as with `Module::is_stripped` set to @@ -61,12 +66,12 @@ impl JsonRenderer<'_> { clean::ModuleItem(_) if self.imported_items.contains(&item_id.expect_def_id()) => { - from_clean_item(item, self)? + from_clean_item(item, self) } _ => return None, } } - _ => from_clean_item(item, self)?, + _ => from_clean_item(item, self), }; Some(Item { id, @@ -268,13 +273,13 @@ impl FromClean for AssocItemConstraintKind { } } -fn from_clean_item(item: &clean::Item, renderer: &JsonRenderer<'_>) -> Option { +fn from_clean_item(item: &clean::Item, renderer: &JsonRenderer<'_>) -> ItemEnum { use clean::ItemKind::*; let name = item.name; let is_crate = item.is_crate(); let header = item.fn_header(renderer.tcx); - Some(match &item.inner.kind { + match &item.inner.kind { ModuleItem(m) => { ItemEnum::Module(Module { is_crate, items: renderer.ids(&m.items), is_stripped: false }) } @@ -309,8 +314,6 @@ fn from_clean_item(item: &clean::Item, renderer: &JsonRenderer<'_>) -> Option ItemEnum::Macro(m.source.clone()), - // They are just placeholders so no need to handle them. - AttrMacroItem | DeriveMacroItem => return None, ProcMacroItem(m) => ItemEnum::ProcMacro(m.into_json(renderer)), PrimitiveItem(p) => { ItemEnum::Primitive(Primitive { @@ -337,8 +340,9 @@ fn from_clean_item(item: &clean::Item, renderer: &JsonRenderer<'_>) -> Option unreachable!(), + // `convert_item` early returns `None` for stripped items, keywords, attributes and + // "special" macro rules. + KeywordItem | AttributeItem | AttrMacroItem | DeriveMacroItem => unreachable!(), StrippedItem(inner) => { match inner.as_ref() { ModuleItem(m) => ItemEnum::Module(Module { @@ -354,7 +358,7 @@ fn from_clean_item(item: &clean::Item, renderer: &JsonRenderer<'_>) -> Option for Struct { From 3cbcc53dacf02ea40014dcfc9aeb37d9ef50dded Mon Sep 17 00:00:00 2001 From: Guillaume Gomez Date: Mon, 20 Oct 2025 14:56:53 +0200 Subject: [PATCH 6/7] Add search test for new macro kinds --- tests/rustdoc-js/macro-kinds.js | 18 ++++++++++++++++++ tests/rustdoc-js/macro-kinds.rs | 21 +++++++++++++++++++++ 2 files changed, 39 insertions(+) create mode 100644 tests/rustdoc-js/macro-kinds.js create mode 100644 tests/rustdoc-js/macro-kinds.rs diff --git a/tests/rustdoc-js/macro-kinds.js b/tests/rustdoc-js/macro-kinds.js new file mode 100644 index 0000000000000..75c6df714cdc4 --- /dev/null +++ b/tests/rustdoc-js/macro-kinds.js @@ -0,0 +1,18 @@ +// exact-check + +const EXPECTED = [ + { + 'query': 'macro:macro', + 'others': [ + { 'path': 'macro_kinds', 'name': 'macro1' }, + { 'path': 'macro_kinds', 'name': 'macro3' }, + ], + }, + { + 'query': 'attr:macro', + 'others': [ + { 'path': 'macro_kinds', 'name': 'macro1' }, + { 'path': 'macro_kinds', 'name': 'macro2' }, + ], + }, +]; diff --git a/tests/rustdoc-js/macro-kinds.rs b/tests/rustdoc-js/macro-kinds.rs new file mode 100644 index 0000000000000..115f25be7c215 --- /dev/null +++ b/tests/rustdoc-js/macro-kinds.rs @@ -0,0 +1,21 @@ +// Test which ensures that attribute macros are correctly handled by the search. +// For example: `macro1` should appear in both `attr` and `macro` filters whereas +// `macro2` and `macro3` should only appear in `attr` or `macro` filters respectively. + +#![feature(macro_attr)] + +#[macro_export] +macro_rules! macro1 { + attr() () => {}; + () => {}; +} + +#[macro_export] +macro_rules! macro2 { + attr() () => {}; +} + +#[macro_export] +macro_rules! macro3 { + () => {}; +} From 7812008c5d53046af0949a27afb557324e5bbe58 Mon Sep 17 00:00:00 2001 From: Guillaume Gomez Date: Tue, 21 Oct 2025 16:51:44 +0200 Subject: [PATCH 7/7] Fix link to attr/derive bang macros --- src/librustdoc/formats/item_type.rs | 12 +-- src/librustdoc/html/render/mod.rs | 10 ++- src/librustdoc/html/render/print_item.rs | 9 ++- src/librustdoc/html/static/js/rustdoc.d.ts | 23 +++++- src/librustdoc/html/static/js/search.js | 16 +++- src/librustdoc/json/conversions.rs | 4 +- tests/rustdoc-gui/attr-macros.goml | 89 +++++++++++++++------- tests/rustdoc-gui/src/test_docs/lib.rs | 1 + tests/rustdoc-gui/src/test_docs/macros.rs | 14 ++++ tests/rustdoc-js/macro-kinds.js | 17 ++++- tests/rustdoc-js/macro-kinds.rs | 7 ++ 11 files changed, 155 insertions(+), 47 deletions(-) diff --git a/src/librustdoc/formats/item_type.rs b/src/librustdoc/formats/item_type.rs index b80bc446e1152..8b969d92a494f 100644 --- a/src/librustdoc/formats/item_type.rs +++ b/src/librustdoc/formats/item_type.rs @@ -99,6 +99,8 @@ item_type! { // This number is reserved for use in JavaScript // Generic = 26, Attribute = 27, + BangMacroAttribute = 28, + BangMacroDerive = 29, } impl<'a> From<&'a clean::Item> for ItemType { @@ -128,10 +130,8 @@ impl<'a> From<&'a clean::Item> for ItemType { clean::ForeignFunctionItem(..) => ItemType::Function, // no ForeignFunction clean::ForeignStaticItem(..) => ItemType::Static, // no ForeignStatic clean::MacroItem(..) => ItemType::Macro, - // Is this a good idea? - clean::AttrMacroItem => ItemType::ProcAttribute, - // Is this a good idea? - clean::DeriveMacroItem => ItemType::ProcDerive, + clean::AttrMacroItem => ItemType::BangMacroAttribute, + clean::DeriveMacroItem => ItemType::BangMacroDerive, clean::PrimitiveItem(..) => ItemType::Primitive, clean::RequiredAssocConstItem(..) | clean::ProvidedAssocConstItem(..) @@ -225,8 +225,8 @@ impl ItemType { ItemType::AssocConst => "associatedconstant", ItemType::ForeignType => "foreigntype", ItemType::Keyword => "keyword", - ItemType::ProcAttribute => "attr", - ItemType::ProcDerive => "derive", + ItemType::ProcAttribute | ItemType::BangMacroAttribute => "attr", + ItemType::ProcDerive | ItemType::BangMacroDerive => "derive", ItemType::TraitAlias => "traitalias", ItemType::Attribute => "attribute", } diff --git a/src/librustdoc/html/render/mod.rs b/src/librustdoc/html/render/mod.rs index 8a3a8db8b7b7a..726f2da5fb9be 100644 --- a/src/librustdoc/html/render/mod.rs +++ b/src/librustdoc/html/render/mod.rs @@ -523,10 +523,12 @@ impl AllTypes { ItemType::TypeAlias => self.type_aliases.insert(ItemEntry::new(new_url, name)), ItemType::Static => self.statics.insert(ItemEntry::new(new_url, name)), ItemType::Constant => self.constants.insert(ItemEntry::new(new_url, name)), - ItemType::ProcAttribute => { + ItemType::ProcAttribute | ItemType::BangMacroAttribute => { self.attribute_macros.insert(ItemEntry::new(new_url, name)) } - ItemType::ProcDerive => self.derive_macros.insert(ItemEntry::new(new_url, name)), + ItemType::ProcDerive | ItemType::BangMacroDerive => { + self.derive_macros.insert(ItemEntry::new(new_url, name)) + } ItemType::TraitAlias => self.trait_aliases.insert(ItemEntry::new(new_url, name)), _ => true, }; @@ -2610,8 +2612,8 @@ fn item_ty_to_section(ty: ItemType) -> ItemSection { ItemType::ForeignType => ItemSection::ForeignTypes, ItemType::Keyword => ItemSection::Keywords, ItemType::Attribute => ItemSection::Attributes, - ItemType::ProcAttribute => ItemSection::AttributeMacros, - ItemType::ProcDerive => ItemSection::DeriveMacros, + ItemType::ProcAttribute | ItemType::BangMacroAttribute => ItemSection::AttributeMacros, + ItemType::ProcDerive | ItemType::BangMacroDerive => ItemSection::DeriveMacros, ItemType::TraitAlias => ItemSection::TraitAliases, } } diff --git a/src/librustdoc/html/render/print_item.rs b/src/librustdoc/html/render/print_item.rs index b72ab699ef978..2d77d41454839 100644 --- a/src/librustdoc/html/render/print_item.rs +++ b/src/librustdoc/html/render/print_item.rs @@ -314,7 +314,14 @@ fn item_module(cx: &Context<'_>, item: &clean::Item, items: &[clean::Item]) -> i FxHashMap::default(); for (index, item) in items.iter().filter(|i| !i.is_stripped()).enumerate() { - not_stripped_items.entry(item.type_()).or_default().push((index, item)); + // To prevent having new "bang macro attribute/derive" sections in the module, + // we cheat by turning them into their "proc-macro equivalent". + let type_ = match item.type_() { + ItemType::BangMacroAttribute => ItemType::ProcAttribute, + ItemType::BangMacroDerive => ItemType::ProcDerive, + type_ => type_, + }; + not_stripped_items.entry(type_).or_default().push((index, item)); } // the order of item types in the listing diff --git a/src/librustdoc/html/static/js/rustdoc.d.ts b/src/librustdoc/html/static/js/rustdoc.d.ts index e206d6633e630..a68e3791db34c 100644 --- a/src/librustdoc/html/static/js/rustdoc.d.ts +++ b/src/librustdoc/html/static/js/rustdoc.d.ts @@ -244,6 +244,27 @@ declare namespace rustdoc { traitParent: number?, deprecated: boolean, associatedItemDisambiguator: string?, + /** + * If `true`, this item is a `macro_rules!` macro that supports + * multiple usage syntaxes, as described in RFC 3697 and 3698. + * The syntax for such a macro looks like this: + * + * ```rust + * /// Doc Comment. + * macro_rules! NAME { + * attr(key = $value:literal) ($attached:item) => { ... }; + * derive() ($attached:item) => { ... }; + * ($bang:tt) => { ... }; + * } + * ``` + * + * Each usage syntax gets a separate EntryData---one for the attr, + * one for the derive, and one for the bang syntax---with a corresponding + * `ty` field that can be used for filtering and presenting results. + * But the documentation lives in a single `macro.NAME.html` page, and + * this boolean flag is used for generating that HREF. + */ + isBangMacro: boolean, } /** @@ -300,7 +321,7 @@ declare namespace rustdoc { type ItemType = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | - 21 | 22 | 23 | 24 | 25 | 26; + 21 | 22 | 23 | 24 | 25 | 26 | 28 | 29; /** * The viewmodel for the search engine results page. diff --git a/src/librustdoc/html/static/js/search.js b/src/librustdoc/html/static/js/search.js index 0929d351463cc..eb88b8068df02 100644 --- a/src/librustdoc/html/static/js/search.js +++ b/src/librustdoc/html/static/js/search.js @@ -120,6 +120,8 @@ const itemTypes = [ "traitalias", // 25 "generic", "attribute", + null, // bang macro attribute + null, // bang macro derive ]; // used for special search precedence @@ -1640,7 +1642,7 @@ class DocSearch { * ], [string]>} */ const raw = JSON.parse(encoded); - return { + const item = { krate: raw[0], ty: raw[1], modulePath: raw[2] === 0 ? null : raw[2] - 1, @@ -1649,7 +1651,15 @@ class DocSearch { traitParent: raw[5] === 0 ? null : raw[5] - 1, deprecated: raw[6] === 1 ? true : false, associatedItemDisambiguator: raw.length === 7 ? null : raw[7], + isBangMacro: false, }; + if (item.ty === 28 || item.ty === 29) { + // "proc attribute" is 23, "proc derive" is 24 whereas "bang macro attribute" is 28 and + // "bang macro derive" is 29, so 5 of difference to go from the latter to the former. + item.ty -= 5; + item.isBangMacro = true; + } + return item; } /** @@ -2146,7 +2156,7 @@ class DocSearch { let displayPath; let href; let traitPath = null; - const type = itemTypes[item.ty]; + const type = item.entry && item.entry.isBangMacro ? "macro" : itemTypes[item.ty]; const name = item.name; let path = item.modulePath; let exactPath = item.exactModulePath; @@ -3949,7 +3959,7 @@ class DocSearch { * @param {Promise[]} data * @returns {AsyncGenerator} */ - const flush = async function* (data) { + const flush = async function*(data) { const satr = sortAndTransformResults( await Promise.all(data), null, diff --git a/src/librustdoc/json/conversions.rs b/src/librustdoc/json/conversions.rs index 8bb72e54f65e3..a446df38bd46c 100644 --- a/src/librustdoc/json/conversions.rs +++ b/src/librustdoc/json/conversions.rs @@ -898,8 +898,8 @@ impl FromClean for ItemKind { Keyword => ItemKind::Keyword, Attribute => ItemKind::Attribute, TraitAlias => ItemKind::TraitAlias, - ProcAttribute => ItemKind::ProcAttribute, - ProcDerive => ItemKind::ProcDerive, + ProcAttribute | BangMacroAttribute => ItemKind::ProcAttribute, + ProcDerive | BangMacroDerive => ItemKind::ProcDerive, } } } diff --git a/tests/rustdoc-gui/attr-macros.goml b/tests/rustdoc-gui/attr-macros.goml index cf7337703651f..8fde3d54fa7ea 100644 --- a/tests/rustdoc-gui/attr-macros.goml +++ b/tests/rustdoc-gui/attr-macros.goml @@ -2,38 +2,75 @@ // the sidebar and in the module. go-to: "file://" + |DOC_PATH| + "/test_docs/macro.b.html" -// It should be present twice in the sidebar. -assert-count: ("#rustdoc-modnav a[href='macro.attr_macro.html']", 2) -assert-count: ("//*[@id='rustdoc-modnav']//a[text()='attr_macro']", 2) // We check that the current item in the sidebar is the correct one. assert-text: ("#rustdoc-modnav .block.macro .current", "b") -// We now go to the attribute macro page. -click: "#rustdoc-modnav a[href='macro.attr_macro.html']" -// It should be present twice in the sidebar. -assert-count: ("#rustdoc-modnav a[href='macro.attr_macro.html']", 2) -assert-count: ("//*[@id='rustdoc-modnav']//a[text()='attr_macro']", 2) -// We check that the current item is the "attr_macro". -assert-text: ("#rustdoc-modnav .block.macro .current", "attr_macro") -// Since the item is present twice in the sidebar, we should have two "current" items. -assert-count: ("#rustdoc-modnav .current", 2) -// We check it has the expected information. -assert-text: ("h3.macro-info", "ⓘ This is an attribute/function macro") +define-function: ( + "check_macro", + [name, info], + block { + // It should be present twice in the sidebar. + assert-count: ("#rustdoc-modnav a[href='macro." + |name| + ".html']", 2) + assert-count: ("//*[@id='rustdoc-modnav']//a[text()='" + |name| + "']", 2) + + // We now go to the macro page. + click: "#rustdoc-modnav a[href='macro." + |name| + ".html']" + // It should be present twice in the sidebar. + assert-count: ("#rustdoc-modnav a[href='macro." + |name| + ".html']", 2) + assert-count: ("//*[@id='rustdoc-modnav']//a[text()='" + |name| + "']", 2) + // We check that the current item is the macro. + assert-text: ("#rustdoc-modnav .block.macro .current", |name|) + // Since the item is present twice in the sidebar, we should have two "current" items. + assert-count: ("#rustdoc-modnav .current", 2) + // We check it has the expected information. + assert-text: ("h3.macro-info", "ⓘ This is " + |info| + "/function macro") + } +) + +call-function: ("check_macro", {"name": "attr_macro", "info": "an attribute"}) +call-function: ("check_macro", {"name": "derive_macro", "info": "a derive"}) + +define-function: ( + "crate_page", + [name, section_id], + block { + // It should be only present twice. + assert-count: ("#main-content a[href='macro." + |name| + ".html']", 2) + // First in the "Macros" section. + assert-text: ("#macros + .item-table a[href='macro." + |name| + ".html']", |name|) + // Then in the other macro section. + assert-text: ( + "#" + |section_id| + " + .item-table a[href='macro." + |name| + ".html']", + |name|, + ) + } +) // Now we check it's correctly listed in the crate page. go-to: "file://" + |DOC_PATH| + "/test_docs/index.html" -// It should be only present twice. -assert-count: ("#main-content a[href='macro.attr_macro.html']", 2) -// First in the "Macros" section. -assert-text: ("#macros + .item-table a[href='macro.attr_macro.html']", "attr_macro") -// Then in the "Attribute Macros" section. -assert-text: ("#attribute-macros + .item-table a[href='macro.attr_macro.html']", "attr_macro") +call-function: ("crate_page", {"name": "attr_macro", "section_id": "attribute-macros"}) +call-function: ("crate_page", {"name": "derive_macro", "section_id": "derives"}) +// We also check we don't have duplicated sections. +assert-count: ("//*[@id='main-content']/h2[text()='Attribute Macros']", 1) +assert-count: ("//*[@id='main-content']/h2[text()='Derive Macros']", 1) + +define-function: ( + "all_items_page", + [name, section_id], + block { + // It should be only present twice. + assert-count: ("#main-content a[href='macro." + |name| + ".html']", 2) + // First in the "Macros" section. + assert-text: ("#macros + .all-items a[href='macro." + |name| + ".html']", |name|) + // Then in the "Attribute Macros" section. + assert-text: ( + "#" + |section_id| + " + .all-items a[href='macro." + |name| + ".html']", + |name|, + ) + } +) // And finally we check it's correctly listed in the "all items" page. go-to: "file://" + |DOC_PATH| + "/test_docs/all.html" -// It should be only present twice. -assert-count: ("#main-content a[href='macro.attr_macro.html']", 2) -// First in the "Macros" section. -assert-text: ("#macros + .all-items a[href='macro.attr_macro.html']", "attr_macro") -// Then in the "Attribute Macros" section. -assert-text: ("#attribute-macros + .all-items a[href='macro.attr_macro.html']", "attr_macro") +call-function: ("all_items_page", {"name": "attr_macro", "section_id": "attribute-macros"}) +call-function: ("all_items_page", {"name": "derive_macro", "section_id": "derives"}) diff --git a/tests/rustdoc-gui/src/test_docs/lib.rs b/tests/rustdoc-gui/src/test_docs/lib.rs index a9e280b974aa9..7f1d495d27993 100644 --- a/tests/rustdoc-gui/src/test_docs/lib.rs +++ b/tests/rustdoc-gui/src/test_docs/lib.rs @@ -9,6 +9,7 @@ #![feature(doc_cfg)] #![feature(associated_type_defaults)] #![feature(macro_attr)] +#![feature(macro_derive)] /*! Enable the feature some-feature to enjoy diff --git a/tests/rustdoc-gui/src/test_docs/macros.rs b/tests/rustdoc-gui/src/test_docs/macros.rs index 9d8e8baca9d57..01da9c3bbdeb7 100644 --- a/tests/rustdoc-gui/src/test_docs/macros.rs +++ b/tests/rustdoc-gui/src/test_docs/macros.rs @@ -9,3 +9,17 @@ macro_rules! attr_macro { attr() () => {}; () => {}; } + +/// An attribute bang macro. +#[macro_export] +macro_rules! derive_macro { + derive() () => {}; + () => {}; +} + +#[macro_export] +macro_rules! one_for_all_macro { + attr() () => {}; + derive() () => {}; + () => {}; +} diff --git a/tests/rustdoc-js/macro-kinds.js b/tests/rustdoc-js/macro-kinds.js index 75c6df714cdc4..c752814e8f29a 100644 --- a/tests/rustdoc-js/macro-kinds.js +++ b/tests/rustdoc-js/macro-kinds.js @@ -4,15 +4,24 @@ const EXPECTED = [ { 'query': 'macro:macro', 'others': [ - { 'path': 'macro_kinds', 'name': 'macro1' }, - { 'path': 'macro_kinds', 'name': 'macro3' }, + { 'path': 'macro_kinds', 'name': 'macro1', 'href': '../macro_kinds/macro.macro1.html' }, + { 'path': 'macro_kinds', 'name': 'macro3', 'href': '../macro_kinds/macro.macro3.html' }, ], }, { 'query': 'attr:macro', 'others': [ - { 'path': 'macro_kinds', 'name': 'macro1' }, - { 'path': 'macro_kinds', 'name': 'macro2' }, + { 'path': 'macro_kinds', 'name': 'macro1', 'href': '../macro_kinds/macro.macro1.html' }, + { 'path': 'macro_kinds', 'name': 'macro2', 'href': '../macro_kinds/attr.macro2.html' }, + ], + }, + { + 'query': 'derive:macro', + 'others': [ + { 'path': 'macro_kinds', 'name': 'macro1', 'href': '../macro_kinds/macro.macro1.html' }, + { + 'path': 'macro_kinds', 'name': 'macro4', 'href': '../macro_kinds/derive.macro4.html' + }, ], }, ]; diff --git a/tests/rustdoc-js/macro-kinds.rs b/tests/rustdoc-js/macro-kinds.rs index 115f25be7c215..3bd753cadc677 100644 --- a/tests/rustdoc-js/macro-kinds.rs +++ b/tests/rustdoc-js/macro-kinds.rs @@ -3,10 +3,12 @@ // `macro2` and `macro3` should only appear in `attr` or `macro` filters respectively. #![feature(macro_attr)] +#![feature(macro_derive)] #[macro_export] macro_rules! macro1 { attr() () => {}; + derive() () => {}; () => {}; } @@ -19,3 +21,8 @@ macro_rules! macro2 { macro_rules! macro3 { () => {}; } + +#[macro_export] +macro_rules! macro4 { + derive() () => {}; +}