From 0a4507cf64ef26463e24b41d14c576cd31f2a8ce Mon Sep 17 00:00:00 2001 From: Nicholas Nethercote Date: Sat, 14 Jun 2025 22:01:12 +1000 Subject: [PATCH 1/5] rustdoc_json: Add static asserts for the size of important types. A lot of these are large! Lots of room for improvement in the future. --- src/librustdoc/json/mod.rs | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/src/librustdoc/json/mod.rs b/src/librustdoc/json/mod.rs index 2feadce26d09f..8cf3432d3bf12 100644 --- a/src/librustdoc/json/mod.rs +++ b/src/librustdoc/json/mod.rs @@ -382,3 +382,33 @@ impl<'tcx> FormatRenderer<'tcx> for JsonRenderer<'tcx> { &self.cache } } + +// Some nodes are used a lot. Make sure they don't unintentionally get bigger. +// +// These assertions are here, not in `src/rustdoc-json-types/lib.rs` where the types are defined, +// because we have access to `static_assert_size` here. +#[cfg(target_pointer_width = "64")] +mod size_asserts { + use rustc_data_structures::static_assert_size; + + use super::types::*; + // tidy-alphabetical-start + static_assert_size!(AssocItemConstraint, 208); + static_assert_size!(Crate, 184); + static_assert_size!(ExternalCrate, 48); + static_assert_size!(FunctionPointer, 168); + static_assert_size!(GenericArg, 80); + static_assert_size!(GenericArgs, 104); + static_assert_size!(GenericBound, 72); + static_assert_size!(GenericParamDef, 136); + static_assert_size!(Impl, 304); + // `Item` contains a `PathBuf`, which is different sizes on different OSes. + static_assert_size!(Item, 528 + size_of::()); + static_assert_size!(ItemSummary, 32); + static_assert_size!(PolyTrait, 64); + static_assert_size!(PreciseCapturingArg, 32); + static_assert_size!(TargetFeature, 80); + static_assert_size!(Type, 80); + static_assert_size!(WherePredicate, 160); + // tidy-alphabetical-end +} From 82f8220922599ae6123d42b6db2a634af282238e Mon Sep 17 00:00:00 2001 From: Nicholas Nethercote Date: Mon, 16 Jun 2025 09:52:23 +1000 Subject: [PATCH 2/5] rustdoc_json: Add a test for some `GenericArgs` cases. --- tests/rustdoc-json/generic-args.rs | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 tests/rustdoc-json/generic-args.rs diff --git a/tests/rustdoc-json/generic-args.rs b/tests/rustdoc-json/generic-args.rs new file mode 100644 index 0000000000000..e48c3329f56ba --- /dev/null +++ b/tests/rustdoc-json/generic-args.rs @@ -0,0 +1,20 @@ +pub struct MyStruct(u32); + +pub trait MyTrait { + type MyType; + fn my_fn(&self); +} + +impl MyTrait for MyStruct { + type MyType = u32; + fn my_fn(&self) {} +} + +//@ is "$.index[?(@.name=='my_fn1')].inner.function.sig.inputs[0][1].qualified_path.args" {\"angle_bracketed\":{\"args\":[],\"constraints\":[]}} +//@ is "$.index[?(@.name=='my_fn1')].inner.function.sig.inputs[0][1].qualified_path.self_type.resolved_path.args" {\"angle_bracketed\":{\"args\":[],\"constraints\":[]}} +pub fn my_fn1(_: ::MyType) {} + +//@ is "$.index[?(@.name=='my_fn2')].inner.function.sig.inputs[0][1].dyn_trait.traits[0].trait.args.angle_bracketed.constraints[0].args" {\"angle_bracketed\":{\"args\":[],\"constraints\":[]}} +pub fn my_fn2(_: IntoIterator) {} + +fn main() {} From 74371cab27add086cd2a3b895c5501205fadae02 Mon Sep 17 00:00:00 2001 From: Nicholas Nethercote Date: Sat, 14 Jun 2025 20:14:51 +1000 Subject: [PATCH 3/5] rustdoc_json: Fix handling of paths with no generic args. A path without generic args, like `Reader`, currently has JSON produced like this: ``` {"path":"Reader","id":286,"args":{"angle_bracketed":{"args":[],"constraints":[]}}} ``` Even though `types::Path::args` is `Option` and allows for "no args", instead it gets represented as "empty args". (More like `Reader<>` than `Reader`.) This is due to a problem in `clean::Path::from_clean`. It only produces `None` if the path is an empty string. This commit changes it to also produce `None` if there are no generic args. The example above becomes: ``` {"path":"Reader","id":286,"args":null} ``` I looked at a few examples and saw this reduce the size of the JSON output by 3-9%. The commit also adds an assertion that non-final segments don't have any generics; something the old code was implicitly relying on. Note: the original sin here is that `clean::PathSegment::args` is not an `Option`, unlike `{ast,hir}::PathSegment::args`. I want to fix that, but it can be done separately. --- src/librustdoc/json/conversions.rs | 19 ++++++++++++++++++- tests/rustdoc-json/generic-args.rs | 2 +- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/src/librustdoc/json/conversions.rs b/src/librustdoc/json/conversions.rs index 6bdf3b5fe3876..3b270d111a17e 100644 --- a/src/librustdoc/json/conversions.rs +++ b/src/librustdoc/json/conversions.rs @@ -580,7 +580,24 @@ impl FromClean for Path { Path { path: path.whole_name(), id: renderer.id_from_item_default(path.def_id().into()), - args: path.segments.last().map(|args| Box::new(args.args.into_json(renderer))), + args: { + if let Some((final_seg, rest_segs)) = path.segments.split_last() { + // In general, `clean::Path` can hold things like + // `std::vec::Vec::::new`, where generic args appear + // in a middle segment. But for the places where `Path` is + // used by rustdoc-json-types, generic args can only be + // used in the final segment, e.g. `std::vec::Vec`. So + // check that the non-final segments have no generic args. + assert!(rest_segs.iter().all(|seg| seg.args.is_empty())); + if final_seg.args.is_empty() { + None + } else { + Some(Box::new(final_seg.args.into_json(renderer))) + } + } else { + None // no generics on any segments because there are no segments + } + }, } } } diff --git a/tests/rustdoc-json/generic-args.rs b/tests/rustdoc-json/generic-args.rs index e48c3329f56ba..e87c1e6230432 100644 --- a/tests/rustdoc-json/generic-args.rs +++ b/tests/rustdoc-json/generic-args.rs @@ -11,7 +11,7 @@ impl MyTrait for MyStruct { } //@ is "$.index[?(@.name=='my_fn1')].inner.function.sig.inputs[0][1].qualified_path.args" {\"angle_bracketed\":{\"args\":[],\"constraints\":[]}} -//@ is "$.index[?(@.name=='my_fn1')].inner.function.sig.inputs[0][1].qualified_path.self_type.resolved_path.args" {\"angle_bracketed\":{\"args\":[],\"constraints\":[]}} +//@ is "$.index[?(@.name=='my_fn1')].inner.function.sig.inputs[0][1].qualified_path.self_type.resolved_path.args" null pub fn my_fn1(_: ::MyType) {} //@ is "$.index[?(@.name=='my_fn2')].inner.function.sig.inputs[0][1].dyn_trait.traits[0].trait.args.angle_bracketed.constraints[0].args" {\"angle_bracketed\":{\"args\":[],\"constraints\":[]}} From dba34b324d617f10a91ce3070ea2c1d04cdd4fa9 Mon Sep 17 00:00:00 2001 From: Nicholas Nethercote Date: Sat, 14 Jun 2025 20:22:30 +1000 Subject: [PATCH 4/5] Fix some comments. As per the previous commit, generic args here can only appear on the final segment. So make the comments obey that constraint. --- src/rustdoc-json-types/lib.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/rustdoc-json-types/lib.rs b/src/rustdoc-json-types/lib.rs index 1f93895ae0762..f9f4016eb6651 100644 --- a/src/rustdoc-json-types/lib.rs +++ b/src/rustdoc-json-types/lib.rs @@ -277,8 +277,8 @@ pub struct PolyTrait { /// A set of generic arguments provided to a path segment, e.g. /// /// ```text -/// std::option::Option::::None -/// ^^^^^ +/// std::option::Option +/// ^^^^^ /// ``` #[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] @@ -331,7 +331,7 @@ pub enum GenericArg { Const(Constant), /// A generic argument that's explicitly set to be inferred. /// ```text - /// std::vec::Vec::<_>::new() + /// std::vec::Vec::<_> /// ^ /// ``` Infer, From 91818bab01b22001e026686009e5b839fc49e0c4 Mon Sep 17 00:00:00 2001 From: Nicholas Nethercote Date: Sat, 14 Jun 2025 21:39:09 +1000 Subject: [PATCH 5/5] rustdoc_json: represent generic args consistently. They show up in three places: once as `Option>`, once as `Box`, and once as `GenericArgs`. The first option is best. It is more compact because generic args are often missing. This commit changes the latter two to the former. Example output, before and after, for the `AssocItemConstraint` change: ``` {"name":"Offset","args":{"angle_bracketed":{"args":[],"constraints":[]}},"binding":{...}} {"name":"Offset","args":null,"binding":{...}} ``` Example output, before and after, for the `Type::QualifiedPath` change: ``` {"qualified_path":{"name":"Offset","args":{"angle_bracketed":{"args":[],"constraints":[]}}, ...}} {"qualified_path":{"name":"Offset","args":null, ...}} ``` This reduces JSON output size, but not by much (e.g. 0.5%), because `AssocItemConstraint` and `Type::QualifiedPath` are uncommon. --- src/librustdoc/json/conversions.rs | 17 ++++++++--------- src/librustdoc/json/mod.rs | 2 +- src/rustdoc-json-types/lib.rs | 8 ++++---- src/tools/jsondoclint/src/validator.rs | 13 ++++++------- tests/rustdoc-json/generic-args.rs | 4 ++-- 5 files changed, 21 insertions(+), 23 deletions(-) diff --git a/src/librustdoc/json/conversions.rs b/src/librustdoc/json/conversions.rs index 3b270d111a17e..b9df594dcca96 100644 --- a/src/librustdoc/json/conversions.rs +++ b/src/librustdoc/json/conversions.rs @@ -170,10 +170,13 @@ pub(crate) fn from_deprecation(deprecation: attrs::Deprecation) -> Deprecation { Deprecation { since, note: note.map(|s| s.to_string()) } } -impl FromClean for GenericArgs { +impl FromClean for Option> { fn from_clean(args: &clean::GenericArgs, renderer: &JsonRenderer<'_>) -> Self { use clean::GenericArgs::*; - match args { + if args.is_empty() { + return None; + } + Some(Box::new(match args { AngleBracketed { args, constraints } => GenericArgs::AngleBracketed { args: args.into_json(renderer), constraints: constraints.into_json(renderer), @@ -183,7 +186,7 @@ impl FromClean for GenericArgs { output: output.as_ref().map(|a| a.as_ref().into_json(renderer)), }, ReturnTypeNotation => GenericArgs::ReturnTypeNotation, - } + })) } } @@ -589,11 +592,7 @@ impl FromClean for Path { // used in the final segment, e.g. `std::vec::Vec`. So // check that the non-final segments have no generic args. assert!(rest_segs.iter().all(|seg| seg.args.is_empty())); - if final_seg.args.is_empty() { - None - } else { - Some(Box::new(final_seg.args.into_json(renderer))) - } + final_seg.args.into_json(renderer) } else { None // no generics on any segments because there are no segments } @@ -608,7 +607,7 @@ impl FromClean for Type { Self::QualifiedPath { name: assoc.name.to_string(), - args: Box::new(assoc.args.into_json(renderer)), + args: assoc.args.into_json(renderer), self_type: Box::new(self_type.into_json(renderer)), trait_: trait_.as_ref().map(|trait_| trait_.into_json(renderer)), } diff --git a/src/librustdoc/json/mod.rs b/src/librustdoc/json/mod.rs index 8cf3432d3bf12..3aceffa3fdc96 100644 --- a/src/librustdoc/json/mod.rs +++ b/src/librustdoc/json/mod.rs @@ -393,7 +393,7 @@ mod size_asserts { use super::types::*; // tidy-alphabetical-start - static_assert_size!(AssocItemConstraint, 208); + static_assert_size!(AssocItemConstraint, 112); static_assert_size!(Crate, 184); static_assert_size!(ExternalCrate, 48); static_assert_size!(FunctionPointer, 168); diff --git a/src/rustdoc-json-types/lib.rs b/src/rustdoc-json-types/lib.rs index f9f4016eb6651..44bb14eeaf78f 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 inline attributes changed -pub const FORMAT_VERSION: u32 = 48; +// Latest feature: improve handling of generic args +pub const FORMAT_VERSION: u32 = 49; /// The root of the emitted JSON blob. /// @@ -362,7 +362,7 @@ pub struct AssocItemConstraint { /// The name of the associated type/constant. pub name: String, /// Arguments provided to the associated type/constant. - pub args: GenericArgs, + pub args: Option>, /// The kind of bound applied to the associated type/constant. pub binding: AssocItemConstraintKind, } @@ -1118,7 +1118,7 @@ pub enum Type { /// as BetterIterator>::Item<'static> /// // ^^^^^^^^^ /// ``` - args: Box, + args: Option>, /// The type with which this type is associated. /// /// ```ignore (incomplete expression) diff --git a/src/tools/jsondoclint/src/validator.rs b/src/tools/jsondoclint/src/validator.rs index 8c9e4c8bb3a60..0a4051fcbe8cd 100644 --- a/src/tools/jsondoclint/src/validator.rs +++ b/src/tools/jsondoclint/src/validator.rs @@ -271,7 +271,7 @@ impl<'a> Validator<'a> { Type::RawPointer { is_mutable: _, type_ } => self.check_type(&**type_), Type::BorrowedRef { lifetime: _, is_mutable: _, type_ } => self.check_type(&**type_), Type::QualifiedPath { name: _, args, self_type, trait_ } => { - self.check_generic_args(&**args); + self.check_opt_generic_args(&args); self.check_type(&**self_type); if let Some(trait_) = trait_ { self.check_path(trait_, PathKind::Trait); @@ -309,13 +309,12 @@ impl<'a> Validator<'a> { self.fail(&x.id, ErrorKind::Custom(format!("No entry in '$.paths' for {x:?}"))); } - if let Some(args) = &x.args { - self.check_generic_args(&**args); - } + self.check_opt_generic_args(&x.args); } - fn check_generic_args(&mut self, x: &'a GenericArgs) { - match x { + fn check_opt_generic_args(&mut self, x: &'a Option>) { + let Some(x) = x else { return }; + match &**x { GenericArgs::AngleBracketed { args, constraints } => { args.iter().for_each(|arg| self.check_generic_arg(arg)); constraints.iter().for_each(|bind| self.check_assoc_item_constraint(bind)); @@ -355,7 +354,7 @@ impl<'a> Validator<'a> { } fn check_assoc_item_constraint(&mut self, bind: &'a AssocItemConstraint) { - self.check_generic_args(&bind.args); + self.check_opt_generic_args(&bind.args); match &bind.binding { AssocItemConstraintKind::Equality(term) => self.check_term(term), AssocItemConstraintKind::Constraint(bounds) => { diff --git a/tests/rustdoc-json/generic-args.rs b/tests/rustdoc-json/generic-args.rs index e87c1e6230432..0f588820da75d 100644 --- a/tests/rustdoc-json/generic-args.rs +++ b/tests/rustdoc-json/generic-args.rs @@ -10,11 +10,11 @@ impl MyTrait for MyStruct { fn my_fn(&self) {} } -//@ is "$.index[?(@.name=='my_fn1')].inner.function.sig.inputs[0][1].qualified_path.args" {\"angle_bracketed\":{\"args\":[],\"constraints\":[]}} +//@ is "$.index[?(@.name=='my_fn1')].inner.function.sig.inputs[0][1].qualified_path.args" null //@ is "$.index[?(@.name=='my_fn1')].inner.function.sig.inputs[0][1].qualified_path.self_type.resolved_path.args" null pub fn my_fn1(_: ::MyType) {} -//@ is "$.index[?(@.name=='my_fn2')].inner.function.sig.inputs[0][1].dyn_trait.traits[0].trait.args.angle_bracketed.constraints[0].args" {\"angle_bracketed\":{\"args\":[],\"constraints\":[]}} +//@ is "$.index[?(@.name=='my_fn2')].inner.function.sig.inputs[0][1].dyn_trait.traits[0].trait.args.angle_bracketed.constraints[0].args" null pub fn my_fn2(_: IntoIterator) {} fn main() {}