Skip to content

rustdoc: hide #[repr] if it isn't part of the public ABI #116882

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
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
10 changes: 8 additions & 2 deletions src/doc/rustdoc/src/advanced-features.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,13 @@ 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.

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

You can read more about `#[repr(transparent)]` itself in the [Rust Reference][repr-trans-ref] and
in the [Rustonomicon][repr-trans-nomicon].
Expand All @@ -102,7 +108,7 @@ fields are 1-ZST fields. The term *1-ZST* refers to types that are one-aligned a
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
143 changes: 89 additions & 54 deletions src/librustdoc/clean/types.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use std::borrow::Cow;
use std::hash::Hash;
use std::path::PathBuf;
use std::sync::{Arc, OnceLock as OnceCell};
Expand Down Expand Up @@ -765,9 +766,7 @@ impl Item {
const ALLOWED_ATTRIBUTES: &[Symbol] =
&[sym::export_name, sym::link_section, sym::no_mangle, sym::non_exhaustive];

use rustc_abi::IntegerType;

let mut attrs: Vec<String> = self
let mut attrs: Vec<_> = self
.attrs
.other_attrs
.iter()
Expand Down Expand Up @@ -798,23 +797,40 @@ impl Item {
})
.collect();

// Add #[repr(...)]
if let Some(def_id) = self.def_id()
&& let ItemType::Struct | ItemType::Enum | ItemType::Union = self.type_()
{
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 = is_json
|| cache.document_private
|| adt
.all_fields()
if let Some(repr) = self.repr(tcx, cache, is_json) {
attrs.push(repr);
}

attrs
}

/// Compute the *public* `#[repr]` of this item.
///
/// Read more about it here:
/// <https://doc.rust-lang.org/nightly/rustdoc/advanced-features.html#repr-documenting-the-representation-of-a-type>.
fn repr<'tcx>(&self, tcx: TyCtxt<'tcx>, cache: &Cache, is_json: bool) -> Option<String> {
let def_id = self.def_id()?;
let (ItemType::Struct | ItemType::Enum | ItemType::Union) = self.type_() else {
return None;
};
let adt = tcx.adt_def(def_id);
let repr = adt.repr();

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

if repr.transparent() {
// `repr(transparent)` can only be applied to structs and single-variant enums.
let var = adt.variant(rustc_abi::FIRST_VARIANT);
// `repr(transparent)` is public iff the non-1-ZST field is public or
// at least one field is public in case all fields are 1-ZST fields.
let is_public = is_json
|| is_visible(var.def_id)
&& var
.fields
.iter()
.find(|field| {
let ty =
field.ty(tcx, ty::GenericArgs::identity_for_item(tcx, field.did));
Expand All @@ -824,44 +840,63 @@ impl Item {
.is_ok_and(|layout| !layout.is_1zst())
})
.map_or_else(
|| adt.all_fields().any(|field| field.vis.is_public()),
|field| field.vis.is_public(),
|| var.fields.is_empty() || var.fields.iter().any(is_field_public),
is_field_public,
);

if render_transparent {
out.push("transparent");
// Since `repr(transparent)` 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 forward compatible!
if !repr.c()
&& !repr.simd()
&& repr.int.is_none()
&& repr.pack.is_none()
&& repr.align.is_none()
{
return None;
}

let is_public = is_json
|| adt.variants().iter().all(|variant| {
is_visible(variant.def_id) && variant.fields.iter().all(is_field_public)
});
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)
}
}
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() {
attrs.push(format!("#[repr({})]", out.join(", ")));
}
};
result.push(int.into());
}
attrs

// 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(", ")))
}

