about summary refs log tree commit diff
diff options
context:
space:
mode:
authorbors <bors@rust-lang.org>2023-04-14 15:06:51 +0000
committerbors <bors@rust-lang.org>2023-04-14 15:06:51 +0000
commit660c966ff941ddf995d3251df32508b383cd4cee (patch)
treea3fe0d1d5a9da6629ff5a3a4f18af694eb4d5d32
parentb0884a3528c45a5d575e182f407c759d243fdcba (diff)
parent425913367b79680068d7cb358b7609ea2be0da14 (diff)
downloadrust-660c966ff941ddf995d3251df32508b383cd4cee.tar.gz
rust-660c966ff941ddf995d3251df32508b383cd4cee.zip
Auto merge of #110324 - JohnTitor:rollup-jq31pd1, r=JohnTitor
Rollup of 7 pull requests

Successful merges:

 - #103682 (Stabilize rustdoc `--test-run-directory`)
 - #106249 (Create "suggested tests" tool in `rustbuild`)
 - #110047 (Add link to `collections` docs to `extend` trait)
 - #110269 (Add `tidy-alphabetical` to features in `core`)
 - #110292 (Add `tidy-alphabetical` to features in `alloc` & `std`)
 - #110305 (rustdoc-search: use ES6 `Map` and `Set` where they make sense)
 - #110315 (Add a stable MIR way to get the main function)

Failed merges:

