From f658bf6bb8642f7ce8b8046210c4150aacbd75e4 Mon Sep 17 00:00:00 2001 From: Shivendra Sharma Date: Sun, 10 May 2026 20:58:46 +0530 Subject: [PATCH] rustdoc: deterministic sorting for `doc_cfg` badges --- src/librustdoc/clean/cfg.rs | 48 ++++++++++++++++++- src/librustdoc/clean/cfg/tests.rs | 31 ++++++++++++ tests/rustdoc-gui/item-info-overflow.goml | 4 +- tests/rustdoc-gui/item-info.goml | 4 +- tests/rustdoc-html/doc-cfg/all-targets.rs | 39 +++++++-------- .../doc-cfg/doc-cfg-simplification.rs | 6 +-- tests/rustdoc-html/doc-cfg/doc-cfg.rs | 4 +- tests/rustdoc-html/doc-cfg/duplicate-cfg.rs | 10 ++-- tests/rustdoc-html/doc-cfg/sort.rs | 42 ++++++++++++++++ .../rustdoc-html/inline_cross/doc-auto-cfg.rs | 4 +- tests/rustdoc-html/target-feature.rs | 4 +- 11 files changed, 157 insertions(+), 39 deletions(-) create mode 100644 tests/rustdoc-html/doc-cfg/sort.rs diff --git a/src/librustdoc/clean/cfg.rs b/src/librustdoc/clean/cfg.rs index ec3407d361ebd..b0a8c7e4ccd5b 100644 --- a/src/librustdoc/clean/cfg.rs +++ b/src/librustdoc/clean/cfg.rs @@ -187,6 +187,44 @@ impl Cfg { } } + /// Recursively sorts the configuration tree to ensure deterministic rendering. + /// + /// Sorting groups predicates logically: Targets first, then Target Features, + /// then Crate Features, and finally nested Any/All/Not groupings. + /// Within each group, a fallback alphabetical sort is applied. + pub(crate) fn sort_for_rendering(&mut self) { + fn sort_cfg_entry(cfg: &mut CfgEntry) { + match cfg { + CfgEntry::Any(sub_cfgs, _) | CfgEntry::All(sub_cfgs, _) => { + for sub_cfg in sub_cfgs.iter_mut() { + sort_cfg_entry(sub_cfg); + } + + sub_cfgs.sort_by_cached_key(|a| { + ( + cfg_category(a), + Display(a, Format::LongPlain).to_string().to_ascii_lowercase(), + ) + }); + } + CfgEntry::Not(box_cfg, _) => sort_cfg_entry(box_cfg), + _ => {} + } + } + + fn cfg_category(cfg: &CfgEntry) -> u8 { + match cfg { + CfgEntry::NameValue { name, .. } if *name == sym::feature => 2, + CfgEntry::NameValue { name, .. } if *name == sym::target_feature => 1, + CfgEntry::NameValue { .. } | CfgEntry::Bool(..) => 0, + CfgEntry::Any(..) | CfgEntry::All(..) | CfgEntry::Not(..) => 3, + _ => 4, + } + } + + sort_cfg_entry(&mut self.0); + } + fn omit_preposition(&self) -> bool { matches!(self.0, CfgEntry::Bool(..)) } @@ -843,14 +881,20 @@ pub(crate) fn extract_cfg_from_attrs<'a, I: Iterator if matches!(cfg_info.current_cfg.0, CfgEntry::Bool(true, _)) { None } else { - Some(Arc::new(cfg_info.current_cfg.clone())) + let mut cfg = cfg_info.current_cfg.clone(); + cfg.sort_for_rendering(); + Some(Arc::new(cfg)) } } else { // If `doc(auto_cfg)` feature is enabled, we want to collect all `cfg` items, we remove the // hidden ones afterward. match strip_hidden(&cfg_info.current_cfg.0, &cfg_info.hidden_cfg) { None | Some(CfgEntry::Bool(true, _)) => None, - Some(cfg) => Some(Arc::new(Cfg(cfg))), + Some(cfg_entry) => { + let mut cfg = Cfg(cfg_entry); + cfg.sort_for_rendering(); + Some(Arc::new(cfg)) + } } } } diff --git a/src/librustdoc/clean/cfg/tests.rs b/src/librustdoc/clean/cfg/tests.rs index e0c21865d8dff..97f9d1fe71673 100644 --- a/src/librustdoc/clean/cfg/tests.rs +++ b/src/librustdoc/clean/cfg/tests.rs @@ -418,3 +418,34 @@ fn test_simplify_with() { assert_eq!(foobar.simplify_with(&foobarbaz), None); }); } + +#[test] +fn test_sort_for_rendering() { + create_default_session_globals_then(|| { + let mut cfg = cfg_any(thin_vec![ + name_value_cfg_e("feature", "sync"), + name_value_cfg_e("target_os", "linux"), + cfg_all_e(thin_vec![word_cfg_e("unix")]), + name_value_cfg_e("target_feature", "sse2"), + name_value_cfg_e("target_os", "android"), + name_value_cfg_e("feature", "alloc"), + ]); + + cfg.sort_for_rendering(); + + let expected = cfg_any(thin_vec![ + // Category 0: Targets (Sorted Alphabetically: Android -> Linux) + name_value_cfg_e("target_os", "android"), + name_value_cfg_e("target_os", "linux"), + // Category 1: Target Features + name_value_cfg_e("target_feature", "sse2"), + // Category 2: Crate Features (Sorted Alphabetically: alloc -> sync) + name_value_cfg_e("feature", "alloc"), + name_value_cfg_e("feature", "sync"), + // Category 3: Nested logic pushed to the end + cfg_all_e(thin_vec![word_cfg_e("unix")]), + ]); + + assert_eq!(cfg, expected); + }); +} diff --git a/tests/rustdoc-gui/item-info-overflow.goml b/tests/rustdoc-gui/item-info-overflow.goml index b53ffb00f1c36..08ace7686e74f 100644 --- a/tests/rustdoc-gui/item-info-overflow.goml +++ b/tests/rustdoc-gui/item-info-overflow.goml @@ -8,7 +8,7 @@ assert-property: (".item-info", {"scrollWidth": "940"}) // Just to be sure we're comparing the correct "item-info": assert-text: ( ".item-info", - "Available on Android or Linux or Emscripten or DragonFly BSD or FreeBSD or NetBSD or OpenBSD", + "Available on Android or DragonFly BSD or Emscripten or FreeBSD or Linux or NetBSD or OpenBSD", STARTS_WITH, ) @@ -26,6 +26,6 @@ assert-property: ( // Just to be sure we're comparing the correct "item-info": assert-text: ( "#impl-SimpleTrait-for-LongItemInfo2 .item-info", - "Available on Android or Linux or Emscripten or DragonFly BSD or FreeBSD or NetBSD or OpenBSD", + "Available on Android or DragonFly BSD or Emscripten or FreeBSD or Linux or NetBSD or OpenBSD", STARTS_WITH, ) diff --git a/tests/rustdoc-gui/item-info.goml b/tests/rustdoc-gui/item-info.goml index 11388c79e0b80..b0cb6b5b9251b 100644 --- a/tests/rustdoc-gui/item-info.goml +++ b/tests/rustdoc-gui/item-info.goml @@ -19,8 +19,8 @@ store-position: ( "//*[@class='stab portability']//code[normalize-space()='Win32_System_Diagnostics']", {"x": second_line_x, "y": second_line_y}, ) -assert: |first_line_x| != |second_line_x| && |first_line_x| == 521 && |second_line_x| == 277 -assert: |first_line_y| != |second_line_y| && |first_line_y| == 676 && |second_line_y| == 699 +assert: |first_line_x| != |second_line_x| && |first_line_x| == 509 && |second_line_x| == 277 +assert: |first_line_y| == |second_line_y| && |first_line_y| == 699 // Now we ensure that they're not rendered on the same line. set-window-size: (1100, 800) diff --git a/tests/rustdoc-html/doc-cfg/all-targets.rs b/tests/rustdoc-html/doc-cfg/all-targets.rs index 5b61d6164ee56..605a27a7d8927 100644 --- a/tests/rustdoc-html/doc-cfg/all-targets.rs +++ b/tests/rustdoc-html/doc-cfg/all-targets.rs @@ -2,10 +2,10 @@ //@ has all_targets/fn.foo.html \ // '//*[@id="main-content"]/*[@class="item-info"]/*[@class="stab portability"]' \ -// 'Available on GNU or Catalyst or Managarm C Library or MSVC or musl or Newlib or \ -// Neutrino 7.0 or Neutrino 7.1 or Neutrino 7.1 with io-sock or Neutrino 8.0 or \ -// OpenHarmony or relibc or SGX or Simulator or WASIp1 or WASIp2 or WASIp3 or \ -// uClibc or V5 or target_env=fake_env only.' +// 'Available on target_env=fake_env or Catalyst or GNU or Managarm C Library \ +// or MSVC or musl or Neutrino 7.0 or Neutrino 7.1 or Neutrino 7.1 with io-sock \ +// or Neutrino 8.0 or Newlib or OpenHarmony or relibc or SGX or Simulator or \ +// uClibc or V5 or WASIp1 or WASIp2 or WASIp3 only.' #[doc(cfg(any( target_env = "gnu", target_env = "macabi", @@ -32,12 +32,12 @@ pub fn foo() {} //@ has all_targets/fn.bar.html \ // '//*[@id="main-content"]/*[@class="item-info"]/*[@class="stab portability"]' \ -// 'Available on AArch64 or AMD GPU or ARM or ARM64EC or AVR or BPF or C-SKY or \ -// Hexagon or LoongArch32 or LoongArch64 or Motorola 680x0 or MIPS or MIPS release \ -// 6 or MIPS-64 or MIPS-64 release 6 or MSP430 or NVidia GPU or PowerPC or \ -// PowerPC64 or RISC-V RV32 or RISC-V RV64 or s390x or SPARC or SPARC-64 or SPIR-V \ -// or WebAssembly or WebAssembly or x86 or x86-64 or Xtensa or \ -// target_arch=fake_arch only.' +// 'Available on target_arch=fake_arch or AArch64 or AMD GPU or ARM or \ +// ARM64EC or AVR or BPF or C-SKY or Hexagon or LoongArch32 or LoongArch64 \ +// or MIPS or MIPS release 6 or MIPS-64 or MIPS-64 release 6 or Motorola 680x0 \ +// or MSP430 or NVidia GPU or PowerPC or PowerPC64 or RISC-V RV32 or RISC-V RV64 \ +// or s390x or SPARC or SPARC-64 or SPIR-V or WebAssembly or WebAssembly or x86 \ +// or x86-64 or Xtensa only.' #[doc(cfg(any( target_arch = "aarch64", target_arch = "amdgpu", @@ -75,15 +75,16 @@ pub fn bar() {} //@ has all_targets/fn.baz.html \ // '//*[@id="main-content"]/*[@class="item-info"]/*[@class="stab portability"]' \ -// 'Available on AIX and AMD HSA and Android and CUDA and Cygwin and DragonFly \ -// BSD and Emscripten and ESP-IDF and FreeBSD and Fuchsia and Haiku and HelenOS \ -// and Hermit and Horizon and GNU/Hurd and illumos and iOS and L4Re and Linux \ -// and LynxOS-178 and macOS and Managarm and Motor OS and NetBSD and bare-metal \ -// and QNX Neutrino and NuttX and OpenBSD and Play Station Portable and Play \ -// Station 1 and QuRT and Redox OS and RTEMS OS and Solaris and SOLID ASP3 and \ -// TEEOS and Trusty and tvOS and UEFI and VEXos and visionOS and Play Station \ -// Vita and VxWorks and WASI and watchOS and Windows and Xous and zero knowledge \ -// Virtual Machine and target_os=unknown and target_os=fake_os only.' +// 'Available on target_os=fake_os and target_os=unknown and AIX and AMD HSA \ +// and Android and bare-metal and CUDA and Cygwin and DragonFly BSD and \ +// Emscripten and ESP-IDF and FreeBSD and Fuchsia and GNU/Hurd and Haiku \ +// and HelenOS and Hermit and Horizon and illumos and iOS and L4Re and Linux \ +// and LynxOS-178 and macOS and Managarm and Motor OS and NetBSD and NuttX \ +// and OpenBSD and Play Station 1 and Play Station Portable and Play Station Vita \ +// and QNX Neutrino and QuRT and Redox OS and RTEMS OS and Solaris and \ +// SOLID ASP3 and TEEOS and Trusty and tvOS and UEFI and VEXos and visionOS \ +// and VxWorks and WASI and watchOS and Windows and Xous and zero knowledge \ +// Virtual Machine only.' #[doc(cfg(all( target_os = "aix", target_os = "amdhsa", diff --git a/tests/rustdoc-html/doc-cfg/doc-cfg-simplification.rs b/tests/rustdoc-html/doc-cfg/doc-cfg-simplification.rs index ce70de289c623..d984b48ff9957 100644 --- a/tests/rustdoc-html/doc-cfg/doc-cfg-simplification.rs +++ b/tests/rustdoc-html/doc-cfg/doc-cfg-simplification.rs @@ -46,7 +46,7 @@ pub mod ratel { //@ has 'globuliferous/ratel/static.NUNCIATIVE.html' //@ count - '//*[@class="stab portability"]' 1 - //@ matches - '//*[@class="stab portability"]' 'crate features ratel and nunciative' + //@ matches - '//*[@class="stab portability"]' 'crate features nunciative and ratel' #[doc(cfg(feature = "nunciative"))] pub static NUNCIATIVE: () = (); @@ -80,7 +80,7 @@ pub mod ratel { //@ has 'globuliferous/ratel/enum.Cosmotellurian.html' //@ count - '//*[@class="stab portability"]' 10 - //@ matches - '//*[@class="stab portability"]' 'crate features ratel and cosmotellurian' + //@ matches - '//*[@class="stab portability"]' 'crate features cosmotellurian and ratel' //@ matches - '//*[@class="stab portability"]' 'crate feature biotaxy' //@ matches - '//*[@class="stab portability"]' 'crate feature xiphopagus' //@ matches - '//*[@class="stab portability"]' 'crate feature juxtapositive' @@ -158,7 +158,7 @@ pub mod ratel { //@ has 'globuliferous/ratel/trait.Aposiopesis.html' //@ count - '//*[@class="stab portability"]' 4 - //@ matches - '//*[@class="stab portability"]' 'crate features ratel and aposiopesis' + //@ matches - '//*[@class="stab portability"]' 'crate features aposiopesis and ratel' //@ matches - '//*[@class="stab portability"]' 'crate feature umbracious' //@ matches - '//*[@class="stab portability"]' 'crate feature uakari' //@ matches - '//*[@class="stab portability"]' 'crate feature rotograph' diff --git a/tests/rustdoc-html/doc-cfg/doc-cfg.rs b/tests/rustdoc-html/doc-cfg/doc-cfg.rs index ba2a8de5b29e5..730c7e41decb7 100644 --- a/tests/rustdoc-html/doc-cfg/doc-cfg.rs +++ b/tests/rustdoc-html/doc-cfg/doc-cfg.rs @@ -3,7 +3,7 @@ //@ has doc_cfg/struct.Portable.html //@ !has - '//*[@id="main-content"]/*[@class="item-info"]/*[@class="stab portability"]' '' //@ has - '//*[@id="method.unix_and_arm_only_function"]' 'fn unix_and_arm_only_function()' -//@ has - '//*[@class="stab portability"]' 'Available on Unix and ARM only.' +//@ has - '//*[@class="stab portability"]' 'Available on ARM and Unix only.' //@ has - '//*[@id="method.wasi_and_wasm32_only_function"]' 'fn wasi_and_wasm32_only_function()' //@ has - '//*[@class="stab portability"]' 'Available on WASI and WebAssembly only.' pub struct Portable; @@ -25,7 +25,7 @@ pub mod unix_only { //@ has doc_cfg/unix_only/trait.ArmOnly.html \ // '//*[@id="main-content"]/*[@class="item-info"]/*[@class="stab portability"]' \ - // 'Available on Unix and ARM only.' + // 'Available on ARM and Unix only.' //@ count - '//*[@class="stab portability"]' 1 #[doc(cfg(target_arch = "arm"))] pub trait ArmOnly { diff --git a/tests/rustdoc-html/doc-cfg/duplicate-cfg.rs b/tests/rustdoc-html/doc-cfg/duplicate-cfg.rs index 93f26ab944d34..3cd8b626558b7 100644 --- a/tests/rustdoc-html/doc-cfg/duplicate-cfg.rs +++ b/tests/rustdoc-html/doc-cfg/duplicate-cfg.rs @@ -23,11 +23,11 @@ pub mod bar { } //@ has 'foo/baz/index.html' -//@ has '-' '//*[@class="stab portability"]' 'Available on crate features sync and send only.' +//@ has '-' '//*[@class="stab portability"]' 'Available on crate features send and sync only.' #[doc(cfg(all(feature = "sync", feature = "send")))] pub mod baz { //@ has 'foo/baz/struct.Baz.html' - //@ has '-' '//*[@class="stab portability"]' 'Available on crate features sync and send only.' + //@ has '-' '//*[@class="stab portability"]' 'Available on crate features send and sync only.' #[doc(cfg(feature = "sync"))] pub struct Baz; } @@ -37,17 +37,17 @@ pub mod baz { #[doc(cfg(feature = "sync"))] pub mod qux { //@ has 'foo/qux/struct.Qux.html' - //@ has '-' '//*[@class="stab portability"]' 'Available on crate features sync and send only.' + //@ has '-' '//*[@class="stab portability"]' 'Available on crate features send and sync only.' #[doc(cfg(all(feature = "sync", feature = "send")))] pub struct Qux; } //@ has 'foo/quux/index.html' -//@ has '-' '//*[@class="stab portability"]' 'Available on crate feature sync and crate feature send and foo only.' +//@ has '-' '//*[@class="stab portability"]' 'Available on foo and crate feature send and crate feature sync only.' #[doc(cfg(all(feature = "sync", feature = "send", foo)))] pub mod quux { //@ has 'foo/quux/struct.Quux.html' - //@ has '-' '//*[@class="stab portability"]' 'Available on crate feature sync and crate feature send and foo and bar only.' + //@ has '-' '//*[@class="stab portability"]' 'Available on bar and foo and crate feature send and crate feature sync only.' #[doc(cfg(all(feature = "send", feature = "sync", bar)))] pub struct Quux; } diff --git a/tests/rustdoc-html/doc-cfg/sort.rs b/tests/rustdoc-html/doc-cfg/sort.rs new file mode 100644 index 0000000000000..3aa79b7836271 --- /dev/null +++ b/tests/rustdoc-html/doc-cfg/sort.rs @@ -0,0 +1,42 @@ +// Tests that `doc(cfg)` badges and auto-detected `cfg` badges are sorted deterministically. +//@ edition:2021 +//@ compile-flags: --cfg target_os="linux" + +#![crate_name = "foo"] +#![feature(doc_cfg)] + +// TEST 1: Explicit `#[doc(cfg(...))]` +// Tests that OS targets are sorted alphabetically. +//@ has 'foo/fn.foo.html' +//@ has - '//*[@class="stab portability"]' 'Available on Android or Apple or Cygwin \ +// or DragonFly BSD or FreeBSD or Linux or NetBSD or OpenBSD or QNX Neutrino only.' +#[doc(cfg(any( + target_os = "android", + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd", + target_os = "nto", + target_vendor = "apple", + target_os = "cygwin" +)))] +pub fn foo() {} + +// TEST 2: Implicit `#[cfg(...)]` via auto-detection +// Tests that targets are sorted alphabetically just like explicit `doc(cfg)`. +//@ has 'foo/fn.bar.html' +//@ has - '//*[@class="stab portability"]' 'Available on Android or Apple or Cygwin \ +// or DragonFly BSD or FreeBSD or Linux or NetBSD or OpenBSD or QNX Neutrino only.' +#[cfg(any( + target_os = "android", + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd", + target_os = "nto", + target_vendor = "apple", + target_os = "cygwin" +))] +pub fn bar() {} diff --git a/tests/rustdoc-html/inline_cross/doc-auto-cfg.rs b/tests/rustdoc-html/inline_cross/doc-auto-cfg.rs index 6e0f6e22f8c78..6c69dfe5ac8a9 100644 --- a/tests/rustdoc-html/inline_cross/doc-auto-cfg.rs +++ b/tests/rustdoc-html/inline_cross/doc-auto-cfg.rs @@ -37,11 +37,11 @@ pub mod pre { pub mod post { // issue: //@ has 'it/post/index.html' '//*[@class="stab portability"]' 'extra' - //@ has - '//*[@class="stab portability"]' 'extra and extension' + //@ has - '//*[@class="stab portability"]' 'extension and extra' //@ has 'it/post/struct.Type.html' '//*[@class="stab portability"]' \ // 'Available on crate feature extra only.' //@ has 'it/post/fn.compute.html' '//*[@class="stab portability"]' \ - // 'Available on crate feature extra and extension only.' + // 'Available on extension and crate feature extra only.' #[cfg(feature = "extra")] pub use doc_auto_cfg::*; diff --git a/tests/rustdoc-html/target-feature.rs b/tests/rustdoc-html/target-feature.rs index f2686f81fbff0..ce51735c77d5c 100644 --- a/tests/rustdoc-html/target-feature.rs +++ b/tests/rustdoc-html/target-feature.rs @@ -28,13 +28,13 @@ pub unsafe fn f2_not_safe() {} //@ has 'foo/fn.f3_multifeatures_in_attr.html' //@ has - '//*[@id="main-content"]/*[@class="item-info"]/*[@class="stab portability"]' \ -// 'Available on target features popcnt and avx2 only.' +// 'Available on target features avx2 and popcnt only.' #[target_feature(enable = "popcnt", enable = "avx2")] pub fn f3_multifeatures_in_attr() {} //@ has 'foo/fn.f4_multi_attrs.html' //@ has - '//*[@id="main-content"]/*[@class="item-info"]/*[@class="stab portability"]' \ -// 'Available on target features popcnt and avx2 only.' +// 'Available on target features avx2 and popcnt only.' #[target_feature(enable = "popcnt")] #[target_feature(enable = "avx2")] pub fn f4_multi_attrs() {}