pub fn is_doc_hidden(&self) -> bool {
Expand Down
8 changes: 4 additions & 4 deletions tests/rustdoc-gui/src/test_docs/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -455,10 +455,10 @@ pub fn safe_fn() {}

#[repr(C)]
pub struct WithGenerics<T: TraitWithNoDocblocks, S = String, E = WhoLetTheDogOut, P = i8> {
s: S,
t: T,
e: E,
p: P,
pub s: S,
pub t: T,
pub e: E,
pub p: P,
}

pub struct StructWithPublicUndocumentedFields {
Expand Down
4 changes: 2 additions & 2 deletions tests/rustdoc-json/attrs/repr_combination.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,13 +66,13 @@ pub struct ReversedReprCAlign {
b: i64,
}

//@ is "$.index[?(@.name=='AlignedExplicitRepr')].attrs" '["#[repr(C, align(16), isize)]"]'
//@ is "$.index[?(@.name=='AlignedExplicitRepr')].attrs" '["#[repr(C, isize, align(16))]"]'
#[repr(C, align(16), isize)]
pub enum AlignedExplicitRepr {
First,
}

//@ is "$.index[?(@.name=='ReorderedAlignedExplicitRepr')].attrs" '["#[repr(C, align(16), isize)]"]'
//@ is "$.index[?(@.name=='ReorderedAlignedExplicitRepr')].attrs" '["#[repr(C, isize, align(16))]"]'
#[repr(isize, C, align(16))]
pub enum ReorderedAlignedExplicitRepr {
First,
Expand Down
54 changes: 44 additions & 10 deletions tests/rustdoc/inline_cross/auxiliary/repr.rs
Original file line number Diff line number Diff line change
@@ -1,32 +1,66 @@
#![feature(repr_simd)]
#![feature(repr_simd)] // only used for the `ReprSimd` test case

#[repr(C, align(8))]
#[repr(Rust)]
pub struct ReprRust;

#[repr(C, align(8))] // **public**
pub struct ReprC {
field: u8,
pub field: u8,
}

#[repr(C)] // private
pub struct ReprCPrivField {
private: u8,
pub public: i8,
}
#[repr(simd, packed(2))]

#[repr(align(4))] // private
pub struct ReprAlignHiddenField {
#[doc(hidden)]
pub hidden: i16,
}

#[repr(simd, packed(2))] // **public**
pub struct ReprSimd {
field: [u8; 1],
pub field: [u8; 1],
}
#[repr(transparent)]

#[repr(transparent)] // **public**
pub struct ReprTransparent {
pub field: u8,
pub field: u8, // non-1-ZST field
}
#[repr(isize)]

#[repr(isize)] // **public**
pub enum ReprIsize {
Bla,
}
#[repr(u8)]

#[repr(u8)] // **public**
pub enum ReprU8 {
Bla,
}

#[repr(u32)] // private
pub enum ReprU32 {
#[doc(hidden)]
Hidden,
Public,
}

#[repr(u64)] // private
pub enum ReprU64HiddenVariants {
#[doc(hidden)]
A,
#[doc(hidden)]
B,
}

#[repr(transparent)] // private
pub struct ReprTransparentPrivField {
field: u32, // non-1-ZST field
}

#[repr(transparent)] // public
#[repr(transparent)] // **public**
pub struct ReprTransparentPriv1ZstFields {
marker0: Marker,
pub main: u64, // non-1-ZST field
Expand Down
25 changes: 25 additions & 0 deletions tests/rustdoc/inline_cross/repr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,22 +7,47 @@

extern crate repr;

// Never display `repr(Rust)` since it's the default anyway.
//@ has 'foo/struct.ReprRust.html'
//@ !has - '//*[@class="rust item-decl"]//*[@class="code-attribute"]' '#[repr(Rust)]'
pub use repr::ReprRust;

//@ has 'foo/struct.ReprC.html'
//@ has - '//*[@class="rust item-decl"]//*[@class="code-attribute"]' '#[repr(C, align(8))]'
pub use repr::ReprC;

//@ has 'foo/struct.ReprCPrivField.html'
//@ !has - '//*[@class="rust item-decl"]//*[@class="code-attribute"]' '#[repr(C)]'
pub use repr::ReprCPrivField;

//@ has 'foo/struct.ReprAlignHiddenField.html'
//@ !has - '//*[@class="rust item-decl"]//*[@class="code-attribute"]' '#[repr(align(4))]'
pub use repr::ReprAlignHiddenField;

//@ has 'foo/struct.ReprSimd.html'
//@ has - '//*[@class="rust item-decl"]//*[@class="code-attribute"]' '#[repr(simd, packed(2))]'
pub use repr::ReprSimd;

//@ has 'foo/struct.ReprTransparent.html'
//@ has - '//*[@class="rust item-decl"]//*[@class="code-attribute"]' '#[repr(transparent)]'
pub use repr::ReprTransparent;

//@ has 'foo/enum.ReprIsize.html'
//@ has - '//*[@class="rust item-decl"]//*[@class="code-attribute"]' '#[repr(isize)]'
pub use repr::ReprIsize;

//@ has 'foo/enum.ReprU8.html'
//@ has - '//*[@class="rust item-decl"]//*[@class="code-attribute"]' '#[repr(u8)]'
pub use repr::ReprU8;

//@ has 'foo/enum.ReprU32.html'
//@ !has - '//*[@class="rust item-decl"]//*[@class="code-attribute"]' '#[repr(u32)]'
pub use repr::ReprU32;

//@ has 'foo/enum.ReprU64HiddenVariants.html'
//@ !has - '//*[@class="rust item-decl"]//*[@class="code-attribute"]' '#[repr(u64)]'
pub use repr::ReprU64HiddenVariants;

// Regression test for <https://github.com/rust-lang/rust/issues/90435>.
// Check that we show `#[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.
Expand Down
Loading
Loading