r? `@ghost`
`@rustbot` modify labels: rollup
-rw-r--r--Cargo.lock13
-rw-r--r--Cargo.toml1
-rw-r--r--compiler/rustc_smir/src/rustc_smir/mod.rs4
-rw-r--r--compiler/rustc_smir/src/stable_mir/mod.rs7
-rw-r--r--library/alloc/src/lib.rs46
-rw-r--r--library/core/src/lib.rs50
-rw-r--r--library/std/src/collections/mod.rs7
-rw-r--r--library/std/src/lib.rs26
-rw-r--r--src/bootstrap/builder.rs22
-rw-r--r--src/bootstrap/config.rs24
-rw-r--r--src/bootstrap/flags.rs14
-rw-r--r--src/bootstrap/lib.rs19
-rw-r--r--src/bootstrap/suggest.rs80
-rw-r--r--src/bootstrap/test.rs36
-rw-r--r--src/bootstrap/tool.rs1
-rw-r--r--src/doc/rustdoc/src/command-line-arguments.md15
-rw-r--r--src/doc/rustdoc/src/write-documentation/documentation-tests.md12
-rw-r--r--src/librustdoc/html/static/js/externs.js7
-rw-r--r--src/librustdoc/html/static/js/search.js124
-rw-r--r--src/librustdoc/lib.rs2
-rw-r--r--src/tools/suggest-tests/Cargo.toml9
-rw-r--r--src/tools/suggest-tests/src/dynamic_suggestions.rs23
-rw-r--r--src/tools/suggest-tests/src/lib.rs96
-rw-r--r--src/tools/suggest-tests/src/main.rs27
-rw-r--r--src/tools/suggest-tests/src/static_suggestions.rs24
-rw-r--r--src/tools/suggest-tests/src/tests.rs21
-rw-r--r--tests/rustdoc-ui/run-directory.rs4
-rw-r--r--tests/ui-fulldeps/stable-mir/crate-info.rs2
28 files changed, 577 insertions, 139 deletions
diff --git a/Cargo.lock b/Cargo.lock
index 3f617af19b3..12be36ef861 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -3451,9 +3451,9 @@ dependencies = [
 
 [[package]]
 name = "once_cell"
-version = "1.16.0"
+version = "1.17.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "86f0b0d4bf799edbc74508c1e8bf170ff5f41238e5f8225603ca7caaae2b7860"
+checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3"
 
 [[package]]
 name = "opener"
@@ -6102,6 +6102,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601"
 
 [[package]]
+name = "suggest-tests"
+version = "0.1.0"
+dependencies = [
+ "build_helper",
+ "glob",
+ "once_cell",
+]
+
+[[package]]
 name = "syn"
 version = "1.0.102"
 source = "registry+https://github.com/rust-lang/crates.io-index"
diff --git a/Cargo.toml b/Cargo.toml
index 15cbb2659c9..1fcaaf6ddc4 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -44,6 +44,7 @@ members = [
   "src/tools/lld-wrapper",
   "src/tools/collect-license-metadata",
   "src/tools/generate-copyright",
+  "src/tools/suggest-tests",
 ]
 
 exclude = [
diff --git a/compiler/rustc_smir/src/rustc_smir/mod.rs b/compiler/rustc_smir/src/rustc_smir/mod.rs
index 0befff894ef..4dad3c6bce7 100644
--- a/compiler/rustc_smir/src/rustc_smir/mod.rs
+++ b/compiler/rustc_smir/src/rustc_smir/mod.rs
@@ -40,6 +40,10 @@ pub fn all_local_items() -> stable_mir::CrateItems {
     with(|tcx| tcx.mir_keys(()).iter().map(|item| crate_item(item.to_def_id())).collect())
 }
 
+pub fn entry_fn() -> Option<stable_mir::CrateItem> {
+    with(|tcx| Some(crate_item(tcx.entry_fn(())?.0)))
+}
+
 /// Build a stable mir crate from a given crate number.
 fn smir_crate(tcx: TyCtxt<'_>, crate_num: CrateNum) -> stable_mir::Crate {
     let crate_name = tcx.crate_name(crate_num).to_string();
diff --git a/compiler/rustc_smir/src/stable_mir/mod.rs b/compiler/rustc_smir/src/stable_mir/mod.rs
index ba23186224a..1d2efb5eab9 100644
--- a/compiler/rustc_smir/src/stable_mir/mod.rs
+++ b/compiler/rustc_smir/src/stable_mir/mod.rs
@@ -45,6 +45,13 @@ impl CrateItem {
     }
 }
 
+/// Return the function where execution starts if the current
+/// crate defines that. This is usually `main`, but could be
+/// `start` if the crate is a no-std crate.
+pub fn entry_fn() -> Option<CrateItem> {
+    crate::rustc_smir::entry_fn()
+}
+
 /// Access to the local crate.
 pub fn local_crate() -> Crate {
     crate::rustc_smir::local_crate()
diff --git a/library/alloc/src/lib.rs b/library/alloc/src/lib.rs
index 55e18b04956..aa240c37e84 100644
--- a/library/alloc/src/lib.rs
+++ b/library/alloc/src/lib.rs
@@ -90,6 +90,11 @@
 #![warn(multiple_supertrait_upcastable)]
 //
 // Library features:
+// tidy-alphabetical-start
+#![cfg_attr(not(no_global_oom_handling), feature(const_alloc_error))]
+#![cfg_attr(not(no_global_oom_handling), feature(const_btree_len))]
+#![cfg_attr(test, feature(is_sorted))]
+#![cfg_attr(test, feature(new_uninit))]
 #![feature(alloc_layout_extra)]
 #![feature(allocator_api)]
 #![feature(array_chunks)]
@@ -99,23 +104,21 @@
 #![feature(assert_matches)]
 #![feature(async_iterator)]
 #![feature(coerce_unsized)]
-#![cfg_attr(not(no_global_oom_handling), feature(const_alloc_error))]
+#![feature(const_align_of_val)]
 #![feature(const_box)]
-#![cfg_attr(not(no_global_oom_handling), feature(const_btree_len))]
-#![feature(const_cow_is_borrowed)]
 #![feature(const_convert)]
-#![feature(const_size_of_val)]
-#![feature(const_align_of_val)]
-#![feature(const_ptr_read)]
-#![feature(const_maybe_uninit_zeroed)]
-#![feature(const_maybe_uninit_write)]
+#![feature(const_cow_is_borrowed)]
+#![feature(const_eval_select)]
 #![feature(const_maybe_uninit_as_mut_ptr)]
+#![feature(const_maybe_uninit_write)]
+#![feature(const_maybe_uninit_zeroed)]
+#![feature(const_pin)]
+#![feature(const_ptr_read)]
 #![feature(const_refs_to_cell)]
+#![feature(const_size_of_val)]
+#![feature(const_waker)]
 #![feature(core_intrinsics)]
 #![feature(core_panic)]
-#![feature(const_eval_select)]
-#![feature(const_pin)]
-#![feature(const_waker)]
 #![feature(dispatch_from_dyn)]
 #![feature(error_generic_member_access)]
 #![feature(error_in_core)]
@@ -126,7 +129,6 @@
 #![feature(hasher_prefixfree_extras)]
 #![feature(inline_const)]
 #![feature(inplace_iteration)]
-#![cfg_attr(test, feature(is_sorted))]
 #![feature(iter_advance_by)]
 #![feature(iter_next_chunk)]
 #![feature(iter_repeat_n)]
@@ -134,7 +136,6 @@
 #![feature(maybe_uninit_slice)]
 #![feature(maybe_uninit_uninit_array)]
 #![feature(maybe_uninit_uninit_array_transpose)]
-#![cfg_attr(test, feature(new_uninit))]
 #![feature(pattern)]
 #![feature(pointer_byte_offsets)]
 #![feature(provide_any)]
@@ -150,6 +151,7 @@
 #![feature(slice_ptr_get)]
 #![feature(slice_ptr_len)]
 #![feature(slice_range)]
+#![feature(std_internals)]
 #![feature(str_internals)]
 #![feature(strict_provenance)]
 #![feature(trusted_len)]
@@ -160,41 +162,43 @@
 #![feature(unicode_internals)]
 #![feature(unsize)]
 #![feature(utf8_chunks)]
-#![feature(std_internals)]
+// tidy-alphabetical-end
 //
 // Language features:
+// tidy-alphabetical-start
+#![cfg_attr(not(test), feature(generator_trait))]
+#![cfg_attr(test, feature(panic_update_hook))]
+#![cfg_attr(test, feature(test))]
 #![feature(allocator_internals)]
 #![feature(allow_internal_unstable)]
 #![feature(associated_type_bounds)]
+#![feature(c_unwind)]
 #![feature(cfg_sanitize)]
 #![feature(const_deref)]
 #![feature(const_mut_refs)]
-#![feature(const_ptr_write)]
 #![feature(const_precise_live_drops)]
+#![feature(const_ptr_write)]
 #![feature(const_trait_impl)]
 #![feature(const_try)]
 #![feature(dropck_eyepatch)]
 #![feature(exclusive_range_pattern)]
 #![feature(fundamental)]
-#![cfg_attr(not(test), feature(generator_trait))]
 #![feature(hashmap_internals)]
 #![feature(lang_items)]
 #![feature(min_specialization)]
+#![feature(multiple_supertrait_upcastable)]
 #![feature(negative_impls)]
 #![feature(never_type)]
+#![feature(pointer_is_aligned)]
 #![feature(rustc_allow_const_fn_unstable)]
 #![feature(rustc_attrs)]
-#![feature(pointer_is_aligned)]
 #![feature(slice_internals)]
 #![feature(staged_api)]
 #![feature(stmt_expr_attributes)]
-#![cfg_attr(test, feature(test))]
 #![feature(unboxed_closures)]
 #![feature(unsized_fn_params)]
-#![feature(c_unwind)]
 #![feature(with_negative_coherence)]
-#![cfg_attr(test, feature(panic_update_hook))]
-#![feature(multiple_supertrait_upcastable)]
+// tidy-alphabetical-end
 //
 // Rustdoc features:
 #![feature(doc_cfg)]
diff --git a/library/core/src/lib.rs b/library/core/src/lib.rs
index 4fd5a4bfc65..04243544b83 100644
--- a/library/core/src/lib.rs
+++ b/library/core/src/lib.rs
@@ -98,11 +98,14 @@
 #![warn(multiple_supertrait_upcastable)]
 //
 // Library features:
-#![feature(const_align_offset)]
+// tidy-alphabetical-start
+#![feature(char_indices_offset)]
 #![feature(const_align_of_val)]
 #![feature(const_align_of_val_raw)]
+#![feature(const_align_offset)]
 #![feature(const_alloc_layout)]
 #![feature(const_arguments_as_str)]
+#![feature(const_array_from_ref)]
 #![feature(const_array_into_iter_constructors)]
 #![feature(const_bigint_helper_methods)]
 #![feature(const_black_box)]
@@ -111,6 +114,9 @@
 #![feature(const_char_from_u32_unchecked)]
 #![feature(const_clone)]
 #![feature(const_cmp)]
+#![feature(const_convert)]
+#![feature(const_cstr_methods)]
+#![feature(const_default_impls)]
 #![feature(const_discriminant)]
 #![feature(const_eval_select)]
 #![feature(const_exact_div)]
@@ -119,17 +125,17 @@
 #![feature(const_fmt_arguments_new)]
 #![feature(const_hash)]
 #![feature(const_heap)]
-#![feature(const_convert)]
 #![feature(const_index_range_slice_index)]
 #![feature(const_inherent_unchecked_arith)]
 #![feature(const_int_unchecked_arith)]
 #![feature(const_intrinsic_forget)]
 #![feature(const_ipv4)]
 #![feature(const_ipv6)]
+#![feature(const_is_char_boundary)]
 #![feature(const_likely)]
-#![feature(const_maybe_uninit_uninit_array)]
 #![feature(const_maybe_uninit_as_mut_ptr)]
 #![feature(const_maybe_uninit_assume_init)]
+#![feature(const_maybe_uninit_uninit_array)]
 #![feature(const_nonnull_new)]
 #![feature(const_num_from_num)]
 #![feature(const_ops)]
@@ -138,32 +144,35 @@
 #![feature(const_pin)]
 #![feature(const_pointer_byte_offsets)]
 #![feature(const_pointer_is_aligned)]
-#![feature(const_ptr_sub_ptr)]
-#![feature(const_replace)]
-#![feature(const_result_drop)]
 #![feature(const_ptr_as_ref)]
 #![feature(const_ptr_is_null)]
 #![feature(const_ptr_read)]
+#![feature(const_ptr_sub_ptr)]
 #![feature(const_ptr_write)]
 #![feature(const_raw_ptr_comparison)]
+#![feature(const_replace)]
+#![feature(const_result_drop)]
 #![feature(const_size_of_val)]
 #![feature(const_size_of_val_raw)]
 #![feature(const_slice_from_raw_parts_mut)]
+#![feature(const_slice_from_ref)]
+#![feature(const_slice_index)]
 #![feature(const_slice_ptr_len)]
 #![feature(const_slice_split_at_mut)]
 #![feature(const_str_from_utf8_unchecked_mut)]
 #![feature(const_swap)]
 #![feature(const_trait_impl)]
+#![feature(const_transmute_copy)]
 #![feature(const_try)]
 #![feature(const_type_id)]
 #![feature(const_type_name)]
-#![feature(const_default_impls)]
 #![feature(const_unicode_case_lookup)]
 #![feature(const_unsafecell_get_mut)]
 #![feature(const_waker)]
 #![feature(core_panic)]
-#![feature(char_indices_offset)]
 #![feature(duration_consts_float)]
+#![feature(ip)]
+#![feature(is_ascii_octdigit)]
 #![feature(maybe_uninit_uninit_array)]
 #![feature(ptr_alignment_type)]
 #![feature(ptr_metadata)]
@@ -171,25 +180,21 @@
 #![feature(slice_ptr_get)]
 #![feature(slice_split_at_unchecked)]
 #![feature(str_internals)]
-#![feature(str_split_remainder)]
 #![feature(str_split_inclusive_remainder)]
+#![feature(str_split_remainder)]
 #![feature(strict_provenance)]
 #![feature(utf16_extra)]
 #![feature(utf16_extra_const)]
 #![feature(variant_count)]
-#![feature(const_array_from_ref)]
-#![feature(const_slice_from_ref)]
-#![feature(const_slice_index)]
-#![feature(const_is_char_boundary)]
-#![feature(const_cstr_methods)]
-#![feature(ip)]
-#![feature(is_ascii_octdigit)]
+// tidy-alphabetical-end
 //
 // Language features:
+// tidy-alphabetical-start
 #![feature(abi_unadjusted)]
 #![feature(adt_const_params)]
 #![feature(allow_internal_unsafe)]
 #![feature(allow_internal_unstable)]
+#![feature(asm_const)]
 #![feature(associated_type_bounds)]
 #![feature(auto_traits)]
 #![feature(c_unwind)]
@@ -206,13 +211,12 @@
 #![feature(deprecated_suggestion)]
 #![feature(derive_const)]
 #![feature(doc_cfg)]
+#![feature(doc_cfg_hide)]
 #![feature(doc_notable_trait)]
-#![feature(generic_arg_infer)]
-#![feature(rustdoc_internals)]
 #![feature(exhaustive_patterns)]
-#![feature(doc_cfg_hide)]
 #![feature(extern_types)]
 #![feature(fundamental)]
+#![feature(generic_arg_infer)]
 #![feature(if_let_guard)]
 #![feature(inline_const)]
 #![feature(intra_doc_pointers)]
@@ -221,6 +225,7 @@
 #![feature(link_llvm_intrinsics)]
 #![feature(macro_metavar_expr)]
 #![feature(min_specialization)]
+#![feature(multiple_supertrait_upcastable)]
 #![feature(must_not_suspend)]
 #![feature(negative_impls)]
 #![feature(never_type)]
@@ -231,6 +236,7 @@
 #![feature(repr_simd)]
 #![feature(rustc_allow_const_fn_unstable)]
 #![feature(rustc_attrs)]
+#![feature(rustdoc_internals)]
 #![feature(simd_ffi)]
 #![feature(staged_api)]
 #![feature(stmt_expr_attributes)]
@@ -240,11 +246,10 @@
 #![feature(try_blocks)]
 #![feature(unboxed_closures)]
 #![feature(unsized_fn_params)]
-#![feature(asm_const)]
-#![feature(const_transmute_copy)]
-#![feature(multiple_supertrait_upcastable)]
+// tidy-alphabetical-end
 //
 // Target features:
+// tidy-alphabetical-start
 #![feature(arm_target_feature)]
 #![feature(avx512_target_feature)]
 #![feature(hexagon_target_feature)]
@@ -255,6 +260,7 @@
 #![feature(sse4a_target_feature)]
 #![feature(tbm_target_feature)]
 #![feature(wasm_target_feature)]
+// tidy-alphabetical-end
 
 // allow using `core::` in intra-doc links
 #[allow(unused_extern_crates)]
diff --git a/library/std/src/collections/mod.rs b/library/std/src/collections/mod.rs
index 575f56ff4df..23ed577ea60 100644
--- a/library/std/src/collections/mod.rs
+++ b/library/std/src/collections/mod.rs
@@ -172,7 +172,8 @@
 //!
 //! ## Iterators
 //!
-//! Iterators are a powerful and robust mechanism used throughout Rust's
+//! [Iterators][crate::iter]
+//! are a powerful and robust mechanism used throughout Rust's
 //! standard libraries. Iterators provide a sequence of values in a generic,
 //! safe, efficient and convenient way. The contents of an iterator are usually
 //! *lazily* evaluated, so that only the values that are actually needed are
@@ -252,7 +253,9 @@
 //!
 //! Several other collection methods also return iterators to yield a sequence
 //! of results but avoid allocating an entire collection to store the result in.
-//! This provides maximum flexibility as `collect` or `extend` can be called to
+//! This provides maximum flexibility as
+//! [`collect`][crate::iter::Iterator::collect] or
+//! [`extend`][crate::iter::Extend::extend] can be called to
 //! "pipe" the sequence into any collection if desired. Otherwise, the sequence
 //! can be looped over with a `for` loop. The iterator can also be discarded
 //! after partial use, preventing the computation of the unused items.
diff --git a/library/std/src/lib.rs b/library/std/src/lib.rs
index 71f3576c93d..98fcc76aa98 100644
--- a/library/std/src/lib.rs
+++ b/library/std/src/lib.rs
@@ -235,6 +235,7 @@
 #![cfg_attr(windows, feature(round_char_boundary))]
 //
 // Language features:
+// tidy-alphabetical-start
 #![feature(alloc_error_handler)]
 #![feature(allocator_internals)]
 #![feature(allow_internal_unsafe)]
@@ -256,8 +257,8 @@
 #![feature(intra_doc_pointers)]
 #![feature(lang_items)]
 #![feature(let_chains)]
-#![feature(linkage)]
 #![feature(link_cfg)]
+#![feature(linkage)]
 #![feature(min_specialization)]
 #![feature(must_not_suspend)]
 #![feature(needs_panic_runtime)]
@@ -271,8 +272,10 @@
 #![feature(thread_local)]
 #![feature(try_blocks)]
 #![feature(utf8_chunks)]
+// tidy-alphabetical-end
 //
 // Library features (core):
+// tidy-alphabetical-start
 #![feature(char_internals)]
 #![feature(core_intrinsics)]
 #![feature(duration_constants)]
@@ -289,6 +292,7 @@
 #![feature(ip)]
 #![feature(ip_in_core)]
 #![feature(maybe_uninit_slice)]
+#![feature(maybe_uninit_uninit_array)]
 #![feature(maybe_uninit_write_slice)]
 #![feature(panic_can_unwind)]
 #![feature(panic_info_message)]
@@ -306,25 +310,28 @@
 #![feature(std_internals)]
 #![feature(str_internals)]
 #![feature(strict_provenance)]
-#![feature(maybe_uninit_uninit_array)]
-#![feature(const_maybe_uninit_uninit_array)]
-#![feature(const_waker)]
+// tidy-alphabetical-end
 //
 // Library features (alloc):
+// tidy-alphabetical-start
 #![feature(alloc_layout_extra)]
 #![feature(allocator_api)]
 #![feature(get_mut_unchecked)]
 #![feature(map_try_insert)]
 #![feature(new_uninit)]
+#![feature(slice_concat_trait)]
 #![feature(thin_box)]
 #![feature(try_reserve_kind)]
 #![feature(vec_into_raw_parts)]
-#![feature(slice_concat_trait)]
+// tidy-alphabetical-end
 //
 // Library features (unwind):
+// tidy-alphabetical-start
 #![feature(panic_unwind)]
+// tidy-alphabetical-end
 //
 // Only for re-exporting:
+// tidy-alphabetical-start
 #![feature(assert_matches)]
 #![feature(async_iterator)]
 #![feature(c_variadic)]
@@ -336,24 +343,29 @@
 #![feature(custom_test_frameworks)]
 #![feature(edition_panic)]
 #![feature(format_args_nl)]
-#![feature(log_syntax)]
+#![feature(get_many_mut)]
 #![feature(lazy_cell)]
+#![feature(log_syntax)]
 #![feature(saturating_int_impl)]
 #![feature(stdsimd)]
 #![feature(test)]
 #![feature(trace_macros)]
-#![feature(get_many_mut)]
+// tidy-alphabetical-end
 //
 // Only used in tests/benchmarks:
 //
 // Only for const-ness:
+// tidy-alphabetical-start
 #![feature(const_collections_with_hasher)]
 #![feature(const_hash)]
 #![feature(const_io_structs)]
 #![feature(const_ip)]
 #![feature(const_ipv4)]
 #![feature(const_ipv6)]
+#![feature(const_maybe_uninit_uninit_array)]
+#![feature(const_waker)]
 #![feature(thread_local_internals)]
+// tidy-alphabetical-end
 //
 #![default_lib_allocator]
 
diff --git a/src/bootstrap/builder.rs b/src/bootstrap/builder.rs
index ade8fa4c74d..e959ea06f8b 100644
--- a/src/bootstrap/builder.rs
+++ b/src/bootstrap/builder.rs
@@ -591,6 +591,7 @@ pub enum Kind {
     Install,
     Run,
     Setup,
+    Suggest,
 }
 
 impl Kind {
@@ -610,6 +611,7 @@ impl Kind {
             "install" => Kind::Install,
             "run" | "r" => Kind::Run,
             "setup" => Kind::Setup,
+            "suggest" => Kind::Suggest,
             _ => return None,
         })
     }
@@ -629,6 +631,7 @@ impl Kind {
             Kind::Install => "install",
             Kind::Run => "run",
             Kind::Setup => "setup",
+            Kind::Suggest => "suggest",
         }
     }
 }
