From 098c41ebcd63c652a0ff4145cb778f575d1543d9 Mon Sep 17 00:00:00 2001 From: Alona Enraght-Moony Date: Fri, 20 Jun 2025 02:48:15 +0000 Subject: [PATCH 1/3] WIP: rustdoc-json: Structured attributes [squash-before-merge] better `#[repr]` [squash-before-merge] update tests [squash-before-merge] sufficient now! --- src/librustdoc/clean/types.rs | 51 ++++---- src/librustdoc/html/render/mod.rs | 6 +- src/librustdoc/html/render/print_item.rs | 3 +- src/librustdoc/json/conversions.rs | 110 +++++++++++++++++- src/rustdoc-json-types/lib.rs | 65 ++++++++++- .../attrs/automatically_derived.rs | 2 +- tests/rustdoc-json/attrs/export_name_2021.rs | 2 +- tests/rustdoc-json/attrs/export_name_2024.rs | 4 +- tests/rustdoc-json/attrs/inline.rs | 6 +- tests/rustdoc-json/attrs/must_use.rs | 4 +- tests/rustdoc-json/attrs/no_mangle_2021.rs | 2 +- tests/rustdoc-json/attrs/no_mangle_2024.rs | 2 +- tests/rustdoc-json/attrs/non_exhaustive.rs | 6 +- tests/rustdoc-json/attrs/repr_align.rs | 3 +- tests/rustdoc-json/attrs/repr_c.rs | 18 ++- tests/rustdoc-json/attrs/repr_c_int_enum.rs | 6 + tests/rustdoc-json/attrs/repr_combination.rs | 25 ++-- tests/rustdoc-json/attrs/repr_int_enum.rs | 15 ++- tests/rustdoc-json/attrs/repr_packed.rs | 8 +- .../rustdoc-json/enums/discriminant/struct.rs | 2 +- .../rustdoc-json/enums/discriminant/tuple.rs | 2 +- tests/rustdoc-json/keyword_private.rs | 4 +- 22 files changed, 267 insertions(+), 79 deletions(-) create mode 100644 tests/rustdoc-json/attrs/repr_c_int_enum.rs diff --git a/src/librustdoc/clean/types.rs b/src/librustdoc/clean/types.rs index 69899539b4523..3a05a9ebddbcf 100644 --- a/src/librustdoc/clean/types.rs +++ b/src/librustdoc/clean/types.rs @@ -746,34 +746,23 @@ impl Item { Some(tcx.visibility(def_id)) } - fn attributes_without_repr(&self, tcx: TyCtxt<'_>, is_json: bool) -> Vec { + /// Get a list of attributes excluding `#[repr]`. + /// + /// Only used by the HTML output-format. + fn attributes_without_repr(&self, tcx: TyCtxt<'_>) -> Vec { const ALLOWED_ATTRIBUTES: &[Symbol] = - &[sym::export_name, sym::link_section, sym::no_mangle, sym::non_exhaustive]; + &[sym::export_name, sym::link_section, sym::non_exhaustive]; self.attrs .other_attrs .iter() .filter_map(|attr| { - // NoMangle is special cased, as it appears in HTML output, and we want to show it in source form, not HIR printing. - // It is also used by cargo-semver-checks. - if matches!(attr, hir::Attribute::Parsed(AttributeKind::NoMangle(..))) { + // NoMangle is special cased, as it's currently the only structured + // attribute that we want to appear in HTML output. + if matches!(attr, hir::Attribute::Parsed(AttributeKind::NoMangle(_))) { Some("#[no_mangle]".to_string()) - } else if is_json { - match attr { - // rustdoc-json stores this in `Item::deprecation`, so we - // don't want it it `Item::attrs`. - hir::Attribute::Parsed(AttributeKind::Deprecation { .. }) => None, - // We have separate pretty-printing logic for `#[repr(..)]` attributes. - hir::Attribute::Parsed(AttributeKind::Repr(..)) => None, - _ => Some({ - let mut s = rustc_hir_pretty::attribute_to_string(&tcx, attr); - assert_eq!(s.pop(), Some('\n')); - s - }), - } + } else if !attr.has_any_name(ALLOWED_ATTRIBUTES) { + None } else { - if !attr.has_any_name(ALLOWED_ATTRIBUTES) { - return None; - } Some( rustc_hir_pretty::attribute_to_string(&tcx, attr) .replace("\\\n", "") @@ -785,18 +774,23 @@ impl Item { .collect() } - pub(crate) fn attributes(&self, tcx: TyCtxt<'_>, cache: &Cache, is_json: bool) -> Vec { - let mut attrs = self.attributes_without_repr(tcx, is_json); + /// 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 { + let mut attrs = self.attributes_without_repr(tcx); - if let Some(repr_attr) = self.repr(tcx, cache, is_json) { + if let Some(repr_attr) = self.repr(tcx, cache) { attrs.push(repr_attr); } attrs } /// Returns a stringified `#[repr(...)]` attribute. - pub(crate) fn repr(&self, tcx: TyCtxt<'_>, cache: &Cache, is_json: bool) -> Option { - repr_attributes(tcx, cache, self.def_id()?, self.type_(), is_json) + /// + /// Only used by the HTML output-format. + pub(crate) fn repr(&self, tcx: TyCtxt<'_>, cache: &Cache) -> Option { + repr_attributes(tcx, cache, self.def_id()?, self.type_()) } pub fn is_doc_hidden(&self) -> bool { @@ -808,12 +802,14 @@ 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, - is_json: bool, ) -> Option { use rustc_abi::IntegerType; @@ -830,7 +826,6 @@ pub(crate) fn repr_attributes( // 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 - || is_json || adt .all_fields() .find(|field| { diff --git a/src/librustdoc/html/render/mod.rs b/src/librustdoc/html/render/mod.rs index ed58bae70bd46..5f23fd094df9c 100644 --- a/src/librustdoc/html/render/mod.rs +++ b/src/librustdoc/html/render/mod.rs @@ -1194,7 +1194,7 @@ fn render_assoc_item( // 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(), false) { + for a in it.attributes(cx.tcx(), cx.cache()) { writeln!(f, "{prefix}{a}")?; } Ok(()) @@ -1210,7 +1210,7 @@ fn render_code_attribute(code_attr: CodeAttribute, w: &mut impl fmt::Write) { // When an attribute is rendered inside a 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<'_>) { - for attr in it.attributes(cx.tcx(), cx.cache(), false) { + for attr in it.attributes(cx.tcx(), cx.cache()) { render_code_attribute(CodeAttribute(attr), w); } } @@ -1222,7 +1222,7 @@ fn render_repr_attributes_in_code( def_id: DefId, item_type: ItemType, ) { - if let Some(repr) = clean::repr_attributes(cx.tcx(), cx.cache(), def_id, item_type, false) { + if let Some(repr) = clean::repr_attributes(cx.tcx(), cx.cache(), def_id, item_type) { render_code_attribute(CodeAttribute(repr), w); } } diff --git a/src/librustdoc/html/render/print_item.rs b/src/librustdoc/html/render/print_item.rs index 515424cbef19b..d2e80cb3f1adb 100644 --- a/src/librustdoc/html/render/print_item.rs +++ b/src/librustdoc/html/render/print_item.rs @@ -1486,12 +1486,11 @@ impl<'a, 'cx: 'a> ItemUnion<'a, 'cx> { self.cx.cache(), self.def_id, ItemType::Union, - false, ) { writeln!(f, "{repr}")?; }; } else { - for a in self.it.attributes(self.cx.tcx(), self.cx.cache(), false) { + for a in self.it.attributes(self.cx.tcx(), self.cx.cache()) { writeln!(f, "{a}")?; } } diff --git a/src/librustdoc/json/conversions.rs b/src/librustdoc/json/conversions.rs index f51b35097f66a..ecffd5b4270c3 100644 --- a/src/librustdoc/json/conversions.rs +++ b/src/librustdoc/json/conversions.rs @@ -7,9 +7,11 @@ use rustc_abi::ExternAbi; use rustc_ast::ast; use rustc_attr_data_structures::{self as attrs, DeprecatedSince}; +use rustc_hir as hir; use rustc_hir::def::CtorKind; use rustc_hir::def_id::DefId; use rustc_metadata::rendered_const; +use rustc_middle::ty::TyCtxt; use rustc_middle::{bug, ty}; use rustc_span::{Pos, kw, sym}; use rustdoc_json_types::*; @@ -40,7 +42,12 @@ impl JsonRenderer<'_> { }) .collect(); let docs = item.opt_doc_value(); - let attrs = item.attributes(self.tcx, &self.cache, true); + let attrs = item + .attrs + .other_attrs + .iter() + .filter_map(|a| maybe_from_hir_attr(a, item.item_id, self.tcx)) + .collect(); let span = item.span(self.tcx); let visibility = item.visibility(self.tcx); let clean::ItemInner { name, item_id, .. } = *item.inner; @@ -875,3 +882,104 @@ impl FromClean for ItemKind { } } } + +/// Maybe convert a attribue from hir to json. +/// +/// Returns `None` if the attribute shouldn't be in the output. +fn maybe_from_hir_attr( + attr: &hir::Attribute, + item_id: ItemId, + tcx: TyCtxt<'_>, +) -> Option { + use attrs::AttributeKind as AK; + + let kind = match attr { + hir::Attribute::Parsed(kind) => kind, + + // There are some currently unstrucured attrs that we *do* care about. + // As the attribute migration progresses (#131229), this is expected to shrink + // and eventually be removed as all attributes gain a strutured representation in + // HIR. + hir::Attribute::Unparsed(_) => { + return Some(if attr.has_name(sym::non_exhaustive) { + Attribute::NonExhaustive + } else if attr.has_name(sym::automatically_derived) { + Attribute::AutomaticallyDerived + } else if attr.has_name(sym::export_name) { + Attribute::ExportName( + attr.value_str().expect("checked by attr validation").to_string(), + ) + } else if attr.has_name(sym::doc) + && attr + .meta_item_list() + .is_some_and(|metas| metas.iter().any(|item| item.has_name(sym::hidden))) + { + Attribute::DocHidden + } else { + other_attr(tcx, attr) + }); + } + }; + + Some(match kind { + AK::Deprecation { .. } => return None, // Handled seperatly into Item::deprecation. + AK::DocComment { .. } => unreachable!("doc comments stripped out earlier"), + + AK::MustUse { reason, span: _ } => { + Attribute::MustUse { reason: reason.map(|s| s.to_string()) } + } + AK::Repr { .. } => repr_attr( + tcx, + item_id.as_def_id().expect("all items that could have #[repr] have a DefId"), + ), + AK::NoMangle(_) => Attribute::NoMangle, + + _ => other_attr(tcx, attr), + }) +} + +fn other_attr(tcx: TyCtxt<'_>, attr: &hir::Attribute) -> Attribute { + let mut s = rustc_hir_pretty::attribute_to_string(&tcx, attr); + assert_eq!(s.pop(), Some('\n')); + Attribute::Other(s) +} + +fn repr_attr(tcx: TyCtxt<'_>, def_id: DefId) -> Attribute { + let repr = tcx.adt_def(def_id).repr(); + + let kind = if repr.c() { + ReprKind::C + } else if repr.transparent() { + ReprKind::Transparent + } else if repr.simd() { + ReprKind::Simd + } else { + ReprKind::Rust + }; + + let align = repr.align.map(|a| a.bytes()); + let packed = repr.pack.map(|p| p.bytes()); + let int = repr.int.map(format_integer_type); + + Attribute::Repr(AttributeRepr { kind, align, packed, int }) +} + +fn format_integer_type(it: rustc_abi::IntegerType) -> String { + use rustc_abi::Integer::*; + use rustc_abi::IntegerType::*; + match it { + Pointer(true) => "isize", + Pointer(false) => "usize", + Fixed(I8, true) => "i8", + Fixed(I8, false) => "u8", + Fixed(I16, true) => "i16", + Fixed(I16, false) => "u16", + Fixed(I32, true) => "i32", + Fixed(I32, false) => "u32", + Fixed(I64, true) => "i64", + Fixed(I64, false) => "u64", + Fixed(I128, true) => "i128", + Fixed(I128, false) => "u128", + } + .to_owned() +} diff --git a/src/rustdoc-json-types/lib.rs b/src/rustdoc-json-types/lib.rs index 0e72ddd9db1ed..108c151860e36 100644 --- a/src/rustdoc-json-types/lib.rs +++ b/src/rustdoc-json-types/lib.rs @@ -37,8 +37,8 @@ pub type FxHashMap = HashMap; // re-export for use in src/librustdoc // will instead cause conflicts. See #94591 for more. (This paragraph and the "Latest feature" line // are deliberately not in a doc comment, because they need not be in public docs.) // -// Latest feature: Pretty printing of no_mangle attributes changed -pub const FORMAT_VERSION: u32 = 53; +// Latest feature: Structured Attributes +pub const FORMAT_VERSION: u32 = 54; /// The root of the emitted JSON blob. /// @@ -195,13 +195,70 @@ pub struct Item { /// - `#[repr(C)]` and other reprs also appear as themselves, /// though potentially with a different order: e.g. `repr(i8, C)` may become `repr(C, i8)`. /// Multiple repr attributes on the same item may be combined into an equivalent single attr. - pub attrs: Vec, + pub attrs: Vec, /// Information about the item’s deprecation, if present. pub deprecation: Option, /// The type-specific fields describing this item. pub inner: ItemEnum, } +/// An attribute, eg `#[repr(C)]` +/// +/// This doesn't include: +/// - `#[doc = "Doc Comment"]` or `/// Doc comment`. These are in [`Item::docs`] instead. +/// - `#[deprecated]`. These are in [`Item::deprecation`] instead. +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum Attribute { + /// `#[non_exhaustive]` + NonExhaustive, + + /// `#[must_use]` + MustUse { + reason: Option, + }, + + /// `#[automatically_derived]` + AutomaticallyDerived, + + /// `#[repr]` + Repr(AttributeRepr), + + ExportName(String), + /// `#[doc(hidden)]` + DocHidden, + /// `#[no_mangle]` + NoMangle, + + /// Something else. + /// + /// Things here are explicitly *not* covered by the [`FORMAT_VERSION`] + /// constant, and may change without bumping the format version. If you rely + /// on an attribute here, please open an issue about adding a new variant for + /// that attr. + Other(String), +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct AttributeRepr { + pub kind: ReprKind, + + /// Alignment, in bytes. + pub align: Option, + pub packed: Option, + + pub int: Option, +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum ReprKind { + Rust, + C, + Transparent, + Simd, +} + /// A range of source code. #[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] pub struct Span { @@ -1343,7 +1400,7 @@ pub struct Static { /// Is the static `unsafe`? /// - /// This is only true if it's in an `extern` block, and not explicity marked + /// This is only true if it's in an `extern` block, and not explicitly marked /// as `safe`. /// /// ```rust diff --git a/tests/rustdoc-json/attrs/automatically_derived.rs b/tests/rustdoc-json/attrs/automatically_derived.rs index 4e1ab3d145e5d..9ba310d3655b8 100644 --- a/tests/rustdoc-json/attrs/automatically_derived.rs +++ b/tests/rustdoc-json/attrs/automatically_derived.rs @@ -9,5 +9,5 @@ impl Default for Manual { } } -//@ is '$.index[?(@.inner.impl.for.resolved_path.path == "Derive" && @.inner.impl.trait.path == "Default")].attrs' '["#[automatically_derived]"]' +//@ is '$.index[?(@.inner.impl.for.resolved_path.path == "Derive" && @.inner.impl.trait.path == "Default")].attrs' '["automatically_derived"]' //@ is '$.index[?(@.inner.impl.for.resolved_path.path == "Manual" && @.inner.impl.trait.path == "Default")].attrs' '[]' diff --git a/tests/rustdoc-json/attrs/export_name_2021.rs b/tests/rustdoc-json/attrs/export_name_2021.rs index 254e9f6ef5bfd..451d9b9eb375e 100644 --- a/tests/rustdoc-json/attrs/export_name_2021.rs +++ b/tests/rustdoc-json/attrs/export_name_2021.rs @@ -1,6 +1,6 @@ //@ edition: 2021 #![no_std] -//@ is "$.index[?(@.name=='example')].attrs" '["#[export_name = \"altered\"]"]' +//@ is "$.index[?(@.name=='example')].attrs" '[{"export_name": "altered"}]' #[export_name = "altered"] pub extern "C" fn example() {} diff --git a/tests/rustdoc-json/attrs/export_name_2024.rs b/tests/rustdoc-json/attrs/export_name_2024.rs index 8129c109306c0..7e398db92ab3b 100644 --- a/tests/rustdoc-json/attrs/export_name_2024.rs +++ b/tests/rustdoc-json/attrs/export_name_2024.rs @@ -2,8 +2,8 @@ #![no_std] // The representation of `#[unsafe(export_name = ..)]` in rustdoc in edition 2024 -// is still `#[export_name = ..]` without the `unsafe` attribute wrapper. +// doesn't mention the `unsafe`. -//@ is "$.index[?(@.name=='example')].attrs" '["#[export_name = \"altered\"]"]' +//@ is "$.index[?(@.name=='example')].attrs" '[{"export_name": "altered"}]' #[unsafe(export_name = "altered")] pub extern "C" fn example() {} diff --git a/tests/rustdoc-json/attrs/inline.rs b/tests/rustdoc-json/attrs/inline.rs index b9ea6ab1d10cc..2aed49a48a5c0 100644 --- a/tests/rustdoc-json/attrs/inline.rs +++ b/tests/rustdoc-json/attrs/inline.rs @@ -1,11 +1,11 @@ -//@ is "$.index[?(@.name=='just_inline')].attrs" '["#[attr = Inline(Hint)]"]' +//@ is "$.index[?(@.name=='just_inline')].attrs" '[{"other": "#[attr = Inline(Hint)]"}]' #[inline] pub fn just_inline() {} -//@ is "$.index[?(@.name=='inline_always')].attrs" '["#[attr = Inline(Always)]"]' +//@ is "$.index[?(@.name=='inline_always')].attrs" '[{"other": "#[attr = Inline(Always)]"}]' #[inline(always)] pub fn inline_always() {} -//@ is "$.index[?(@.name=='inline_never')].attrs" '["#[attr = Inline(Never)]"]' +//@ is "$.index[?(@.name=='inline_never')].attrs" '[{"other": "#[attr = Inline(Never)]"}]' #[inline(never)] pub fn inline_never() {} diff --git a/tests/rustdoc-json/attrs/must_use.rs b/tests/rustdoc-json/attrs/must_use.rs index 3ca6f5a75a5a0..3f924c5169ca6 100644 --- a/tests/rustdoc-json/attrs/must_use.rs +++ b/tests/rustdoc-json/attrs/must_use.rs @@ -1,9 +1,9 @@ #![no_std] -//@ is "$.index[?(@.name=='example')].attrs" '["#[attr = MustUse]"]' +//@ is "$.index[?(@.name=='example')].attrs[*].must_use.reason" null #[must_use] pub fn example() -> impl Iterator {} -//@ is "$.index[?(@.name=='explicit_message')].attrs" '["#[attr = MustUse {reason: \"does nothing if you do not use it\"}]"]' +//@ is "$.index[?(@.name=='explicit_message')].attrs[*].must_use.reason" '"does nothing if you do not use it"' #[must_use = "does nothing if you do not use it"] pub fn explicit_message() -> impl Iterator {} diff --git a/tests/rustdoc-json/attrs/no_mangle_2021.rs b/tests/rustdoc-json/attrs/no_mangle_2021.rs index 588be7256db5a..703dcb56491ce 100644 --- a/tests/rustdoc-json/attrs/no_mangle_2021.rs +++ b/tests/rustdoc-json/attrs/no_mangle_2021.rs @@ -1,6 +1,6 @@ //@ edition: 2021 #![no_std] -//@ is "$.index[?(@.name=='example')].attrs" '["#[no_mangle]"]' +//@ is "$.index[?(@.name=='example')].attrs" '["no_mangle"]' #[no_mangle] pub extern "C" fn example() {} diff --git a/tests/rustdoc-json/attrs/no_mangle_2024.rs b/tests/rustdoc-json/attrs/no_mangle_2024.rs index 0d500e20e6c50..8af00eeda6bb6 100644 --- a/tests/rustdoc-json/attrs/no_mangle_2024.rs +++ b/tests/rustdoc-json/attrs/no_mangle_2024.rs @@ -4,6 +4,6 @@ // The representation of `#[unsafe(no_mangle)]` in rustdoc in edition 2024 // is still `#[no_mangle]` without the `unsafe` attribute wrapper. -//@ is "$.index[?(@.name=='example')].attrs" '["#[no_mangle]"]' +//@ is "$.index[?(@.name=='example')].attrs" '["no_mangle"]' #[unsafe(no_mangle)] pub extern "C" fn example() {} diff --git a/tests/rustdoc-json/attrs/non_exhaustive.rs b/tests/rustdoc-json/attrs/non_exhaustive.rs index b95f1a8171fd4..e4e6c8fd53bac 100644 --- a/tests/rustdoc-json/attrs/non_exhaustive.rs +++ b/tests/rustdoc-json/attrs/non_exhaustive.rs @@ -1,18 +1,18 @@ #![no_std] -//@ is "$.index[?(@.name=='MyEnum')].attrs" '["#[non_exhaustive]"]' +//@ is "$.index[?(@.name=='MyEnum')].attrs" '["non_exhaustive"]' #[non_exhaustive] pub enum MyEnum { First, } pub enum NonExhaustiveVariant { - //@ is "$.index[?(@.name=='Variant')].attrs" '["#[non_exhaustive]"]' + //@ is "$.index[?(@.name=='Variant')].attrs" '["non_exhaustive"]' #[non_exhaustive] Variant(i64), } -//@ is "$.index[?(@.name=='MyStruct')].attrs" '["#[non_exhaustive]"]' +//@ is "$.index[?(@.name=='MyStruct')].attrs" '["non_exhaustive"]' #[non_exhaustive] pub struct MyStruct { pub x: i64, diff --git a/tests/rustdoc-json/attrs/repr_align.rs b/tests/rustdoc-json/attrs/repr_align.rs index c6debda7f1c9e..f9d3417c48574 100644 --- a/tests/rustdoc-json/attrs/repr_align.rs +++ b/tests/rustdoc-json/attrs/repr_align.rs @@ -1,6 +1,7 @@ #![no_std] -//@ is "$.index[?(@.name=='Aligned')].attrs" '["#[repr(align(4))]"]' +//@ count "$.index[?(@.name=='Aligned')].attrs[*]" 1 +//@ is "$.index[?(@.name=='Aligned')].attrs[*].repr.align" 4 #[repr(align(4))] pub struct Aligned { a: i8, diff --git a/tests/rustdoc-json/attrs/repr_c.rs b/tests/rustdoc-json/attrs/repr_c.rs index e6219413f3086..89dbc16cb2a9e 100644 --- a/tests/rustdoc-json/attrs/repr_c.rs +++ b/tests/rustdoc-json/attrs/repr_c.rs @@ -1,16 +1,28 @@ #![no_std] -//@ is "$.index[?(@.name=='ReprCStruct')].attrs" '["#[repr(C)]"]' +//@ count "$.index[?(@.name=='ReprCStruct')].attrs" 1 +//@ is "$.index[?(@.name=='ReprCStruct')].attrs[*].repr.kind" '"c"' +//@ is "$.index[?(@.name=='ReprCStruct')].attrs[*].repr.int" null +//@ is "$.index[?(@.name=='ReprCStruct')].attrs[*].repr.packed" null +//@ is "$.index[?(@.name=='ReprCStruct')].attrs[*].repr.align" null #[repr(C)] pub struct ReprCStruct(pub i64); -//@ is "$.index[?(@.name=='ReprCEnum')].attrs" '["#[repr(C)]"]' +//@ count "$.index[?(@.name=='ReprCEnum')].attrs" 1 +//@ is "$.index[?(@.name=='ReprCEnum')].attrs[*].repr.kind" '"c"' +//@ is "$.index[?(@.name=='ReprCEnum')].attrs[*].repr.int" null +//@ is "$.index[?(@.name=='ReprCEnum')].attrs[*].repr.packed" null +//@ is "$.index[?(@.name=='ReprCEnum')].attrs[*].repr.align" null #[repr(C)] pub enum ReprCEnum { First, } -//@ is "$.index[?(@.name=='ReprCUnion')].attrs" '["#[repr(C)]"]' +//@ count "$.index[?(@.name=='ReprCUnion')].attrs" 1 +//@ is "$.index[?(@.name=='ReprCUnion')].attrs[*].repr.kind" '"c"' +//@ is "$.index[?(@.name=='ReprCUnion')].attrs[*].repr.int" null +//@ is "$.index[?(@.name=='ReprCUnion')].attrs[*].repr.packed" null +//@ is "$.index[?(@.name=='ReprCUnion')].attrs[*].repr.align" null #[repr(C)] pub union ReprCUnion { pub left: i64, diff --git a/tests/rustdoc-json/attrs/repr_c_int_enum.rs b/tests/rustdoc-json/attrs/repr_c_int_enum.rs new file mode 100644 index 0000000000000..0d2ad971aa044 --- /dev/null +++ b/tests/rustdoc-json/attrs/repr_c_int_enum.rs @@ -0,0 +1,6 @@ + +#[repr(C, u8)] +pub enum Foo { + A(bool) = b'A', + B(char) = b'C', +} diff --git a/tests/rustdoc-json/attrs/repr_combination.rs b/tests/rustdoc-json/attrs/repr_combination.rs index 6fe29c5eac093..bd4a8563b6f27 100644 --- a/tests/rustdoc-json/attrs/repr_combination.rs +++ b/tests/rustdoc-json/attrs/repr_combination.rs @@ -1,35 +1,34 @@ #![no_std] // Combinations of `#[repr(..)]` attributes. -// Rustdoc JSON emits normalized output, regardless of the original source. -//@ is "$.index[?(@.name=='ReprCI8')].attrs" '["#[repr(C, i8)]"]' +//@ is "$.index[?(@.name=='ReprCI8')].attrs" '[{"repr":{"align":null,"int":"i8","kind":"c","packed":null}}]' #[repr(C, i8)] pub enum ReprCI8 { First, } -//@ is "$.index[?(@.name=='SeparateReprCI16')].attrs" '["#[repr(C, i16)]"]' +//@ is "$.index[?(@.name=='SeparateReprCI16')].attrs" '[{"repr":{"align":null,"int":"i16","kind":"c","packed":null}}]' #[repr(C)] #[repr(i16)] pub enum SeparateReprCI16 { First, } -//@ is "$.index[?(@.name=='ReversedReprCUsize')].attrs" '["#[repr(C, usize)]"]' +//@ is "$.index[?(@.name=='ReversedReprCUsize')].attrs" '[{"repr":{"align":null,"int":"usize","kind":"c","packed":null}}]' #[repr(usize, C)] pub enum ReversedReprCUsize { First, } -//@ is "$.index[?(@.name=='ReprCPacked')].attrs" '["#[repr(C, packed(1))]"]' +//@ is "$.index[?(@.name=='ReprCPacked')].attrs" '[{"repr":{"align":null,"int":null,"kind":"c","packed":1}}]' #[repr(C, packed)] pub struct ReprCPacked { a: i8, b: i64, } -//@ is "$.index[?(@.name=='SeparateReprCPacked')].attrs" '["#[repr(C, packed(2))]"]' +//@ is "$.index[?(@.name=='SeparateReprCPacked')].attrs" '[{"repr":{"align":null,"int":null,"kind":"c","packed":2}}]' #[repr(C)] #[repr(packed(2))] pub struct SeparateReprCPacked { @@ -37,21 +36,21 @@ pub struct SeparateReprCPacked { b: i64, } -//@ is "$.index[?(@.name=='ReversedReprCPacked')].attrs" '["#[repr(C, packed(2))]"]' +//@ is "$.index[?(@.name=='ReversedReprCPacked')].attrs" '[{"repr":{"align":null,"int":null,"kind":"c","packed":2}}]' #[repr(packed(2), C)] pub struct ReversedReprCPacked { a: i8, b: i64, } -//@ is "$.index[?(@.name=='ReprCAlign')].attrs" '["#[repr(C, align(16))]"]' +//@ is "$.index[?(@.name=='ReprCAlign')].attrs" '[{"repr":{"align":16,"int":null,"kind":"c","packed":null}}]' #[repr(C, align(16))] pub struct ReprCAlign { a: i8, b: i64, } -//@ is "$.index[?(@.name=='SeparateReprCAlign')].attrs" '["#[repr(C, align(2))]"]' +//@ is "$.index[?(@.name=='SeparateReprCAlign')].attrs" '[{"repr":{"align":2,"int":null,"kind":"c","packed":null}}]' #[repr(C)] #[repr(align(2))] pub struct SeparateReprCAlign { @@ -59,25 +58,25 @@ pub struct SeparateReprCAlign { b: i64, } -//@ is "$.index[?(@.name=='ReversedReprCAlign')].attrs" '["#[repr(C, align(2))]"]' +//@ is "$.index[?(@.name=='ReversedReprCAlign')].attrs" '[{"repr":{"align":2,"int":null,"kind":"c","packed":null}}]' #[repr(align(2), C)] pub struct ReversedReprCAlign { a: i8, b: i64, } -//@ is "$.index[?(@.name=='AlignedExplicitRepr')].attrs" '["#[repr(C, align(16), isize)]"]' +//@ is "$.index[?(@.name=='AlignedExplicitRepr')].attrs" '[{"repr":{"align":16,"int":"isize","kind":"c","packed":null}}]' #[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":{"align":16,"int":"isize","kind":"c","packed":null}}]' #[repr(isize, C, align(16))] pub enum ReorderedAlignedExplicitRepr { First, } -//@ is "$.index[?(@.name=='Transparent')].attrs" '["#[repr(transparent)]"]' +//@ is "$.index[?(@.name=='Transparent')].attrs" '[{"repr":{"align":null,"int":null,"kind":"transparent","packed":null}}]' #[repr(transparent)] pub struct Transparent(i64); diff --git a/tests/rustdoc-json/attrs/repr_int_enum.rs b/tests/rustdoc-json/attrs/repr_int_enum.rs index 9b09f341d4fee..79e17f53ad900 100644 --- a/tests/rustdoc-json/attrs/repr_int_enum.rs +++ b/tests/rustdoc-json/attrs/repr_int_enum.rs @@ -1,18 +1,27 @@ #![no_std] -//@ is "$.index[?(@.name=='I8')].attrs" '["#[repr(i8)]"]' +//@ is "$.index[?(@.name=='I8')].attrs[*].repr.int" '"i8"' +//@ is "$.index[?(@.name=='I8')].attrs[*].repr.kind" '"rust"' +//@ is "$.index[?(@.name=='I8')].attrs[*].repr.align" null +//@ is "$.index[?(@.name=='I8')].attrs[*].repr.packed" null #[repr(i8)] pub enum I8 { First, } -//@ is "$.index[?(@.name=='I32')].attrs" '["#[repr(i32)]"]' +//@ is "$.index[?(@.name=='I32')].attrs[*].repr.int" '"i32"' +//@ is "$.index[?(@.name=='I32')].attrs[*].repr.kind" '"rust"' +//@ is "$.index[?(@.name=='I32')].attrs[*].repr.align" null +//@ is "$.index[?(@.name=='I32')].attrs[*].repr.packed" null #[repr(i32)] pub enum I32 { First, } -//@ is "$.index[?(@.name=='Usize')].attrs" '["#[repr(usize)]"]' +//@ is "$.index[?(@.name=='Usize')].attrs[*].repr.int" '"usize"' +//@ is "$.index[?(@.name=='Usize')].attrs[*].repr.kind" '"rust"' +//@ is "$.index[?(@.name=='Usize')].attrs[*].repr.align" null +//@ is "$.index[?(@.name=='Usize')].attrs[*].repr.packed" null #[repr(usize)] pub enum Usize { First, diff --git a/tests/rustdoc-json/attrs/repr_packed.rs b/tests/rustdoc-json/attrs/repr_packed.rs index 9f3fd86c4b03c..ab573835b45cf 100644 --- a/tests/rustdoc-json/attrs/repr_packed.rs +++ b/tests/rustdoc-json/attrs/repr_packed.rs @@ -1,16 +1,18 @@ #![no_std] // Note the normalization: -// `#[repr(packed)]` in source becomes `#[repr(packed(1))]` in rustdoc JSON. +// `#[repr(packed)]` in source becomes `{"repr": {"packed": 1, ...}}` in rustdoc JSON. // -//@ is "$.index[?(@.name=='Packed')].attrs" '["#[repr(packed(1))]"]' +//@ is "$.index[?(@.name=='Packed')].attrs[*].repr.packed" 1 +//@ is "$.index[?(@.name=='Packed')].attrs[*].repr.kind" '"rust"' #[repr(packed)] pub struct Packed { a: i8, b: i64, } -//@ is "$.index[?(@.name=='PackedAligned')].attrs" '["#[repr(packed(4))]"]' +//@ is "$.index[?(@.name=='PackedAligned')].attrs[*].repr.packed" 4 +//@ is "$.index[?(@.name=='PackedAligned')].attrs[*].repr.kind" '"rust"' #[repr(packed(4))] pub struct PackedAligned { a: i8, diff --git a/tests/rustdoc-json/enums/discriminant/struct.rs b/tests/rustdoc-json/enums/discriminant/struct.rs index ea669e6a0b30f..fed0e545798af 100644 --- a/tests/rustdoc-json/enums/discriminant/struct.rs +++ b/tests/rustdoc-json/enums/discriminant/struct.rs @@ -1,5 +1,5 @@ #[repr(i32)] -//@ is "$.index[?(@.name=='Foo')].attrs" '["#[repr(i32)]"]' +//@ is "$.index[?(@.name=='Foo')].attrs[*].repr.int" '"i32"' pub enum Foo { //@ is "$.index[?(@.name=='Struct')].inner.variant.discriminant" null //@ count "$.index[?(@.name=='Struct')].inner.variant.kind.struct.fields[*]" 0 diff --git a/tests/rustdoc-json/enums/discriminant/tuple.rs b/tests/rustdoc-json/enums/discriminant/tuple.rs index 1b8e791eb2302..54bba76a0634f 100644 --- a/tests/rustdoc-json/enums/discriminant/tuple.rs +++ b/tests/rustdoc-json/enums/discriminant/tuple.rs @@ -1,5 +1,5 @@ #[repr(u32)] -//@ is "$.index[?(@.name=='Foo')].attrs" '["#[repr(u32)]"]' +//@ is "$.index[?(@.name=='Foo')].attrs[*].repr.int" '"u32"' pub enum Foo { //@ is "$.index[?(@.name=='Tuple')].inner.variant.discriminant" null //@ count "$.index[?(@.name=='Tuple')].inner.variant.kind.tuple[*]" 0 diff --git a/tests/rustdoc-json/keyword_private.rs b/tests/rustdoc-json/keyword_private.rs index fea546c9fb605..3198fc2529ef8 100644 --- a/tests/rustdoc-json/keyword_private.rs +++ b/tests/rustdoc-json/keyword_private.rs @@ -5,7 +5,7 @@ //@ !has "$.index[?(@.name=='match')]" //@ has "$.index[?(@.name=='foo')]" -//@ is "$.index[?(@.name=='foo')].attrs" '["#[doc(keyword = \"match\")]"]' +//@ is "$.index[?(@.name=='foo')].attrs[*].other" '"#[doc(keyword = \"match\")]"' //@ is "$.index[?(@.name=='foo')].docs" '"this is a test!"' #[doc(keyword = "match")] /// this is a test! @@ -13,7 +13,7 @@ pub mod foo {} //@ !has "$.index[?(@.name=='break')]" //@ has "$.index[?(@.name=='bar')]" -//@ is "$.index[?(@.name=='bar')].attrs" '["#[doc(keyword = \"break\")]"]' +//@ is "$.index[?(@.name=='bar')].attrs[*].other" '"#[doc(keyword = \"break\")]"' //@ is "$.index[?(@.name=='bar')].docs" '"hello"' #[doc(keyword = "break")] /// hello From 945b9c8f2bf222cc127405a908c98bf33bb3a058 Mon Sep 17 00:00:00 2001 From: Alona Enraght-Moony Date: Tue, 24 Jun 2025 19:12:02 +0000 Subject: [PATCH 2/3] [squash-before-merge]: Fix tests after rebase --- tests/rustdoc-json/attrs/cold.rs | 2 +- tests/rustdoc-json/attrs/optimize.rs | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/rustdoc-json/attrs/cold.rs b/tests/rustdoc-json/attrs/cold.rs index e219345d669c5..ec1926e894e4f 100644 --- a/tests/rustdoc-json/attrs/cold.rs +++ b/tests/rustdoc-json/attrs/cold.rs @@ -1,3 +1,3 @@ -//@ is "$.index[?(@.name=='cold_fn')].attrs" '["#[attr = Cold]"]' +//@ is "$.index[?(@.name=='cold_fn')].attrs" '[{"other": "#[attr = Cold]"}]' #[cold] pub fn cold_fn() {} diff --git a/tests/rustdoc-json/attrs/optimize.rs b/tests/rustdoc-json/attrs/optimize.rs index 0bed0ad18c31b..5988120ab2f75 100644 --- a/tests/rustdoc-json/attrs/optimize.rs +++ b/tests/rustdoc-json/attrs/optimize.rs @@ -1,13 +1,13 @@ #![feature(optimize_attribute)] -//@ is "$.index[?(@.name=='speed')].attrs" '["#[attr = Optimize(Speed)]"]' +//@ is "$.index[?(@.name=='speed')].attrs" '[{"other": "#[attr = Optimize(Speed)]"}]' #[optimize(speed)] pub fn speed() {} -//@ is "$.index[?(@.name=='size')].attrs" '["#[attr = Optimize(Size)]"]' +//@ is "$.index[?(@.name=='size')].attrs" '[{"other": "#[attr = Optimize(Size)]"}]' #[optimize(size)] pub fn size() {} -//@ is "$.index[?(@.name=='none')].attrs" '["#[attr = Optimize(DoNotOptimize)]"]' +//@ is "$.index[?(@.name=='none')].attrs" '[{"other": "#[attr = Optimize(DoNotOptimize)]"}]' #[optimize(none)] pub fn none() {} From c912543a7ed5fa5f8e0d11504ab4d99a90785ad0 Mon Sep 17 00:00:00 2001 From: Alona Enraght-Moony Date: Tue, 24 Jun 2025 19:25:14 +0000 Subject: [PATCH 3/3] [squash-before-merge] docs --- src/librustdoc/json/conversions.rs | 7 +----- src/rustdoc-json-types/lib.rs | 37 ++++++++++++++++++++++-------- 2 files changed, 28 insertions(+), 16 deletions(-) diff --git a/src/librustdoc/json/conversions.rs b/src/librustdoc/json/conversions.rs index ecffd5b4270c3..71c3fd3f02e22 100644 --- a/src/librustdoc/json/conversions.rs +++ b/src/librustdoc/json/conversions.rs @@ -909,13 +909,8 @@ fn maybe_from_hir_attr( Attribute::ExportName( attr.value_str().expect("checked by attr validation").to_string(), ) - } else if attr.has_name(sym::doc) - && attr - .meta_item_list() - .is_some_and(|metas| metas.iter().any(|item| item.has_name(sym::hidden))) - { - Attribute::DocHidden } else { + // FIXME: We should handle `#[doc(hidden)]` here. other_attr(tcx, attr) }); } diff --git a/src/rustdoc-json-types/lib.rs b/src/rustdoc-json-types/lib.rs index 108c151860e36..bce1537ced8fb 100644 --- a/src/rustdoc-json-types/lib.rs +++ b/src/rustdoc-json-types/lib.rs @@ -202,13 +202,13 @@ pub struct Item { pub inner: ItemEnum, } +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] /// An attribute, eg `#[repr(C)]` /// /// This doesn't include: /// - `#[doc = "Doc Comment"]` or `/// Doc comment`. These are in [`Item::docs`] instead. /// - `#[deprecated]`. These are in [`Item::deprecation`] instead. -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "snake_case")] pub enum Attribute { /// `#[non_exhaustive]` NonExhaustive, @@ -224,38 +224,55 @@ pub enum Attribute { /// `#[repr]` Repr(AttributeRepr), - ExportName(String), - /// `#[doc(hidden)]` - DocHidden, /// `#[no_mangle]` NoMangle, /// Something else. /// /// Things here are explicitly *not* covered by the [`FORMAT_VERSION`] - /// constant, and may change without bumping the format version. If you rely - /// on an attribute here, please open an issue about adding a new variant for - /// that attr. + /// constant, and may change without bumping the format version. + /// + /// As an implementation detail, this is currently either: + /// 1. A HIR debug printing, like `"#[attr = Optimize(Speed)]"` + /// 2. The attribute as it appears in source form, like + /// `"#[optimize(speed)]"`. Other(String), -} +g #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +/// The contents of a `#[repr(...)]` attribute. +/// +/// Used in [`Attribute::Repr`]. pub struct AttributeRepr { + /// The representation, e.g. `#[repr(C)]`, `#[repr(transparent)]` pub kind: ReprKind, - /// Alignment, in bytes. + /// Alignment in bytes, if explicitly specified by `#[repr(align(...)]`. pub align: Option, + /// Alignment in bytes, if explicitly specified by `#[repr(packed(...)]]`. pub packed: Option, + /// The integer type for an enum descriminant, if explicitly specified. + /// + /// e.g. `"i32"`, for `#[repr(C, i32)]` pub int: Option, } #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] +/// The kind of `#[repr]`. +/// +/// See [AttributeRepr::kind]`. pub enum ReprKind { + /// `#[repr(Rust)]` + /// + /// Also the default. Rust, + /// `#[repr(C)]` C, + /// `#[repr(transparent)] Transparent, + /// `#[repr(simd)]` Simd, }