about summary refs log tree commit diff
diff options
context:
space:
mode:
authorAlona Enraght-Moony <code@alona.page>2025-06-20 02:48:15 +0000
committerAlona Enraght-Moony <code@alona.page>2025-07-15 16:52:41 +0000
commit078332fdc8e11f7ff8253c019085098538ec3c2a (patch)
tree25c35cd99077885e2ac7d26e2421f87972dab9db
parentcccf075eba88363269e8589ebb8d40874cc542d8 (diff)
downloadrust-078332fdc8e11f7ff8253c019085098538ec3c2a.tar.gz
rust-078332fdc8e11f7ff8253c019085098538ec3c2a.zip
rustdoc-json: Structured attributes
Implements https://www.github.com/rust-lang/rust/issues/141358.

This has 2 primary benefits:

1. For rustdoc-json consumers, they no longer need to parse strings of
   attributes, but it's there in a structured and normalized way.
2. For rustc contributors, the output of HIR pretty printing is no
   longer a versioned thing in the output. People can work on
   https://github.com/rust-lang/rust/issues/131229 without needing to
   bump `FORMAT_VERSION`.

(Over time, as the attribute refractor continues, I expect we'll add new
things to `rustdoc_json_types::Attribute`. But this can be done
separately to the rustc changes).
-rw-r--r--src/librustdoc/clean/types.rs80
-rw-r--r--src/librustdoc/html/render/mod.rs6
-rw-r--r--src/librustdoc/html/render/print_item.rs3
-rw-r--r--src/librustdoc/json/conversions.rs99
-rw-r--r--src/rustdoc-json-types/lib.rs89
-rw-r--r--tests/rustdoc-json/attrs/automatically_derived.rs2
-rw-r--r--tests/rustdoc-json/attrs/cold.rs2
-rw-r--r--tests/rustdoc-json/attrs/export_name_2021.rs2
-rw-r--r--tests/rustdoc-json/attrs/export_name_2024.rs4
-rw-r--r--tests/rustdoc-json/attrs/inline.rs6
-rw-r--r--tests/rustdoc-json/attrs/link_section_2021.rs3
-rw-r--r--tests/rustdoc-json/attrs/link_section_2024.rs3
-rw-r--r--tests/rustdoc-json/attrs/must_use.rs4
-rw-r--r--tests/rustdoc-json/attrs/no_mangle_2021.rs2
-rw-r--r--tests/rustdoc-json/attrs/no_mangle_2024.rs2
-rw-r--r--tests/rustdoc-json/attrs/non_exhaustive.rs6
-rw-r--r--tests/rustdoc-json/attrs/optimize.rs6
-rw-r--r--tests/rustdoc-json/attrs/repr_align.rs3
-rw-r--r--tests/rustdoc-json/attrs/repr_c.rs18
-rw-r--r--tests/rustdoc-json/attrs/repr_c_int_enum.rs11
-rw-r--r--tests/rustdoc-json/attrs/repr_combination.rs25
-rw-r--r--tests/rustdoc-json/attrs/repr_int_enum.rs15
-rw-r--r--tests/rustdoc-json/attrs/repr_packed.rs8
-rw-r--r--tests/rustdoc-json/attrs/target_feature.rs25
-rw-r--r--tests/rustdoc-json/enums/discriminant/struct.rs2
-rw-r--r--tests/rustdoc-json/enums/discriminant/tuple.rs2
-rw-r--r--tests/rustdoc-json/keyword_private.rs4
-rw-r--r--tests/rustdoc-json/visibility/doc_hidden_documented.rs6
28 files changed, 316 insertions, 122 deletions
diff --git a/src/librustdoc/clean/types.rs b/src/librustdoc/clean/types.rs
index 3ecd41db2dd..20babc6168b 100644
--- a/src/librustdoc/clean/types.rs
+++ b/src/librustdoc/clean/types.rs
@@ -759,79 +759,48 @@ impl Item {
         Some(tcx.visibility(def_id))
     }
 
