Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 15 additions & 5 deletions src/doc/rustdoc/src/advanced-features.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,20 +89,30 @@ https://doc.rust-lang.org/stable/std/?search=%s&go_to_first=true
This URL adds the `go_to_first=true` query parameter which can be appended to any `rustdoc` search URL
to automatically go to the first result.

## `#[repr(transparent)]`: Documenting the transparent representation
## `#[repr(...)]`: Documenting the representation of a type

Generally, rustdoc only displays the representation of a given type if none of its variants are
Copy link
Member Author

@fmease fmease May 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm very open to phrasing suggestions and ways to make this section and its subsection clearer! I feel like it could be much better!

`#[doc(hidden)]` and if all of its fields are public and not `#[doc(hidden)]` since it's likely
not meant to be considered part of the public ABI otherwise.

Note that there's no way to overwrite that heuristic and force rustdoc to show the representation
regardless.

### `#[repr(transparent)]`

You can read more about `#[repr(transparent)]` itself in the [Rust Reference][repr-trans-ref] and
in the [Rustonomicon][repr-trans-nomicon].

Since this representation is only considered part of the public ABI if the single field with non-trivial
size or alignment is public and if the documentation does not state otherwise, Rustdoc helpfully displays
the attribute if and only if the non-1-ZST field is public or at least one field is public in case all
fields are 1-ZST fields. The term *1-ZST* refers to types that are one-aligned and zero-sized.
size or alignment is public and if the documentation does not state otherwise, rustdoc helpfully displays
the attribute if and only if the non-1-ZST field is public and not `#[doc(hidden)]` or
– in case all fields are 1-ZST fields — at least one field is public and not `#[doc(hidden)]`.
The term *1-ZST* refers to types that are one-aligned and zero-sized.

It would seem that one can manually hide the attribute with `#[cfg_attr(not(doc), repr(transparent))]`
if one wishes to declare the representation as private even if the non-1-ZST field is public.
However, due to [current limitations][cross-crate-cfg-doc], this method is not always guaranteed to work.
Therefore, if you would like to do so, you should always write it down in prose independently of whether
Therefore, if you would like to do so, you should always write that down in prose independently of whether
you use `cfg_attr` or not.

[repr-trans-ref]: https://doc.rust-lang.org/reference/type-layout.html#the-transparent-representation
Expand Down
112 changes: 0 additions & 112 deletions src/librustdoc/clean/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -772,50 +772,6 @@ impl Item {
Some(tcx.visibility(def_id))
}

/// Get a list of attributes excluding `#[repr]` to display.
///
/// Only used by the HTML output-format.
fn attributes_without_repr(&self) -> Vec<String> {
self.attrs
.other_attrs
.iter()
.filter_map(|attr| match attr {
hir::Attribute::Parsed(AttributeKind::LinkSection { name, .. }) => {
Some(format!("#[unsafe(link_section = \"{name}\")]"))
}
hir::Attribute::Parsed(AttributeKind::NoMangle(..)) => {
Some("#[unsafe(no_mangle)]".to_string())
}
hir::Attribute::Parsed(AttributeKind::ExportName { name, .. }) => {
Some(format!("#[unsafe(export_name = \"{name}\")]"))
}
hir::Attribute::Parsed(AttributeKind::NonExhaustive(..)) => {
Some("#[non_exhaustive]".to_string())
}
_ => None,
})
.collect()
}

/// Get a list of attributes to display on this item.
///
/// Only used by the HTML output-format.
pub(crate) fn attributes(&self, tcx: TyCtxt<'_>, cache: &Cache) -> Vec<String> {
let mut attrs = self.attributes_without_repr();

if let Some(repr_attr) = self.repr(tcx, cache) {
attrs.push(repr_attr);
}
attrs
}

/// Returns a stringified `#[repr(...)]` attribute.
///
/// Only used by the HTML output-format.
pub(crate) fn repr(&self, tcx: TyCtxt<'_>, cache: &Cache) -> Option<String> {
repr_attributes(tcx, cache, self.def_id()?, self.type_())
}

pub fn is_doc_hidden(&self) -> bool {
self.attrs.is_doc_hidden()
}
Expand All @@ -825,74 +781,6 @@ impl Item {
}
}