@@ -709,6 +712,7 @@ impl<'a> Builder<'a> {
                 test::CrateRustdoc,
                 test::CrateRustdocJsonTypes,
                 test::CrateJsonDocLint,
+                test::SuggestTestsCrate,
                 test::Linkcheck,
                 test::TierCheck,
                 test::ReplacePlaceholderTest,
@@ -827,7 +831,7 @@ impl<'a> Builder<'a> {
             Kind::Setup => describe!(setup::Profile, setup::Hook, setup::Link, setup::Vscode),
             Kind::Clean => describe!(clean::CleanAll, clean::Rustc, clean::Std),
             // special-cased in Build::build()
-            Kind::Format => vec![],
+            Kind::Format | Kind::Suggest => vec![],
         }
     }
 
@@ -891,6 +895,7 @@ impl<'a> Builder<'a> {
             Subcommand::Run { ref paths, .. } => (Kind::Run, &paths[..]),
             Subcommand::Clean { ref paths, .. } => (Kind::Clean, &paths[..]),
             Subcommand::Format { .. } => (Kind::Format, &[][..]),
+            Subcommand::Suggest { .. } => (Kind::Suggest, &[][..]),
             Subcommand::Setup { profile: ref path } => (
                 Kind::Setup,
                 path.as_ref().map_or([].as_slice(), |path| std::slice::from_ref(path)),
@@ -900,6 +905,21 @@ impl<'a> Builder<'a> {
         Self::new_internal(build, kind, paths.to_owned())
     }
 
+    /// Creates a new standalone builder for use outside of the normal process
+    pub fn new_standalone(
+        build: &mut Build,
+        kind: Kind,
+        paths: Vec<PathBuf>,
+        stage: Option<u32>,
+    ) -> Builder<'_> {
+        // FIXME: don't mutate `build`
+        if let Some(stage) = stage {
+            build.config.stage = stage;
+        }
+
+        Self::new_internal(build, kind, paths.to_owned())
+    }
+
     pub fn execute_cli(&self) {
         self.run_step_descriptions(&Builder::get_step_descriptions(self.kind), &self.paths);
     }
diff --git a/src/bootstrap/config.rs b/src/bootstrap/config.rs
index dd65dc91c0c..cc3b3bc25f3 100644
--- a/src/bootstrap/config.rs
+++ b/src/bootstrap/config.rs
@@ -56,8 +56,7 @@ pub enum DryRun {
 /// filled out from the decoded forms of the structs below. For documentation
 /// each field, see the corresponding fields in
 /// `config.example.toml`.
-#[derive(Default)]
-#[cfg_attr(test, derive(Clone))]
+#[derive(Default, Clone)]
 pub struct Config {
     pub changelog_seen: Option<usize>,
     pub ccache: Option<String>,
@@ -240,23 +239,20 @@ pub struct Config {
     pub initial_rustfmt: RefCell<RustfmtState>,
 }
 
-#[derive(Default, Deserialize)]
-#[cfg_attr(test, derive(Clone))]
+#[derive(Default, Deserialize, Clone)]
 pub struct Stage0Metadata {
     pub compiler: CompilerMetadata,
     pub config: Stage0Config,
     pub checksums_sha256: HashMap<String, String>,
     pub rustfmt: Option<RustfmtMetadata>,
 }
-#[derive(Default, Deserialize)]
-#[cfg_attr(test, derive(Clone))]
+#[derive(Default, Deserialize, Clone)]
 pub struct CompilerMetadata {
     pub date: String,
     pub version: String,
 }
 
-#[derive(Default, Deserialize)]
-#[cfg_attr(test, derive(Clone))]
+#[derive(Default, Deserialize, Clone)]
 pub struct Stage0Config {
     pub dist_server: String,
     pub artifacts_server: String,
@@ -264,8 +260,7 @@ pub struct Stage0Config {
     pub git_merge_commit_email: String,
     pub nightly_branch: String,
 }
-#[derive(Default, Deserialize)]
-#[cfg_attr(test, derive(Clone))]
+#[derive(Default, Deserialize, Clone)]
 pub struct RustfmtMetadata {
     pub date: String,
     pub version: String,
@@ -443,8 +438,7 @@ impl PartialEq<&str> for TargetSelection {
 }
 
 /// Per-target configuration stored in the global configuration structure.
-#[derive(Default)]
-#[cfg_attr(test, derive(Clone))]
+#[derive(Default, Clone)]
 pub struct Target {
     /// Some(path to llvm-config) if using an external LLVM.
     pub llvm_config: Option<PathBuf>,
@@ -1396,7 +1390,8 @@ impl Config {
             | Subcommand::Fix { .. }
             | Subcommand::Run { .. }
             | Subcommand::Setup { .. }
-            | Subcommand::Format { .. } => flags.stage.unwrap_or(0),
+            | Subcommand::Format { .. }
+            | Subcommand::Suggest { .. } => flags.stage.unwrap_or(0),
         };
 
         // CI should always run stage 2 builds, unless it specifically states otherwise
@@ -1421,7 +1416,8 @@ impl Config {
                 | Subcommand::Fix { .. }
                 | Subcommand::Run { .. }
                 | Subcommand::Setup { .. }
-                | Subcommand::Format { .. } => {}
+                | Subcommand::Format { .. }
+                | Subcommand::Suggest { .. } => {}
             }
         }
 
diff --git a/src/bootstrap/flags.rs b/src/bootstrap/flags.rs
index 2b0b772a618..b6f5f310398 100644
--- a/src/bootstrap/flags.rs
+++ b/src/bootstrap/flags.rs
@@ -84,8 +84,7 @@ pub struct Flags {
     pub free_args: Option<Vec<String>>,
 }
 
-#[derive(Debug)]
-#[cfg_attr(test, derive(Clone))]
+#[derive(Debug, Clone)]
 pub enum Subcommand {
     Build {
         paths: Vec<PathBuf>,
@@ -149,6 +148,9 @@ pub enum Subcommand {
     Setup {
         profile: Option<PathBuf>,
     },
+    Suggest {
+        run: bool,
+    },
 }
 
 impl Default for Subcommand {
@@ -183,6 +185,7 @@ Subcommands:
     install     Install distribution artifacts
     run, r      Run tools contained in this repository
     setup       Create a config.toml (making it easier to use `x.py` itself)
+    suggest     Suggest a subset of tests to run, based on modified files
 
 To learn more about a subcommand, run `./x.py <subcommand> -h`",
         );
@@ -349,6 +352,9 @@ To learn more about a subcommand, run `./x.py <subcommand> -h`",
             Kind::Run => {
                 opts.optmulti("", "args", "arguments for the tool", "ARGS");
             }
+            Kind::Suggest => {
+                opts.optflag("", "run", "run suggested tests");
+            }
             _ => {}
         };
 
@@ -565,7 +571,7 @@ Arguments:
                     Profile::all_for_help("        ").trim_end()
                 ));
             }
-            Kind::Bench | Kind::Clean | Kind::Dist | Kind::Install => {}
+            Kind::Bench | Kind::Clean | Kind::Dist | Kind::Install | Kind::Suggest => {}
         };
         // Get any optional paths which occur after the subcommand
         let mut paths = matches.free[1..].iter().map(|p| p.into()).collect::<Vec<PathBuf>>();
@@ -626,6 +632,7 @@ Arguments:
             Kind::Format => Subcommand::Format { check: matches.opt_present("check"), paths },
             Kind::Dist => Subcommand::Dist { paths },
             Kind::Install => Subcommand::Install { paths },
+            Kind::Suggest => Subcommand::Suggest { run: matches.opt_present("run") },
             Kind::Run => {
                 if paths.is_empty() {
                     println!("\nrun requires at least a path!\n");
@@ -734,6 +741,7 @@ impl Subcommand {
             Subcommand::Install { .. } => Kind::Install,
             Subcommand::Run { .. } => Kind::Run,
             Subcommand::Setup { .. } => Kind::Setup,
+            Subcommand::Suggest { .. } => Kind::Suggest,
         }
     }
 
diff --git a/src/bootstrap/lib.rs b/src/bootstrap/lib.rs
index eaa3afa4b7b..1ecb52e75f1 100644
--- a/src/bootstrap/lib.rs
+++ b/src/bootstrap/lib.rs
@@ -58,6 +58,7 @@ mod render_tests;
 mod run;
 mod sanity;
 mod setup;
+mod suggest;
 mod tarball;
 mod test;
 mod tool;
@@ -190,6 +191,7 @@ pub enum GitRepo {
 /// although most functions are implemented as free functions rather than
 /// methods specifically on this structure itself (to make it easier to
 /// organize).
+#[cfg_attr(not(feature = "build-metrics"), derive(Clone))]
 pub struct Build {
     /// User-specified configuration from `config.toml`.
     config: Config,
@@ -243,7 +245,7 @@ pub struct Build {
     metrics: metrics::BuildMetrics,
 }
 
-#[derive(Debug)]
+#[derive(Debug, Clone)]
 struct Crate {
     name: Interned<String>,
     deps: HashSet<Interned<String>>,
@@ -657,13 +659,20 @@ impl Build {
             job::setup(self);
         }
 
-        if let Subcommand::Format { check, paths } = &self.config.cmd {
-            return format::format(&builder::Builder::new(&self), *check, &paths);
-        }
-
         // Download rustfmt early so that it can be used in rust-analyzer configs.
         let _ = &builder::Builder::new(&self).initial_rustfmt();
 
+        // hardcoded subcommands
+        match &self.config.cmd {
+            Subcommand::Format { check, paths } => {
+                return format::format(&builder::Builder::new(&self), *check, &paths);
+            }
+            Subcommand::Suggest { run } => {
+                return suggest::suggest(&builder::Builder::new(&self), *run);
+            }
+            _ => (),
+        }
+
         {
             let builder = builder::Builder::new(&self);
             if let Some(path) = builder.paths.get(0) {
diff --git a/src/bootstrap/suggest.rs b/src/bootstrap/suggest.rs
new file mode 100644
index 00000000000..ff20ebec267
--- /dev/null
+++ b/src/bootstrap/suggest.rs
@@ -0,0 +1,80 @@
+#![cfg_attr(feature = "build-metrics", allow(unused))]
+
+use std::str::FromStr;
+
+use std::path::PathBuf;
+
+use crate::{
+    builder::{Builder, Kind},
+    tool::Tool,
+};
+
+#[cfg(feature = "build-metrics")]
+pub fn suggest(builder: &Builder<'_>, run: bool) {
+    panic!("`x suggest` is not supported with `build-metrics`")
+}
+
+/// Suggests a list of possible `x.py` commands to run based on modified files in branch.
+#[cfg(not(feature = "build-metrics"))]
+pub fn suggest(builder: &Builder<'_>, run: bool) {
+    let suggestions =
+        builder.tool_cmd(Tool::SuggestTests).output().expect("failed to run `suggest-tests` tool");
+
+    if !suggestions.status.success() {
+        println!("failed to run `suggest-tests` tool ({})", suggestions.status);
+        println!(
+            "`suggest_tests` stdout:\n{}`suggest_tests` stderr:\n{}",
+            String::from_utf8(suggestions.stdout).unwrap(),
+            String::from_utf8(suggestions.stderr).unwrap()
+        );
+        panic!("failed to run `suggest-tests`");
+    }
+
+    let suggestions = String::from_utf8(suggestions.stdout).unwrap();
+    let suggestions = suggestions
+        .lines()
+        .map(|line| {
+            let mut sections = line.split_ascii_whitespace();
+
+            // this code expects one suggestion per line in the following format:
+            // <x_subcommand> {some number of flags} [optional stage number]
+            let cmd = sections.next().unwrap();
+            let stage = sections.next_back().map(|s| str::parse(s).ok()).flatten();
+            let paths: Vec<PathBuf> = sections.map(|p| PathBuf::from_str(p).unwrap()).collect();
+
+            (cmd, stage, paths)
+        })
+        .collect::<Vec<_>>();
+
+    if !suggestions.is_empty() {
+        println!("==== SUGGESTIONS ====");
+        for sug in &suggestions {
+            print!("x {} ", sug.0);
+            if let Some(stage) = sug.1 {
+                print!("--stage {stage} ");
+            }
+
+            for path in &sug.2 {
+                print!("{} ", path.display());
+            }
+            println!();
+        }
+        println!("=====================");
+    } else {
+        println!("No suggestions found!");
+        return;
+    }
+
+    if run {
+        for sug in suggestions {
+            let mut build = builder.build.clone();
+
+            let builder =
+                Builder::new_standalone(&mut build, Kind::parse(&sug.0).unwrap(), sug.2, sug.1);
+
+            builder.execute_cli()
+        }
+    } else {
+        println!("help: consider using the `--run` flag to automatically run suggested tests");
+    }
+}
diff --git a/src/bootstrap/test.rs b/src/bootstrap/test.rs
index 2bf28674d36..aedf1ecab13 100644
--- a/src/bootstrap/test.rs
+++ b/src/bootstrap/test.rs
@@ -129,6 +129,42 @@ impl Step for CrateJsonDocLint {
 }
 
 #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
+pub struct SuggestTestsCrate {
+    host: TargetSelection,
+}
+
+impl Step for SuggestTestsCrate {
+    type Output = ();
+    const ONLY_HOSTS: bool = true;
+    const DEFAULT: bool = true;
+
+    fn should_run(run: ShouldRun<'_>) -> ShouldRun<'_> {
+        run.path("src/tools/suggest-tests")
+    }
+
+    fn make_run(run: RunConfig<'_>) {
+        run.builder.ensure(SuggestTestsCrate { host: run.target });
+    }
+
+    fn run(self, builder: &Builder<'_>) {
+        let bootstrap_host = builder.config.build;
+        let compiler = builder.compiler(0, bootstrap_host);
+
+        let suggest_tests = tool::prepare_tool_cargo(
+            builder,
+            compiler,
+            Mode::ToolBootstrap,
+            bootstrap_host,
+            "test",
+            "src/tools/suggest-tests",
+            SourceType::InTree,
+            &[],
+        );
+        add_flags_and_try_run_tests(builder, &mut suggest_tests.into());
+    }
+}
+
+#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
 pub struct Linkcheck {
     host: TargetSelection,
 }
diff --git a/src/bootstrap/tool.rs b/src/bootstrap/tool.rs
index 6a687a7903e..d1fd2e8c42c 100644
--- a/src/bootstrap/tool.rs
+++ b/src/bootstrap/tool.rs
@@ -433,6 +433,7 @@ bootstrap_tool!(
     ReplaceVersionPlaceholder, "src/tools/replace-version-placeholder", "replace-version-placeholder";
     CollectLicenseMetadata, "src/tools/collect-license-metadata", "collect-license-metadata";
     GenerateCopyright, "src/tools/generate-copyright", "generate-copyright";
+    SuggestTests, "src/tools/suggest-tests", "suggest-tests";
 );
 
 #[derive(Debug, Copy, Clone, Hash, PartialEq, Eq, Ord, PartialOrd)]
diff --git a/src/doc/rustdoc/src/command-line-arguments.md b/src/doc/rustdoc/src/command-line-arguments.md
index dfc80426372..b46d80eb362 100644
--- a/src/doc/rustdoc/src/command-line-arguments.md
+++ b/src/doc/rustdoc/src/command-line-arguments.md
@@ -179,7 +179,7 @@ $ rustdoc src/lib.rs --test
 This flag will run your code examples as tests. For more, see [the chapter
 on documentation tests](write-documentation/documentation-tests.md).
 
-See also `--test-args`.
+See also `--test-args` and `--test-run-directory`.
 
 ## `--test-args`: pass options to test runner
 
@@ -194,6 +194,19 @@ For more, see [the chapter on documentation tests](write-documentation/documenta
 
 See also `--test`.
 
+## `--test-run-directory`: run code examples in a specific directory
+
+Using this flag looks like this:
+
+```bash
+$ rustdoc src/lib.rs --test --test-run-directory=/path/to/working/directory
+```
+
+This flag will run your code examples in the specified working directory.
+For more, see [the chapter on documentation tests](write-documentation/documentation-tests.md).
+
+See also `--test`.
+
 ## `--target`: generate documentation for the specified target triple
 
 Using this flag looks like this:
diff --git a/src/doc/rustdoc/src/write-documentation/documentation-tests.md b/src/doc/rustdoc/src/write-documentation/documentation-tests.md
index 1cb5b049df4..a7d3186fb78 100644
--- a/src/doc/rustdoc/src/write-documentation/documentation-tests.md
+++ b/src/doc/rustdoc/src/write-documentation/documentation-tests.md
@@ -443,3 +443,15 @@ pub struct ReadmeDoctests;
 
 This will include your README as documentation on the hidden struct `ReadmeDoctests`, which will
 then be tested alongside the rest of your doctests.
+
+## Controlling the compilation and run directories
+
+By default, `rustdoc --test` will compile and run documentation test examples
+from the same working directory.
+The compilation directory is being used for compiler diagnostics, the `file!()` macro and
+the output of `rustdoc` test runner itself, whereas the run directory has an influence on file-system
+operations within documentation test examples, such as `std::fs::read_to_string`.
+
+The `--test-run-directory` flag allows controlling the run directory separately from the compilation directory.
+This is particularly useful in workspaces, where compiler invocations and thus diagnostics should be
+relative to the workspace directory, but documentation test examples should run relative to the crate directory.
diff --git a/src/librustdoc/html/static/js/externs.js b/src/librustdoc/html/static/js/externs.js
index ecbe15a59da..4c81a0979c1 100644
--- a/src/librustdoc/html/static/js/externs.js
+++ b/src/librustdoc/html/static/js/externs.js
@@ -66,6 +66,11 @@ let Row;
 let ResultsTable;
 
 /**
+ * @typedef {Map<String, ResultObject>}
+ */
+let Results;
+
+/**
  * @typedef {{
  *     desc: string,
  *     displayPath: string,
@@ -80,7 +85,7 @@ let ResultsTable;
  *     ty: number,
  * }}
  */
-let Results;
+let ResultObject;
 
 /**
  * A pair of [inputs, outputs], or 0 for null. This is stored in the search index.
diff --git a/src/librustdoc/html/static/js/search.js b/src/librustdoc/html/static/js/search.js
index c081578b8d4..40cdc55bbc3 100644
--- a/src/librustdoc/html/static/js/search.js
+++ b/src/librustdoc/html/static/js/search.js
@@ -191,7 +191,7 @@ function initSearch(rawSearchIndex) {
      */
     let searchIndex;
     let currentResults;
-    const ALIASES = Object.create(null);
+    const ALIASES = new Map();
 
     function isWhitespace(c) {
         return " \t\n\r".indexOf(c) !== -1;
@@ -903,10 +903,18 @@ function initSearch(rawSearchIndex) {
      * @return {ResultsTable}
      */
     function execQuery(parsedQuery, searchWords, filterCrates, currentCrate) {
-        const results_others = {}, results_in_args = {}, results_returned = {};
+        const results_others = new Map(), results_in_args = new Map(),
+            results_returned = new Map();
 
+        /**
+         * Add extra data to result objects, and filter items that have been
+         * marked for removal.
+         *
+         * @param {[ResultObject]} results
+         * @returns {[ResultObject]}
+         */
         function transformResults(results) {
-            const duplicates = {};
+            const duplicates = new Set();
             const out = [];
 
             for (const result of results) {
@@ -919,10 +927,10 @@ function initSearch(rawSearchIndex) {
                     // To be sure than it some items aren't considered as duplicate.
                     obj.fullPath += "|" + obj.ty;
 
-                    if (duplicates[obj.fullPath]) {
+                    if (duplicates.has(obj.fullPath)) {
                         continue;
                     }
-                    duplicates[obj.fullPath] = true;
+                    duplicates.add(obj.fullPath);
 
                     obj.href = res[1];
                     out.push(obj);
@@ -934,24 +942,30 @@ function initSearch(rawSearchIndex) {
             return out;
         }
 
+        /**
+         * This function takes a result map, and sorts it by various criteria, including edit
+         * distance, substring match, and the crate it comes from.
+         *
+         * @param {Results} results
+         * @param {boolean} isType
+         * @param {string} preferredCrate
+         * @returns {[ResultObject]}
+         */
         function sortResults(results, isType, preferredCrate) {
-            const userQuery = parsedQuery.userQuery;
-            const ar = [];
-            for (const entry in results) {
-                if (hasOwnPropertyRustdoc(results, entry)) {
-                    const result = results[entry];
-                    result.word = searchWords[result.id];
-                    result.item = searchIndex[result.id] || {};
-                    ar.push(result);
-                }
-            }
-            results = ar;
             // if there are no results then return to default and fail
-            if (results.length === 0) {
+            if (results.size === 0) {
                 return [];
             }
 
-            results.sort((aaa, bbb) => {
+            const userQuery = parsedQuery.userQuery;
+            const result_list = [];
+            for (const result of results.values()) {
+                result.word = searchWords[result.id];
+                result.item = searchIndex[result.id] || {};
+                result_list.push(result);
+            }
+
+            result_list.sort((aaa, bbb) => {
                 let a, b;
 
                 // sort by exact match with regard to the last word (mismatch goes later)
@@ -1060,7 +1074,7 @@ function initSearch(rawSearchIndex) {
                 nameSplit = hasPath ? null : parsedQuery.elems[0].path;
             }
 
-            for (const result of results) {
+            for (const result of result_list) {
                 // this validation does not make sense when searching by types
                 if (result.dontValidate) {
                     continue;
@@ -1073,7 +1087,7 @@ function initSearch(rawSearchIndex) {
                     result.id = -1;
                 }
             }
-            return transformResults(results);
+            return transformResults(result_list);
         }
 
         /**
@@ -1096,7 +1110,7 @@ function initSearch(rawSearchIndex) {
             // The names match, but we need to be sure that all generics kinda
             // match as well.
             if (elem.generics.length > 0 && row.generics.length >= elem.generics.length) {
-                const elems = Object.create(null);
+                const elems = new Map();
                 for (const entry of row.generics) {
                     if (entry.name === "") {
                         // Pure generic, needs to check into it.
@@ -1106,39 +1120,30 @@ function initSearch(rawSearchIndex) {
                         }
                         continue;
                     }
-                    if (elems[entry.name] === undefined) {
-                        elems[entry.name] = [];
+                    let currentEntryElems;
+                    if (elems.has(entry.name)) {
+                        currentEntryElems = elems.get(entry.name);
+                    } else {
+                        currentEntryElems = [];
+                        elems.set(entry.name, currentEntryElems);
                     }
-                    elems[entry.name].push(entry.ty);
+                    currentEntryElems.push(entry.ty);
                 }
                 // We need to find the type that matches the most to remove it in order
                 // to move forward.
                 const handleGeneric = generic => {
-                    let match = null;
-                    if (elems[generic.name]) {
-                        match = generic.name;
-                    } else {
-                        for (const elem_name in elems) {
-                            if (!hasOwnPropertyRustdoc(elems, elem_name)) {
-                                continue;
-                            }
-                            if (elem_name === generic) {
-                                match = elem_name;
-                                break;
-                            }
-                        }
-                    }
-                    if (match === null) {
+                    if (!elems.has(generic.name)) {
                         return false;
                     }
-                    const matchIdx = elems[match].findIndex(tmp_elem =>
+                    const matchElems = elems.get(generic.name);
+                    const matchIdx = matchElems.findIndex(tmp_elem =>
                         typePassesFilter(generic.typeFilter, tmp_elem));
                     if (matchIdx === -1) {
                         return false;
                     }
-                    elems[match].splice(matchIdx, 1);
-                    if (elems[match].length === 0) {
-                        delete elems[match];
+                    matchElems.splice(matchIdx, 1);
+                    if (matchElems.length === 0) {
+                        elems.delete(generic.name);
                     }
                     return true;
                 };
@@ -1424,22 +1429,22 @@ function initSearch(rawSearchIndex) {
             const aliases = [];
             const crateAliases = [];
             if (filterCrates !== null) {
-                if (ALIASES[filterCrates] && ALIASES[filterCrates][lowerQuery]) {
-                    const query_aliases = ALIASES[filterCrates][lowerQuery];
+                if (ALIASES.has(filterCrates) && ALIASES.get(filterCrates).has(lowerQuery)) {
+                    const query_aliases = ALIASES.get(filterCrates).get(lowerQuery);
                     for (const alias of query_aliases) {
                         aliases.push(createAliasFromItem(searchIndex[alias]));
                     }
                 }
             } else {
-                Object.keys(ALIASES).forEach(crate => {
-                    if (ALIASES[crate][lowerQuery]) {
+                for (const [crate, crateAliasesIndex] of ALIASES) {
+                    if (crateAliasesIndex.has(lowerQuery)) {
                         const pushTo = crate === currentCrate ? crateAliases : aliases;
-                        const query_aliases = ALIASES[crate][lowerQuery];
+                        const query_aliases = crateAliasesIndex.get(lowerQuery);
                         for (const alias of query_aliases) {
                             pushTo.push(createAliasFromItem(searchIndex[alias]));
                         }
                     }
-                });
+                }
             }
 
             const sortFunc = (aaa, bbb) => {
@@ -1496,19 +1501,19 @@ function initSearch(rawSearchIndex) {
         function addIntoResults(results, fullId, id, index, dist, path_dist, maxEditDistance) {
             const inBounds = dist <= maxEditDistance || index !== -1;
             if (dist === 0 || (!parsedQuery.literalSearch && inBounds)) {
-                if (results[fullId] !== undefined) {
-                    const result = results[fullId];
+                if (results.has(fullId)) {
+                    const result = results.get(fullId);
                     if (result.dontValidate || result.dist <= dist) {
                         return;
                     }
                 }
-                results[fullId] = {
+                results.set(fullId, {
                     id: id,
                     index: index,
                     dontValidate: parsedQuery.literalSearch,
                     dist: dist,
                     path_dist: path_dist,
-                };
+                });
             }
         }
 
@@ -2345,17 +2350,22 @@ function initSearch(rawSearchIndex) {
             }
 
             if (aliases) {
-                ALIASES[crate] = Object.create(null);
+                const currentCrateAliases = new Map();
+                ALIASES.set(crate, currentCrateAliases);
                 for (const alias_name in aliases) {
                     if (!hasOwnPropertyRustdoc(aliases, alias_name)) {
                         continue;
                     }
 
-                    if (!hasOwnPropertyRustdoc(ALIASES[crate], alias_name)) {
-                        ALIASES[crate][alias_name] = [];
+                    let currentNameAliases;
+                    if (currentCrateAliases.has(alias_name)) {
+                        currentNameAliases = currentCrateAliases.get(alias_name);
+                    } else {
+                        currentNameAliases = [];
+                        currentCrateAliases.set(alias_name, currentNameAliases);
                     }
                     for (const local_alias of aliases[alias_name]) {
-                        ALIASES[crate][alias_name].push(local_alias + currentIndex);
+                        currentNameAliases.push(local_alias + currentIndex);
                     }
                 }
             }
diff --git a/src/librustdoc/lib.rs b/src/librustdoc/lib.rs
index b3640eab953..60c98cc3831 100644
--- a/src/librustdoc/lib.rs
+++ b/src/librustdoc/lib.rs
@@ -284,7 +284,7 @@ fn opts() -> Vec<RustcOptGroup> {
         stable("test-args", |o| {
             o.optmulti("", "test-args", "arguments to pass to the test runner", "ARGS")
         }),
-        unstable("test-run-directory", |o| {
+        stable("test-run-directory", |o| {
             o.optopt(
                 "",
                 "test-run-directory",
diff --git a/src/tools/suggest-tests/Cargo.toml b/src/tools/suggest-tests/Cargo.toml
new file mode 100644
index 00000000000..f4f4d548bb7
--- /dev/null
+++ b/src/tools/suggest-tests/Cargo.toml
@@ -0,0 +1,9 @@
+[package]
+name = "suggest-tests"
+version = "0.1.0"
+edition = "2021"
+
+[dependencies]
+glob = "0.3.0"
+build_helper = { version = "0.1.0", path = "../build_helper" }
+once_cell = "1.17.1"
diff --git a/src/tools/suggest-tests/src/dynamic_suggestions.rs b/src/tools/suggest-tests/src/dynamic_suggestions.rs
new file mode 100644
index 00000000000..2b0213cdc22
--- /dev/null
+++ b/src/tools/suggest-tests/src/dynamic_suggestions.rs
@@ -0,0 +1,23 @@
+use std::path::Path;
+
+use crate::Suggestion;
+
+type DynamicSuggestion = fn(&Path) -> Vec<Suggestion>;
+
+pub(crate) const DYNAMIC_SUGGESTIONS: &[DynamicSuggestion] = &[|path: &Path| -> Vec<Suggestion> {
+    if path.starts_with("compiler/") || path.starts_with("library/") {
+        let path = path.components().take(2).collect::<Vec<_>>();
+
+        vec![Suggestion::with_single_path(
+            "test",
+            None,
+            &format!(
+                "{}/{}",
+                path[0].as_os_str().to_str().unwrap(),
+                path[1].as_os_str().to_str().unwrap()
+            ),
+        )]
+    } else {
+        Vec::new()
+    }
+}];
diff --git a/src/tools/suggest-tests/src/lib.rs b/src/tools/suggest-tests/src/lib.rs
new file mode 100644
index 00000000000..44cd3c7f6a8
--- /dev/null
+++ b/src/tools/suggest-tests/src/lib.rs
@@ -0,0 +1,96 @@
+use std::{
+    fmt::{self, Display},
+    path::Path,
+};
+
+use dynamic_suggestions::DYNAMIC_SUGGESTIONS;
+use glob::Pattern;
+use static_suggestions::STATIC_SUGGESTIONS;
+
+mod dynamic_suggestions;
+mod static_suggestions;
+
+#[cfg(test)]
+mod tests;
+
+macro_rules! sug {
+    ($cmd:expr) => {
+        Suggestion::new($cmd, None, &[])
+    };
+
+    ($cmd:expr, $paths:expr) => {
+        Suggestion::new($cmd, None, $paths.as_slice())
+    };
+
+    ($cmd:expr, $stage:expr, $paths:expr) => {
+        Suggestion::new($cmd, Some($stage), $paths.as_slice())
+    };
+}
+
+pub(crate) use sug;
+
+pub fn get_suggestions<T: AsRef<str>>(modified_files: &[T]) -> Vec<Suggestion> {
+    let mut suggestions = Vec::new();
+
+    // static suggestions
+    for sug in STATIC_SUGGESTIONS.iter() {
+        let glob = Pattern::new(&sug.0).expect("Found invalid glob pattern!");
+
+        for file in modified_files {
+            if glob.matches(file.as_ref()) {
+                suggestions.extend_from_slice(&sug.1);
+            }
+        }
+    }
+
+    // dynamic suggestions
+    for sug in DYNAMIC_SUGGESTIONS {
+        for file in modified_files {
+            let sugs = sug(Path::new(file.as_ref()));
+
+            suggestions.extend_from_slice(&sugs);
+        }
+    }
+
+    suggestions.sort();
+    suggestions.dedup();
+
+    suggestions
+}
+
+#[derive(Clone, PartialOrd, Ord, PartialEq, Eq, Debug)]
+pub struct Suggestion {
+    pub cmd: String,
+    pub stage: Option<u32>,
+    pub paths: Vec<String>,
+}
+
+impl Suggestion {
+    pub fn new(cmd: &str, stage: Option<u32>, paths: &[&str]) -> Self {
+        Self { cmd: cmd.to_owned(), stage, paths: paths.iter().map(|p| p.to_string()).collect() }
+    }
+
+    pub fn with_single_path(cmd: &str, stage: Option<u32>, path: &str) -> Self {
+        Self::new(cmd, stage, &[path])
+    }
+}
+
+impl Display for Suggestion {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
+        write!(f, "{} ", self.cmd)?;
+
+        for path in &self.paths {
+            write!(f, "{} ", path)?;
+        }
+
+        if let Some(stage) = self.stage {
+            write!(f, "{}", stage)?;
+        } else {
+            // write a sentinel value here (in place of a stage) to be consumed
+            // by the shim in bootstrap, it will be read and ignored.
+            write!(f, "N/A")?;
+        }
+
+        Ok(())
+    }
+}
diff --git a/src/tools/suggest-tests/src/main.rs b/src/tools/suggest-tests/src/main.rs
new file mode 100644
index 00000000000..0b541b60cba
--- /dev/null
+++ b/src/tools/suggest-tests/src/main.rs
@@ -0,0 +1,27 @@
+use std::process::ExitCode;
+
+use build_helper::git::get_git_modified_files;
+use suggest_tests::get_suggestions;
+
+fn main() -> ExitCode {
+    let modified_files = get_git_modified_files(None, &Vec::new());
+    let modified_files = match modified_files {
+        Ok(Some(files)) => files,
+        Ok(None) => {
+            eprintln!("git error");
+            return ExitCode::FAILURE;
+        }
+        Err(err) => {
+            eprintln!("Could not get modified files from git: \"{err}\"");
+            return ExitCode::FAILURE;
+        }
+    };
+
+    let suggestions = get_suggestions(&modified_files);
+
+    for sug in &suggestions {
+        println!("{sug}");
+    }
+
+    ExitCode::SUCCESS
+}
diff --git a/src/tools/suggest-tests/src/static_suggestions.rs b/src/tools/suggest-tests/src/static_suggestions.rs
new file mode 100644
index 00000000000..d8166ead8c4
--- /dev/null
+++ b/src/tools/suggest-tests/src/static_suggestions.rs
@@ -0,0 +1,24 @@
+use crate::{sug, Suggestion};
+
+// FIXME: perhaps this could use `std::lazy` when it is stablizied
+macro_rules! static_suggestions {
+    ($( $glob:expr => [ $( $suggestion:expr ),* ] ),*) => {
+        pub(crate) const STATIC_SUGGESTIONS: ::once_cell::unsync::Lazy<Vec<(&'static str, Vec<Suggestion>)>>
+            = ::once_cell::unsync::Lazy::new(|| vec![ $( ($glob, vec![ $($suggestion),* ]) ),*]);
+    }
+}
+
+static_suggestions! {
+    "*.md" => [
+        sug!("test", 0, ["linkchecker"])
+    ],
+
+    "compiler/*" => [
+        sug!("check"),
+        sug!("test", 1, ["src/test/ui", "src/test/run-make"])
+    ],
+
+    "src/librustdoc/*" => [
+        sug!("test", 1, ["rustdoc"])
+    ]
+}
diff --git a/src/tools/suggest-tests/src/tests.rs b/src/tools/suggest-tests/src/tests.rs
new file mode 100644
index 00000000000..5bc1a7df7ca
--- /dev/null
+++ b/src/tools/suggest-tests/src/tests.rs
@@ -0,0 +1,21 @@
+macro_rules! sugg_test {
+    ( $( $name:ident: $paths:expr => $suggestions:expr ),* ) => {
+        $(
+            #[test]
+            fn $name() {
+                let suggestions = crate::get_suggestions(&$paths).into_iter().map(|s| s.to_string()).collect::<Vec<_>>();
+                assert_eq!(suggestions, $suggestions);
+            }
+        )*
+    };
+}
+
+sugg_test! {
+    test_error_code_docs: ["compiler/rustc_error_codes/src/error_codes/E0000.md"] =>
+        ["check N/A", "test compiler/rustc_error_codes N/A", "test linkchecker 0", "test src/test/ui src/test/run-make 1"],
+
+    test_rustdoc: ["src/librustdoc/src/lib.rs"] => ["test rustdoc 1"],
+
+    test_rustdoc_and_libstd: ["src/librustdoc/src/lib.rs", "library/std/src/lib.rs"] =>
+        ["test library/std N/A", "test rustdoc 1"]
+}
diff --git a/tests/rustdoc-ui/run-directory.rs b/tests/rustdoc-ui/run-directory.rs
index bbceaaf824f..b8d0647f08d 100644
--- a/tests/rustdoc-ui/run-directory.rs
+++ b/tests/rustdoc-ui/run-directory.rs
@@ -2,8 +2,8 @@
 
 // revisions: correct incorrect
 // check-pass
-// [correct]compile-flags:--test --test-run-directory={{src-base}} -Zunstable-options
-// [incorrect]compile-flags:--test --test-run-directory={{src-base}}/coverage -Zunstable-options
+// [correct]compile-flags:--test --test-run-directory={{src-base}}
+// [incorrect]compile-flags:--test --test-run-directory={{src-base}}/coverage
 // normalize-stdout-test: "tests/rustdoc-ui" -> "$$DIR"
 // normalize-stdout-test "finished in \d+\.\d+s" -> "finished in $$TIME"
 
diff --git a/tests/ui-fulldeps/stable-mir/crate-info.rs b/tests/ui-fulldeps/stable-mir/crate-info.rs
index 03dab235040..dfde8c97ec2 100644
--- a/tests/ui-fulldeps/stable-mir/crate-info.rs
+++ b/tests/ui-fulldeps/stable-mir/crate-info.rs
@@ -29,6 +29,8 @@ fn test_stable_mir(tcx: TyCtxt<'_>) {
     let local = stable_mir::local_crate();
     assert_eq!(&local.name, CRATE_NAME);
 
+    assert_eq!(stable_mir::entry_fn(), None);
+
     // Find items in the local crate.
     let items = stable_mir::all_local_items();
     assert!(get_item(tcx, &items, (DefKind::Fn, "foo_bar")).is_some());