Skip to content

Commit 244351c

Browse files
committed
rustdoc-json: Structured attributes
Implements #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 #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).
1 parent 2f8eeb2 commit 244351c

27 files changed

+312
-102
lines changed

src/librustdoc/clean/types.rs

Lines changed: 21 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -759,53 +759,27 @@ impl Item {
759759
Some(tcx.visibility(def_id))
760760
}
761761

762-
fn attributes_without_repr(&self, tcx: TyCtxt<'_>, is_json: bool) -> Vec<String> {
763-
const ALLOWED_ATTRIBUTES: &[Symbol] =
764-
&[sym::export_name, sym::link_section, sym::no_mangle, sym::non_exhaustive];
762+
/// Get a list of attributes excluding `#[repr]`.
763+
///
764+
/// Only used by the HTML output-format.
765+
fn attributes_without_repr(&self, tcx: TyCtxt<'_>) -> Vec<String> {
766+
const ALLOWED_ATTRIBUTES: &[Symbol] = &[sym::non_exhaustive];
765767
self.attrs
766768
.other_attrs
767769
.iter()
768770
.filter_map(|attr| {
769771
if let hir::Attribute::Parsed(AttributeKind::LinkSection { name, .. }) = attr {
770772
Some(format!("#[link_section = \"{name}\"]"))
771-
}
772-
// NoMangle is special cased, as it appears in HTML output, and we want to show it in source form, not HIR printing.
773-
// It is also used by cargo-semver-checks.
774-
else if let hir::Attribute::Parsed(AttributeKind::NoMangle(..)) = attr {
773+
} else if let hir::Attribute::Parsed(AttributeKind::NoMangle(..)) = attr {
775774
Some("#[no_mangle]".to_string())
776775
} else if let hir::Attribute::Parsed(AttributeKind::ExportName { name, .. }) = attr
777776
{
778777
Some(format!("#[export_name = \"{name}\"]"))
779778
} else if let hir::Attribute::Parsed(AttributeKind::NonExhaustive(..)) = attr {
780779
Some("#[non_exhaustive]".to_string())
781-
} else if is_json {
782-
match attr {
783-
// rustdoc-json stores this in `Item::deprecation`, so we
784-
// don't want it it `Item::attrs`.
785-
hir::Attribute::Parsed(AttributeKind::Deprecation { .. }) => None,
786-
// We have separate pretty-printing logic for `#[repr(..)]` attributes.
787-
hir::Attribute::Parsed(AttributeKind::Repr { .. }) => None,
788-
// target_feature is special-cased because cargo-semver-checks uses it
789-
hir::Attribute::Parsed(AttributeKind::TargetFeature(features, _)) => {
790-
let mut output = String::new();
791-
for (i, (feature, _)) in features.iter().enumerate() {
792-
if i != 0 {
793-
output.push_str(", ");
794-
}
795-
output.push_str(&format!("enable=\"{}\"", feature.as_str()));
796-
}
797-
Some(format!("#[target_feature({output})]"))
798-
}
799-
_ => Some({
800-
let mut s = rustc_hir_pretty::attribute_to_string(&tcx, attr);
801-
assert_eq!(s.pop(), Some('\n'));
802-
s
803-
}),
804-
}
780+
} else if !attr.has_any_name(ALLOWED_ATTRIBUTES) {
781+
None
805782
} else {
806-
if !attr.has_any_name(ALLOWED_ATTRIBUTES) {
807-
return None;
808-
}
809783
Some(
810784
rustc_hir_pretty::attribute_to_string(&tcx, attr)
811785
.replace("\\\n", "")
@@ -817,18 +791,23 @@ impl Item {
817791
.collect()
818792
}
819793

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

823-
if let Some(repr_attr) = self.repr(tcx, cache, is_json) {
800+
if let Some(repr_attr) = self.repr(tcx, cache) {
824801
attrs.push(repr_attr);
825802
}
826803
attrs
827804
}
828805

829806
/// Returns a stringified `#[repr(...)]` attribute.
830-
pub(crate) fn repr(&self, tcx: TyCtxt<'_>, cache: &Cache, is_json: bool) -> Option<String> {
831-
repr_attributes(tcx, cache, self.def_id()?, self.type_(), is_json)
807+
///
808+
/// Only used by the HTML output-format.
809+
pub(crate) fn repr(&self, tcx: TyCtxt<'_>, cache: &Cache) -> Option<String> {
810+
repr_attributes(tcx, cache, self.def_id()?, self.type_())
832811
}
833812

834813
pub fn is_doc_hidden(&self) -> bool {
@@ -840,12 +819,14 @@ impl Item {
840819
}
841820
}
842821

822+
/// Return a string representing the `#[repr]` attribute if present.
823+
///
824+
/// Only used by the HTML output-format.
843825
pub(crate) fn repr_attributes(
844826
tcx: TyCtxt<'_>,
845827
cache: &Cache,
846828
def_id: DefId,
847829
item_type: ItemType,
848-
is_json: bool,
849830
) -> Option<String> {
850831
use rustc_abi::IntegerType;
851832

@@ -862,7 +843,6 @@ pub(crate) fn repr_attributes(
862843
// Render `repr(transparent)` iff the non-1-ZST field is public or at least one
863844
// field is public in case all fields are 1-ZST fields.
864845
let render_transparent = cache.document_private
865-
|| is_json
866846
|| adt
867847
.all_fields()
868848
.find(|field| {

src/librustdoc/html/render/mod.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1191,7 +1191,7 @@ fn render_assoc_item(
11911191
// a whitespace prefix and newline.
11921192
fn render_attributes_in_pre(it: &clean::Item, prefix: &str, cx: &Context<'_>) -> impl fmt::Display {
11931193
fmt::from_fn(move |f| {
1194-
for a in it.attributes(cx.tcx(), cx.cache(), false) {
1194+
for a in it.attributes(cx.tcx(), cx.cache()) {
11951195
writeln!(f, "{prefix}{a}")?;
11961196
}
11971197
Ok(())
@@ -1207,7 +1207,7 @@ fn render_code_attribute(code_attr: CodeAttribute, w: &mut impl fmt::Write) {
12071207
// When an attribute is rendered inside a <code> tag, it is formatted using
12081208
// a div to produce a newline after it.
12091209
fn render_attributes_in_code(w: &mut impl fmt::Write, it: &clean::Item, cx: &Context<'_>) {
1210-
for attr in it.attributes(cx.tcx(), cx.cache(), false) {
1210+
for attr in it.attributes(cx.tcx(), cx.cache()) {
12111211
render_code_attribute(CodeAttribute(attr), w);
12121212
}
12131213
}
@@ -1219,7 +1219,7 @@ fn render_repr_attributes_in_code(
12191219
def_id: DefId,
12201220
item_type: ItemType,
12211221
) {
1222-
if let Some(repr) = clean::repr_attributes(cx.tcx(), cx.cache(), def_id, item_type, false) {
1222+
if let Some(repr) = clean::repr_attributes(cx.tcx(), cx.cache(), def_id, item_type) {
12231223
render_code_attribute(CodeAttribute(repr), w);
12241224
}
12251225
}

src/librustdoc/html/render/print_item.rs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1487,12 +1487,11 @@ impl<'a, 'cx: 'a> ItemUnion<'a, 'cx> {
14871487
self.cx.cache(),
14881488
self.def_id,
14891489
ItemType::Union,
1490-
false,
14911490
) {
14921491
writeln!(f, "{repr}")?;
14931492
};
14941493
} else {
1495-
for a in self.it.attributes(self.cx.tcx(), self.cx.cache(), false) {
1494+
for a in self.it.attributes(self.cx.tcx(), self.cx.cache()) {
14961495
writeln!(f, "{a}")?;
14971496
}
14981497
}

src/librustdoc/json/conversions.rs

Lines changed: 105 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,11 @@
77
use rustc_abi::ExternAbi;
88
use rustc_ast::ast;
99
use rustc_attr_data_structures::{self as attrs, DeprecatedSince};
10+
use rustc_hir as hir;
1011
use rustc_hir::def::CtorKind;
1112
use rustc_hir::def_id::DefId;
1213
use rustc_metadata::rendered_const;
14+
use rustc_middle::ty::TyCtxt;
1315
use rustc_middle::{bug, ty};
1416
use rustc_span::{Pos, kw, sym};
1517
use rustdoc_json_types::*;
@@ -40,7 +42,12 @@ impl JsonRenderer<'_> {
4042
})
4143
.collect();
4244
let docs = item.opt_doc_value();
43-
let attrs = item.attributes(self.tcx, &self.cache, true);
45+
let attrs = item
46+
.attrs
47+
.other_attrs
48+
.iter()
49+
.filter_map(|a| maybe_from_hir_attr(a, item.item_id, self.tcx))
50+
.collect();
4451
let span = item.span(self.tcx);
4552
let visibility = item.visibility(self.tcx);
4653
let clean::ItemInner { name, item_id, .. } = *item.inner;
@@ -875,3 +882,100 @@ impl FromClean<ItemType> for ItemKind {
875882
}
876883
}
877884
}
885+
886+
/// Maybe convert a attribue from hir to json.
887+
///
888+
/// Returns `None` if the attribute shouldn't be in the output.
889+
fn maybe_from_hir_attr(
890+
attr: &hir::Attribute,
891+
item_id: ItemId,
892+
tcx: TyCtxt<'_>,
893+
) -> Option<Attribute> {
894+
use attrs::AttributeKind as AK;
895+
896+
let kind = match attr {
897+
hir::Attribute::Parsed(kind) => kind,
898+
899+
// There are some currently unstrucured attrs that we *do* care about.
900+
// As the attribute migration progresses (#131229), this is expected to shrink
901+
// and eventually be removed as all attributes gain a strutured representation in
902+
// HIR.
903+
hir::Attribute::Unparsed(_) => {
904+
return Some(if attr.has_name(sym::automatically_derived) {
905+
Attribute::AutomaticallyDerived
906+
} else {
907+
// FIXME: We should handle `#[doc(hidden)]` here.
908+
other_attr(tcx, attr)
909+
});
910+
}
911+
};
912+
913+
Some(match kind {
914+
AK::Deprecation { .. } => return None, // Handled seperatly into Item::deprecation.
915+
AK::DocComment { .. } => unreachable!("doc comments stripped out earlier"),
916+
917+
AK::MustUse { reason, span: _ } => {
918+
Attribute::MustUse { reason: reason.map(|s| s.to_string()) }
919+
}
920+
AK::Repr { .. } => repr_attr(
921+
tcx,
922+
item_id.as_def_id().expect("all items that could have #[repr] have a DefId"),
923+
),
924+
AK::ExportName { name, span: _ } => Attribute::ExportName(name.to_string()),
925+
AK::LinkSection { name, span: _ } => Attribute::LinkSection(name.to_string()),
926+
927+
AK::NoMangle(_) => Attribute::NoMangle,
928+
AK::NonExhaustive(_) => Attribute::NonExhaustive,
929+
AK::TargetFeature(features, _span) => Attribute::TargetFeature(
930+
features.iter().map(|(feat, _span)| feat.to_string()).collect(),
931+
),
932+
933+
_ => other_attr(tcx, attr),
934+
})
935+
}
936+
937+
fn other_attr(tcx: TyCtxt<'_>, attr: &hir::Attribute) -> Attribute {
938+
let mut s = rustc_hir_pretty::attribute_to_string(&tcx, attr);
939+
assert_eq!(s.pop(), Some('\n'));
940+
Attribute::Other(s)
941+
}
942+
943+
fn repr_attr(tcx: TyCtxt<'_>, def_id: DefId) -> Attribute {
944+
let repr = tcx.adt_def(def_id).repr();
945+
946+
let kind = if repr.c() {
947+
ReprKind::C
948+
} else if repr.transparent() {
949+
ReprKind::Transparent
950+
} else if repr.simd() {
951+
ReprKind::Simd
952+
} else {
953+
ReprKind::Rust
954+
};
955+
956+
let align = repr.align.map(|a| a.bytes());
957+
let packed = repr.pack.map(|p| p.bytes());
958+
let int = repr.int.map(format_integer_type);
959+
960+
Attribute::Repr(AttributeRepr { kind, align, packed, int })
961+
}
962+
963+
fn format_integer_type(it: rustc_abi::IntegerType) -> String {
964+
use rustc_abi::Integer::*;
965+
use rustc_abi::IntegerType::*;
966+
match it {
967+
Pointer(true) => "isize",
968+
Pointer(false) => "usize",
969+
Fixed(I8, true) => "i8",
970+
Fixed(I8, false) => "u8",
971+
Fixed(I16, true) => "i16",
972+
Fixed(I16, false) => "u16",
973+
Fixed(I32, true) => "i32",
974+
Fixed(I32, false) => "u32",
975+
Fixed(I64, true) => "i64",
976+
Fixed(I64, false) => "u64",
977+
Fixed(I128, true) => "i128",
978+
Fixed(I128, false) => "u128",
979+
}
980+
.to_owned()
981+
}

src/rustdoc-json-types/lib.rs

Lines changed: 85 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,8 @@ pub type FxHashMap<K, V> = HashMap<K, V>; // re-export for use in src/librustdoc
3737
// will instead cause conflicts. See #94591 for more. (This paragraph and the "Latest feature" line
3838
// are deliberately not in a doc comment, because they need not be in public docs.)
3939
//
40-
// Latest feature: Pretty printing of no_mangle attributes changed
41-
pub const FORMAT_VERSION: u32 = 53;
40+
// Latest feature: Structured Attributes
41+
pub const FORMAT_VERSION: u32 = 54;
4242

4343
/// The root of the emitted JSON blob.
4444
///
@@ -195,13 +195,94 @@ pub struct Item {
195195
/// - `#[repr(C)]` and other reprs also appear as themselves,
196196
/// though potentially with a different order: e.g. `repr(i8, C)` may become `repr(C, i8)`.
197197
/// Multiple repr attributes on the same item may be combined into an equivalent single attr.
198-
pub attrs: Vec<String>,
198+
pub attrs: Vec<Attribute>,
199199
/// Information about the item’s deprecation, if present.
200200
pub deprecation: Option<Deprecation>,
201201
/// The type-specific fields describing this item.
202202
pub inner: ItemEnum,
203203
}
204204

205+
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
206+
#[serde(rename_all = "snake_case")]
207+
/// An attribute, eg `#[repr(C)]`
208+
///
209+
/// This doesn't include:
210+
/// - `#[doc = "Doc Comment"]` or `/// Doc comment`. These are in [`Item::docs`] instead.
211+
/// - `#[deprecated]`. These are in [`Item::deprecation`] instead.
212+
pub enum Attribute {
213+
/// `#[non_exhaustive]`
214+
NonExhaustive,
215+
216+
/// `#[must_use]`
217+
MustUse { reason: Option<String> },
218+
219+
/// `#[export_name = "name"]`
220+
ExportName(String),
221+
222+
/// `#[link_section = "name"]`
223+
LinkSection(String),
224+
225+
/// `#[automatically_derived]`
226+
AutomaticallyDerived,
227+
228+
/// `#[repr]`
229+
Repr(AttributeRepr),
230+
231+
/// `#[no_mangle]`
232+
NoMangle,
233+
234+
/// #[target_feature(enable = "feature1", enable = "feature2")]
235+
TargetFeature(Vec<String>),
236+
237+
/// Something else.
238+
///
239+
/// Things here are explicitly *not* covered by the [`FORMAT_VERSION`]
240+
/// constant, and may change without bumping the format version.
241+
///
242+
/// As an implementation detail, this is currently either:
243+
/// 1. A HIR debug printing, like `"#[attr = Optimize(Speed)]"`
244+
/// 2. The attribute as it appears in source form, like
245+
/// `"#[optimize(speed)]"`.
246+
Other(String),
247+
}
248+
249+
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
250+
/// The contents of a `#[repr(...)]` attribute.
251+
///
252+
/// Used in [`Attribute::Repr`].
253+
pub struct AttributeRepr {
254+
/// The representation, e.g. `#[repr(C)]`, `#[repr(transparent)]`
255+
pub kind: ReprKind,
256+
257+
/// Alignment in bytes, if explicitly specified by `#[repr(align(...)]`.
258+
pub align: Option<u64>,
259+
/// Alignment in bytes, if explicitly specified by `#[repr(packed(...)]]`.
260+
pub packed: Option<u64>,
261+
262+
/// The integer type for an enum descriminant, if explicitly specified.
263+
///
264+
/// e.g. `"i32"`, for `#[repr(C, i32)]`
265+
pub int: Option<String>,
266+
}
267+
268+
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
269+
#[serde(rename_all = "snake_case")]
270+
/// The kind of `#[repr]`.
271+
///
272+
/// See [AttributeRepr::kind]`.
273+
pub enum ReprKind {
274+
/// `#[repr(Rust)]`
275+
///
276+
/// Also the default.
277+
Rust,
278+
/// `#[repr(C)]`
279+
C,
280+
/// `#[repr(transparent)]
281+
Transparent,
282+
/// `#[repr(simd)]`
283+
Simd,
284+
}
285+
205286
/// A range of source code.
206287
#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
207288
pub struct Span {
@@ -1343,7 +1424,7 @@ pub struct Static {
13431424

13441425
/// Is the static `unsafe`?
13451426
///
1346-
/// This is only true if it's in an `extern` block, and not explicity marked
1427+
/// This is only true if it's in an `extern` block, and not explicitly marked
13471428
/// as `safe`.
13481429
///
13491430
/// ```rust

0 commit comments

Comments
 (0)