about summary refs log tree commit diff
path: root/src
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 /src
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
Diffstat (limited to 'src')
-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
18 files changed, 473 insertions, 83 deletions
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"]
+}