Skip to content

Commit 5e6424b

Browse files
Auto merge of #116882 - fmease:rustdoc-generalized-priv-repr-heuristic, r=<try>
rustdoc: hide `#[repr]` if it isn't part of the public ABI Follow-up to #115439. Unblocks #116743, CC `@dtolnay.` Fixes #128364. Fixes #137440. Only display the representation `#[repr(REPR)]` (where `REPR` is not `Rust` or `transparent`) of a given type if the type is not `#[non_exhaustive]`, if none of its variants (incl. the synthetic variants of structs) are `#[doc(hidden)]` or `#[non_exhaustive]` 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. `--document-{private,hidden}-items` works as expected in this context, too. Moreover, we now also factor in the presence of `#[doc(hidden)]` and of `#[non_exhaustive]` when checking whether to show `repr(transparent)` or not.
2 parents 3bc767e + 94911f1 commit 5e6424b

File tree

11 files changed

+332
-202
lines changed

11 files changed

+332
-202
lines changed

src/doc/rustdoc/src/advanced-features.md

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -89,20 +89,32 @@ https://doc.rust-lang.org/stable/std/?search=%s&go_to_first=true
8989
This URL adds the `go_to_first=true` query parameter which can be appended to any `rustdoc` search URL
9090
to automatically go to the first result.
9191

92-
## `#[repr(transparent)]`: Documenting the transparent representation
92+
## `#[repr(...)]`: Documenting the representation of a type
93+
94+
Generally, rustdoc only displays the representation of a given type it's not `#[non_exhaustive]`,
95+
if none of its variants are `#[doc(hidden)]` or `#[non_exhaustive]` and
96+
if all of its fields are public and not `#[doc(hidden)]`
97+
since it's likely not meant to be considered part of the public ABI otherwise.
98+
99+
Note that there's no way to overwrite that heuristic and force rustdoc to show the representation
100+
regardless.
101+
102+
### `#[repr(transparent)]`
93103

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

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

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

108120
[repr-trans-ref]: https://doc.rust-lang.org/reference/type-layout.html#the-transparent-representation

src/librustdoc/clean/types.rs

Lines changed: 116 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use std::borrow::Cow;
12
use std::hash::Hash;
23
use std::path::PathBuf;
34
use std::sync::{Arc, OnceLock as OnceCell};
@@ -764,11 +765,12 @@ impl Item {
764765
Some(tcx.visibility(def_id))
765766
}
766767

767-
pub(crate) fn attributes_without_repr(&self, tcx: TyCtxt<'_>, is_json: bool) -> Vec<String> {
768+
pub(crate) fn attributes(&self, tcx: TyCtxt<'_>, cache: &Cache, is_json: bool) -> Vec<String> {
768769
const ALLOWED_ATTRIBUTES: &[Symbol] =
769770
&[sym::export_name, sym::link_section, sym::no_mangle, sym::non_exhaustive];
770771

771-
self.attrs
772+
let mut attrs: Vec<_> = self
773+
.attrs
772774
.other_attrs
773775
.iter()
774776
.filter_map(|attr| {
@@ -786,36 +788,24 @@ impl Item {
786788
}),
787789
}
788790
} else if attr.has_any_name(ALLOWED_ATTRIBUTES) {
789-
Some(
790-
rustc_hir_pretty::attribute_to_string(&tcx, attr)
791-
.replace("\\\n", "")
792-
.replace('\n', "")
793-
.replace(" ", " "),
794-
)
791+
let attr = rustc_hir_pretty::attribute_to_string(&tcx, attr)
792+
.replace("\\\n", "")
793+
.replace('\n', "")
794+
.replace(" ", " ");
795+
Some(attr)
795796
} else {
796797
None
797798
}
798799
})
799-
.collect()
800-
}
800+
.collect();
801801