/// Return a string representing the `#[repr]` attribute if present.
///
/// Only used by the HTML output-format.
pub(crate) fn repr_attributes(
tcx: TyCtxt<'_>,
cache: &Cache,
def_id: DefId,
item_type: ItemType,
) -> Option<String> {
use rustc_abi::IntegerType;

if !matches!(item_type, ItemType::Struct | ItemType::Enum | ItemType::Union) {
return None;
}
let adt = tcx.adt_def(def_id);
let repr = adt.repr();
let mut out = Vec::new();
if repr.c() {
out.push("C");
}
if repr.transparent() {
// Render `repr(transparent)` iff the non-1-ZST field is public or at least one
// field is public in case all fields are 1-ZST fields.
let render_transparent = cache.document_private
|| adt
.all_fields()
.find(|field| {
let ty = field.ty(tcx, ty::GenericArgs::identity_for_item(tcx, field.did));
tcx.layout_of(ty::TypingEnv::post_analysis(tcx, field.did).as_query_input(ty))
.is_ok_and(|layout| !layout.is_1zst())
})
.map_or_else(
|| adt.all_fields().any(|field| field.vis.is_public()),
|field| field.vis.is_public(),
);

if render_transparent {
out.push("transparent");
}
}
if repr.simd() {
out.push("simd");
}
let pack_s;
if let Some(pack) = repr.pack {
pack_s = format!("packed({})", pack.bytes());
out.push(&pack_s);
}
let align_s;
if let Some(align) = repr.align {
align_s = format!("align({})", align.bytes());
out.push(&align_s);
}
let int_s;
if let Some(int) = repr.int {
int_s = match int {
IntegerType::Pointer(is_signed) => {
format!("{}size", if is_signed { 'i' } else { 'u' })
}
IntegerType::Fixed(size, is_signed) => {
format!("{}{}", if is_signed { 'i' } else { 'u' }, size.size().bytes() * 8)
}
};
out.push(&int_s);
}
if !out.is_empty() { Some(format!("#[repr({})]", out.join(", "))) } else { None }
}

#[derive(Clone, Debug)]
pub(crate) enum ItemKind {
ExternCrateItem {
Expand Down
186 changes: 148 additions & 38 deletions src/librustdoc/html/render/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,9 @@ use askama::Template;
use itertools::Either;
use rustc_ast::join_path_syms;
use rustc_data_structures::fx::{FxHashSet, FxIndexMap, FxIndexSet};
use rustc_hir::attrs::{DeprecatedSince, Deprecation};
use rustc_hir as hir;
use rustc_hir::attrs::{AttributeKind, DeprecatedSince, Deprecation};
use rustc_hir::def::DefKind;
use rustc_hir::def_id::{DefId, DefIdSet};
use rustc_hir::{ConstStability, Mutability, RustcVersion, StabilityLevel, StableSince};
use rustc_middle::ty::print::PrintTraitRefExt;
Expand Down Expand Up @@ -1310,43 +1312,6 @@ fn render_assoc_item(
})
}

struct CodeAttribute(String);

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,
prefix: &str,
cx: &Context<'_>,
) {
for attr in it.attributes(cx.tcx(), cx.cache()) {
render_code_attribute(prefix, CodeAttribute(attr), w);
}
}

/// used for type aliases to only render their `repr` attribute.
fn render_repr_attributes_in_code(
w: &mut impl fmt::Write,
cx: &Context<'_>,
def_id: DefId,
item_type: ItemType,
) {
if let Some(repr) = clean::repr_attributes(cx.tcx(), cx.cache(), def_id, item_type) {
render_code_attribute("", CodeAttribute(repr), w);
}
}

