Skip to content

Commit f9c9828

Browse files
Rollup merge of #158343 - obi1kenobi:pg/item-const-stability, r=GuillaumeGomez
Include `Item::const_stability` info in rustdoc JSON. Include a parallel `Item::const_stability` item in rustdoc JSON, sibling to `Item::stability` from #158230, intended for the same purpose of catching accidental breaking changes in the Rust standard library. I aimed to include `Item::const_stability` judiciously, when it would be genuinely useful without needlessly bloating the JSON. For example, if an item is already unstable, we do not duplicate the `Item::stability` value into `Item::const_stability` as the const-instability is clearly implied. I've covered all such cases in the test suite and documented my reasoning inline. r? @GuillaumeGomez **AI disclosure:** This PR is the product of a combination of manual work and AI tools. I secured approval in advance from the designated reviewer. I stand behind the quality of the code I'm submitting, and I vouch it's as good or better compared to if I had written every line by my own hand. (In particular, local AI review flagged several missing test cases, one of which caught a bug I would have missed otherwise.)
2 parents 843d94f + f8973b7 commit f9c9828

8 files changed

Lines changed: 360 additions & 10 deletions

File tree

src/librustdoc/json/conversions.rs

Lines changed: 74 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,9 @@ use rustc_data_structures::fx::FxHashSet;
88
use rustc_data_structures::thin_vec::ThinVec;
99
use rustc_hir as hir;
1010
use rustc_hir::attrs::{self, DeprecatedSince, DocAttribute, DocInline, HideOrShow};
11-
use rustc_hir::def::CtorKind;
11+
use rustc_hir::def::{CtorKind, DefKind};
1212
use rustc_hir::def_id::DefId;
13-
use rustc_hir::{HeaderSafety, Safety};
13+
use rustc_hir::{HeaderSafety, Safety, find_attr};
1414
use rustc_metadata::rendered_const;
1515
use rustc_middle::ty::TyCtxt;
1616
use rustc_middle::{bug, ty};
@@ -92,6 +92,9 @@ impl JsonRenderer<'_> {
9292
item.item_id.as_def_id()
9393
};
9494
let stability = stability_def_id.and_then(|def_id| self.tcx.lookup_stability(def_id));
95+
let const_stability = item.item_id.as_def_id().and_then(|def_id| {
96+
const_stability_for_def_id(self.tcx, def_id).map(|s| Box::new(s.into_json(self)))
97+
});
9598