802-
pub(crate) fn attributes_and_repr(
803-
&self,
804-
tcx: TyCtxt<'_>,
805-
cache: &Cache,
806-
is_json: bool,
807-
) -> Vec<String> {
808-
let mut attrs = self.attributes_without_repr(tcx, is_json);
809-
810-
if let Some(repr_attr) = self.repr(tcx, cache, is_json) {
811-
attrs.push(repr_attr);
802+
if let Some(def_id) = self.def_id()
803+
&& let Some(attr) = repr_attribute(tcx, cache, def_id, is_json)
804+
{
805+
attrs.push(attr);
812806
}
813-
attrs
814-
}
815807

816-
/// Returns a stringified `#[repr(...)]` attribute.
817-
pub(crate) fn repr(&self, tcx: TyCtxt<'_>, cache: &Cache, is_json: bool) -> Option<String> {
818-
repr_attributes(tcx, cache, self.def_id()?, self.type_(), is_json)
808+
attrs
819809
}
820810

821811
pub fn is_doc_hidden(&self) -> bool {
@@ -827,71 +817,123 @@ impl Item {
827817
}
828818
}
829819

830-
pub(crate) fn repr_attributes(
831-
tcx: TyCtxt<'_>,
820+
/// Compute the *public* `#[repr]` of the item given by `DefId`.
821+
///
822+
/// Read more about it here:
823+
/// <https://doc.rust-lang.org/nightly/rustdoc/advanced-features.html#repr-documenting-the-representation-of-a-type>.
824+
pub(crate) fn repr_attribute<'tcx>(
825+
tcx: TyCtxt<'tcx>,
832826
cache: &Cache,
833827
def_id: DefId,
834-
item_type: ItemType,
835828
is_json: bool,
836829
) -> Option<String> {
837-
use rustc_abi::IntegerType;
830+
let adt = match tcx.def_kind(def_id) {
831+
DefKind::Struct | DefKind::Enum | DefKind::Union => tcx.adt_def(def_id),
832+
_ => return None,
833+
};
834+
let repr = adt.repr();
838835

839-
if !matches!(item_type, ItemType::Struct | ItemType::Enum | ItemType::Union) {
836+
let is_visible = |def_id| cache.document_hidden || !tcx.is_doc_hidden(def_id);
837+
let is_public_field = |field: &ty::FieldDef| {
838+
(cache.document_private || field.vis.is_public()) && is_visible(field.did)
839+
};
840+
let is_exhaustive = |def_id| !tcx.has_attr(def_id, sym::non_exhaustive);
841+
842+
if repr.transparent() {
843+
// The transparent repr is public iff the non-1-ZST field is public and visible or
844+
// – in case all fields are 1-ZST fields — the type is exhaustive and at least
845+
// one field is public and visible.
846+
let is_public = 'is_public: {
847+
if is_json {
848+
break 'is_public true;
849+
}
850+
851+
// `#[repr(transparent)]` can only be applied to structs and single-variant enums.
852+
let var = adt.variant(rustc_abi::FIRST_VARIANT); // the first and only variant
853+
854+
// Therefore, if we have an enum, we don't care if it is `#[non_exhaustive]` or not
855+
// since the user isn't allowed to add more variants to it later anyway.
856+
857+
if !is_visible(var.def_id) {
858+
break 'is_public false;
859+
}
860+
861+
// Side note: There can only ever be one or zero non-1-ZST fields.
862+
let non_1zst_field = var.fields.iter().find(|field| {
863+
let ty = ty::TypingEnv::post_analysis(tcx, field.did)
864+
.as_query_input(tcx.type_of(field.did).instantiate_identity());
865+
tcx.layout_of(ty).is_ok_and(|layout| !layout.is_1zst())
866+
});
867+
868+
match non_1zst_field {
869+
// We don't care if the containing variant is `#[non_exhaustive]` or not as the
870+
// user is only allowed to add more *1-ZST* fields which don't matter in the
871+
// presence of this non-1-ZST field.
872+
Some(field) => is_public_field(field),
873+
None => {
874+
is_exhaustive(var.def_id)
875+
&& (var.fields.is_empty() || var.fields.iter().any(is_public_field))
876+
}
877+
}
878+
};
879+
880+
// Since the transparent repr can't have any other reprs or
881+
// repr modifiers beside it, we can safely return early here.
882+
return is_public.then(|| "#[repr(transparent)]".into());
883+
}
884+
885+
// Fast path which avoids looking through the variants and fields in
886+
// the common case of no `#[repr]` or in the case of `#[repr(Rust)]`.
887+
// FIXME: This check is not very robust / forward compatible!
888+
if !repr.c()
889+
&& !repr.simd()
890+
&& repr.int.is_none()
891+
&& repr.pack.is_none()
892+
&& repr.align.is_none()
893+
{
840894
return None;
841895
}
842-
let adt = tcx.adt_def(def_id);
843-
let repr = adt.repr();
844-
let mut out = Vec::new();
845-
if repr.c() {
846-
out.push("C");
896+
897+
// The repr is public iff all components are public, visible and exhaustive.
898+
let is_public = is_json
899+
|| is_exhaustive(def_id)
900+
&& adt.variants().iter().all(|variant| {
901+
is_exhaustive(variant.def_id)
902+
&& is_visible(variant.def_id)
903+
&& variant.fields.iter().all(is_public_field)
904+
});
905+
if !is_public {
906+
return None;
847907
}
848-
if repr.transparent() {
849-
// Render `repr(transparent)` iff the non-1-ZST field is public or at least one
850-
// field is public in case all fields are 1-ZST fields.
851-
let render_transparent = cache.document_private
852-
|| is_json
853-
|| adt
854-
.all_fields()
855-
.find(|field| {
856-
let ty = field.ty(tcx, ty::GenericArgs::identity_for_item(tcx, field.did));
857-
tcx.layout_of(ty::TypingEnv::post_analysis(tcx, field.did).as_query_input(ty))
858-
.is_ok_and(|layout| !layout.is_1zst())
859-
})
860-
.map_or_else(
861-
|| adt.all_fields().any(|field| field.vis.is_public()),
862-
|field| field.vis.is_public(),
863-
);
864908

865-
if render_transparent {
866-
out.push("transparent");
867-
}
909+
let mut result = Vec::<Cow<'_, _>>::new();
910+
911+
if repr.c() {
912+
result.push("C".into());
868913
}
869914
if repr.simd() {
870-
out.push("simd");
871-
}
872-
let pack_s;
873-
if let Some(pack) = repr.pack {
874-
pack_s = format!("packed({})", pack.bytes());
875-
out.push(&pack_s);
915+
result.push("simd".into());
876916
}
877-
let align_s;
878-
if let Some(align) = repr.align {
879-
align_s = format!("align({})", align.bytes());
880-
out.push(&align_s);
881-
}
882-
let int_s;
883917
if let Some(int) = repr.int {
884-
int_s = match int {
885-
IntegerType::Pointer(is_signed) => {
886-
format!("{}size", if is_signed { 'i' } else { 'u' })
887-
}
888-
IntegerType::Fixed(size, is_signed) => {
889-
format!("{}{}", if is_signed { 'i' } else { 'u' }, size.size().bytes() * 8)
918+
let prefix = if int.is_signed() { 'i' } else { 'u' };
919+
let int = match int {
920+
rustc_abi::IntegerType::Pointer(_) => format!("{prefix}size"),
921+
rustc_abi::IntegerType::Fixed(int, _) => {
922+
format!("{prefix}{}", int.size().bytes() * 8)
890923
}
891924
};
892-
out.push(&int_s);
925+
result.push(int.into());
893926
}
894-
if !out.is_empty() { Some(format!("#[repr({})]", out.join(", "))) } else { None }
927+
928+
// Render modifiers last.
929+
if let Some(pack) = repr.pack {
930+
result.push(format!("packed({})", pack.bytes()).into());
931+
}
932+
if let Some(align) = repr.align {
933+
result.push(format!("align({})", align.bytes()).into());
934+
}
935+
936+
(!result.is_empty()).then(|| format!("#[repr({})]", result.join(", ")))
895937
}
896938

897939
#[derive(Clone, Debug)]

src/librustdoc/html/render/mod.rs

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1194,7 +1194,7 @@ fn render_assoc_item(
11941194
// a whitespace prefix and newline.
11951195
fn render_attributes_in_pre(it: &clean::Item, prefix: &str, cx: &Context<'_>) -> impl fmt::Display {
11961196
fmt::from_fn(move |f| {
1197-
for a in it.attributes_and_repr(cx.tcx(), cx.cache(), false) {
1197+
for a in it.attributes(cx.tcx(), cx.cache(), false) {
11981198
writeln!(f, "{prefix}{a}")?;
11991199
}
12001200
Ok(())
@@ -1210,19 +1210,14 @@ fn render_code_attribute(code_attr: CodeAttribute, w: &mut impl fmt::Write) {
12101210
// When an attribute is rendered inside a <code> tag, it is formatted using
12111211
// a div to produce a newline after it.
12121212
fn render_attributes_in_code(w: &mut impl fmt::Write, it: &clean::Item, cx: &Context<'_>) {
1213-
for attr in it.attributes_and_repr(cx.tcx(), cx.cache(), false) {
1213+
for attr in it.attributes(cx.tcx(), cx.cache(), false) {
12141214
render_code_attribute(CodeAttribute(attr), w);
12151215
}
12161216
}
12171217

1218-
/// used for type aliases to only render their `repr` attribute.
1219-
fn render_repr_attributes_in_code(
1220-
w: &mut impl fmt::Write,
1221-
cx: &Context<'_>,
1222-
def_id: DefId,
1223-
item_type: ItemType,
1224-
) {
1225-
if let Some(repr) = clean::repr_attributes(cx.tcx(), cx.cache(), def_id, item_type, false) {
1218+
/// Render the `repr` attribute.
1219+
fn render_repr_attribute_in_code(w: &mut impl fmt::Write, cx: &Context<'_>, def_id: DefId) {
1220+
if let Some(repr) = clean::repr_attribute(cx.tcx(), cx.cache(), def_id, false) {
12261221
render_code_attribute(CodeAttribute(repr), w);
12271222
}
12281223
}

src/librustdoc/html/render/print_item.rs

Lines changed: 10 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ use super::{
2020
collect_paths_for_type, document, ensure_trailing_slash, get_filtered_impls_for_reference,
2121
item_ty_to_section, notable_traits_button, notable_traits_json, render_all_impls,
2222
render_assoc_item, render_assoc_items, render_attributes_in_code, render_attributes_in_pre,
23-
render_impl, render_repr_attributes_in_code, render_rightside, render_stability_since_raw,
23+
render_impl, render_repr_attribute_in_code, render_rightside, render_stability_since_raw,
2424
render_stability_since_raw_with_extra, write_section_heading,
2525
};
2626
use crate::clean;
@@ -1480,18 +1480,14 @@ impl<'a, 'cx: 'a> ItemUnion<'a, 'cx> {
14801480
fn render_attributes_in_pre(&self) -> impl fmt::Display {
14811481
fmt::from_fn(move |f| {
14821482
if self.is_type_alias {
1483-
// For now the only attributes we render for type aliases are `repr` attributes.
1484-
if let Some(repr) = clean::repr_attributes(
1485-
self.cx.tcx(),
1486-
self.cx.cache(),
1487-
self.def_id,
1488-
ItemType::Union,
1489-
false,
1490-
) {
1483+
// For now the only attribute we render for type aliases is the `repr` attribute.
1484+
if let Some(repr) =
1485+
clean::repr_attribute(self.cx.tcx(), self.cx.cache(), self.def_id, false)
1486+
{
14911487
writeln!(f, "{repr}")?;
14921488
};
14931489
} else {
1494-
for a in self.it.attributes_and_repr(self.cx.tcx(), self.cx.cache(), false) {
1490+
for a in self.it.attributes(self.cx.tcx(), self.cx.cache(), false) {
14951491
writeln!(f, "{a}")?;
14961492
}
14971493
}
@@ -1558,8 +1554,8 @@ impl<'clean> DisplayEnum<'clean> {
15581554

15591555
wrap_item(w, |w| {
15601556
if is_type_alias {
1561-
// For now the only attributes we render for type aliases are `repr` attributes.
1562-
render_repr_attributes_in_code(w, cx, self.def_id, ItemType::Enum);
1557+
// For now the only attribute we render for type aliases is the `repr` attribute.
1558+
render_repr_attribute_in_code(w, cx, self.def_id);
15631559
} else {
15641560
render_attributes_in_code(w, it, cx);
15651561
}
@@ -2016,8 +2012,8 @@ impl<'a> DisplayStruct<'a> {
20162012
) -> fmt::Result {
20172013
wrap_item(w, |w| {
20182014
if is_type_alias {
2019-
// For now the only attributes we render for type aliases are `repr` attributes.
2020-
render_repr_attributes_in_code(w, cx, self.def_id, ItemType::Struct);
2015+
// For now the only attribute we render for type aliases is the `repr` attribute.
2016+
render_repr_attribute_in_code(w, cx, self.def_id);
20212017
} else {
20222018
render_attributes_in_code(w, it, cx);
20232019
}

src/librustdoc/json/conversions.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ impl JsonRenderer<'_> {
4141
})
4242
.collect();
4343
let docs = item.opt_doc_value();
44-
let attrs = item.attributes_and_repr(self.tcx, self.cache(), true);
44+
let attrs = item.attributes(self.tcx, self.cache(), true);
4545
let span = item.span(self.tcx);
4646
let visibility = item.visibility(self.tcx);
4747
let clean::ItemInner { name, item_id, .. } = *item.inner;

tests/rustdoc-gui/src/test_docs/lib.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -455,10 +455,10 @@ pub fn safe_fn() {}
455455

456456
#[repr(C)]
457457
pub struct WithGenerics<T: TraitWithNoDocblocks, S = String, E = WhoLetTheDogOut, P = i8> {
458-
s: S,
459-
t: T,
460-
e: E,
461-
p: P,
458+
pub s: S,
459+
pub t: T,
460+
pub e: E,
461+
pub p: P,
462462
}
463463

464464
pub struct StructWithPublicUndocumentedFields {

0 commit comments

Comments
 (0)