#[derive(Copy, Clone)]
enum AssocItemLink<'a> {
Anchor(Option<&'a str>),
Expand Down Expand Up @@ -2932,3 +2897,148 @@ fn render_call_locations<W: fmt::Write>(

w.write_str("</div>")
}

fn render_attributes_in_code(
w: &mut impl fmt::Write,
it: &clean::Item,
prefix: &str,
cx: &Context<'_>,
) {
for attr in &it.attrs.other_attrs {
render_code_attribute(
prefix,
match attr {
hir::Attribute::Parsed(AttributeKind::LinkSection { name, .. }) => {
Cow::Owned(format!("#[unsafe(link_section = \"{}\")]", Escape(name.as_str())))
}
hir::Attribute::Parsed(AttributeKind::NoMangle(..)) => {
Cow::Borrowed("#[unsafe(no_mangle)]")
}
hir::Attribute::Parsed(AttributeKind::ExportName { name, .. }) => {
Cow::Owned(format!("#[unsafe(export_name = \"{}\")]", Escape(name.as_str())))
}
hir::Attribute::Parsed(AttributeKind::NonExhaustive(..)) => {
Cow::Borrowed("#[non_exhaustive]")
}
_ => continue,
}
.as_ref(),
w,
);
}

if let Some(def_id) = it.def_id()
&& let Some(repr) = repr_attribute(cx.tcx(), cx.cache(), def_id)
{
render_code_attribute(prefix, &repr, w);
}
}

fn render_repr_attribute_in_code(w: &mut impl fmt::Write, cx: &Context<'_>, def_id: DefId) {
if let Some(repr) = repr_attribute(cx.tcx(), cx.cache(), def_id) {
render_code_attribute("", &repr, w);
}
}

fn render_code_attribute(prefix: &str, attr: &str, w: &mut impl fmt::Write) {
write!(w, "<div class=\"code-attribute\">{prefix}{attr}</div>").unwrap();
}

fn repr_attribute<'tcx>(
tcx: TyCtxt<'tcx>,
cache: &Cache,
def_id: DefId,
) -> Option<Cow<'static, str>> {
// Read more about private vs. public `#[repr]` here:
// <https://doc.rust-lang.org/nightly/rustdoc/advanced-features.html#repr-documenting-the-representation-of-a-type>.

let adt = match tcx.def_kind(def_id) {
DefKind::Struct | DefKind::Enum | DefKind::Union => tcx.adt_def(def_id),
_ => return None,
};
let repr = adt.repr();

let is_visible = |def_id| cache.document_hidden || !tcx.is_doc_hidden(def_id);
let is_public_field = |field: &ty::FieldDef| {
(cache.document_private || field.vis.is_public()) && is_visible(field.did)
};

if repr.transparent() {
// The transparent repr is public iff the non-1-ZST field is public and visible or
// – in case all fields are 1-ZST fields — at least one field is public and visible.
let is_public = 'is_public: {
// `#[repr(transparent)]` can only be applied to structs and single-variant enums.
let var = adt.variant(rustc_abi::FIRST_VARIANT); // the first and only variant

if !is_visible(var.def_id) {
break 'is_public false;
}

// Side note: There can only ever be one or zero non-1-ZST fields.
let non_1zst_field = var.fields.iter().find(|field| {
let ty = ty::TypingEnv::post_analysis(tcx, field.did)
.as_query_input(tcx.type_of(field.did).instantiate_identity());
tcx.layout_of(ty).is_ok_and(|layout| !layout.is_1zst())
});

match non_1zst_field {
Some(field) => is_public_field(field),
None => var.fields.is_empty() || var.fields.iter().any(is_public_field),
}
};

// Since the transparent repr can't have any other reprs or
// repr modifiers beside it, we can safely return early here.
return is_public.then(|| "#[repr(transparent)]".into());
}

// Fast path which avoids looking through the variants and fields in
// the common case of no `#[repr]` or in the case of `#[repr(Rust)]`.
// FIXME: This check is not very robust / forward compatible!
if !repr.c()
&& !repr.simd()
&& repr.int.is_none()
&& repr.pack.is_none()
&& repr.align.is_none()
{
return None;
}

// The repr is public iff all components are public and visible.
let is_public = adt
.variants()
.iter()
.all(|variant| is_visible(variant.def_id) && variant.fields.iter().all(is_public_field));
if !is_public {
return None;
}

let mut result = Vec::<Cow<'_, _>>::new();

if repr.c() {
result.push("C".into());
}
if repr.simd() {
result.push("simd".into());
}
if let Some(int) = repr.int {
let prefix = if int.is_signed() { 'i' } else { 'u' };
let int = match int {
rustc_abi::IntegerType::Pointer(_) => format!("{prefix}size"),
rustc_abi::IntegerType::Fixed(int, _) => {
format!("{prefix}{}", int.size().bytes() * 8)
}
};
result.push(int.into());
}

// Render modifiers last.
if let Some(pack) = repr.pack {
result.push(format!("packed({})", pack.bytes()).into());
}
if let Some(align) = repr.align {
result.push(format!("align({})", align.bytes()).into());
}

(!result.is_empty()).then(|| format!("#[repr({})]", result.join(", ")).into())
}
Loading
Loading