-    fn attributes_without_repr(&self, tcx: TyCtxt<'_>, is_json: bool) -> Vec<String> {
-        const ALLOWED_ATTRIBUTES: &[Symbol] =
-            &[sym::export_name, sym::link_section, sym::no_mangle, sym::non_exhaustive];
+    /// 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| {
-                if let hir::Attribute::Parsed(AttributeKind::LinkSection { name, .. }) = attr {
+            .filter_map(|attr| match attr {
+                hir::Attribute::Parsed(AttributeKind::LinkSection { name, .. }) => {
                     Some(format!("#[link_section = \"{name}\"]"))
                 }
-                // 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.
-                else if let hir::Attribute::Parsed(AttributeKind::NoMangle(..)) = attr {
+                hir::Attribute::Parsed(AttributeKind::NoMangle(..)) => {
                     Some("#[no_mangle]".to_string())
-                } else if let hir::Attribute::Parsed(AttributeKind::ExportName { name, .. }) = attr
-                {
+                }
+                hir::Attribute::Parsed(AttributeKind::ExportName { name, .. }) => {
                     Some(format!("#[export_name = \"{name}\"]"))
-                } else if let hir::Attribute::Parsed(AttributeKind::NonExhaustive(..)) = attr {
+                }
+                hir::Attribute::Parsed(AttributeKind::NonExhaustive(..)) => {
                     Some("#[non_exhaustive]".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,
-                        // target_feature is special-cased because cargo-semver-checks uses it
-                        hir::Attribute::Parsed(AttributeKind::TargetFeature(features, _)) => {
-                            let mut output = String::new();
-                            for (i, (feature, _)) in features.iter().enumerate() {
-                                if i != 0 {
-                                    output.push_str(", ");
-                                }
-                                output.push_str(&format!("enable=\"{}\"", feature.as_str()));
-                            }
-                            Some(format!("#[target_feature({output})]"))
-                        }
-                        hir::Attribute::Parsed(AttributeKind::AutomaticallyDerived(..)) => {
-                            Some("#[automatically_derived]".to_string())
-                        }
-                        _ => 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) {
-                        return None;
-                    }
-                    Some(
-                        rustc_hir_pretty::attribute_to_string(&tcx, attr)
-                            .replace("\\\n", "")
-                            .replace('\n', "")
-                            .replace("  ", " "),
-                    )
                 }
+                _ => None,
             })
             .collect()
     }
 
-    pub(crate) fn attributes(&self, tcx: TyCtxt<'_>, cache: &Cache, is_json: bool) -> Vec<String> {
-        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<String> {
+        let mut attrs = self.attributes_without_repr();
 
-        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<String> {
-        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<String> {
+        repr_attributes(tcx, cache, self.def_id()?, self.type_())
     }
 
     pub fn is_doc_hidden(&self) -> bool {
@@ -843,12 +812,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<String> {
     use rustc_abi::IntegerType;
 
@@ -865,7 +836,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 70f3f54e4c0..06de4944d97 100644
--- a/src/librustdoc/html/render/mod.rs
+++ b/src/librustdoc/html/render/mod.rs
@@ -1191,7 +1191,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(())
@@ -1207,7 +1207,7 @@ fn render_code_attribute(code_attr: CodeAttribute, w: &mut impl fmt::Write) {
 // 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<'_>) {
-    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);
     }
 }
@@ -1219,7 +1219,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 e33bdc0db32..667d39e9bc2 100644
--- a/src/librustdoc/html/render/print_item.rs
+++ b/src/librustdoc/html/render/print_item.rs
@@ -1487,12 +1487,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 e7163bead92..0a84d8caa30 100644
--- a/src/librustdoc/json/conversions.rs
+++ b/src/librustdoc/json/conversions.rs
@@ -5,10 +5,12 @@
 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_hir::{HeaderSafety, Safety};
 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::*;
@@ -39,7 +41,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;
@@ -886,3 +893,93 @@ impl FromClean<ItemType> for ItemKind {
         }
     }
 }
+
+/// Maybe convert a attribute 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<Attribute> {
+    use attrs::AttributeKind as AK;
+
+    let kind = match attr {
+        hir::Attribute::Parsed(kind) => kind,
+
+        hir::Attribute::Unparsed(_) => {
+            // FIXME: We should handle `#[doc(hidden)]`.
+            return Some(other_attr(tcx, attr));
+        }
+    };
+
+    Some(match kind {
+        AK::Deprecation { .. } => return None, // Handled separately 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::ExportName { name, span: _ } => Attribute::ExportName(name.to_string()),
+        AK::LinkSection { name, span: _ } => Attribute::LinkSection(name.to_string()),
+        AK::TargetFeature(features, _span) => Attribute::TargetFeature {
+            enable: features.iter().map(|(feat, _span)| feat.to_string()).collect(),
+        },
+
+        AK::NoMangle(_) => Attribute::NoMangle,
+        AK::NonExhaustive(_) => Attribute::NonExhaustive,
+        AK::AutomaticallyDerived(_) => Attribute::AutomaticallyDerived,
+
+        _ => 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 0e72ddd9db1..6235b0e8576 100644
--- a/src/rustdoc-json-types/lib.rs
+++ b/src/rustdoc-json-types/lib.rs
@@ -37,8 +37,8 @@ pub type FxHashMap<K, V> = HashMap<K, V>; // 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,94 @@ 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<String>,
+    pub attrs: Vec<Attribute>,
     /// Information about the item’s deprecation, if present.
     pub deprecation: Option<Deprecation>,
     /// The type-specific fields describing this item.
     pub inner: ItemEnum,
 }
 
+#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
+#[serde(rename_all = "snake_case")]
+/// An attribute, e.g. `#[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.
+pub enum Attribute {
+    /// `#[non_exhaustive]`
+    NonExhaustive,
+
+    /// `#[must_use]`
+    MustUse { reason: Option<String> },
+
+    /// `#[export_name = "name"]`
+    ExportName(String),
+
+    /// `#[link_section = "name"]`
+    LinkSection(String),
+
+    /// `#[automatically_derived]`
+    AutomaticallyDerived,
+
+    /// `#[repr]`
+    Repr(AttributeRepr),
+
+    /// `#[no_mangle]`
+    NoMangle,
+
+    /// #[target_feature(enable = "feature1", enable = "feature2")]
+    TargetFeature { enable: Vec<String> },
+
+    /// Something else.
+    ///
+    /// Things here are explicitly *not* covered by the [`FORMAT_VERSION`]
+    /// 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),
+}
+
+#[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, if explicitly specified by `#[repr(align(...)]`.
+    pub align: Option<u64>,
+    /// Alignment in bytes, if explicitly specified by `#[repr(packed(...)]]`.
+    pub packed: Option<u64>,
+
+    /// The integer type for an enum descriminant, if explicitly specified.
+    ///
+    /// e.g. `"i32"`, for `#[repr(C, i32)]`
+    pub int: Option<String>,
+}
+
+#[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,
+}
+
 /// A range of source code.
 #[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
 pub struct Span {
@@ -1343,7 +1424,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 4e1ab3d145e..9ba310d3655 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/cold.rs b/tests/rustdoc-json/attrs/cold.rs
index e219345d669..ec1926e894e 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/export_name_2021.rs b/tests/rustdoc-json/attrs/export_name_2021.rs
index 254e9f6ef5b..451d9b9eb37 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 8129c109306..7e398db92ab 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 b9ea6ab1d10..2aed49a48a5 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/link_section_2021.rs b/tests/rustdoc-json/attrs/link_section_2021.rs
index a1312f4210b..acd8ecd0e30 100644
--- a/tests/rustdoc-json/attrs/link_section_2021.rs
+++ b/tests/rustdoc-json/attrs/link_section_2021.rs
@@ -1,6 +1,7 @@
 //@ edition: 2021
 #![no_std]
 
-//@ is "$.index[?(@.name=='example')].attrs" '["#[link_section = \".text\"]"]'
+//@ count "$.index[?(@.name=='example')].attrs[*]" 1
+//@ is "$.index[?(@.name=='example')].attrs[*].link_section" '".text"'
 #[link_section = ".text"]
 pub extern "C" fn example() {}
diff --git a/tests/rustdoc-json/attrs/link_section_2024.rs b/tests/rustdoc-json/attrs/link_section_2024.rs
index edb028451a8..8107493229b 100644
--- a/tests/rustdoc-json/attrs/link_section_2024.rs
+++ b/tests/rustdoc-json/attrs/link_section_2024.rs
@@ -4,6 +4,7 @@
 // Since the 2024 edition the link_section attribute must use the unsafe qualification.
 // However, the unsafe qualification is not shown by rustdoc.
 
-//@ is "$.index[?(@.name=='example')].attrs" '["#[link_section = \".text\"]"]'
+//@ count "$.index[?(@.name=='example')].attrs[*]" 1
+//@ is "$.index[?(@.name=='example')].attrs[*].link_section" '".text"'
 #[unsafe(link_section = ".text")]
 pub extern "C" fn example() {}
diff --git a/tests/rustdoc-json/attrs/must_use.rs b/tests/rustdoc-json/attrs/must_use.rs
index 3ca6f5a75a5..3f924c5169c 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<Item = i64> {}
 
-//@ 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<Item = i64> {}
diff --git a/tests/rustdoc-json/attrs/no_mangle_2021.rs b/tests/rustdoc-json/attrs/no_mangle_2021.rs
index 588be7256db..703dcb56491 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 0d500e20e6c..8af00eeda6b 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 b95f1a8171f..e4e6c8fd53b 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/optimize.rs b/tests/rustdoc-json/attrs/optimize.rs
index 0bed0ad18c3..5988120ab2f 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() {}
diff --git a/tests/rustdoc-json/attrs/repr_align.rs b/tests/rustdoc-json/attrs/repr_align.rs
index c6debda7f1c..f9d3417c485 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 e6219413f30..89dbc16cb2a 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 00000000000..e90bcf2b5c6
--- /dev/null
+++ b/tests/rustdoc-json/attrs/repr_c_int_enum.rs
@@ -0,0 +1,11 @@
+//@ count "$.index[?(@.name=='Foo')].attrs" 1
+//@ is "$.index[?(@.name=='Foo')].attrs[*].repr.kind" '"c"'
+//@ is "$.index[?(@.name=='Foo')].attrs[*].repr.int" '"u8"'
+//@ is "$.index[?(@.name=='Foo')].attrs[*].repr.packed" null
+//@ is "$.index[?(@.name=='Foo')].attrs[*].repr.align" 16
+#[repr(C, u8)]
+#[repr(align(16))]
+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 6fe29c5eac0..bd4a8563b6f 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 9b09f341d4f..79e17f53ad9 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 9f3fd86c4b0..ab573835b45 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/attrs/target_feature.rs b/tests/rustdoc-json/attrs/target_feature.rs
index 01bc4f54d32..efe3752c166 100644
--- a/tests/rustdoc-json/attrs/target_feature.rs
+++ b/tests/rustdoc-json/attrs/target_feature.rs
@@ -1,38 +1,49 @@
-//@ is "$.index[?(@.name=='test1')].attrs" '["#[target_feature(enable=\"avx\")]"]'
 //@ is "$.index[?(@.name=='test1')].inner.function.header.is_unsafe" false
+//@ count "$.index[?(@.name=='test1')].attrs[*]" 1
+//@ is    "$.index[?(@.name=='test1')].attrs[*].target_feature.enable" '["avx"]'
 #[target_feature(enable = "avx")]
 pub fn test1() {}
 
-//@ is "$.index[?(@.name=='test2')].attrs" '["#[target_feature(enable=\"avx\", enable=\"avx2\")]"]'
 //@ is "$.index[?(@.name=='test2')].inner.function.header.is_unsafe" false
+//@ count "$.index[?(@.name=='test2')].attrs[*]" 1
+//@ is    "$.index[?(@.name=='test2')].attrs[*].target_feature.enable" '["avx", "avx2"]'
 #[target_feature(enable = "avx,avx2")]
 pub fn test2() {}
 
-//@ is "$.index[?(@.name=='test3')].attrs" '["#[target_feature(enable=\"avx\", enable=\"avx2\")]"]'
 //@ is "$.index[?(@.name=='test3')].inner.function.header.is_unsafe" false
+//@ count "$.index[?(@.name=='test3')].attrs[*]" 1
+//@ is    "$.index[?(@.name=='test3')].attrs[*].target_feature.enable" '["avx", "avx2"]'
 #[target_feature(enable = "avx", enable = "avx2")]
 pub fn test3() {}
 
-//@ is "$.index[?(@.name=='test4')].attrs" '["#[target_feature(enable=\"avx\", enable=\"avx2\", enable=\"avx512f\")]"]'
 //@ is "$.index[?(@.name=='test4')].inner.function.header.is_unsafe" false
+//@ count "$.index[?(@.name=='test4')].attrs[*]" 1
+//@ is    "$.index[?(@.name=='test4')].attrs[*].target_feature.enable" '["avx", "avx2", "avx512f"]'
 #[target_feature(enable = "avx", enable = "avx2,avx512f")]
 pub fn test4() {}
 
-//@ is "$.index[?(@.name=='test_unsafe_fn')].attrs" '["#[target_feature(enable=\"avx\")]"]'
+//@ count "$.index[?(@.name=='test5')].attrs[*]" 1
+//@ is    "$.index[?(@.name=='test5')].attrs[*].target_feature.enable" '["avx", "avx2"]'
+#[target_feature(enable = "avx")]
+#[target_feature(enable = "avx2")]
+pub fn test5() {}
+
 //@ is "$.index[?(@.name=='test_unsafe_fn')].inner.function.header.is_unsafe" true
+//@ count "$.index[?(@.name=='test_unsafe_fn')].attrs[*]" 1
+//@ is    "$.index[?(@.name=='test_unsafe_fn')].attrs[*].target_feature.enable" '["avx"]'
 #[target_feature(enable = "avx")]
 pub unsafe fn test_unsafe_fn() {}
 
 pub struct Example;
 
 impl Example {
-    //@ is "$.index[?(@.name=='safe_assoc_fn')].attrs" '["#[target_feature(enable=\"avx\")]"]'
     //@ is "$.index[?(@.name=='safe_assoc_fn')].inner.function.header.is_unsafe" false
+    //@ is "$.index[?(@.name=='safe_assoc_fn')].attrs[*].target_feature.enable" '["avx"]'
     #[target_feature(enable = "avx")]
     pub fn safe_assoc_fn() {}
 
-    //@ is "$.index[?(@.name=='unsafe_assoc_fn')].attrs" '["#[target_feature(enable=\"avx\")]"]'
     //@ is "$.index[?(@.name=='unsafe_assoc_fn')].inner.function.header.is_unsafe" true
+    //@ is "$.index[?(@.name=='unsafe_assoc_fn')].attrs[*].target_feature.enable" '["avx"]'
     #[target_feature(enable = "avx")]
     pub unsafe fn unsafe_assoc_fn() {}
 }
diff --git a/tests/rustdoc-json/enums/discriminant/struct.rs b/tests/rustdoc-json/enums/discriminant/struct.rs
index ea669e6a0b3..fed0e545798 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 1b8e791eb23..54bba76a063 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 fea546c9fb6..3198fc2529e 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
diff --git a/tests/rustdoc-json/visibility/doc_hidden_documented.rs b/tests/rustdoc-json/visibility/doc_hidden_documented.rs
index 6e9ef48680b..f05e4f9d92d 100644
--- a/tests/rustdoc-json/visibility/doc_hidden_documented.rs
+++ b/tests/rustdoc-json/visibility/doc_hidden_documented.rs
@@ -1,15 +1,15 @@
 //@ compile-flags: --document-hidden-items
 #![no_std]
 
-//@ is "$.index[?(@.name=='func')].attrs" '["#[doc(hidden)]"]'
+//@ is "$.index[?(@.name=='func')].attrs" '[{"other": "#[doc(hidden)]"}]'
 #[doc(hidden)]
 pub fn func() {}
 
-//@ is "$.index[?(@.name=='Unit')].attrs" '["#[doc(hidden)]"]'
+//@ is "$.index[?(@.name=='Unit')].attrs" '[{"other": "#[doc(hidden)]"}]'
 #[doc(hidden)]
 pub struct Unit;
 
-//@ is "$.index[?(@.name=='hidden')].attrs" '["#[doc(hidden)]"]'
+//@ is "$.index[?(@.name=='hidden')].attrs" '[{"other": "#[doc(hidden)]"}]'
 #[doc(hidden)]
 pub mod hidden {
     //@ is "$.index[?(@.name=='Inner')].attrs" '[]'