9699
Some(Item {
97100
id,
@@ -100,6 +103,7 @@ impl JsonRenderer<'_> {
100103
span: span.and_then(|span| span.into_json(self)),
101104
visibility: visibility.into_json(self),
102105
stability: stability.map(|s| Box::new(s.into_json(self))),
106+
const_stability,
103107
docs,
104108
attrs,
105109
deprecation: deprecation.into_json(self),
@@ -246,6 +250,24 @@ impl FromClean<hir::Stability> for Stability {
246250
}
247251
}
248252

253+
impl FromClean<hir::ConstStability> for Stability {
254+
fn from_clean(stab: &hir::ConstStability, _renderer: &JsonRenderer<'_>) -> Self {
255+
let feature = stab.feature.to_string();
256+
let level = match stab.level {
257+
hir::StabilityLevel::Stable { since, .. } => StabilityLevel::Stable {
258+
since: match since {
259+
hir::StableSince::Version(since) => Some(since.to_string()),
260+
hir::StableSince::Current => Some(hir::RustcVersion::CURRENT.to_string()),
261+
// Match rustdoc HTML: malformed stable-since values are omitted.
262+
hir::StableSince::Err(_) => None,
263+
},
264+
},
265+
hir::StabilityLevel::Unstable { .. } => StabilityLevel::Unstable,
266+
};
267+
Stability { feature, level }
268+
}
269+
}
270+
249271
impl FromClean<clean::GenericArgs> for Option<Box<GenericArgs>> {
250272
fn from_clean(generic_args: &clean::GenericArgs, renderer: &JsonRenderer<'_>) -> Self {
251273
use clean::GenericArgs::*;
@@ -948,6 +970,55 @@ impl FromClean<ItemType> for ItemKind {
948970
}
949971
}
950972

973+
fn const_stability_for_def_id(tcx: TyCtxt<'_>, def_id: DefId) -> Option<hir::ConstStability> {
974+
if !tcx.is_conditionally_const(def_id) {
975+
// The item cannot be conditionally-const. No const stability here.
976+
//
977+
// This includes associated consts, which are an interesting exception
978+
// to the general rule that items inside `const impl` and `const trait` carry
979+
// the const-stability of that block. Associated consts are already const, always.
980+
return None;
981+
}
982+
983+
let const_stability = tcx.lookup_const_stability(def_id)?;
984+
if find_attr!(tcx, def_id, RustcConstStability { .. }) {
985+
// Direct const-stability attribute on the item itself. Return it directly.
986+
return Some(const_stability);
987+
}
988+
989+
if const_stability.is_const_stable() {
990+
// Items that are const-stable without an explicit attribute on their own item
991+
// must be associated items inside `const trait` or `const impl`.
992+
// We don't want to duplicate their parent item's const-stability attribute.
993+
return None;
994+
}
995+
996+
// We're dealing with an item that is const-unstable,
997+
// but doesn't have an explicit const-stability attribute on it.
998+
//
999+
// Today, this means one of two cases:
1000+
// - The item is enclosed within a `#[rustc_const_unstable]` block,
1001+
// like a `const trait` or `const impl`, in which case our query propagated the parent's
1002+
// const-instability info. This const-instability is desirable to place into JSON
1003+
// because *only some* associated items inside such a block are const-unstable.
1004+
// Associated consts are the exception, and were handled earlier.
1005+
// - The item is `#[unstable]` which implies it's const-unstable under the same feature,
1006+
// in which case we don't want to duplicate the existing stability attribute
1007+
// which would already appear in an adjacent field in the JSON anyway.
1008+
if let Some(parent_def_id) = tcx.opt_parent(def_id)
1009+
&& matches!(tcx.def_kind(parent_def_id), DefKind::Trait | DefKind::Impl { .. })
1010+
&& tcx.lookup_const_stability(parent_def_id) == Some(const_stability)
1011+
{
1012+
Some(const_stability)
1013+
} else {
1014+
std::debug_assert_matches!(
1015+
tcx.lookup_stability(def_id).map(|s| s.level),
1016+
Some(hir::StabilityLevel::Unstable { .. })
1017+
);
1018+
None
1019+
}
1020+
}
1021+
9511022
/// Maybe convert a attribute from hir to json.
9521023
///
9531024
/// Returns `None` if the attribute shouldn't be in the output.
@@ -966,6 +1037,7 @@ fn maybe_from_hir_attr(attr: &hir::Attribute, item_id: ItemId, tcx: TyCtxt<'_>)
9661037
vec![match kind {
9671038
AK::Deprecated { .. } => return Vec::new(), // Handled separately into Item::deprecation.
9681039
AK::Stability { .. } => return Vec::new(), // Handled separately into Item::stability
1040+
AK::RustcConstStability { .. } => return Vec::new(), // Handled separately into Item::const_stability.
9691041

9701042
AK::DocComment { .. } => unreachable!("doc comments stripped out earlier"),
9711043

src/librustdoc/json/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -361,6 +361,6 @@ mod size_asserts {
361361
// tidy-alphabetical-end
362362

363363
// These contains a `PathBuf`, which is different sizes on different OSes.
364-
static_assert_size!(Item, 536 + size_of::<std::path::PathBuf>());
364+
static_assert_size!(Item, 544 + size_of::<std::path::PathBuf>());
365365
static_assert_size!(ExternalCrate, 48 + size_of::<std::path::PathBuf>());
366366
}

src/rustdoc-json-types/lib.rs

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -114,8 +114,8 @@ pub type FxHashMap<K, V> = HashMap<K, V>; // re-export for use in src/librustdoc
114114
// will instead cause conflicts. See #94591 for more. (This paragraph and the "Latest feature" line
115115
// are deliberately not in a doc comment, because they need not be in public docs.)
116116
//
117-
// Latest feature: Add `Item::stability`.
118-
pub const FORMAT_VERSION: u32 = 58;
117+
// Latest feature: Add `Item::const_stability`.
118+
pub const FORMAT_VERSION: u32 = 59;
119119

120120
/// The root of the emitted JSON blob.
121121
///
@@ -286,6 +286,8 @@ pub struct Item {
286286
/// - `#[doc = "Doc Comment"]` or `/// Doc comment`: see [`Self::docs`] instead.
287287
/// - `#[deprecated]` attributes: see the [`Self::deprecation`] field instead.
288288
/// - `#[stable]` and `#[unstable]` attributes: see the [`Self::stability`] field instead.
289+
/// - `#[rustc_const_stable]` and `#[rustc_const_unstable]` attributes:
290+
/// see the [`Self::const_stability`] field instead.
289291
///
290292
/// Attributes appear in pretty-printed Rust form, regardless of their formatting
291293
/// in the original source code. For example:
@@ -319,20 +321,31 @@ pub struct Item {
319321
/// most ordinary third-party crates usually have no data here.
320322
pub stability: Option<Box<Stability>>,
321323

324+
/// Stability information for using this item in const contexts, if any.
325+
///
326+
/// This is separate from [`Self::stability`]. An item can be stable as regular API while its
327+
/// const use is unstable. An unstable item may have no separate const-stability value here.
328+
///
329+
/// This field is only populated for item kinds whose const behavior can have separate
330+
/// stability information, such as const functions, const traits, const trait impls,
331+
/// and associated items whose const behavior is controlled by a const trait or const impl.
332+
pub const_stability: Option<Box<Stability>>,
333+
322334
/// The type-specific fields describing this item.
323335
pub inner: ItemEnum,
324336
}
325337

326338
/// Stability information for an item.
327339
///
328-
/// This only refers to regular item stability: whether the item is stable or unstable
329-
/// as represented by the `#[stable]` or `#[unstable]` attributes.
330-
/// Const stability and default-body stability are different things and not captured here.
340+
/// In [`Item::stability`], this refers to regular item stability: whether the item is
341+
/// stable or unstable as represented by the `#[stable]` or `#[unstable]` attributes.
342+
/// In [`Item::const_stability`], this refers to using the item in const contexts,
343+
/// as represented by `#[rustc_const_stable]` or `#[rustc_const_unstable]`.
331344
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
332345
#[cfg_attr(feature = "rkyv_0_8", derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize))]
333346
#[cfg_attr(feature = "rkyv_0_8", rkyv(derive(Debug)))]
334347
pub struct Stability {
335-
/// The stability feature associated with this item.
348+
/// The feature associated with this stability record.
336349
///
337350
/// For unstable items, this is the feature gate associated with the item.
338351
/// For stable items, this is the historical label recorded when the item was stabilized.
@@ -342,7 +355,6 @@ pub struct Stability {
342355
pub level: StabilityLevel,
343356
}
344357

345-
/// Whether an item is stable or unstable as regular public API.
346358
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
347359
#[cfg_attr(feature = "rkyv_0_8", derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize))]
348360
#[cfg_attr(feature = "rkyv_0_8", rkyv(derive(Debug)))]
@@ -365,6 +377,8 @@ pub enum StabilityLevel {
365377
/// - `#[doc = "Doc Comment"]` or `/// Doc comment`. These are in [`Item::docs`] instead.
366378
/// - `#[deprecated]`. These are in [`Item::deprecation`] instead.
367379
/// - `#[stable]` and `#[unstable]`. These are in [`Item::stability`] instead.
380+
/// - `#[rustc_const_stable]` and `#[rustc_const_unstable]`. These are in
381+
/// [`Item::const_stability`] instead.
368382
pub enum Attribute {
369383
/// `#[non_exhaustive]`
370384
NonExhaustive,

src/tools/jsondoclint/src/validator/tests.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ fn errors_on_missing_links() {
3434
attrs: vec![],
3535
deprecation: None,
3636
stability: None,
37+
const_stability: None,
3738
inner: ItemEnum::Module(Module {
3839
is_crate: true,
3940
items: vec![],
@@ -83,6 +84,7 @@ fn errors_on_local_in_paths_and_not_index() {
8384
attrs: Vec::new(),
8485
deprecation: None,
8586
stability: None,
87+
const_stability: None,
8688
inner: ItemEnum::Module(Module {
8789
is_crate: true,
8890
items: vec![Id(1)],
@@ -103,6 +105,7 @@ fn errors_on_local_in_paths_and_not_index() {
103105
attrs: Vec::new(),
104106
deprecation: None,
105107
stability: None,
108+
const_stability: None,
106109
inner: ItemEnum::Primitive(Primitive { name: "i32".to_owned(), impls: vec![] }),
107110
},
108111
),
@@ -157,6 +160,7 @@ fn errors_on_missing_path() {
157160
attrs: Vec::new(),
158161
deprecation: None,
159162
stability: None,
163+
const_stability: None,
160164
inner: ItemEnum::Module(Module {
161165
is_crate: true,
162166
items: vec![Id(1), Id(2)],
@@ -177,6 +181,7 @@ fn errors_on_missing_path() {
177181
attrs: Vec::new(),
178182
deprecation: None,
179183
stability: None,
184+
const_stability: None,
180185
inner: ItemEnum::Struct(Struct {
181186
kind: StructKind::Unit,
182187
generics: generics.clone(),
@@ -197,6 +202,7 @@ fn errors_on_missing_path() {
197202
attrs: Vec::new(),
198203
deprecation: None,
199204
stability: None,
205+
const_stability: None,
200206
inner: ItemEnum::Function(Function {
201207
sig: FunctionSignature {
202208
inputs: vec![],
@@ -260,6 +266,7 @@ fn checks_local_crate_id_is_correct() {
260266
attrs: Vec::new(),
261267
deprecation: None,
262268
stability: None,
269+
const_stability: None,
263270
inner: ItemEnum::Module(Module {
264271
is_crate: true,
265272
items: vec![],
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
#![feature(staged_api)]
2+
3+
//@ is "$.index[?(@.name=='non_const_function')].const_stability" null
4+
#[stable(feature = "non_const_function_feature", since = "0.9.0")]
5+
pub fn non_const_function() {}
6+
7+
//@ set stable_const_fn = "$.index[?(@.name=='stable_const_fn')].id"
8+
//@ is "$.index[?(@.name=='stable_const_fn')].stability.level" '"stable"'
9+
//@ is "$.index[?(@.name=='stable_const_fn')].stability.feature" '"stable_const_fn_feature"'
10+
//@ is "$.index[?(@.name=='stable_const_fn')].stability.since" '"1.0.0"'
11+
//@ is "$.index[?(@.name=='stable_const_fn')].const_stability.level" '"stable"'
12+
//@ is "$.index[?(@.name=='stable_const_fn')].const_stability.feature" '"stable_const_fn_const_feature"'
13+
//@ is "$.index[?(@.name=='stable_const_fn')].const_stability.since" '"1.1.0"'
14+
//@ is "$.index[?(@.name=='stable_const_fn')].attrs" []
15+
#[stable(feature = "stable_const_fn_feature", since = "1.0.0")]
16+
#[rustc_const_stable(feature = "stable_const_fn_const_feature", since = "1.1.0")]
17+
pub const fn stable_const_fn() {}
18+
19+
//@ is "$.index[?(@.name=='const_unstable_fn')].stability.level" '"stable"'
20+
//@ is "$.index[?(@.name=='const_unstable_fn')].stability.feature" '"const_unstable_fn_feature"'
21+
//@ is "$.index[?(@.name=='const_unstable_fn')].stability.since" '"2.0.0"'
22+
//@ is "$.index[?(@.name=='const_unstable_fn')].const_stability.level" '"unstable"'
23+
//@ is "$.index[?(@.name=='const_unstable_fn')].const_stability.feature" '"const_unstable_fn_const_feature"'
24+
//@ !has "$.index[?(@.name=='const_unstable_fn')].const_stability.since"
25+
//@ is "$.index[?(@.name=='const_unstable_fn')].attrs" []
26+
#[stable(feature = "const_unstable_fn_feature", since = "2.0.0")]
27+
#[rustc_const_unstable(feature = "const_unstable_fn_const_feature", issue = "none")]
28+
pub const fn const_unstable_fn() {}
29+
30+
// Even when the item itself is unstable, if a separate const-stability attribute is present,
31+
// that's a distinct fact possibly associated with a different feature gate.
32+
// It should therefore be exposed on its own, instead of being collapsed into regular stability.
33+
//@ is "$.index[?(@.name=='unstable_fn_with_explicit_const_gate')].stability.level" '"unstable"'
34+
//@ is "$.index[?(@.name=='unstable_fn_with_explicit_const_gate')].stability.feature" '"unstable_fn_with_explicit_const_gate_feature"'
35+
//@ !has "$.index[?(@.name=='unstable_fn_with_explicit_const_gate')].stability.since"
36+
//@ is "$.index[?(@.name=='unstable_fn_with_explicit_const_gate')].const_stability.level" '"unstable"'
37+
//@ is "$.index[?(@.name=='unstable_fn_with_explicit_const_gate')].const_stability.feature" '"explicit_const_gate_on_unstable_fn"'
38+
//@ !has "$.index[?(@.name=='unstable_fn_with_explicit_const_gate')].const_stability.since"
39+
//@ is "$.index[?(@.name=='unstable_fn_with_explicit_const_gate')].attrs" []
40+
#[unstable(feature = "unstable_fn_with_explicit_const_gate_feature", issue = "none")]
41+
#[rustc_const_unstable(feature = "explicit_const_gate_on_unstable_fn", issue = "none")]
42+
pub const fn unstable_fn_with_explicit_const_gate() {}
43+
44+
// `lookup_const_stability` synthesizes a const-unstable record for this item from its regular
45+
// instability. Rustdoc JSON filters that out because there is no separate const feature gate.
46+
//@ is "$.index[?(@.name=='unstable_const_fn_without_const_gate')].stability.level" '"unstable"'
47+
//@ is "$.index[?(@.name=='unstable_const_fn_without_const_gate')].stability.feature" '"unstable_const_fn_without_const_gate_feature"'
48+
//@ is "$.index[?(@.name=='unstable_const_fn_without_const_gate')].const_stability" null
49+
#[unstable(feature = "unstable_const_fn_without_const_gate_feature", issue = "none")]
50+
pub const fn unstable_const_fn_without_const_gate() {}
51+
52+
// The `Use` item describes the re-export. It doesn't have `const_stability` of its own.
53+
//@ is "$.index[?(@.inner.use.name=='stable_const_fn_reexport')].const_stability" null
54+
//@ is "$.index[?(@.inner.use.name=='stable_const_fn_reexport')].inner.use.id" $stable_const_fn
55+
pub use stable_const_fn as stable_const_fn_reexport;

0 commit comments

Comments
 (0)