rustdoc: make attributes render consistently

* make attributes render inside code elements and inside divs with class `code-attribute`
* render attributes for macros, associated constants, and struct/union fields
This commit is contained in:
Karol Zwolak 2025-08-22 18:04:12 +02:00
parent f5703d5dd3
commit 3ac32cace1
3 changed files with 68 additions and 70 deletions

View file

@ -1029,6 +1029,7 @@ fn assoc_const(
) -> impl fmt::Display {
let tcx = cx.tcx();
fmt::from_fn(move |w| {
render_attributes_in_code(w, it, &" ".repeat(indent), cx);
write!(
w,
"{indent}{vis}const <a{href} class=\"constant\">{name}</a>{generics}: {ty}",
@ -1136,10 +1137,10 @@ fn assoc_method(
let (indent, indent_str, end_newline) = if parent == ItemType::Trait {
header_len += 4;
let indent_str = " ";
write!(w, "{}", render_attributes_in_pre(meth, indent_str, cx))?;
render_attributes_in_code(w, meth, indent_str, cx);
(4, indent_str, Ending::NoNewline)
} else {
render_attributes_in_code(w, meth, cx);
render_attributes_in_code(w, meth, "", cx);
(0, "", Ending::Newline)
};
write!(
@ -1309,28 +1310,28 @@ fn render_assoc_item(
})
}
// When an attribute is rendered inside a `<pre>` tag, it is formatted using
// a whitespace prefix and newline.
fn render_attributes_in_pre(it: &clean::Item, prefix: &str, cx: &Context<'_>) -> impl fmt::Display {
fmt::from_fn(move |f| {
for a in it.attributes(cx.tcx(), cx.cache()) {
writeln!(f, "{prefix}{a}")?;
}
Ok(())
})
}
struct CodeAttribute(String);
fn render_code_attribute(code_attr: CodeAttribute, w: &mut impl fmt::Write) {
write!(w, "<div class=\"code-attribute\">{}</div>", code_attr.0).unwrap();
fn render_code_attribute(prefix: &str, code_attr: CodeAttribute, w: &mut impl fmt::Write) {
write!(
w,
"<div class=\"code-attribute\">{prefix}{attr}</div>",
prefix = prefix,
attr = code_attr.0
)
.unwrap();
}
// When an attribute is rendered inside a <code> tag, it is formatted using
// a div to produce a newline after it.
fn render_attributes_in_code(w: &mut impl fmt::Write, it: &clean::Item, cx: &Context<'_>) {
fn render_attributes_in_code(
w: &mut impl fmt::Write,
it: &clean::Item,
prefix: &str,
cx: &Context<'_>,
) {
for attr in it.attributes(cx.tcx(), cx.cache()) {
render_code_attribute(CodeAttribute(attr), w);
render_code_attribute(prefix, CodeAttribute(attr), w);
}
}
@ -1342,7 +1343,7 @@ fn render_repr_attributes_in_code(
item_type: ItemType,
) {
if let Some(repr) = clean::repr_attributes(cx.tcx(), cx.cache(), def_id, item_type) {
render_code_attribute(CodeAttribute(repr), w);
render_code_attribute("", CodeAttribute(repr), w);
}
}

View file

@ -20,8 +20,8 @@ use super::{
AssocItemLink, AssocItemRender, Context, ImplRenderingParameters, RenderMode,
collect_paths_for_type, document, ensure_trailing_slash, get_filtered_impls_for_reference,
item_ty_to_section, notable_traits_button, notable_traits_json, render_all_impls,
render_assoc_item, render_assoc_items, render_attributes_in_code, render_attributes_in_pre,
render_impl, render_repr_attributes_in_code, render_rightside, render_stability_since_raw,
render_assoc_item, render_assoc_items, render_attributes_in_code, render_impl,
render_repr_attributes_in_code, render_rightside, render_stability_since_raw,
render_stability_since_raw_with_extra, write_section_heading,
};
use crate::clean;
@ -107,13 +107,6 @@ macro_rules! item_template_methods {
}
item_template_methods!($($rest)*);
};
(render_attributes_in_pre $($rest:tt)*) => {
fn render_attributes_in_pre(&self) -> impl fmt::Display {
let (item, cx) = self.item_and_cx();
render_attributes_in_pre(item, "", cx)
}
item_template_methods!($($rest)*);
};
(render_assoc_items $($rest:tt)*) => {
fn render_assoc_items(&self) -> impl fmt::Display {
let (item, cx) = self.item_and_cx();
@ -457,7 +450,12 @@ fn item_module(cx: &Context<'_>, item: &clean::Item, items: &[clean::Item]) -> i
write!(
w,
"<dt{id}>\
<code>{vis}{imp}</code>{stab_tags}\
<code>"
)?;
render_attributes_in_code(w, myitem, "", cx);
write!(
w,
"{vis}{imp}</code>{stab_tags}\
</dt>",
vis = visibility_print_with_space(myitem, cx),
imp = import.print(cx)
@ -625,11 +623,11 @@ fn item_function(cx: &Context<'_>, it: &clean::Item, f: &clean::Function) -> imp
let notable_traits = notable_traits_button(&f.decl.output, cx).maybe_display();
wrap_item(w, |w| {
render_attributes_in_code(w, it, "", cx);
write!(
w,
"{attrs}{vis}{constness}{asyncness}{safety}{abi}fn \
"{vis}{constness}{asyncness}{safety}{abi}fn \
{name}{generics}{decl}{notable_traits}{where_clause}",
attrs = render_attributes_in_pre(it, "", cx),
vis = visibility,
constness = constness,
asyncness = asyncness,
@ -666,10 +664,10 @@ fn item_trait(cx: &Context<'_>, it: &clean::Item, t: &clean::Trait) -> impl fmt:
// Output the trait definition
wrap_item(w, |mut w| {
render_attributes_in_code(&mut w, it, "", cx);
write!(
w,
"{attrs}{vis}{safety}{is_auto}trait {name}{generics}{bounds}",
attrs = render_attributes_in_pre(it, "", cx),
"{vis}{safety}{is_auto}trait {name}{generics}{bounds}",
vis = visibility_print_with_space(it, cx),
safety = t.safety(tcx).print_with_space(),
is_auto = if t.is_auto(tcx) { "auto " } else { "" },
@ -1240,10 +1238,10 @@ fn item_trait_alias(
) -> impl fmt::Display {
fmt::from_fn(|w| {
wrap_item(w, |w| {
render_attributes_in_code(w, it, "", cx);
write!(
w,
"{attrs}trait {name}{generics} = {bounds}{where_clause};",
attrs = render_attributes_in_pre(it, "", cx),
"trait {name}{generics} = {bounds}{where_clause};",
name = it.name.unwrap(),
generics = t.generics.print(cx),
bounds = print_bounds(&t.bounds, true, cx),
@ -1268,10 +1266,10 @@ fn item_trait_alias(
fn item_type_alias(cx: &Context<'_>, it: &clean::Item, t: &clean::TypeAlias) -> impl fmt::Display {
fmt::from_fn(|w| {
wrap_item(w, |w| {
render_attributes_in_code(w, it, "", cx);
write!(
w,
"{attrs}{vis}type {name}{generics}{where_clause} = {type_};",
attrs = render_attributes_in_pre(it, "", cx),
"{vis}type {name}{generics}{where_clause} = {type_};",
vis = visibility_print_with_space(it, cx),
name = it.name.unwrap(),
generics = t.generics.print(cx),
@ -1452,7 +1450,14 @@ item_template!(
impl<'a, 'cx: 'a> ItemUnion<'a, 'cx> {
fn render_union(&self) -> impl Display {
render_union(self.it, Some(self.generics), self.fields, self.cx)
render_union(
self.it,
Some(self.generics),
self.fields,
self.def_id,
self.is_type_alias,
self.cx,
)
}
fn document_field(&self, field: &'a clean::Item) -> impl Display {
@ -1479,27 +1484,6 @@ impl<'a, 'cx: 'a> ItemUnion<'a, 'cx> {
_ => None,
})
}
fn render_attributes_in_pre(&self) -> impl fmt::Display {
fmt::from_fn(move |f| {
if self.is_type_alias {
// For now the only attributes we render for type aliases are `repr` attributes.
if let Some(repr) = clean::repr_attributes(
self.cx.tcx(),
self.cx.cache(),
self.def_id,
ItemType::Union,
) {
writeln!(f, "{repr}")?;
};
} else {
for a in self.it.attributes(self.cx.tcx(), self.cx.cache()) {
writeln!(f, "{a}")?;
}
}
Ok(())
})
}
}
fn item_union(cx: &Context<'_>, it: &clean::Item, s: &clean::Union) -> impl fmt::Display {
@ -1563,7 +1547,7 @@ impl<'clean> DisplayEnum<'clean> {
// For now the only attributes we render for type aliases are `repr` attributes.
render_repr_attributes_in_code(w, cx, self.def_id, ItemType::Enum);
} else {
render_attributes_in_code(w, it, cx);
render_attributes_in_code(w, it, "", cx);
}
write!(
w,
@ -1702,7 +1686,7 @@ fn render_enum_fields(
if v.is_stripped() {
continue;
}
write!(w, "{}", render_attributes_in_pre(v, TAB, cx))?;
render_attributes_in_code(w, v, TAB, cx);
w.write_str(TAB)?;
match v.kind {
clean::VariantItem(ref var) => match var.kind {
@ -1882,6 +1866,7 @@ fn item_macro(cx: &Context<'_>, it: &clean::Item, t: &clean::Macro) -> impl fmt:
fmt::from_fn(|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);
if !t.macro_rules {
write!(w, "{}", visibility_print_with_space(it, cx))?;
}
@ -1950,7 +1935,7 @@ fn item_constant(
fmt::from_fn(|w| {
wrap_item(w, |w| {
let tcx = cx.tcx();
render_attributes_in_code(w, it, cx);
render_attributes_in_code(w, it, "", cx);
write!(
w,
@ -2018,7 +2003,7 @@ impl<'a> DisplayStruct<'a> {
// For now the only attributes we render for type aliases are `repr` attributes.
render_repr_attributes_in_code(w, cx, self.def_id, ItemType::Struct);
} else {
render_attributes_in_code(w, it, cx);
render_attributes_in_code(w, it, "", cx);
}
write!(
w,
@ -2115,7 +2100,7 @@ fn item_static(
) -> impl fmt::Display {
fmt::from_fn(move |w| {
wrap_item(w, |w| {
render_attributes_in_code(w, it, cx);
render_attributes_in_code(w, it, "", cx);
write!(
w,
"{vis}{safe}static {mutability}{name}: {typ}",
@ -2135,7 +2120,7 @@ fn item_foreign_type(cx: &Context<'_>, it: &clean::Item) -> impl fmt::Display {
fmt::from_fn(|w| {
wrap_item(w, |w| {
w.write_str("extern {\n")?;
render_attributes_in_code(w, it, cx);
render_attributes_in_code(w, it, "", cx);
write!(w, " {}type {};\n}}", visibility_print_with_space(it, cx), it.name.unwrap(),)
})?;
@ -2358,9 +2343,17 @@ fn render_union(
it: &clean::Item,
g: Option<&clean::Generics>,
fields: &[clean::Item],
def_id: DefId,
is_type_alias: bool,
cx: &Context<'_>,
) -> impl Display {
fmt::from_fn(move |mut f| {
if is_type_alias {
// For now the only attributes we render for type aliases are `repr` attributes.
render_repr_attributes_in_code(f, cx, def_id, ItemType::Union);
} else {
render_attributes_in_code(f, it, "", cx);
}
write!(f, "{}union {}", visibility_print_with_space(it, cx), it.name.unwrap(),)?;
let where_displayed = if let Some(generics) = g {
@ -2390,6 +2383,7 @@ fn render_union(
for field in fields {
if let clean::StructFieldItem(ref ty) = field.kind {
render_attributes_in_code(&mut f, field, " ", cx);
writeln!(
f,
" {}{}: {},",
@ -2481,11 +2475,15 @@ fn render_struct_fields(
if toggle {
toggle_open(&mut *w, format_args!("{count_fields} fields"));
}
if has_visible_fields {
writeln!(w)?;
}
for field in fields {
if let clean::StructFieldItem(ref ty) = field.kind {
write!(
render_attributes_in_code(w, field, &format!("{tab} "), cx);
writeln!(
w,
"\n{tab} {vis}{name}: {ty},",
"{tab} {vis}{name}: {ty},",
vis = visibility_print_with_space(field, cx),
name = field.name.unwrap(),
ty = ty.print(cx)
@ -2495,12 +2493,12 @@ fn render_struct_fields(
if has_visible_fields {
if has_stripped_entries {
write!(
writeln!(
w,
"\n{tab} <span class=\"comment\">/* private fields */</span>"
"{tab} <span class=\"comment\">/* private fields */</span>"
)?;
}
write!(w, "\n{tab}")?;
write!(w, "{tab}")?;
} else if has_stripped_entries {
write!(w, " <span class=\"comment\">/* private fields */</span> ")?;
}

View file

@ -1,5 +1,4 @@
<pre class="rust item-decl"><code>
{{ self.render_attributes_in_pre()|safe }}
{{ self.render_union()|safe }}
</code></pre>
{% if !self.is_type_alias %}