about summary refs log tree commit diff
path: root/src/librustdoc
diff options
context:
space:
mode:
authorRalf Jung <post@ralfj.de>2023-02-15 10:44:40 +0100
committerRalf Jung <post@ralfj.de>2023-02-15 10:44:40 +0100
commit1a2908bfaa4e8283b08aa3c29ff41515f247e322 (patch)
treeba0769c8b2e29eb20b5f935fba4567c73e4ec0b9 /src/librustdoc
parentf407e96b263e5ce71cde4bf93ad1865119ea45ad (diff)
parent90f642bb3d74ee0ba8e0faf967748f36ff78d572 (diff)
Merge from rustc
Diffstat (limited to 'src/librustdoc')
-rw-r--r--src/librustdoc/Cargo.toml1
-rw-r--r--src/librustdoc/clean/auto_trait.rs4
-rw-r--r--src/librustdoc/clean/mod.rs13
-rw-r--r--src/librustdoc/clean/types.rs197
-rw-r--r--src/librustdoc/clean/types/tests.rs1
-rw-r--r--src/librustdoc/core.rs64
-rw-r--r--src/librustdoc/doctest.rs5
-rw-r--r--src/librustdoc/formats/item_type.rs1
-rw-r--r--src/librustdoc/html/format.rs16
-rw-r--r--src/librustdoc/html/highlight.rs16
-rw-r--r--src/librustdoc/html/markdown.rs86
-rw-r--r--src/librustdoc/html/markdown/tests.rs2
-rw-r--r--src/librustdoc/html/render/context.rs14
-rw-r--r--src/librustdoc/html/render/mod.rs80
-rw-r--r--src/librustdoc/html/render/print_item.rs43
-rw-r--r--src/librustdoc/html/render/search_index.rs11
-rw-r--r--src/librustdoc/html/static/css/rustdoc.css62
-rw-r--r--src/librustdoc/html/static/css/themes/ayu.css2
-rw-r--r--src/librustdoc/html/static/css/themes/dark.css2
-rw-r--r--src/librustdoc/html/static/css/themes/light.css2
-rw-r--r--src/librustdoc/html/static/js/main.js118
-rw-r--r--src/librustdoc/html/static/js/search.js19
-rw-r--r--src/librustdoc/lib.rs38
-rw-r--r--src/librustdoc/passes/collect_intra_doc_links.rs262
-rw-r--r--src/librustdoc/passes/collect_intra_doc_links/early.rs431
-rw-r--r--src/librustdoc/passes/collect_trait_impls.rs16
-rw-r--r--src/librustdoc/passes/mod.rs3
-rw-r--r--src/librustdoc/visit_ast.rs2
-rw-r--r--src/librustdoc/visit_lib.rs51
29 files changed, 361 insertions, 1201 deletions
diff --git a/src/librustdoc/Cargo.toml b/src/librustdoc/Cargo.toml
index 0271c27b4f5..5e592227d49 100644
--- a/src/librustdoc/Cargo.toml
+++ b/src/librustdoc/Cargo.toml
@@ -12,7 +12,6 @@ askama = { version = "0.11", default-features = false, features = ["config"] }
 itertools = "0.10.1"
 minifier = "0.2.2"
 once_cell = "1.10.0"
-pulldown-cmark = { version = "0.9.2", default-features = false }
 regex = "1"
 rustdoc-json-types = { path = "../rustdoc-json-types" }
 serde_json = "1.0"
diff --git a/src/librustdoc/clean/auto_trait.rs b/src/librustdoc/clean/auto_trait.rs
index a302750aa1a..6c15eac2c19 100644
--- a/src/librustdoc/clean/auto_trait.rs
+++ b/src/librustdoc/clean/auto_trait.rs
@@ -734,8 +734,8 @@ struct RegionReplacer<'a, 'tcx> {
     tcx: TyCtxt<'tcx>,
 }
 
-impl<'a, 'tcx> TypeFolder<'tcx> for RegionReplacer<'a, 'tcx> {
-    fn tcx<'b>(&'b self) -> TyCtxt<'tcx> {
+impl<'a, 'tcx> TypeFolder<TyCtxt<'tcx>> for RegionReplacer<'a, 'tcx> {
+    fn interner(&self) -> TyCtxt<'tcx> {
         self.tcx
     }
 
diff --git a/src/librustdoc/clean/mod.rs b/src/librustdoc/clean/mod.rs
index 80493b100bb..bf3bbeb2dd1 100644
--- a/src/librustdoc/clean/mod.rs
+++ b/src/librustdoc/clean/mod.rs
@@ -20,7 +20,7 @@ use rustc_hir::PredicateOrigin;
 use rustc_hir_analysis::hir_ty_to_ty;
 use rustc_infer::infer::region_constraints::{Constraint, RegionConstraintData};
 use rustc_middle::middle::resolve_lifetime as rl;
-use rustc_middle::ty::fold::TypeFolder;
+use rustc_middle::ty::fold::ir::TypeFolder;
 use rustc_middle::ty::InternalSubsts;
 use rustc_middle::ty::TypeVisitable;
 use rustc_middle::ty::{self, AdtKind, DefIdTree, EarlyBinder, Ty, TyCtxt};
@@ -242,6 +242,7 @@ pub(crate) fn clean_middle_region<'tcx>(region: ty::Region<'tcx>) -> Option<Life
         ty::ReLateBound(..)
         | ty::ReFree(..)
         | ty::ReVar(..)
+        | ty::ReError(_)
         | ty::RePlaceholder(..)
         | ty::ReErased => {
             debug!("cannot clean region {:?}", region);
@@ -310,10 +311,12 @@ pub(crate) fn clean_predicate<'tcx>(
         ty::PredicateKind::Clause(ty::Clause::Projection(pred)) => {
             Some(clean_projection_predicate(bound_predicate.rebind(pred), cx))
         }
+        // FIXME(generic_const_exprs): should this do something?
         ty::PredicateKind::ConstEvaluatable(..) => None,
         ty::PredicateKind::WellFormed(..) => None,
 
         ty::PredicateKind::Subtype(..)
+        | ty::PredicateKind::AliasEq(..)
         | ty::PredicateKind::Coerce(..)
         | ty::PredicateKind::ObjectSafe(..)
         | ty::PredicateKind::ClosureKind(..)
@@ -2206,10 +2209,12 @@ fn clean_maybe_renamed_item<'tcx>(
         };
 
         let mut extra_attrs = Vec::new();
-        if let Some(hir::Node::Item(use_node)) =
-            import_id.and_then(|def_id| cx.tcx.hir().find_by_def_id(def_id))
+        if let Some(import_id) = import_id &&
+            let Some(hir::Node::Item(use_node)) = cx.tcx.hir().find_by_def_id(import_id)
         {
-            // We get all the various imports' attributes.
+            // First, we add the attributes from the current import.
+            extra_attrs.extend_from_slice(inline::load_attrs(cx, import_id.to_def_id()));
+            // Then we get all the various imports' attributes.
             get_all_import_attributes(use_node, cx.tcx, item.owner_id.def_id, &mut extra_attrs);
         }
 
diff --git a/src/librustdoc/clean/types.rs b/src/librustdoc/clean/types.rs
index 85dd3881593..b00cefdddb5 100644
--- a/src/librustdoc/clean/types.rs
+++ b/src/librustdoc/clean/types.rs
@@ -5,13 +5,12 @@ use std::path::PathBuf;
 use std::rc::Rc;
 use std::sync::Arc;
 use std::sync::OnceLock as OnceCell;
-use std::{cmp, fmt, iter};
+use std::{fmt, iter};
 
 use arrayvec::ArrayVec;
 use thin_vec::ThinVec;
 
-use rustc_ast::util::comments::beautify_doc_string;
-use rustc_ast::{self as ast, AttrStyle};
+use rustc_ast as ast;
 use rustc_attr::{ConstStability, Deprecation, Stability, StabilityLevel};
 use rustc_const_eval::const_eval::is_unstable_const_fn;
 use rustc_data_structures::fx::{FxHashMap, FxHashSet};
@@ -24,6 +23,7 @@ use rustc_hir_analysis::check::intrinsic::intrinsic_operation_unsafety;
 use rustc_index::vec::IndexVec;
 use rustc_middle::ty::fast_reject::SimplifiedType;
 use rustc_middle::ty::{self, DefIdTree, TyCtxt, Visibility};
+use rustc_resolve::rustdoc::{add_doc_fragment, attrs_to_doc_fragments, inner_docs, DocFragment};
 use rustc_session::Session;
 use rustc_span::hygiene::MacroKind;
 use rustc_span::symbol::{kw, sym, Ident, Symbol};
@@ -405,7 +405,7 @@ impl Item {
     pub(crate) fn inner_docs(&self, tcx: TyCtxt<'_>) -> bool {
         self.item_id
             .as_def_id()
-            .map(|did| tcx.get_attrs_unchecked(did).inner_docs())
+            .map(|did| inner_docs(tcx.get_attrs_unchecked(did)))
             .unwrap_or(false)
     }
 
@@ -482,16 +482,16 @@ impl Item {
     }
 
     pub(crate) fn links(&self, cx: &Context<'_>) -> Vec<RenderedLink> {
-        use crate::html::format::href;
+        use crate::html::format::{href, link_tooltip};
 
         cx.cache()
             .intra_doc_links
             .get(&self.item_id)
             .map_or(&[][..], |v| v.as_slice())
             .iter()
-            .filter_map(|ItemLink { link: s, link_text, page_id: did, ref fragment }| {
-                debug!(?did);
-                if let Ok((mut href, ..)) = href(*did, cx) {
+            .filter_map(|ItemLink { link: s, link_text, page_id: id, ref fragment }| {
+                debug!(?id);
+                if let Ok((mut href, ..)) = href(*id, cx) {
                     debug!(?href);
                     if let Some(ref fragment) = *fragment {
                         fragment.render(&mut href, cx.tcx())
@@ -499,6 +499,7 @@ impl Item {
                     Some(RenderedLink {
                         original_text: s.clone(),
                         new_text: link_text.clone(),
+                        tooltip: link_tooltip(*id, fragment, cx),
                         href,
                     })
                 } else {
@@ -523,6 +524,7 @@ impl Item {
                 original_text: s.clone(),
                 new_text: link_text.clone(),
                 href: String::new(),
+                tooltip: String::new(),
             })
             .collect()
     }
@@ -874,8 +876,6 @@ pub(crate) trait AttributesExt {
 
     fn span(&self) -> Option<rustc_span::Span>;
 
-    fn inner_docs(&self) -> bool;
-
     fn cfg(&self, tcx: TyCtxt<'_>, hidden_cfg: &FxHashSet<Cfg>) -> Option<Arc<Cfg>>;
 }
 
@@ -894,14 +894,6 @@ impl AttributesExt for [ast::Attribute] {
         self.iter().find(|attr| attr.doc_str().is_some()).map(|attr| attr.span)
     }
 
-    /// Returns whether the first doc-comment is an inner attribute.
-    ///
-    //// If there are no doc-comments, return true.
-    /// FIXME(#78591): Support both inner and outer attributes on the same item.
-    fn inner_docs(&self) -> bool {
-        self.iter().find(|a| a.doc_str().is_some()).map_or(true, |a| a.style == AttrStyle::Inner)
-    }
-
     fn cfg(&self, tcx: TyCtxt<'_>, hidden_cfg: &FxHashSet<Cfg>) -> Option<Arc<Cfg>> {
         let sess = tcx.sess;
         let doc_cfg_active = tcx.features().doc_cfg;
@@ -1010,58 +1002,6 @@ impl<I: Iterator<Item = ast::NestedMetaItem>> NestedAttributesExt for I {
     }
 }
 
-/// A portion of documentation, extracted from a `#[doc]` attribute.
-///
-/// Each variant contains the line number within the complete doc-comment where the fragment
-/// starts, as well as the Span where the corresponding doc comment or attribute is located.
-///
-/// Included files are kept separate from inline doc comments so that proper line-number
-/// information can be given when a doctest fails. Sugared doc comments and "raw" doc comments are
-/// kept separate because of issue #42760.
-#[derive(Clone, PartialEq, Eq, Debug)]
-pub(crate) struct DocFragment {
-    pub(crate) span: rustc_span::Span,
-    /// The module this doc-comment came from.
-    ///
-    /// This allows distinguishing between the original documentation and a pub re-export.
-    /// If it is `None`, the item was not re-exported.
-    pub(crate) parent_module: Option<DefId>,
-    pub(crate) doc: Symbol,
-    pub(crate) kind: DocFragmentKind,
-    pub(crate) indent: usize,
-}
-
-#[derive(Clone, Copy, PartialEq, Eq, Debug)]
-pub(crate) enum DocFragmentKind {
-    /// A doc fragment created from a `///` or `//!` doc comment.
-    SugaredDoc,
-    /// A doc fragment created from a "raw" `#[doc=""]` attribute.
-    RawDoc,
-}
-
-/// The goal of this function is to apply the `DocFragment` transformation that is required when
-/// transforming into the final Markdown, which is applying the computed indent to each line in
-/// each doc fragment (a `DocFragment` can contain multiple lines in case of `#[doc = ""]`).
-///
-/// Note: remove the trailing newline where appropriate
-fn add_doc_fragment(out: &mut String, frag: &DocFragment) {
-    let s = frag.doc.as_str();
-    let mut iter = s.lines();
-    if s.is_empty() {
-        out.push('\n');
-        return;
-    }
-    while let Some(line) = iter.next() {
-        if line.chars().any(|c| !c.is_whitespace()) {
-            assert!(line.len() >= frag.indent);
-            out.push_str(&line[frag.indent..]);
-        } else {
-            out.push_str(line);
-        }
-        out.push('\n');
-    }
-}
-
 /// Collapse a collection of [`DocFragment`]s into one string,
 /// handling indentation and newlines as needed.
 pub(crate) fn collapse_doc_fragments(doc_strings: &[DocFragment]) -> String {
@@ -1073,86 +1013,6 @@ pub(crate) fn collapse_doc_fragments(doc_strings: &[DocFragment]) -> String {
     acc
 }
 
-/// Removes excess indentation on comments in order for the Markdown
-/// to be parsed correctly. This is necessary because the convention for
-/// writing documentation is to provide a space between the /// or //! marker
-/// and the doc text, but Markdown is whitespace-sensitive. For example,
-/// a block of text with four-space indentation is parsed as a code block,
-/// so if we didn't unindent comments, these list items
-///
-/// /// A list:
-/// ///
-/// ///    - Foo
-/// ///    - Bar
-///
-/// would be parsed as if they were in a code block, which is likely not what the user intended.
-fn unindent_doc_fragments(docs: &mut Vec<DocFragment>) {
-    // `add` is used in case the most common sugared doc syntax is used ("/// "). The other
-    // fragments kind's lines are never starting with a whitespace unless they are using some
-    // markdown formatting requiring it. Therefore, if the doc block have a mix between the two,
-    // we need to take into account the fact that the minimum indent minus one (to take this
-    // whitespace into account).
-    //
-    // For example:
-    //
-    // /// hello!
-    // #[doc = "another"]
-    //
-    // In this case, you want "hello! another" and not "hello!  another".
-    let add = if docs.windows(2).any(|arr| arr[0].kind != arr[1].kind)
-        && docs.iter().any(|d| d.kind == DocFragmentKind::SugaredDoc)
-    {
-        // In case we have a mix of sugared doc comments and "raw" ones, we want the sugared one to
-        // "decide" how much the minimum indent will be.
-        1
-    } else {
-        0
-    };
-
-    // `min_indent` is used to know how much whitespaces from the start of each lines must be
-    // removed. Example:
-    //
-    // ///     hello!
-    // #[doc = "another"]
-    //
-    // In here, the `min_indent` is 1 (because non-sugared fragment are always counted with minimum
-    // 1 whitespace), meaning that "hello!" will be considered a codeblock because it starts with 4
-    // (5 - 1) whitespaces.
-    let Some(min_indent) = docs
-        .iter()
-        .map(|fragment| {
-            fragment.doc.as_str().lines().fold(usize::MAX, |min_indent, line| {
-                if line.chars().all(|c| c.is_whitespace()) {
-                    min_indent
-                } else {
-                    // Compare against either space or tab, ignoring whether they are
-                    // mixed or not.
-                    let whitespace = line.chars().take_while(|c| *c == ' ' || *c == '\t').count();
-                    cmp::min(min_indent, whitespace)
-                        + if fragment.kind == DocFragmentKind::SugaredDoc { 0 } else { add }
-                }
-            })
-        })
-        .min()
-    else {
-        return;
-    };
-
-    for fragment in docs {
-        if fragment.doc == kw::Empty {
-            continue;
-        }
-
-        let min_indent = if fragment.kind != DocFragmentKind::SugaredDoc && min_indent > 0 {
-            min_indent - add
-        } else {
-            min_indent
-        };
-
-        fragment.indent = min_indent;
-    }
-}
-
 /// A link that has not yet been rendered.
 ///
 /// This link will be turned into a rendered link by [`Item::links`].
@@ -1182,6 +1042,8 @@ pub struct RenderedLink {
     pub(crate) new_text: String,
     /// The URL to put in the `href`
     pub(crate) href: String,
+    /// The tooltip.
+    pub(crate) tooltip: String,
 }
 
 /// The attributes on an [`Item`], including attributes like `#[derive(...)]` and `#[inline]`,
@@ -1231,26 +1093,7 @@ impl Attributes {
         attrs: impl Iterator<Item = (&'a ast::Attribute, Option<DefId>)>,
         doc_only: bool,
     ) -> Attributes {
-        let mut doc_strings = Vec::new();
-        let mut other_attrs = ast::AttrVec::new();
-        for (attr, parent_module) in attrs {
-            if let Some((doc_str, comment_kind)) = attr.doc_str_and_comment_kind() {
-                trace!("got doc_str={doc_str:?}");
-                let doc = beautify_doc_string(doc_str, comment_kind);
-                let kind = if attr.is_doc_comment() {
-                    DocFragmentKind::SugaredDoc
-                } else {
-                    DocFragmentKind::RawDoc
-                };
-                let fragment = DocFragment { span: attr.span, doc, kind, parent_module, indent: 0 };
-                doc_strings.push(fragment);
-            } else if !doc_only {
-                other_attrs.push(attr.clone());
-            }
-        }
-
-        unindent_doc_fragments(&mut doc_strings);
-
+        let (doc_strings, other_attrs) = attrs_to_doc_fragments(attrs, doc_only);
         Attributes { doc_strings, other_attrs }
     }
 
@@ -1269,20 +1112,6 @@ impl Attributes {
         if out.is_empty() { None } else { Some(out) }
     }
 
-    /// Return the doc-comments on this item, grouped by the module they came from.
-    /// The module can be different if this is a re-export with added documentation.
-    ///
-    /// The last newline is not trimmed so the produced strings are reusable between
-    /// early and late doc link resolution regardless of their position.
-    pub(crate) fn prepare_to_doc_link_resolution(&self) -> FxHashMap<Option<DefId>, String> {
-        let mut res = FxHashMap::default();
-        for fragment in &self.doc_strings {
-            let out_str = res.entry(fragment.parent_module).or_default();
-            add_doc_fragment(out_str, fragment);
-        }
-        res
-    }
-
     /// Finds all `doc` attributes as NameValues and returns their corresponding values, joined
     /// with newlines.
     pub(crate) fn collapsed_doc_value(&self) -> Option<String> {
diff --git a/src/librustdoc/clean/types/tests.rs b/src/librustdoc/clean/types/tests.rs
index 71eddf4348f..20627c2cfc1 100644
--- a/src/librustdoc/clean/types/tests.rs
+++ b/src/librustdoc/clean/types/tests.rs
@@ -2,6 +2,7 @@ use super::*;
 
 use crate::clean::collapse_doc_fragments;
 
+use rustc_resolve::rustdoc::{unindent_doc_fragments, DocFragment, DocFragmentKind};
 use rustc_span::create_default_session_globals_then;
 use rustc_span::source_map::DUMMY_SP;
 use rustc_span::symbol::Symbol;
diff --git a/src/librustdoc/core.rs b/src/librustdoc/core.rs
index 0ce43f7db8e..5ab7056be44 100644
--- a/src/librustdoc/core.rs
+++ b/src/librustdoc/core.rs
@@ -1,23 +1,22 @@
-use rustc_ast::NodeId;
 use rustc_data_structures::fx::{FxHashMap, FxHashSet};
 use rustc_data_structures::sync::{self, Lrc};
 use rustc_data_structures::unord::UnordSet;
 use rustc_errors::emitter::{Emitter, EmitterWriter};
 use rustc_errors::json::JsonEmitter;
+use rustc_errors::TerminalUrl;
 use rustc_feature::UnstableFeatures;
-use rustc_hir::def::{Namespace, Res};
+use rustc_hir::def::Res;
 use rustc_hir::def_id::{DefId, DefIdMap, DefIdSet, LocalDefId};
 use rustc_hir::intravisit::{self, Visitor};
-use rustc_hir::{HirId, Path, TraitCandidate};
+use rustc_hir::{HirId, Path};
 use rustc_interface::interface;
 use rustc_middle::hir::nested_filter;
 use rustc_middle::ty::{ParamEnv, Ty, TyCtxt};
-use rustc_resolve as resolve;
-use rustc_session::config::{self, CrateType, ErrorOutputType};
+use rustc_session::config::{self, CrateType, ErrorOutputType, ResolveDocLinks};
 use rustc_session::lint;
 use rustc_session::Session;
 use rustc_span::symbol::sym;
-use rustc_span::{source_map, Span, Symbol};
+use rustc_span::{source_map, Span};
 
 use std::cell::RefCell;
 use std::mem;
@@ -28,31 +27,12 @@ use crate::clean::inline::build_external_trait;
 use crate::clean::{self, ItemId};
 use crate::config::{Options as RustdocOptions, OutputFormat, RenderOptions};
 use crate::formats::cache::Cache;
-use crate::passes::collect_intra_doc_links::PreprocessedMarkdownLink;
 use crate::passes::{self, Condition::*};
 
 pub(crate) use rustc_session::config::{Input, Options, UnstableOptions};
 
-pub(crate) struct ResolverCaches {
-    pub(crate) markdown_links: Option<FxHashMap<String, Vec<PreprocessedMarkdownLink>>>,
-    pub(crate) doc_link_resolutions: FxHashMap<(Symbol, Namespace, DefId), Option<Res<NodeId>>>,
-    /// Traits in scope for a given module.
-    /// See `collect_intra_doc_links::traits_implemented_by` for more details.
-    pub(crate) traits_in_scope: DefIdMap<Vec<TraitCandidate>>,
-    pub(crate) all_trait_impls: Option<Vec<DefId>>,
-    pub(crate) all_macro_rules: FxHashMap<Symbol, Res<NodeId>>,
-    pub(crate) extern_doc_reachable: DefIdSet,
-}
-
 pub(crate) struct DocContext<'tcx> {
     pub(crate) tcx: TyCtxt<'tcx>,
-    /// Name resolver. Used for intra-doc links.
-    ///
-    /// The `Rc<RefCell<...>>` wrapping is needed because that is what's returned by
-    /// [`rustc_interface::Queries::expansion()`].
-    // FIXME: see if we can get rid of this RefCell somehow
-    pub(crate) resolver: Rc<RefCell<interface::BoxedResolver>>,
-    pub(crate) resolver_caches: ResolverCaches,
     /// Used for normalization.
     ///
     /// Most of this logic is copied from rustc_lint::late.
@@ -100,13 +80,6 @@ impl<'tcx> DocContext<'tcx> {
         ret
     }
 
-    pub(crate) fn enter_resolver<F, R>(&self, f: F) -> R
-    where
-        F: FnOnce(&mut resolve::Resolver<'_>) -> R,
-    {
-        self.resolver.borrow_mut().access(f)
-    }
-
     /// Call the closure with the given parameters set as
     /// the substitutions for a type alias' RHS.
     pub(crate) fn enter_alias<F, R>(&mut self, substs: DefIdMap<clean::SubstParam>, f: F) -> R
@@ -130,12 +103,6 @@ impl<'tcx> DocContext<'tcx> {
             _ => None,
         }
     }
-
-    pub(crate) fn with_all_trait_impls(&mut self, f: impl FnOnce(&mut Self, &[DefId])) {
-        let all_trait_impls = self.resolver_caches.all_trait_impls.take();
-        f(self, all_trait_impls.as_ref().expect("`all_trait_impls` are already borrowed"));
-        self.resolver_caches.all_trait_impls = all_trait_impls;
-    }
 }
 
 /// Creates a new diagnostic `Handler` that can be used to emit warnings and errors.
@@ -164,6 +131,7 @@ pub(crate) fn new_handler(
                     diagnostic_width,
                     false,
                     unstable_opts.track_diagnostics,
+                    TerminalUrl::No,
                 )
                 .ui_testing(unstable_opts.ui_testing),
             )
@@ -183,6 +151,7 @@ pub(crate) fn new_handler(
                     diagnostic_width,
                     false,
                     unstable_opts.track_diagnostics,
+                    TerminalUrl::No,
                 )
                 .ui_testing(unstable_opts.ui_testing),
             )
@@ -218,6 +187,7 @@ pub(crate) fn create_config(
         scrape_examples_options,
         ..
     }: RustdocOptions,
+    RenderOptions { document_private, .. }: &RenderOptions,
 ) -> rustc_interface::Config {
     // Add the doc cfg into the doc build.
     cfgs.push("doc".to_string());
@@ -245,6 +215,13 @@ pub(crate) fn create_config(
 
     let crate_types =
         if proc_macro_crate { vec![CrateType::ProcMacro] } else { vec![CrateType::Rlib] };
+    let resolve_doc_links = if *document_private {
+        ResolveDocLinks::All
+    } else {
+        // Should be `ResolveDocLinks::Exported` in theory, but for some reason rustdoc
+        // still tries to request resolutions for links on private items.
+        ResolveDocLinks::All
+    };
     let test = scrape_examples_options.map(|opts| opts.scrape_tests).unwrap_or(false);
     // plays with error output here!
     let sessopts = config::Options {
@@ -258,6 +235,7 @@ pub(crate) fn create_config(
         target_triple: target,
         unstable_features: UnstableFeatures::from_environment(crate_name.as_deref()),
         actually_rustdoc: true,
+        resolve_doc_links,
         unstable_opts,
         error_format,
         diagnostic_width,
@@ -313,8 +291,6 @@ pub(crate) fn create_config(
 
 pub(crate) fn run_global_ctxt(
     tcx: TyCtxt<'_>,
-    resolver: Rc<RefCell<interface::BoxedResolver>>,
-    resolver_caches: ResolverCaches,
     show_coverage: bool,
     render_options: RenderOptions,
     output_format: OutputFormat,
@@ -348,8 +324,6 @@ pub(crate) fn run_global_ctxt(
 
     let mut ctxt = DocContext {
         tcx,
-        resolver,
-        resolver_caches,
         param_env: ParamEnv::empty(),
         external_traits: Default::default(),
         active_extern_traits: Default::default(),
@@ -364,9 +338,9 @@ pub(crate) fn run_global_ctxt(
         show_coverage,
     };
 
-    ctxt.cache
-        .effective_visibilities
-        .init(mem::take(&mut ctxt.resolver_caches.extern_doc_reachable));
+    for cnum in tcx.crates(()) {
+        crate::visit_lib::lib_embargo_visit_item(&mut ctxt, cnum.as_def_id());
+    }
 
     // Small hack to force the Sized trait to be present.
     //
diff --git a/src/librustdoc/doctest.rs b/src/librustdoc/doctest.rs
index 37a1005cba1..57c41b57311 100644
--- a/src/librustdoc/doctest.rs
+++ b/src/librustdoc/doctest.rs
@@ -1,7 +1,7 @@
 use rustc_ast as ast;
 use rustc_data_structures::fx::{FxHashMap, FxHashSet};
 use rustc_data_structures::sync::Lrc;
-use rustc_errors::{ColorConfig, ErrorGuaranteed, FatalError};
+use rustc_errors::{ColorConfig, ErrorGuaranteed, FatalError, TerminalUrl};
 use rustc_hir::def_id::{LocalDefId, CRATE_DEF_ID, LOCAL_CRATE};
 use rustc_hir::{self as hir, intravisit, CRATE_HIR_ID};
 use rustc_interface::interface;
@@ -557,6 +557,7 @@ pub(crate) fn make_test(
                 Some(80),
                 false,
                 false,
+                TerminalUrl::No,
             )
             .supports_color();
 
@@ -571,6 +572,7 @@ pub(crate) fn make_test(
                 None,
                 false,
                 false,
+                TerminalUrl::No,
             );
 
             // FIXME(misdreavus): pass `-Z treat-err-as-bug` to the doctest parser
@@ -756,6 +758,7 @@ fn check_if_attr_is_complete(source: &str, edition: Edition) -> bool {
                 None,
                 false,
                 false,
+                TerminalUrl::No,
             );
 
             let handler = Handler::with_emitter(false, None, Box::new(emitter));
diff --git a/src/librustdoc/formats/item_type.rs b/src/librustdoc/formats/item_type.rs
index 2f1f4cbf359..aafedc17499 100644
--- a/src/librustdoc/formats/item_type.rs
+++ b/src/librustdoc/formats/item_type.rs
@@ -21,6 +21,7 @@ use crate::clean;
 /// a heading, edit the listing in `html/render.rs`, function `sidebar_module`. This uses an
 /// ordering based on a helper function inside `item_module`, in the same file.
 #[derive(Copy, PartialEq, Eq, Hash, Clone, Debug, PartialOrd, Ord)]
+#[repr(u8)]
 pub(crate) enum ItemType {
     Module = 0,
     ExternCrate = 1,
diff --git a/src/librustdoc/html/format.rs b/src/librustdoc/html/format.rs
index 8a7a8ea5fd1..314f0612249 100644
--- a/src/librustdoc/html/format.rs
+++ b/src/librustdoc/html/format.rs
@@ -34,6 +34,7 @@ use crate::clean::{
 use crate::formats::item_type::ItemType;
 use crate::html::escape::Escape;
 use crate::html::render::Context;
+use crate::passes::collect_intra_doc_links::UrlFragment;
 
 use super::url_parts_builder::estimate_item_path_byte_length;
 use super::url_parts_builder::UrlPartsBuilder;
@@ -768,6 +769,21 @@ pub(crate) fn href_relative_parts<'fqp>(
     }
 }
 
+pub(crate) fn link_tooltip(did: DefId, fragment: &Option<UrlFragment>, cx: &Context<'_>) -> String {
+    let cache = cx.cache();
+    let Some((fqp, shortty)) = cache.paths.get(&did)
+        .or_else(|| cache.external_paths.get(&did))
+        else { return String::new() };
+    let fqp = fqp.iter().map(|sym| sym.as_str()).join("::");
+    if let &Some(UrlFragment::Item(id)) = fragment {
+        let name = cx.tcx().item_name(id);
+        let descr = cx.tcx().def_kind(id).descr(id);
+        format!("{descr} {fqp}::{name}")
+    } else {
+        format!("{shortty} {fqp}")
+    }
+}
+
 /// Used to render a [`clean::Path`].
 fn resolved_path<'cx>(
     w: &mut fmt::Formatter<'_>,
diff --git a/src/librustdoc/html/highlight.rs b/src/librustdoc/html/highlight.rs
index 1644d1c5a29..3c10eb524d7 100644
--- a/src/librustdoc/html/highlight.rs
+++ b/src/librustdoc/html/highlight.rs
@@ -96,13 +96,19 @@ fn write_header(out: &mut Buffer, class: &str, extra_content: Option<Buffer>, to
     );
 
     if tooltip != Tooltip::None {
+        let edition_code;
         write!(
             out,
-            "<div class='tooltip'{}>ⓘ</div>",
-            if let Tooltip::Edition(edition_info) = tooltip {
-                format!(" data-edition=\"{}\"", edition_info)
-            } else {
-                String::new()
+            "<a href=\"#\" class=\"tooltip\" title=\"{}\">ⓘ</a>",
+            match tooltip {
+                Tooltip::Ignore => "This example is not tested",
+                Tooltip::CompileFail => "This example deliberately fails to compile",
+                Tooltip::ShouldPanic => "This example panics",
+                Tooltip::Edition(edition) => {
+                    edition_code = format!("This example runs with edition {edition}");
+                    &edition_code
+                }
+                Tooltip::None => unreachable!(),
             },
         );
     }
diff --git a/src/librustdoc/html/markdown.rs b/src/librustdoc/html/markdown.rs
index fb7c34118a4..e4adee6ae4d 100644
--- a/src/librustdoc/html/markdown.rs
+++ b/src/librustdoc/html/markdown.rs
@@ -28,6 +28,7 @@
 use rustc_data_structures::fx::FxHashMap;
 use rustc_hir::def_id::DefId;
 use rustc_middle::ty::TyCtxt;
+pub(crate) use rustc_resolve::rustdoc::main_body_opts;
 use rustc_span::edition::Edition;
 use rustc_span::{Span, Symbol};
 
@@ -46,6 +47,7 @@ use crate::html::escape::Escape;
 use crate::html::format::Buffer;
 use crate::html::highlight;
 use crate::html::length_limit::HtmlWithLimit;
+use crate::html::render::small_url_encode;
 use crate::html::toc::TocBuilder;
 
 use pulldown_cmark::{
@@ -57,15 +59,6 @@ mod tests;
 
 const MAX_HEADER_LEVEL: u32 = 6;
 
-/// Options for rendering Markdown in the main body of documentation.
-pub(crate) fn main_body_opts() -> Options {
-    Options::ENABLE_TABLES
-        | Options::ENABLE_FOOTNOTES
-        | Options::ENABLE_STRIKETHROUGH
-        | Options::ENABLE_TASKLISTS
-        | Options::ENABLE_SMART_PUNCTUATION
-}
-
 /// Options for rendering Markdown in summaries (e.g., in search results).
 pub(crate) fn summary_opts() -> Options {
     Options::ENABLE_TABLES
@@ -294,47 +287,7 @@ impl<'a, I: Iterator<Item = Event<'a>>> Iterator for CodeBlocks<'_, 'a, I> {
                 doctest::make_test(&test, krate, false, &Default::default(), edition, None);
             let channel = if test.contains("#![feature(") { "&amp;version=nightly" } else { "" };
 
-            // These characters don't need to be escaped in a URI.
-            // See https://url.spec.whatwg.org/#query-percent-encode-set
-            // and https://url.spec.whatwg.org/#urlencoded-parsing
-            // and https://url.spec.whatwg.org/#url-code-points
-            fn dont_escape(c: u8) -> bool {
-                (b'a' <= c && c <= b'z')
-                    || (b'A' <= c && c <= b'Z')
-                    || (b'0' <= c && c <= b'9')
-                    || c == b'-'
-                    || c == b'_'
-                    || c == b'.'
-                    || c == b','
-                    || c == b'~'
-                    || c == b'!'
-                    || c == b'\''
-                    || c == b'('
-                    || c == b')'
-                    || c == b'*'
-                    || c == b'/'
-                    || c == b';'
-                    || c == b':'
-                    || c == b'?'
-                    // As described in urlencoded-parsing, the
-                    // first `=` is the one that separates key from
-                    // value. Following `=`s are part of the value.
-                    || c == b'='
-            }
-            let mut test_escaped = String::new();
-            for b in test.bytes() {
-                if dont_escape(b) {
-                    test_escaped.push(char::from(b));
-                } else if b == b' ' {
-                    // URL queries are decoded with + replaced with SP
-                    test_escaped.push('+');
-                } else if b == b'%' {
-                    test_escaped.push('%');
-                    test_escaped.push('%');
-                } else {
-                    write!(test_escaped, "%{:02X}", b).unwrap();
-                }
-            }
+            let test_escaped = small_url_encode(test);
             Some(format!(
                 r#"<a class="test-arrow" target="_blank" href="{}?code={}{}&amp;edition={}">Run</a>"#,
                 url, test_escaped, channel, edition,
@@ -407,6 +360,9 @@ impl<'a, I: Iterator<Item = Event<'a>>> Iterator for LinkReplacer<'a, I> {
                     trace!("it matched");
                     assert!(self.shortcut_link.is_none(), "shortcut links cannot be nested");
                     self.shortcut_link = Some(link);
+                    if title.is_empty() && !link.tooltip.is_empty() {
+                        *title = CowStr::Borrowed(link.tooltip.as_ref());
+                    }
                 }
             }
             // Now that we're done with the shortcut link, don't replace any more text.
@@ -457,9 +413,12 @@ impl<'a, I: Iterator<Item = Event<'a>>> Iterator for LinkReplacer<'a, I> {
             }
             // If this is a link, but not a shortcut link,
             // replace the URL, since the broken_link_callback was not called.
-            Some(Event::Start(Tag::Link(_, dest, _))) => {
+            Some(Event::Start(Tag::Link(_, dest, title))) => {
                 if let Some(link) = self.links.iter().find(|&link| *link.original_text == **dest) {
                     *dest = CowStr::Borrowed(link.href.as_ref());
+                    if title.is_empty() && !link.tooltip.is_empty() {
+                        *title = CowStr::Borrowed(link.tooltip.as_ref());
+                    }
                 }
             }
             // Anything else couldn't have been a valid Rust path, so no need to replace the text.
@@ -787,11 +746,7 @@ pub(crate) fn find_testable_code<T: doctest::Tester>(
             }
             Event::Text(ref s) if register_header.is_some() => {
                 let level = register_header.unwrap();
-                if s.is_empty() {
-                    tests.register_header("", level);
-                } else {
-                    tests.register_header(s, level);
-                }
+                tests.register_header(s, level);
                 register_header = None;
             }
             _ => {}
@@ -1027,7 +982,7 @@ impl Markdown<'_> {
             links
                 .iter()
                 .find(|link| link.original_text.as_str() == &*broken_link.reference)
-                .map(|link| (link.href.as_str().into(), link.new_text.as_str().into()))
+                .map(|link| (link.href.as_str().into(), link.tooltip.as_str().into()))
         };
 
         let p = Parser::new_with_broken_link_callback(md, main_body_opts(), Some(&mut replacer));
@@ -1110,7 +1065,7 @@ impl MarkdownSummaryLine<'_> {
             links
                 .iter()
                 .find(|link| link.original_text.as_str() == &*broken_link.reference)
-                .map(|link| (link.href.as_str().into(), link.new_text.as_str().into()))
+                .map(|link| (link.href.as_str().into(), link.tooltip.as_str().into()))
         };
 
         let p = Parser::new_with_broken_link_callback(md, summary_opts(), Some(&mut replacer))
@@ -1157,7 +1112,7 @@ fn markdown_summary_with_limit(
         link_names
             .iter()
             .find(|link| link.original_text.as_str() == &*broken_link.reference)
-            .map(|link| (link.href.as_str().into(), link.new_text.as_str().into()))
+            .map(|link| (link.href.as_str().into(), link.tooltip.as_str().into()))
     };
 
     let p = Parser::new_with_broken_link_callback(md, summary_opts(), Some(&mut replacer));
@@ -1227,14 +1182,23 @@ pub(crate) fn short_markdown_summary(markdown: &str, link_names: &[RenderedLink]
 /// - Headings, links, and formatting are stripped.
 /// - Inline code is rendered as-is, surrounded by backticks.
 /// - HTML and code blocks are ignored.
-pub(crate) fn plain_text_summary(md: &str) -> String {
+pub(crate) fn plain_text_summary(md: &str, link_names: &[RenderedLink]) -> String {
     if md.is_empty() {
         return String::new();
     }
 
     let mut s = String::with_capacity(md.len() * 3 / 2);
 
-    for event in Parser::new_ext(md, summary_opts()) {
+    let mut replacer = |broken_link: BrokenLink<'_>| {
+        link_names
+            .iter()
+            .find(|link| link.original_text.as_str() == &*broken_link.reference)
+            .map(|link| (link.href.as_str().into(), link.tooltip.as_str().into()))
+    };
+
+    let p = Parser::new_with_broken_link_callback(md, summary_opts(), Some(&mut replacer));
+
+    for event in p {
         match &event {
             Event::Text(text) => s.push_str(text),
             Event::Code(code) => {
diff --git a/src/librustdoc/html/markdown/tests.rs b/src/librustdoc/html/markdown/tests.rs
index 5878c58264e..e05635a0207 100644
--- a/src/librustdoc/html/markdown/tests.rs
+++ b/src/librustdoc/html/markdown/tests.rs
@@ -249,7 +249,7 @@ fn test_short_markdown_summary() {
 #[test]
 fn test_plain_text_summary() {
     fn t(input: &str, expect: &str) {
-        let output = plain_text_summary(input);
+        let output = plain_text_summary(input, &[]);
         assert_eq!(output, expect, "original: {}", input);
     }
 
diff --git a/src/librustdoc/html/render/context.rs b/src/librustdoc/html/render/context.rs
index b59645ec2e2..6762fba9275 100644
--- a/src/librustdoc/html/render/context.rs
+++ b/src/librustdoc/html/render/context.rs
@@ -18,7 +18,7 @@ use super::search_index::build_index;
 use super::write_shared::write_shared;
 use super::{
     collect_spans_and_sources, print_sidebar, scrape_examples_help, sidebar_module_like, AllTypes,
-    LinkFromSrc, NameDoc, StylePath,
+    LinkFromSrc, StylePath,
 };
 
 use crate::clean::{self, types::ExternalLocation, ExternalCrate};
@@ -182,7 +182,10 @@ impl<'tcx> Context<'tcx> {
         };
         title.push_str(" - Rust");
         let tyname = it.type_();
-        let desc = it.doc_value().as_ref().map(|doc| plain_text_summary(doc));
+        let desc = it
+            .doc_value()
+            .as_ref()
+            .map(|doc| plain_text_summary(doc, &it.link_names(&self.cache())));
         let desc = if let Some(desc) = desc {
             desc
         } else if it.is_crate() {
@@ -256,7 +259,7 @@ impl<'tcx> Context<'tcx> {
     }
 
     /// Construct a map of items shown in the sidebar to a plain-text summary of their docs.
-    fn build_sidebar_items(&self, m: &clean::Module) -> BTreeMap<String, Vec<NameDoc>> {
+    fn build_sidebar_items(&self, m: &clean::Module) -> BTreeMap<String, Vec<String>> {
         // BTreeMap instead of HashMap to get a sorted output
         let mut map: BTreeMap<_, Vec<_>> = BTreeMap::new();
         let mut inserted: FxHashMap<ItemType, FxHashSet<Symbol>> = FxHashMap::default();
@@ -274,10 +277,7 @@ impl<'tcx> Context<'tcx> {
             if inserted.entry(short).or_default().insert(myname) {
                 let short = short.to_string();
                 let myname = myname.to_string();
-                map.entry(short).or_default().push((
-                    myname,
-                    Some(item.doc_value().map_or_else(String::new, |s| plain_text_summary(&s))),
-                ));
+                map.entry(short).or_default().push(myname);
             }
         }
 
diff --git a/src/librustdoc/html/render/mod.rs b/src/librustdoc/html/render/mod.rs
index 816a8f4e274..2086faf78ac 100644
--- a/src/librustdoc/html/render/mod.rs
+++ b/src/librustdoc/html/render/mod.rs
@@ -38,7 +38,7 @@ pub(crate) use self::span_map::{collect_spans_and_sources, LinkFromSrc};
 
 use std::collections::VecDeque;
 use std::default::Default;
-use std::fmt;
+use std::fmt::{self, Write};
 use std::fs;
 use std::iter::Peekable;
 use std::path::PathBuf;
@@ -83,9 +83,6 @@ use crate::scrape_examples::{CallData, CallLocation};
 use crate::try_none;
 use crate::DOC_RUST_LANG_ORG_CHANNEL;
 
-/// A pair of name and its optional document.
-pub(crate) type NameDoc = (String, Option<String>);
-
 pub(crate) fn ensure_trailing_slash(v: &str) -> impl fmt::Display + '_ {
     crate::html::format::display_fn(move |f| {
         if !v.ends_with('/') && !v.is_empty() { write!(f, "{}/", v) } else { f.write_str(v) }
@@ -1313,7 +1310,7 @@ pub(crate) fn notable_traits_button(ty: &clean::Type, cx: &mut Context<'_>) -> O
     if has_notable_trait {
         cx.types_with_notable_traits.insert(ty.clone());
         Some(format!(
-            " <a href=\"#\" class=\"notable-traits\" data-ty=\"{ty}\">ⓘ</a>",
+            " <a href=\"#\" class=\"tooltip\" data-notable-ty=\"{ty}\">ⓘ</a>",
             ty = Escape(&format!("{:#}", ty.print(cx))),
         ))
     } else {
@@ -2020,31 +2017,60 @@ fn get_associated_constants(
         .collect::<Vec<_>>()
 }
 
-// The point is to url encode any potential character from a type with genericity.
-fn small_url_encode(s: String) -> String {
+pub(crate) fn small_url_encode(s: String) -> String {
+    // These characters don't need to be escaped in a URI.
+    // See https://url.spec.whatwg.org/#query-percent-encode-set
+    // and https://url.spec.whatwg.org/#urlencoded-parsing
+    // and https://url.spec.whatwg.org/#url-code-points
+    fn dont_escape(c: u8) -> bool {
+        (b'a' <= c && c <= b'z')
+            || (b'A' <= c && c <= b'Z')
+            || (b'0' <= c && c <= b'9')
+            || c == b'-'
+            || c == b'_'
+            || c == b'.'
+            || c == b','
+            || c == b'~'
+            || c == b'!'
+            || c == b'\''
+            || c == b'('
+            || c == b')'
+            || c == b'*'
+            || c == b'/'
+            || c == b';'
+            || c == b':'
+            || c == b'?'
+            // As described in urlencoded-parsing, the
+            // first `=` is the one that separates key from
+            // value. Following `=`s are part of the value.
+            || c == b'='
+    }
     let mut st = String::new();
     let mut last_match = 0;
-    for (idx, c) in s.char_indices() {
-        let escaped = match c {
-            '<' => "%3C",
-            '>' => "%3E",
-            ' ' => "%20",
-            '?' => "%3F",
-            '\'' => "%27",
-            '&' => "%26",
-            ',' => "%2C",
-            ':' => "%3A",
-            ';' => "%3B",
-            '[' => "%5B",
-            ']' => "%5D",
-            '"' => "%22",
-            _ => continue,
-        };
+    for (idx, b) in s.bytes().enumerate() {
+        if dont_escape(b) {
+            continue;
+        }
 
-        st += &s[last_match..idx];
-        st += escaped;
-        // NOTE: we only expect single byte characters here - which is fine as long as we
-        // only match single byte characters
+        if last_match != idx {
+            // Invariant: `idx` must be the first byte in a character at this point.
+            st += &s[last_match..idx];
+        }
+        if b == b' ' {
+            // URL queries are decoded with + replaced with SP.
+            // While the same is not true for hashes, rustdoc only needs to be
+            // consistent with itself when encoding them.
+            st += "+";
+        } else if b == b'%' {
+            st += "%%";
+        } else {
+            write!(st, "%{:02X}", b).unwrap();
+        }
+        // Invariant: if the current byte is not at the start of a multi-byte character,
+        // we need to get down here so that when the next turn of the loop comes around,
+        // last_match winds up equalling idx.
+        //
+        // In other words, dont_escape must always return `false` in multi-byte character.
         last_match = idx + 1;
     }
 
diff --git a/src/librustdoc/html/render/print_item.rs b/src/librustdoc/html/render/print_item.rs
index c2d24e51484..4ff1c93dc5d 100644
--- a/src/librustdoc/html/render/print_item.rs
+++ b/src/librustdoc/html/render/print_item.rs
@@ -10,7 +10,7 @@ use rustc_middle::ty::layout::LayoutError;
 use rustc_middle::ty::{self, Adt, TyCtxt};
 use rustc_span::hygiene::MacroKind;
 use rustc_span::symbol::{kw, sym, Symbol};
-use rustc_target::abi::{LayoutS, Primitive, TagEncoding, VariantIdx, Variants};
+use rustc_target::abi::{LayoutS, Primitive, TagEncoding, Variants};
 use std::cmp::Ordering;
 use std::fmt;
 use std::rc::Rc;
@@ -1081,10 +1081,10 @@ fn item_typedef(w: &mut Buffer, cx: &mut Context<'_>, it: &clean::Item, t: &clea
     fn write_content(w: &mut Buffer, cx: &Context<'_>, it: &clean::Item, t: &clean::Typedef) {
         wrap_item(w, |w| {
             render_attributes_in_pre(w, it, "");
-            write!(w, "{}", visibility_print_with_space(it.visibility(cx.tcx()), it.item_id, cx));
             write!(
                 w,
-                "type {}{}{where_clause} = {type_};",
+                "{}type {}{}{where_clause} = {type_};",
+                visibility_print_with_space(it.visibility(cx.tcx()), it.item_id, cx),
                 it.name.unwrap(),
                 t.generics.print(cx),
                 where_clause = print_where_clause(&t.generics, cx, 0, Ending::Newline),
@@ -1109,7 +1109,7 @@ fn item_typedef(w: &mut Buffer, cx: &mut Context<'_>, it: &clean::Item, t: &clea
 fn item_union(w: &mut Buffer, cx: &mut Context<'_>, it: &clean::Item, s: &clean::Union) {
     wrap_item(w, |w| {
         render_attributes_in_pre(w, it, "");
-        render_union(w, it, Some(&s.generics), &s.fields, "", cx);
+        render_union(w, it, Some(&s.generics), &s.fields, cx);
     });
 
     document(w, cx, it, None, HeadingOffset::H2);
@@ -1138,13 +1138,11 @@ fn item_union(w: &mut Buffer, cx: &mut Context<'_>, it: &clean::Item, s: &clean:
                      <a href=\"#{id}\" class=\"anchor field\">§</a>\
                      <code>{name}: {ty}</code>\
                  </span>",
-                id = id,
-                name = name,
                 shortty = ItemType::StructField,
                 ty = ty.print(cx),
             );
             if let Some(stability_class) = field.stability_class(cx.tcx()) {
-                write!(w, "<span class=\"stab {stab}\"></span>", stab = stability_class);
+                write!(w, "<span class=\"stab {stability_class}\"></span>");
             }
             document(w, cx, field, Some(it), HeadingOffset::H3);
         }
@@ -1242,7 +1240,6 @@ fn item_enum(w: &mut Buffer, cx: &mut Context<'_>, it: &clean::Item, e: &clean::
                 w,
                 "<section id=\"{id}\" class=\"variant\">\
                     <a href=\"#{id}\" class=\"anchor\">§</a>",
-                id = id,
             );
             render_stability_since_raw_with_extra(
                 w,
@@ -1280,8 +1277,11 @@ fn item_enum(w: &mut Buffer, cx: &mut Context<'_>, it: &clean::Item, e: &clean::
             if let Some((heading, fields)) = heading_and_fields {
                 let variant_id =
                     cx.derive_id(format!("{}.{}.fields", ItemType::Variant, variant.name.unwrap()));
-                write!(w, "<div class=\"sub-variant\" id=\"{id}\">", id = variant_id);
-                write!(w, "<h4>{heading}</h4>", heading = heading);
+                write!(
+                    w,
+                    "<div class=\"sub-variant\" id=\"{variant_id}\">\
+                        <h4>{heading}</h4>",
+                );
                 document_non_exhaustive(w, variant);
                 for field in fields {
                     match *field.kind {
@@ -1299,7 +1299,6 @@ fn item_enum(w: &mut Buffer, cx: &mut Context<'_>, it: &clean::Item, e: &clean::
                                      <a href=\"#{id}\" class=\"anchor field\">§</a>\
                                      <code>{f}: {t}</code>\
                                  </span>",
-                                id = id,
                                 f = field.name.unwrap(),
                                 t = ty.print(cx)
                             );
@@ -1450,11 +1449,9 @@ fn item_struct(w: &mut Buffer, cx: &mut Context<'_>, it: &clean::Item, s: &clean
                     w,
                     "<span id=\"{id}\" class=\"{item_type} small-section-header\">\
                          <a href=\"#{id}\" class=\"anchor field\">§</a>\
-                         <code>{name}: {ty}</code>\
+                         <code>{field_name}: {ty}</code>\
                      </span>",
                     item_type = ItemType::StructField,
-                    id = id,
-                    name = field_name,
                     ty = ty.print(cx)
                 );
                 document(w, cx, field, Some(it), HeadingOffset::H3);
@@ -1628,7 +1625,6 @@ fn render_union(
     it: &clean::Item,
     g: Option<&clean::Generics>,
     fields: &[clean::Item],
-    tab: &str,
     cx: &Context<'_>,
 ) {
     let tcx = cx.tcx();
@@ -1651,7 +1647,7 @@ fn render_union(
         w.write_str(" ");
     }
 
-    write!(w, "{{\n{}", tab);
+    write!(w, "{{\n");
     let count_fields =
         fields.iter().filter(|f| matches!(*f.kind, clean::StructFieldItem(..))).count();
     let toggle = should_hide_fields(count_fields);
@@ -1663,17 +1659,16 @@ fn render_union(
         if let clean::StructFieldItem(ref ty) = *field.kind {
             write!(
                 w,
-                "    {}{}: {},\n{}",
+                "    {}{}: {},\n",
                 visibility_print_with_space(field.visibility(tcx), field.item_id, cx),
                 field.name.unwrap(),
-                ty.print(cx),
-                tab
+                ty.print(cx)
             );
         }
     }
 
     if it.has_stripped_entries().unwrap() {
-        write!(w, "    /* private fields */\n{}", tab);
+        write!(w, "    /* private fields */\n");
     }
     if toggle {
         toggle_close(w);
@@ -1838,12 +1833,12 @@ fn document_non_exhaustive(w: &mut Buffer, item: &clean::Item) {
 }
 
 fn document_type_layout(w: &mut Buffer, cx: &Context<'_>, ty_def_id: DefId) {
-    fn write_size_of_layout(w: &mut Buffer, layout: &LayoutS<VariantIdx>, tag_size: u64) {
+    fn write_size_of_layout(w: &mut Buffer, layout: &LayoutS, tag_size: u64) {
         if layout.abi.is_unsized() {
             write!(w, "(unsized)");
         } else {
-            let bytes = layout.size.bytes() - tag_size;
-            write!(w, "{size} byte{pl}", size = bytes, pl = if bytes == 1 { "" } else { "s" },);
+            let size = layout.size.bytes() - tag_size;
+            write!(w, "{size} byte{pl}", pl = if size == 1 { "" } else { "s" },);
         }
     }
 
@@ -1898,7 +1893,7 @@ fn document_type_layout(w: &mut Buffer, cx: &Context<'_>, ty_def_id: DefId) {
 
                     for (index, layout) in variants.iter_enumerated() {
                         let name = adt.variant(index).name;
-                        write!(w, "<li><code>{name}</code>: ", name = name);
+                        write!(w, "<li><code>{name}</code>: ");
                         write_size_of_layout(w, layout, tag_size);
                         writeln!(w, "</li>");
                     }
diff --git a/src/librustdoc/html/render/search_index.rs b/src/librustdoc/html/render/search_index.rs
index 5b0caac099b..090ea2cb157 100644
--- a/src/librustdoc/html/render/search_index.rs
+++ b/src/librustdoc/html/render/search_index.rs
@@ -236,7 +236,16 @@ pub(crate) fn build_index<'tcx>(
             crate_data.serialize_field("doc", &self.doc)?;
             crate_data.serialize_field(
                 "t",
-                &self.items.iter().map(|item| &item.ty).collect::<Vec<_>>(),
+                &self
+                    .items
+                    .iter()
+                    .map(|item| {
+                        let n = item.ty as u8;
+                        let c = char::try_from(n + b'A').expect("item types must fit in ASCII");
+                        assert!(c <= 'z', "item types must fit within ASCII printables");
+                        c
+                    })
+                    .collect::<String>(),
             )?;
             crate_data.serialize_field(
                 "n",
diff --git a/src/librustdoc/html/static/css/rustdoc.css b/src/librustdoc/html/static/css/rustdoc.css
index 2a9548712f0..476dd606680 100644
--- a/src/librustdoc/html/static/css/rustdoc.css
+++ b/src/librustdoc/html/static/css/rustdoc.css
@@ -174,6 +174,14 @@ h1, h2, h3, h4 {
 .top-doc .docblock > h4 {
 	border-bottom: 1px solid var(--headings-border-bottom-color);
 }
+/* while line-height 1.5 is required for any "block of text",
+	which WCAG defines as more than one sentence, it looks weird for
+	very large main headers */
+h1, h2 {
+	line-height: 1.25;
+	padding-top: 3px;
+	padding-bottom: 9px;
+}
 h3.code-header {
 	font-size: 1.125rem; /* 18px */
 }
@@ -707,8 +715,8 @@ h2.small-section-header > .anchor {
 .main-heading a:hover,
 .example-wrap > pre.rust a:hover,
 .all-items a:hover,
-.docblock a:not(.test-arrow):not(.scrape-help):hover,
-.docblock-short a:not(.test-arrow):not(.scrape-help):hover,
+.docblock a:not(.test-arrow):not(.scrape-help):not(.tooltip):hover,
+.docblock-short a:not(.test-arrow):not(.scrape-help):not(.tooltip):hover,
 .item-info a {
 	text-decoration: underline;
 }
@@ -1101,44 +1109,8 @@ pre.rust .doccomment {
 	display: block;
 	left: -25px;
 	top: 5px;
-}
-
-.example-wrap .tooltip:hover::after {
-	padding: 5px 3px 3px 3px;
-	border-radius: 6px;
-	margin-left: 5px;
-	font-size: 1rem;
-	border: 1px solid var(--border-color);
-	position: absolute;
-	width: max-content;
-	top: -2px;
-	z-index: 1;
-	background-color: var(--tooltip-background-color);
-	color: var(--tooltip-color);
-}
-
-.example-wrap .tooltip:hover::before {
-	content: " ";
-	position: absolute;
-	top: 50%;
-	left: 16px;
-	margin-top: -5px;
-	z-index: 1;
-	border: 5px solid transparent;
-	border-right-color: var(--tooltip-background-color);
-}
-
-.example-wrap.ignore .tooltip:hover::after {
-	content: "This example is not tested";
-}
-.example-wrap.compile_fail .tooltip:hover::after {
-	content: "This example deliberately fails to compile";
-}
-.example-wrap.should_panic .tooltip:hover::after {
-	content: "This example panics";
-}
-.example-wrap.edition .tooltip:hover::after {
-	content: "This code runs with edition " attr(data-edition);
+	margin: 0;
+	line-height: 1;
 }
 
 .example-wrap.compile_fail .tooltip,
@@ -1205,7 +1177,7 @@ a.test-arrow:hover {
 	border-right: 3px solid var(--target-border-color);
 }
 
-.notable-traits {
+.code-header a.tooltip {
 	color: inherit;
 	margin-right: 15px;
 	position: relative;
@@ -1214,7 +1186,7 @@ a.test-arrow:hover {
 /* placeholder thunk so that the mouse can easily travel from "(i)" to popover
 	the resulting "hover tunnel" is a stepped triangle, approximating
 	https://bjk5.com/post/44698559168/breaking-down-amazons-mega-dropdown */
-.notable-traits:hover::after {
+a.tooltip:hover::after {
 	position: absolute;
 	top: calc(100% - 10px);
 	left: -15px;
@@ -1223,11 +1195,11 @@ a.test-arrow:hover {
 	content: "\00a0";
 }
 
-.notable .content {
+.popover.tooltip .content {
 	margin: 0.25em 0.5em;
 }
 
-.notable .content pre, .notable .content code {
+.popover.tooltip .content pre, .popover.tooltip .content code {
 	background: transparent;
 	margin: 0;
 	padding: 0;
@@ -1235,7 +1207,7 @@ a.test-arrow:hover {
 	white-space: pre-wrap;
 }
 
-.notable .content > h3:first-child {
+.popover.tooltip .content > h3:first-child {
 	margin: 0 0 5px 0;
 }
 
diff --git a/src/librustdoc/html/static/css/themes/ayu.css b/src/librustdoc/html/static/css/themes/ayu.css
index b5a2cf7f28b..472a725f053 100644
--- a/src/librustdoc/html/static/css/themes/ayu.css
+++ b/src/librustdoc/html/static/css/themes/ayu.css
@@ -74,8 +74,6 @@ Original by Dempfi (https://github.com/dempfi/ayu)
 	--test-arrow-hover-background-color: rgba(57, 175, 215, 0.368);
 	--target-background-color: rgba(255, 236, 164, 0.06);
 	--target-border-color: rgba(255, 180, 76, 0.85);
-	--tooltip-background-color: #314559;
-	--tooltip-color: #c5c5c5;
 	--kbd-color: #c5c5c5;
 	--kbd-background: #314559;
 	--kbd-box-shadow-color: #5c6773;
diff --git a/src/librustdoc/html/static/css/themes/dark.css b/src/librustdoc/html/static/css/themes/dark.css
index b84d87c4a54..5612bde96a8 100644
--- a/src/librustdoc/html/static/css/themes/dark.css
+++ b/src/librustdoc/html/static/css/themes/dark.css
@@ -69,8 +69,6 @@
 	--test-arrow-hover-background-color: #4e8bca;
 	--target-background-color: #494a3d;
 	--target-border-color: #bb7410;
-	--tooltip-background-color: #000;
-	--tooltip-color: #fff;
 	--kbd-color: #000;
 	--kbd-background: #fafbfc;
 	--kbd-box-shadow-color: #c6cbd1;
diff --git a/src/librustdoc/html/static/css/themes/light.css b/src/librustdoc/html/static/css/themes/light.css
index 342274e6767..34b35c405a8 100644
--- a/src/librustdoc/html/static/css/themes/light.css
+++ b/src/librustdoc/html/static/css/themes/light.css
@@ -69,8 +69,6 @@
 	--test-arrow-hover-background-color: #4e8bca;
 	--target-background-color: #fdffd3;
 	--target-border-color: #ad7c37;
-	--tooltip-background-color: #000;
-	--tooltip-color: #fff;
 	--kbd-color: #000;
 	--kbd-background: #fafbfc;
 	--kbd-box-shadow-color: #c6cbd1;
diff --git a/src/librustdoc/html/static/js/main.js b/src/librustdoc/html/static/js/main.js
index c66f500f423..5e8c0e8d10c 100644
--- a/src/librustdoc/html/static/js/main.js
+++ b/src/librustdoc/html/static/js/main.js
@@ -378,7 +378,7 @@ function loadCss(cssUrl) {
         }
         ev.preventDefault();
         searchState.defocus();
-        window.hideAllModals(true); // true = reset focus for notable traits
+        window.hideAllModals(true); // true = reset focus for tooltips
     }
 
     function handleShortcut(ev) {
@@ -455,10 +455,7 @@ function loadCss(cssUrl) {
             const ul = document.createElement("ul");
             ul.className = "block " + shortty;
 
-            for (const item of filtered) {
-                const name = item[0];
-                const desc = item[1]; // can be null
-
+            for (const name of filtered) {
                 let path;
                 if (shortty === "mod") {
                     path = name + "/index.html";
@@ -468,7 +465,6 @@ function loadCss(cssUrl) {
                 const current_page = document.location.href.split("/").pop();
                 const link = document.createElement("a");
                 link.href = path;
-                link.title = desc;
                 if (path === current_page) {
                     link.className = "current";
                 }
@@ -788,17 +784,17 @@ function loadCss(cssUrl) {
             // we need to switch away from mobile mode and make the main content area scrollable.
             hideSidebar();
         }
-        if (window.CURRENT_NOTABLE_ELEMENT) {
-            // As a workaround to the behavior of `contains: layout` used in doc togglers, the
-            // notable traits popup is positioned using javascript.
+        if (window.CURRENT_TOOLTIP_ELEMENT) {
+            // As a workaround to the behavior of `contains: layout` used in doc togglers,
+            // tooltip popovers are positioned using javascript.
             //
             // This means when the window is resized, we need to redo the layout.
-            const base = window.CURRENT_NOTABLE_ELEMENT.NOTABLE_BASE;
-            const force_visible = base.NOTABLE_FORCE_VISIBLE;
-            hideNotable(false);
+            const base = window.CURRENT_TOOLTIP_ELEMENT.TOOLTIP_BASE;
+            const force_visible = base.TOOLTIP_FORCE_VISIBLE;
+            hideTooltip(false);
             if (force_visible) {
-                showNotable(base);
-                base.NOTABLE_FORCE_VISIBLE = true;
+                showTooltip(base);
+                base.TOOLTIP_FORCE_VISIBLE = true;
             }
         }
     });
@@ -826,27 +822,35 @@ function loadCss(cssUrl) {
         });
     });
 
-    function showNotable(e) {
-        if (!window.NOTABLE_TRAITS) {
+    function showTooltip(e) {
+        const notable_ty = e.getAttribute("data-notable-ty");
+        if (!window.NOTABLE_TRAITS && notable_ty) {
             const data = document.getElementById("notable-traits-data");
             if (data) {
                 window.NOTABLE_TRAITS = JSON.parse(data.innerText);
             } else {
-                throw new Error("showNotable() called on page without any notable traits!");
+                throw new Error("showTooltip() called with notable without any notable traits!");
             }
         }
-        if (window.CURRENT_NOTABLE_ELEMENT && window.CURRENT_NOTABLE_ELEMENT.NOTABLE_BASE === e) {
+        if (window.CURRENT_TOOLTIP_ELEMENT && window.CURRENT_TOOLTIP_ELEMENT.TOOLTIP_BASE === e) {
             // Make this function idempotent.
             return;
         }
         window.hideAllModals(false);
-        const ty = e.getAttribute("data-ty");
         const wrapper = document.createElement("div");
-        wrapper.innerHTML = "<div class=\"content\">" + window.NOTABLE_TRAITS[ty] + "</div>";
-        wrapper.className = "notable popover";
+        if (notable_ty) {
+            wrapper.innerHTML = "<div class=\"content\">" +
+                window.NOTABLE_TRAITS[notable_ty] + "</div>";
+        } else if (e.getAttribute("title") !== undefined) {
+            const titleContent = document.createElement("div");
+            titleContent.className = "content";
+            titleContent.appendChild(document.createTextNode(e.getAttribute("title")));
+            wrapper.appendChild(titleContent);
+        }
+        wrapper.className = "tooltip popover";
         const focusCatcher = document.createElement("div");
         focusCatcher.setAttribute("tabindex", "0");
-        focusCatcher.onfocus = hideNotable;
+        focusCatcher.onfocus = hideTooltip;
         wrapper.appendChild(focusCatcher);
         const pos = e.getBoundingClientRect();
         // 5px overlap so that the mouse can easily travel from place to place
@@ -868,62 +872,62 @@ function loadCss(cssUrl) {
             );
         }
         wrapper.style.visibility = "";
-        window.CURRENT_NOTABLE_ELEMENT = wrapper;
-        window.CURRENT_NOTABLE_ELEMENT.NOTABLE_BASE = e;
+        window.CURRENT_TOOLTIP_ELEMENT = wrapper;
+        window.CURRENT_TOOLTIP_ELEMENT.TOOLTIP_BASE = e;
         wrapper.onpointerleave = function(ev) {
             // If this is a synthetic touch event, ignore it. A click event will be along shortly.
             if (ev.pointerType !== "mouse") {
                 return;
             }
-            if (!e.NOTABLE_FORCE_VISIBLE && !elemIsInParent(event.relatedTarget, e)) {
-                hideNotable(true);
+            if (!e.TOOLTIP_FORCE_VISIBLE && !elemIsInParent(event.relatedTarget, e)) {
+                hideTooltip(true);
             }
         };
     }
 
-    function notableBlurHandler(event) {
-        if (window.CURRENT_NOTABLE_ELEMENT &&
-            !elemIsInParent(document.activeElement, window.CURRENT_NOTABLE_ELEMENT) &&
-            !elemIsInParent(event.relatedTarget, window.CURRENT_NOTABLE_ELEMENT) &&
-            !elemIsInParent(document.activeElement, window.CURRENT_NOTABLE_ELEMENT.NOTABLE_BASE) &&
-            !elemIsInParent(event.relatedTarget, window.CURRENT_NOTABLE_ELEMENT.NOTABLE_BASE)
+    function tooltipBlurHandler(event) {
+        if (window.CURRENT_TOOLTIP_ELEMENT &&
+            !elemIsInParent(document.activeElement, window.CURRENT_TOOLTIP_ELEMENT) &&
+            !elemIsInParent(event.relatedTarget, window.CURRENT_TOOLTIP_ELEMENT) &&
+            !elemIsInParent(document.activeElement, window.CURRENT_TOOLTIP_ELEMENT.TOOLTIP_BASE) &&
+            !elemIsInParent(event.relatedTarget, window.CURRENT_TOOLTIP_ELEMENT.TOOLTIP_BASE)
         ) {
             // Work around a difference in the focus behaviour between Firefox, Chrome, and Safari.
-            // When I click the button on an already-opened notable trait popover, Safari
+            // When I click the button on an already-opened tooltip popover, Safari
             // hides the popover and then immediately shows it again, while everyone else hides it
             // and it stays hidden.
             //
             // To work around this, make sure the click finishes being dispatched before
-            // hiding the popover. Since `hideNotable()` is idempotent, this makes Safari behave
+            // hiding the popover. Since `hideTooltip()` is idempotent, this makes Safari behave
             // consistently with the other two.
-            setTimeout(() => hideNotable(false), 0);
+            setTimeout(() => hideTooltip(false), 0);
         }
     }
 
-    function hideNotable(focus) {
-        if (window.CURRENT_NOTABLE_ELEMENT) {
-            if (window.CURRENT_NOTABLE_ELEMENT.NOTABLE_BASE.NOTABLE_FORCE_VISIBLE) {
+    function hideTooltip(focus) {
+        if (window.CURRENT_TOOLTIP_ELEMENT) {
+            if (window.CURRENT_TOOLTIP_ELEMENT.TOOLTIP_BASE.TOOLTIP_FORCE_VISIBLE) {
                 if (focus) {
-                    window.CURRENT_NOTABLE_ELEMENT.NOTABLE_BASE.focus();
+                    window.CURRENT_TOOLTIP_ELEMENT.TOOLTIP_BASE.focus();
                 }
-                window.CURRENT_NOTABLE_ELEMENT.NOTABLE_BASE.NOTABLE_FORCE_VISIBLE = false;
+                window.CURRENT_TOOLTIP_ELEMENT.TOOLTIP_BASE.TOOLTIP_FORCE_VISIBLE = false;
             }
             const body = document.getElementsByTagName("body")[0];
-            body.removeChild(window.CURRENT_NOTABLE_ELEMENT);
-            window.CURRENT_NOTABLE_ELEMENT = null;
+            body.removeChild(window.CURRENT_TOOLTIP_ELEMENT);
+            window.CURRENT_TOOLTIP_ELEMENT = null;
         }
     }
 
-    onEachLazy(document.getElementsByClassName("notable-traits"), e => {
+    onEachLazy(document.getElementsByClassName("tooltip"), e => {
         e.onclick = function() {
-            this.NOTABLE_FORCE_VISIBLE = this.NOTABLE_FORCE_VISIBLE ? false : true;
-            if (window.CURRENT_NOTABLE_ELEMENT && !this.NOTABLE_FORCE_VISIBLE) {
-                hideNotable(true);
+            this.TOOLTIP_FORCE_VISIBLE = this.TOOLTIP_FORCE_VISIBLE ? false : true;
+            if (window.CURRENT_TOOLTIP_ELEMENT && !this.TOOLTIP_FORCE_VISIBLE) {
+                hideTooltip(true);
             } else {
-                showNotable(this);
-                window.CURRENT_NOTABLE_ELEMENT.setAttribute("tabindex", "0");
-                window.CURRENT_NOTABLE_ELEMENT.focus();
-                window.CURRENT_NOTABLE_ELEMENT.onblur = notableBlurHandler;
+                showTooltip(this);
+                window.CURRENT_TOOLTIP_ELEMENT.setAttribute("tabindex", "0");
+                window.CURRENT_TOOLTIP_ELEMENT.focus();
+                window.CURRENT_TOOLTIP_ELEMENT.onblur = tooltipBlurHandler;
             }
             return false;
         };
@@ -932,16 +936,16 @@ function loadCss(cssUrl) {
             if (ev.pointerType !== "mouse") {
                 return;
             }
-            showNotable(this);
+            showTooltip(this);
         };
         e.onpointerleave = function(ev) {
             // If this is a synthetic touch event, ignore it. A click event will be along shortly.
             if (ev.pointerType !== "mouse") {
                 return;
             }
-            if (!this.NOTABLE_FORCE_VISIBLE &&
-                !elemIsInParent(ev.relatedTarget, window.CURRENT_NOTABLE_ELEMENT)) {
-                hideNotable(true);
+            if (!this.TOOLTIP_FORCE_VISIBLE &&
+                !elemIsInParent(ev.relatedTarget, window.CURRENT_TOOLTIP_ELEMENT)) {
+                hideTooltip(true);
             }
         };
     });
@@ -1043,14 +1047,14 @@ function loadCss(cssUrl) {
     }
 
     /**
-     * Hide popover menus, notable trait tooltips, and the sidebar (if applicable).
+     * Hide popover menus, clickable tooltips, and the sidebar (if applicable).
      *
-     * Pass "true" to reset focus for notable traits.
+     * Pass "true" to reset focus for tooltip popovers.
      */
     window.hideAllModals = function(switchFocus) {
         hideSidebar();
         window.hidePopoverMenus();
-        hideNotable(switchFocus);
+        hideTooltip(switchFocus);
     };
 
     /**
diff --git a/src/librustdoc/html/static/js/search.js b/src/librustdoc/html/static/js/search.js
index 251e806c2d9..1e6c94d29ba 100644
--- a/src/librustdoc/html/static/js/search.js
+++ b/src/librustdoc/html/static/js/search.js
@@ -142,13 +142,11 @@ function initSearch(rawSearchIndex) {
     }
 
     function itemTypeFromName(typename) {
-        for (let i = 0, len = itemTypes.length; i < len; ++i) {
-            if (itemTypes[i] === typename) {
-                return i;
-            }
+        const index = itemTypes.findIndex(i => i === typename);
+        if (index < 0) {
+            throw new Error("Unknown type filter `" + typename + "`");
         }
-
-        throw new Error("Unknown type filter `" + typename + "`");
+        return index;
     }
 
     /**
@@ -1941,6 +1939,7 @@ function initSearch(rawSearchIndex) {
          * @type {Array<string>}
          */
         const searchWords = [];
+        const charA = "A".charCodeAt(0);
         let i, word;
         let currentIndex = 0;
         let id = 0;
@@ -1955,7 +1954,7 @@ function initSearch(rawSearchIndex) {
             /**
              * The raw search data for a given crate. `n`, `t`, `d`, and `q`, `i`, and `f`
              * are arrays with the same length. n[i] contains the name of an item.
-             * t[i] contains the type of that item (as a small integer that represents an
+             * t[i] contains the type of that item (as a string of characters that represent an
              * offset in `itemTypes`). d[i] contains the description of that item.
              *
              * q[i] contains the full path of the item, or an empty string indicating
@@ -1982,7 +1981,7 @@ function initSearch(rawSearchIndex) {
              *   doc: string,
              *   a: Object,
              *   n: Array<string>,
-             *   t: Array<Number>,
+             *   t: String,
              *   d: Array<string>,
              *   q: Array<string>,
              *   i: Array<Number>,
@@ -2011,7 +2010,7 @@ function initSearch(rawSearchIndex) {
             searchIndex.push(crateRow);
             currentIndex += 1;
 
-            // an array of (Number) item types
+            // a String of one character item type codes
             const itemTypes = crateCorpus.t;
             // an array of (String) item names
             const itemNames = crateCorpus.n;
@@ -2062,7 +2061,7 @@ function initSearch(rawSearchIndex) {
                 }
                 const row = {
                     crate: crate,
-                    ty: itemTypes[i],
+                    ty: itemTypes.charCodeAt(i) - charA,
                     name: itemNames[i],
                     path: itemPaths[i] ? itemPaths[i] : lastPath,
                     desc: itemDescs[i],
diff --git a/src/librustdoc/lib.rs b/src/librustdoc/lib.rs
index 64108c88285..910a7190b58 100644
--- a/src/librustdoc/lib.rs
+++ b/src/librustdoc/lib.rs
@@ -31,6 +31,7 @@ extern crate tracing;
 //
 // Dependencies listed in Cargo.toml do not need `extern crate`.
 
+extern crate pulldown_cmark;
 extern crate rustc_ast;
 extern crate rustc_ast_pretty;
 extern crate rustc_attr;
@@ -81,7 +82,6 @@ use rustc_session::getopts;
 use rustc_session::{early_error, early_warn};
 
 use crate::clean::utils::DOC_RUST_LANG_ORG_CHANNEL;
-use crate::passes::collect_intra_doc_links;
 
 /// A macro to create a FxHashMap.
 ///
@@ -741,7 +741,7 @@ fn main_args(at_args: &[String]) -> MainResult {
         (false, true) => {
             let input = options.input.clone();
             let edition = options.edition;
-            let config = core::create_config(options);
+            let config = core::create_config(options, &render_options);
 
             // `markdown::render` can invoke `doctest::make_test`, which
             // requires session globals and a thread pool, so we use
@@ -774,7 +774,7 @@ fn main_args(at_args: &[String]) -> MainResult {
     let scrape_examples_options = options.scrape_examples_options.clone();
     let bin_crate = options.bin_crate;
 
-    let config = core::create_config(options);
+    let config = core::create_config(options, &render_options);
 
     interface::run_compiler(config, |compiler| {
         let sess = compiler.session();
@@ -792,40 +792,14 @@ fn main_args(at_args: &[String]) -> MainResult {
         }
 
         compiler.enter(|queries| {
-            // We need to hold on to the complete resolver, so we cause everything to be
-            // cloned for the analysis passes to use. Suboptimal, but necessary in the
-            // current architecture.
-            // FIXME(#83761): Resolver cloning can lead to inconsistencies between data in the
-            // two copies because one of the copies can be modified after `TyCtxt` construction.
-            let (resolver, resolver_caches) = {
-                let expansion = abort_on_err(queries.expansion(), sess);
-                let (krate, resolver, _) = &*expansion.borrow();
-                let resolver_caches = resolver.borrow_mut().access(|resolver| {
-                    collect_intra_doc_links::early_resolve_intra_doc_links(
-                        resolver,
-                        krate,
-                        render_options.document_private,
-                    )
-                });
-                (resolver.clone(), resolver_caches)
-            };
-
+            let mut gcx = abort_on_err(queries.global_ctxt(), sess);
             if sess.diagnostic().has_errors_or_lint_errors().is_some() {
                 sess.fatal("Compilation failed, aborting rustdoc");
             }
 
-            let mut global_ctxt = abort_on_err(queries.global_ctxt(), sess);
-
-            global_ctxt.enter(|tcx| {
+            gcx.enter(|tcx| {
                 let (krate, render_opts, mut cache) = sess.time("run_global_ctxt", || {
-                    core::run_global_ctxt(
-                        tcx,
-                        resolver,
-                        resolver_caches,
-                        show_coverage,
-                        render_options,
-                        output_format,
-                    )
+                    core::run_global_ctxt(tcx, show_coverage, render_options, output_format)
                 });
                 info!("finished with rustc");
 
diff --git a/src/librustdoc/passes/collect_intra_doc_links.rs b/src/librustdoc/passes/collect_intra_doc_links.rs
index 8435972bb11..7e3149a59e3 100644
--- a/src/librustdoc/passes/collect_intra_doc_links.rs
+++ b/src/librustdoc/passes/collect_intra_doc_links.rs
@@ -15,7 +15,8 @@ use rustc_hir::def_id::{DefId, CRATE_DEF_ID};
 use rustc_hir::Mutability;
 use rustc_middle::ty::{DefIdTree, Ty, TyCtxt};
 use rustc_middle::{bug, ty};
-use rustc_resolve::ParentScope;
+use rustc_resolve::rustdoc::MalformedGenerics;
+use rustc_resolve::rustdoc::{prepare_to_doc_link_resolution, strip_generics_from_path};
 use rustc_session::lint::Lint;
 use rustc_span::hygiene::MacroKind;
 use rustc_span::symbol::{sym, Ident, Symbol};
@@ -34,9 +35,6 @@ use crate::lint::{BROKEN_INTRA_DOC_LINKS, PRIVATE_INTRA_DOC_LINKS};
 use crate::passes::Pass;
 use crate::visit::DocVisitor;
 
-mod early;
-pub(crate) use early::early_resolve_intra_doc_links;
-
 pub(crate) const COLLECT_INTRA_DOC_LINKS: Pass = Pass {
     name: "collect-intra-doc-links",
     run: collect_intra_doc_links,
@@ -179,47 +177,6 @@ enum ResolutionFailure<'a> {
     NotResolved(UnresolvedPath<'a>),
 }
 
-#[derive(Clone, Copy, Debug)]
-enum MalformedGenerics {
-    /// This link has unbalanced angle brackets.
-    ///
-    /// For example, `Vec<T` should trigger this, as should `Vec<T>>`.
-    UnbalancedAngleBrackets,
-    /// The generics are not attached to a type.
-    ///
-    /// For example, `<T>` should trigger this.
-    ///
-    /// This is detected by checking if the path is empty after the generics are stripped.
-    MissingType,
-    /// The link uses fully-qualified syntax, which is currently unsupported.
-    ///
-    /// For example, `<Vec as IntoIterator>::into_iter` should trigger this.
-    ///
-    /// This is detected by checking if ` as ` (the keyword `as` with spaces around it) is inside
-    /// angle brackets.
-    HasFullyQualifiedSyntax,
-    /// The link has an invalid path separator.
-    ///
-    /// For example, `Vec:<T>:new()` should trigger this. Note that `Vec:new()` will **not**
-    /// trigger this because it has no generics and thus [`strip_generics_from_path`] will not be
-    /// called.
-    ///
-    /// Note that this will also **not** be triggered if the invalid path separator is inside angle
-    /// brackets because rustdoc mostly ignores what's inside angle brackets (except for
-    /// [`HasFullyQualifiedSyntax`](MalformedGenerics::HasFullyQualifiedSyntax)).
-    ///
-    /// This is detected by checking if there is a colon followed by a non-colon in the link.
-    InvalidPathSeparator,
-    /// The link has too many angle brackets.
-    ///
-    /// For example, `Vec<<T>>` should trigger this.
-    TooManyAngleBrackets,
-    /// The link has empty angle brackets.
-    ///
-    /// For example, `Vec<>` should trigger this.
-    EmptyAngleBrackets,
-}
-
 #[derive(Clone, Debug, Hash, PartialEq, Eq)]
 pub(crate) enum UrlFragment {
     Item(DefId),
@@ -338,7 +295,8 @@ impl<'a, 'tcx> LinkCollector<'a, 'tcx> {
         match ty_res {
             Res::Def(DefKind::Enum, did) => match tcx.type_of(did).kind() {
                 ty::Adt(def, _) if def.is_enum() => {
-                    if let Some(field) = def.all_fields().find(|f| f.name == variant_field_name) {
+                    if let Some(variant) = def.variants().iter().find(|v| v.name == variant_name)
+                        && let Some(field) = variant.fields.iter().find(|f| f.name == variant_field_name) {
                         Ok((ty_res, field.did))
                     } else {
                         Err(UnresolvedPath {
@@ -407,10 +365,10 @@ impl<'a, 'tcx> LinkCollector<'a, 'tcx> {
             })
     }
 
-    /// Convenience wrapper around `resolve_rustdoc_path`.
+    /// Convenience wrapper around `doc_link_resolutions`.
     ///
     /// This also handles resolving `true` and `false` as booleans.
-    /// NOTE: `resolve_rustdoc_path` knows only about paths, not about types.
+    /// NOTE: `doc_link_resolutions` knows only about paths, not about types.
     /// Associated items will never be resolved by this function.
     fn resolve_path(
         &self,
@@ -426,17 +384,11 @@ impl<'a, 'tcx> LinkCollector<'a, 'tcx> {
         // Resolver doesn't know about true, false, and types that aren't paths (e.g. `()`).
         let result = self
             .cx
-            .resolver_caches
-            .doc_link_resolutions
-            .get(&(Symbol::intern(path_str), ns, module_id))
+            .tcx
+            .doc_link_resolutions(module_id)
+            .get(&(Symbol::intern(path_str), ns))
             .copied()
-            .unwrap_or_else(|| {
-                self.cx.enter_resolver(|resolver| {
-                    let parent_scope =
-                        ParentScope::module(resolver.expect_module(module_id), resolver);
-                    resolver.resolve_rustdoc_path(path_str, ns, parent_scope)
-                })
-            })
+            .unwrap_or_else(|| panic!("no resolution for {:?} {:?} {:?}", path_str, ns, module_id))
             .and_then(|res| res.try_into().ok())
             .or_else(|| resolve_primitive(path_str, ns));
         debug!("{} resolved to {:?} in namespace {:?}", path_str, result, ns);
@@ -562,27 +514,27 @@ impl<'a, 'tcx> LinkCollector<'a, 'tcx> {
         // FIXME: Only simple types are supported here, see if we can support
         // other types such as Tuple, Array, Slice, etc.
         // See https://github.com/rust-lang/rust/issues/90703#issuecomment-1004263455
-        Some(tcx.mk_ty(match prim {
-            Bool => ty::Bool,
-            Str => ty::Str,
-            Char => ty::Char,
-            Never => ty::Never,
-            I8 => ty::Int(ty::IntTy::I8),
-            I16 => ty::Int(ty::IntTy::I16),
-            I32 => ty::Int(ty::IntTy::I32),
-            I64 => ty::Int(ty::IntTy::I64),
-            I128 => ty::Int(ty::IntTy::I128),
-            Isize => ty::Int(ty::IntTy::Isize),
-            F32 => ty::Float(ty::FloatTy::F32),
-            F64 => ty::Float(ty::FloatTy::F64),
-            U8 => ty::Uint(ty::UintTy::U8),
-            U16 => ty::Uint(ty::UintTy::U16),
-            U32 => ty::Uint(ty::UintTy::U32),
-            U64 => ty::Uint(ty::UintTy::U64),
-            U128 => ty::Uint(ty::UintTy::U128),
-            Usize => ty::Uint(ty::UintTy::Usize),
+        Some(match prim {
+            Bool => tcx.types.bool,
+            Str => tcx.types.str_,
+            Char => tcx.types.char,
+            Never => tcx.types.never,
+            I8 => tcx.types.i8,
+            I16 => tcx.types.i16,
+            I32 => tcx.types.i32,
+            I64 => tcx.types.i64,
+            I128 => tcx.types.i128,
+            Isize => tcx.types.isize,
+            F32 => tcx.types.f32,
+            F64 => tcx.types.f64,
+            U8 => tcx.types.u8,
+            U16 => tcx.types.u16,
+            U32 => tcx.types.u32,
+            U64 => tcx.types.u64,
+            U128 => tcx.types.u128,
+            Usize => tcx.types.usize,
             _ => return None,
-        }))
+        })
     }
 
     /// Resolve an associated item, returning its containing page's `Res`
@@ -779,8 +731,7 @@ fn trait_impls_for<'a>(
     module: DefId,
 ) -> FxHashSet<(DefId, DefId)> {
     let tcx = cx.tcx;
-    let iter = cx.resolver_caches.traits_in_scope[&module].iter().flat_map(|trait_candidate| {
-        let trait_ = trait_candidate.def_id;
+    let iter = tcx.doc_link_traits_in_scope(module).iter().flat_map(|&trait_| {
         trace!("considering explicit impl for trait {:?}", trait_);
 
         // Look at each trait implementation to see if it's an impl for `did`
@@ -846,7 +797,7 @@ impl<'a, 'tcx> DocVisitor for LinkCollector<'a, 'tcx> {
         // In the presence of re-exports, this is not the same as the module of the item.
         // Rather than merging all documentation into one, resolve it one attribute at a time
         // so we know which module it came from.
-        for (parent_module, doc) in item.attrs.prepare_to_doc_link_resolution() {
+        for (parent_module, doc) in prepare_to_doc_link_resolution(&item.attrs.doc_strings) {
             if !may_have_doc_links(&doc) {
                 continue;
             }
@@ -854,22 +805,12 @@ impl<'a, 'tcx> DocVisitor for LinkCollector<'a, 'tcx> {
             // NOTE: if there are links that start in one crate and end in another, this will not resolve them.
             // This is a degenerate case and it's not supported by rustdoc.
             let parent_node = parent_module.or(parent_node);
-            let mut tmp_links = self
-                .cx
-                .resolver_caches
-                .markdown_links
-                .take()
-                .expect("`markdown_links` are already borrowed");
-            if !tmp_links.contains_key(&doc) {
-                tmp_links.insert(doc.clone(), preprocessed_markdown_links(&doc));
-            }
-            for md_link in &tmp_links[&doc] {
-                let link = self.resolve_link(item, &doc, parent_node, md_link);
+            for md_link in preprocessed_markdown_links(&doc) {
+                let link = self.resolve_link(item, &doc, parent_node, &md_link);
                 if let Some(link) = link {
                     self.cx.cache.intra_doc_links.entry(item.item_id).or_default().push(link);
                 }
             }
-            self.cx.resolver_caches.markdown_links = Some(tmp_links);
         }
 
         if item.is_mod() {
@@ -975,16 +916,12 @@ fn preprocess_link(
     }
 
     // Strip generics from the path.
-    let path_str = if path_str.contains(['<', '>'].as_slice()) {
-        match strip_generics_from_path(path_str) {
-            Ok(path) => path,
-            Err(err) => {
-                debug!("link has malformed generics: {}", path_str);
-                return Some(Err(PreprocessingError::MalformedGenerics(err, path_str.to_owned())));
-            }
+    let path_str = match strip_generics_from_path(path_str) {
+        Ok(path) => path,
+        Err(err) => {
+            debug!("link has malformed generics: {}", path_str);
+            return Some(Err(PreprocessingError::MalformedGenerics(err, path_str.to_owned())));
         }
-    } else {
-        path_str.to_owned()
     };
 
     // Sanity check to make sure we don't have any angle brackets after stripping generics.
@@ -1677,7 +1614,7 @@ fn resolution_failure(
             // ignore duplicates
             let mut variants_seen = SmallVec::<[_; 3]>::new();
             for mut failure in kinds {
-                let variant = std::mem::discriminant(&failure);
+                let variant = mem::discriminant(&failure);
                 if variants_seen.contains(&variant) {
                     continue;
                 }
@@ -1747,7 +1684,7 @@ fn resolution_failure(
 
                         if !path_str.contains("::") {
                             if disambiguator.map_or(true, |d| d.ns() == MacroNS)
-                                && let Some(&res) = collector.cx.resolver_caches.all_macro_rules
+                                && let Some(&res) = collector.cx.tcx.resolutions(()).all_macro_rules
                                                              .get(&Symbol::intern(path_str))
                             {
                                 diag.note(format!(
@@ -1768,15 +1705,35 @@ fn resolution_failure(
 
                     // Otherwise, it must be an associated item or variant
                     let res = partial_res.expect("None case was handled by `last_found_module`");
-                    let kind = match res {
-                        Res::Def(kind, _) => Some(kind),
+                    let kind_did = match res {
+                        Res::Def(kind, did) => Some((kind, did)),
                         Res::Primitive(_) => None,
                     };
-                    let path_description = if let Some(kind) = kind {
+                    let is_struct_variant = |did| {
+                        if let ty::Adt(def, _) = tcx.type_of(did).kind()
+                        && def.is_enum()
+                        && let Some(variant) = def.variants().iter().find(|v| v.name == res.name(tcx)) {
+                            // ctor is `None` if variant is a struct
+                            variant.ctor.is_none()
+                        } else {
+                            false
+                        }
+                    };
+                    let path_description = if let Some((kind, did)) = kind_did {
                         match kind {
                             Mod | ForeignMod => "inner item",
                             Struct => "field or associated item",
                             Enum | Union => "variant or associated item",
+                            Variant if is_struct_variant(did) => {
+                                let variant = res.name(tcx);
+                                let note = format!("variant `{variant}` has no such field");
+                                if let Some(span) = sp {
+                                    diag.span_label(span, &note);
+                                } else {
+                                    diag.note(&note);
+                                }
+                                return;
+                            }
                             Variant
                             | Field
                             | Closure
@@ -2064,94 +2021,3 @@ fn resolve_primitive(path_str: &str, ns: Namespace) -> Option<Res> {
     debug!("resolved primitives {:?}", prim);
     Some(Res::Primitive(prim))
 }
-
-fn strip_generics_from_path(path_str: &str) -> Result<String, MalformedGenerics> {
-    let mut stripped_segments = vec![];
-    let mut path = path_str.chars().peekable();
-    let mut segment = Vec::new();
-
-    while let Some(chr) = path.next() {
-        match chr {
-            ':' => {
-                if path.next_if_eq(&':').is_some() {
-                    let stripped_segment =
-                        strip_generics_from_path_segment(mem::take(&mut segment))?;
-                    if !stripped_segment.is_empty() {
-                        stripped_segments.push(stripped_segment);
-                    }
-                } else {
-                    return Err(MalformedGenerics::InvalidPathSeparator);
-                }
-            }
-            '<' => {
-                segment.push(chr);
-
-                match path.next() {
-                    Some('<') => {
-                        return Err(MalformedGenerics::TooManyAngleBrackets);
-                    }
-                    Some('>') => {
-                        return Err(MalformedGenerics::EmptyAngleBrackets);
-                    }
-                    Some(chr) => {
-                        segment.push(chr);
-
-                        while let Some(chr) = path.next_if(|c| *c != '>') {
-                            segment.push(chr);
-                        }
-                    }
-                    None => break,
-                }
-            }
-            _ => segment.push(chr),
-        }
-        trace!("raw segment: {:?}", segment);
-    }
-
-    if !segment.is_empty() {
-        let stripped_segment = strip_generics_from_path_segment(segment)?;
-        if !stripped_segment.is_empty() {
-            stripped_segments.push(stripped_segment);
-        }
-    }
-
-    debug!("path_str: {:?}\nstripped segments: {:?}", path_str, &stripped_segments);
-
-    let stripped_path = stripped_segments.join("::");
-
-    if !stripped_path.is_empty() { Ok(stripped_path) } else { Err(MalformedGenerics::MissingType) }
-}
-
-fn strip_generics_from_path_segment(segment: Vec<char>) -> Result<String, MalformedGenerics> {
-    let mut stripped_segment = String::new();
-    let mut param_depth = 0;
-
-    let mut latest_generics_chunk = String::new();
-
-    for c in segment {
-        if c == '<' {
-            param_depth += 1;
-            latest_generics_chunk.clear();
-        } else if c == '>' {
-            param_depth -= 1;
-            if latest_generics_chunk.contains(" as ") {
-                // The segment tries to use fully-qualified syntax, which is currently unsupported.
-                // Give a helpful error message instead of completely ignoring the angle brackets.
-                return Err(MalformedGenerics::HasFullyQualifiedSyntax);
-            }
-        } else {
-            if param_depth == 0 {
-                stripped_segment.push(c);
-            } else {
-                latest_generics_chunk.push(c);
-            }
-        }
-    }
-
-    if param_depth == 0 {
-        Ok(stripped_segment)
-    } else {
-        // The segment has unbalanced angle brackets, e.g. `Vec<T` or `Vec<T>>`
-        Err(MalformedGenerics::UnbalancedAngleBrackets)
-    }
-}
diff --git a/src/librustdoc/passes/collect_intra_doc_links/early.rs b/src/librustdoc/passes/collect_intra_doc_links/early.rs
deleted file mode 100644
index f690c49005d..00000000000
--- a/src/librustdoc/passes/collect_intra_doc_links/early.rs
+++ /dev/null
@@ -1,431 +0,0 @@
-use crate::clean::Attributes;
-use crate::core::ResolverCaches;
-use crate::passes::collect_intra_doc_links::preprocessed_markdown_links;
-use crate::passes::collect_intra_doc_links::{Disambiguator, PreprocessedMarkdownLink};
-use crate::visit_lib::early_lib_embargo_visit_item;
-
-use rustc_ast::visit::{self, AssocCtxt, Visitor};
-use rustc_ast::{self as ast, ItemKind};
-use rustc_data_structures::fx::FxHashMap;
-use rustc_hir::def::Namespace::*;
-use rustc_hir::def::{DefKind, Namespace, Res};
-use rustc_hir::def_id::{DefId, DefIdMap, DefIdSet, CRATE_DEF_ID};
-use rustc_hir::TraitCandidate;
-use rustc_middle::ty::{DefIdTree, Visibility};
-use rustc_resolve::{ParentScope, Resolver};
-use rustc_span::symbol::sym;
-use rustc_span::{Symbol, SyntaxContext};
-
-use std::collections::hash_map::Entry;
-use std::mem;
-
-pub(crate) fn early_resolve_intra_doc_links(
-    resolver: &mut Resolver<'_>,
-    krate: &ast::Crate,
-    document_private_items: bool,
-) -> ResolverCaches {
-    let parent_scope =
-        ParentScope::module(resolver.expect_module(CRATE_DEF_ID.to_def_id()), resolver);
-    let mut link_resolver = EarlyDocLinkResolver {
-        resolver,
-        parent_scope,
-        visited_mods: Default::default(),
-        markdown_links: Default::default(),
-        doc_link_resolutions: Default::default(),
-        traits_in_scope: Default::default(),
-        all_trait_impls: Default::default(),
-        all_macro_rules: Default::default(),
-        extern_doc_reachable: Default::default(),
-        local_doc_reachable: Default::default(),
-        document_private_items,
-    };
-
-    // Overridden `visit_item` below doesn't apply to the crate root,
-    // so we have to visit its attributes and reexports separately.
-    link_resolver.resolve_doc_links_local(&krate.attrs);
-    link_resolver.process_module_children_or_reexports(CRATE_DEF_ID.to_def_id());
-    visit::walk_crate(&mut link_resolver, krate);
-
-    // FIXME: somehow rustdoc is still missing crates even though we loaded all
-    // the known necessary crates. Load them all unconditionally until we find a way to fix this.
-    // DO NOT REMOVE THIS without first testing on the reproducer in
-    // https://github.com/jyn514/objr/commit/edcee7b8124abf0e4c63873e8422ff81beb11ebb
-    for (extern_name, _) in
-        link_resolver.resolver.sess().opts.externs.iter().filter(|(_, entry)| entry.add_prelude)
-    {
-        link_resolver.resolver.resolve_rustdoc_path(extern_name, TypeNS, parent_scope);
-    }
-
-    link_resolver.process_extern_impls();
-
-    ResolverCaches {
-        markdown_links: Some(link_resolver.markdown_links),
-        doc_link_resolutions: link_resolver.doc_link_resolutions,
-        traits_in_scope: link_resolver.traits_in_scope,
-        all_trait_impls: Some(link_resolver.all_trait_impls),
-        all_macro_rules: link_resolver.all_macro_rules,
-        extern_doc_reachable: link_resolver.extern_doc_reachable,
-    }
-}
-
-fn doc_attrs<'a>(attrs: impl Iterator<Item = &'a ast::Attribute>) -> Attributes {
-    Attributes::from_ast_iter(attrs.map(|attr| (attr, None)), true)
-}
-
-struct EarlyDocLinkResolver<'r, 'ra> {
-    resolver: &'r mut Resolver<'ra>,
-    parent_scope: ParentScope<'ra>,
-    visited_mods: DefIdSet,
-    markdown_links: FxHashMap<String, Vec<PreprocessedMarkdownLink>>,
-    doc_link_resolutions: FxHashMap<(Symbol, Namespace, DefId), Option<Res<ast::NodeId>>>,
-    traits_in_scope: DefIdMap<Vec<TraitCandidate>>,
-    all_trait_impls: Vec<DefId>,
-    all_macro_rules: FxHashMap<Symbol, Res<ast::NodeId>>,
-    /// This set is used as a seed for `effective_visibilities`, which are then extended by some
-    /// more items using `lib_embargo_visit_item` during doc inlining.
-    extern_doc_reachable: DefIdSet,
-    /// This is an easily identifiable superset of items added to `effective_visibilities`
-    /// using `lib_embargo_visit_item` during doc inlining.
-    /// The union of `(extern,local)_doc_reachable` is therefore a superset of
-    /// `effective_visibilities` and can be used for pruning extern impls here
-    /// in early doc link resolution.
-    local_doc_reachable: DefIdSet,
-    document_private_items: bool,
-}
-
-impl<'ra> EarlyDocLinkResolver<'_, 'ra> {
-    fn add_traits_in_scope(&mut self, def_id: DefId) {
-        // Calls to `traits_in_scope` are expensive, so try to avoid them if only possible.
-        // Keys in the `traits_in_scope` cache are always module IDs.
-        if let Entry::Vacant(entry) = self.traits_in_scope.entry(def_id) {
-            let module = self.resolver.get_nearest_non_block_module(def_id);
-            let module_id = module.def_id();
-            let entry = if module_id == def_id {
-                Some(entry)
-            } else if let Entry::Vacant(entry) = self.traits_in_scope.entry(module_id) {
-                Some(entry)
-            } else {
-                None
-            };
-            if let Some(entry) = entry {
-                entry.insert(self.resolver.traits_in_scope(
-                    None,
-                    &ParentScope::module(module, self.resolver),
-                    SyntaxContext::root(),
-                    None,
-                ));
-            }
-        }
-    }
-
-    fn is_doc_reachable(&self, def_id: DefId) -> bool {
-        self.extern_doc_reachable.contains(&def_id) || self.local_doc_reachable.contains(&def_id)
-    }
-
-    /// Add traits in scope for links in impls collected by the `collect-intra-doc-links` pass.
-    /// That pass filters impls using type-based information, but we don't yet have such
-    /// information here, so we just conservatively calculate traits in scope for *all* modules
-    /// having impls in them.
-    fn process_extern_impls(&mut self) {
-        // Resolving links in already existing crates may trigger loading of new crates.
-        let mut start_cnum = 0;
-        loop {
-            let crates = Vec::from_iter(self.resolver.cstore().crates_untracked());
-            for cnum in &crates[start_cnum..] {
-                early_lib_embargo_visit_item(
-                    self.resolver,
-                    &mut self.extern_doc_reachable,
-                    cnum.as_def_id(),
-                    true,
-                );
-            }
-            for &cnum in &crates[start_cnum..] {
-                let all_trait_impls =
-                    Vec::from_iter(self.resolver.cstore().trait_impls_in_crate_untracked(cnum));
-                let all_inherent_impls =
-                    Vec::from_iter(self.resolver.cstore().inherent_impls_in_crate_untracked(cnum));
-                let all_incoherent_impls = Vec::from_iter(
-                    self.resolver.cstore().incoherent_impls_in_crate_untracked(cnum),
-                );
-
-                // Querying traits in scope is expensive so we try to prune the impl lists using
-                // privacy, private traits and impls from other crates are never documented in
-                // the current crate, and links in their doc comments are not resolved.
-                for &(trait_def_id, impl_def_id, simplified_self_ty) in &all_trait_impls {
-                    if self.is_doc_reachable(trait_def_id)
-                        && simplified_self_ty
-                            .and_then(|ty| ty.def())
-                            .map_or(true, |ty_def_id| self.is_doc_reachable(ty_def_id))
-                    {
-                        if self.visited_mods.insert(trait_def_id) {
-                            self.resolve_doc_links_extern_impl(trait_def_id, false);
-                        }
-                        self.resolve_doc_links_extern_impl(impl_def_id, false);
-                    }
-                    self.all_trait_impls.push(impl_def_id);
-                }
-                for (ty_def_id, impl_def_id) in all_inherent_impls {
-                    if self.is_doc_reachable(ty_def_id) {
-                        self.resolve_doc_links_extern_impl(impl_def_id, true);
-                    }
-                }
-                for impl_def_id in all_incoherent_impls {
-                    self.resolve_doc_links_extern_impl(impl_def_id, true);
-                }
-            }
-
-            if crates.len() > start_cnum {
-                start_cnum = crates.len();
-            } else {
-                break;
-            }
-        }
-    }
-
-    fn resolve_doc_links_extern_impl(&mut self, def_id: DefId, is_inherent: bool) {
-        self.resolve_doc_links_extern_outer_fixme(def_id, def_id);
-        let assoc_item_def_ids = Vec::from_iter(
-            self.resolver.cstore().associated_item_def_ids_untracked(def_id, self.resolver.sess()),
-        );
-        for assoc_def_id in assoc_item_def_ids {
-            if !is_inherent || self.resolver.cstore().visibility_untracked(assoc_def_id).is_public()
-            {
-                self.resolve_doc_links_extern_outer_fixme(assoc_def_id, def_id);
-            }
-        }
-    }
-
-    // FIXME: replace all uses with `resolve_doc_links_extern_outer` to actually resolve links, not
-    // just add traits in scope. This may be expensive and require benchmarking and optimization.
-    fn resolve_doc_links_extern_outer_fixme(&mut self, def_id: DefId, scope_id: DefId) {
-        if !self.resolver.cstore().may_have_doc_links_untracked(def_id) {
-            return;
-        }
-        if let Some(parent_id) = self.resolver.opt_parent(scope_id) {
-            self.add_traits_in_scope(parent_id);
-        }
-    }
-
-    fn resolve_doc_links_extern_outer(&mut self, def_id: DefId, scope_id: DefId) {
-        if !self.resolver.cstore().may_have_doc_links_untracked(def_id) {
-            return;
-        }
-        let attrs = Vec::from_iter(
-            self.resolver.cstore().item_attrs_untracked(def_id, self.resolver.sess()),
-        );
-        let parent_scope = ParentScope::module(
-            self.resolver.get_nearest_non_block_module(
-                self.resolver.opt_parent(scope_id).unwrap_or(scope_id),
-            ),
-            self.resolver,
-        );
-        self.resolve_doc_links(doc_attrs(attrs.iter()), parent_scope);
-    }
-
-    fn resolve_doc_links_extern_inner(&mut self, def_id: DefId) {
-        if !self.resolver.cstore().may_have_doc_links_untracked(def_id) {
-            return;
-        }
-        let attrs = Vec::from_iter(
-            self.resolver.cstore().item_attrs_untracked(def_id, self.resolver.sess()),
-        );
-        let parent_scope = ParentScope::module(self.resolver.expect_module(def_id), self.resolver);
-        self.resolve_doc_links(doc_attrs(attrs.iter()), parent_scope);
-    }
-
-    fn resolve_doc_links_local(&mut self, attrs: &[ast::Attribute]) {
-        if !attrs.iter().any(|attr| attr.may_have_doc_links()) {
-            return;
-        }
-        self.resolve_doc_links(doc_attrs(attrs.iter()), self.parent_scope);
-    }
-
-    fn resolve_and_cache(
-        &mut self,
-        path_str: &str,
-        ns: Namespace,
-        parent_scope: &ParentScope<'ra>,
-    ) -> bool {
-        // FIXME: This caching may be incorrect in case of multiple `macro_rules`
-        // items with the same name in the same module.
-        self.doc_link_resolutions
-            .entry((Symbol::intern(path_str), ns, parent_scope.module.def_id()))
-            .or_insert_with_key(|(path, ns, _)| {
-                self.resolver.resolve_rustdoc_path(path.as_str(), *ns, *parent_scope)
-            })
-            .is_some()
-    }
-
-    fn resolve_doc_links(&mut self, attrs: Attributes, parent_scope: ParentScope<'ra>) {
-        let mut need_traits_in_scope = false;
-        for (doc_module, doc) in attrs.prepare_to_doc_link_resolution() {
-            assert_eq!(doc_module, None);
-            let mut tmp_links = mem::take(&mut self.markdown_links);
-            let links =
-                tmp_links.entry(doc).or_insert_with_key(|doc| preprocessed_markdown_links(doc));
-            for PreprocessedMarkdownLink(pp_link, _) in links {
-                if let Ok(pinfo) = pp_link {
-                    // The logic here is a conservative approximation for path resolution in
-                    // `resolve_with_disambiguator`.
-                    if let Some(ns) = pinfo.disambiguator.map(Disambiguator::ns) {
-                        if self.resolve_and_cache(&pinfo.path_str, ns, &parent_scope) {
-                            continue;
-                        }
-                    }
-
-                    // Resolve all namespaces due to no disambiguator or for diagnostics.
-                    let mut any_resolved = false;
-                    let mut need_assoc = false;
-                    for ns in [TypeNS, ValueNS, MacroNS] {
-                        if self.resolve_and_cache(&pinfo.path_str, ns, &parent_scope) {
-                            any_resolved = true;
-                        } else if ns != MacroNS {
-                            need_assoc = true;
-                        }
-                    }
-
-                    // Resolve all prefixes for type-relative resolution or for diagnostics.
-                    if need_assoc || !any_resolved {
-                        let mut path = &pinfo.path_str[..];
-                        while let Some(idx) = path.rfind("::") {
-                            path = &path[..idx];
-                            need_traits_in_scope = true;
-                            for ns in [TypeNS, ValueNS, MacroNS] {
-                                self.resolve_and_cache(path, ns, &parent_scope);
-                            }
-                        }
-                    }
-                }
-            }
-            self.markdown_links = tmp_links;
-        }
-
-        if need_traits_in_scope {
-            self.add_traits_in_scope(parent_scope.module.def_id());
-        }
-    }
-
-    /// When reexports are inlined, they are replaced with item which they refer to, those items
-    /// may have links in their doc comments, those links are resolved at the item definition site,
-    /// so we need to know traits in scope at that definition site.
-    fn process_module_children_or_reexports(&mut self, module_id: DefId) {
-        if !self.visited_mods.insert(module_id) {
-            return; // avoid infinite recursion
-        }
-
-        for child in self.resolver.module_children_or_reexports(module_id) {
-            // This condition should give a superset of `denied` from `fn clean_use_statement`.
-            if child.vis.is_public()
-                || self.document_private_items
-                    && child.vis != Visibility::Restricted(module_id)
-                    && module_id.is_local()
-            {
-                if let Some(def_id) = child.res.opt_def_id() && !def_id.is_local() {
-                    self.local_doc_reachable.insert(def_id);
-                    let scope_id = match child.res {
-                        Res::Def(
-                            DefKind::Variant
-                            | DefKind::AssocTy
-                            | DefKind::AssocFn
-                            | DefKind::AssocConst,
-                            ..,
-                        ) => self.resolver.parent(def_id),
-                        _ => def_id,
-                    };
-                    self.resolve_doc_links_extern_outer(def_id, scope_id); // Outer attribute scope
-                    if let Res::Def(DefKind::Mod, ..) = child.res {
-                        self.resolve_doc_links_extern_inner(def_id); // Inner attribute scope
-                    }
-                    if let Res::Def(DefKind::Mod | DefKind::Enum | DefKind::Trait, ..) = child.res {
-                        self.process_module_children_or_reexports(def_id);
-                    }
-                    if let Res::Def(DefKind::Struct | DefKind::Union | DefKind::Variant, _) =
-                        child.res
-                    {
-                        let field_def_ids = Vec::from_iter(
-                            self.resolver
-                                .cstore()
-                                .associated_item_def_ids_untracked(def_id, self.resolver.sess()),
-                        );
-                        for field_def_id in field_def_ids {
-                            self.resolve_doc_links_extern_outer(field_def_id, scope_id);
-                        }
-                    }
-                }
-            }
-        }
-    }
-}
-
-impl Visitor<'_> for EarlyDocLinkResolver<'_, '_> {
-    fn visit_item(&mut self, item: &ast::Item) {
-        self.resolve_doc_links_local(&item.attrs); // Outer attribute scope
-        if let ItemKind::Mod(..) = item.kind {
-            let module_def_id = self.resolver.local_def_id(item.id).to_def_id();
-            let module = self.resolver.expect_module(module_def_id);
-            let old_module = mem::replace(&mut self.parent_scope.module, module);
-            let old_macro_rules = self.parent_scope.macro_rules;
-            self.resolve_doc_links_local(&item.attrs); // Inner attribute scope
-            self.process_module_children_or_reexports(module_def_id);
-            visit::walk_item(self, item);
-            if item
-                .attrs
-                .iter()
-                .all(|attr| !attr.has_name(sym::macro_use) && !attr.has_name(sym::macro_escape))
-            {
-                self.parent_scope.macro_rules = old_macro_rules;
-            }
-            self.parent_scope.module = old_module;
-        } else {
-            match &item.kind {
-                ItemKind::Impl(box ast::Impl { of_trait: Some(trait_ref), .. }) => {
-                    if let Some(partial_res) = self.resolver.get_partial_res(trait_ref.ref_id)
-                        && let Some(res) = partial_res.full_res()
-                        && let Some(trait_def_id) = res.opt_def_id()
-                        && !trait_def_id.is_local()
-                        && self.visited_mods.insert(trait_def_id) {
-                        self.resolve_doc_links_extern_impl(trait_def_id, false);
-                    }
-                    self.all_trait_impls.push(self.resolver.local_def_id(item.id).to_def_id());
-                }
-                ItemKind::MacroDef(macro_def) if macro_def.macro_rules => {
-                    let (macro_rules_scope, res) =
-                        self.resolver.macro_rules_scope(self.resolver.local_def_id(item.id));
-                    self.parent_scope.macro_rules = macro_rules_scope;
-                    self.all_macro_rules.insert(item.ident.name, res);
-                }
-                _ => {}
-            }
-            visit::walk_item(self, item);
-        }
-    }
-
-    fn visit_assoc_item(&mut self, item: &ast::AssocItem, ctxt: AssocCtxt) {
-        self.resolve_doc_links_local(&item.attrs);
-        visit::walk_assoc_item(self, item, ctxt)
-    }
-
-    fn visit_foreign_item(&mut self, item: &ast::ForeignItem) {
-        self.resolve_doc_links_local(&item.attrs);
-        visit::walk_foreign_item(self, item)
-    }
-
-    fn visit_variant(&mut self, v: &ast::Variant) {
-        self.resolve_doc_links_local(&v.attrs);
-        visit::walk_variant(self, v)
-    }
-
-    fn visit_field_def(&mut self, field: &ast::FieldDef) {
-        self.resolve_doc_links_local(&field.attrs);
-        visit::walk_field_def(self, field)
-    }
-
-    fn visit_block(&mut self, block: &ast::Block) {
-        let old_macro_rules = self.parent_scope.macro_rules;
-        visit::walk_block(self, block);
-        self.parent_scope.macro_rules = old_macro_rules;
-    }
-
-    // NOTE: if doc-comments are ever allowed on other nodes (e.g. function parameters),
-    // then this will have to implement other visitor methods too.
-}
diff --git a/src/librustdoc/passes/collect_trait_impls.rs b/src/librustdoc/passes/collect_trait_impls.rs
index 7d15a207d06..878e738fe50 100644
--- a/src/librustdoc/passes/collect_trait_impls.rs
+++ b/src/librustdoc/passes/collect_trait_impls.rs
@@ -45,18 +45,20 @@ pub(crate) fn collect_trait_impls(mut krate: Crate, cx: &mut DocContext<'_>) ->
     let mut new_items_local = Vec::new();
 
     // External trait impls.
-    cx.with_all_trait_impls(|cx, all_trait_impls| {
+    {
         let _prof_timer = cx.tcx.sess.prof.generic_activity("build_extern_trait_impls");
-        for &impl_def_id in all_trait_impls.iter().skip_while(|def_id| def_id.is_local()) {
-            inline::build_impl(cx, None, impl_def_id, None, &mut new_items_external);
+        for &cnum in cx.tcx.crates(()) {
+            for &impl_def_id in cx.tcx.trait_impls_in_crate(cnum) {
+                inline::build_impl(cx, None, impl_def_id, None, &mut new_items_external);
+            }
         }
-    });
+    }
 
     // Local trait impls.
-    cx.with_all_trait_impls(|cx, all_trait_impls| {
+    {
         let _prof_timer = cx.tcx.sess.prof.generic_activity("build_local_trait_impls");
         let mut attr_buf = Vec::new();
-        for &impl_def_id in all_trait_impls.iter().take_while(|def_id| def_id.is_local()) {
+        for &impl_def_id in cx.tcx.trait_impls_in_crate(LOCAL_CRATE) {
             let mut parent = Some(cx.tcx.parent(impl_def_id));
             while let Some(did) = parent {
                 attr_buf.extend(
@@ -76,7 +78,7 @@ pub(crate) fn collect_trait_impls(mut krate: Crate, cx: &mut DocContext<'_>) ->
             inline::build_impl(cx, None, impl_def_id, Some(&attr_buf), &mut new_items_local);
             attr_buf.clear();
         }
-    });
+    }
 
     cx.tcx.sess.prof.generic_activity("build_primitive_trait_impls").run(|| {
         for def_id in PrimitiveType::all_impls(cx.tcx) {
diff --git a/src/librustdoc/passes/mod.rs b/src/librustdoc/passes/mod.rs
index 634e70ec97a..4b1ff68df50 100644
--- a/src/librustdoc/passes/mod.rs
+++ b/src/librustdoc/passes/mod.rs
@@ -2,11 +2,12 @@
 //! process.
 
 use rustc_middle::ty::TyCtxt;
+use rustc_resolve::rustdoc::DocFragmentKind;
 use rustc_span::{InnerSpan, Span, DUMMY_SP};
 use std::ops::Range;
 
 use self::Condition::*;
-use crate::clean::{self, DocFragmentKind};
+use crate::clean;
 use crate::core::DocContext;
 
 mod stripper;
diff --git a/src/librustdoc/visit_ast.rs b/src/librustdoc/visit_ast.rs
index 088cb3f3394..9c1e5f4a3cd 100644
--- a/src/librustdoc/visit_ast.rs
+++ b/src/librustdoc/visit_ast.rs
@@ -378,7 +378,7 @@ impl<'a, 'tcx> RustdocVisitor<'a, 'tcx> {
                 let nonexported = !tcx.has_attr(def_id, sym::macro_export);
 
                 if is_macro_2_0 || nonexported || self.inlining {
-                    self.add_to_current_mod(item, renamed, None);
+                    self.add_to_current_mod(item, renamed, import_id);
                 }
             }
             hir::ItemKind::Mod(ref m) => {
diff --git a/src/librustdoc/visit_lib.rs b/src/librustdoc/visit_lib.rs
index 07d8b78d767..fd4f9254107 100644
--- a/src/librustdoc/visit_lib.rs
+++ b/src/librustdoc/visit_lib.rs
@@ -1,8 +1,7 @@
 use crate::core::DocContext;
-use rustc_hir::def::{DefKind, Res};
+use rustc_hir::def::DefKind;
 use rustc_hir::def_id::{DefId, DefIdSet};
 use rustc_middle::ty::TyCtxt;
-use rustc_resolve::Resolver;
 
 // FIXME: this may not be exhaustive, but is sufficient for rustdocs current uses
 
@@ -26,10 +25,6 @@ impl RustdocEffectiveVisibilities {
     define_method!(is_directly_public);
     define_method!(is_exported);
     define_method!(is_reachable);
-
-    pub(crate) fn init(&mut self, extern_public: DefIdSet) {
-        self.extern_public = extern_public;
-    }
 }
 
 pub(crate) fn lib_embargo_visit_item(cx: &mut DocContext<'_>, def_id: DefId) {
@@ -42,17 +37,6 @@ pub(crate) fn lib_embargo_visit_item(cx: &mut DocContext<'_>, def_id: DefId) {
     .visit_item(def_id)
 }
 
-pub(crate) fn early_lib_embargo_visit_item(
-    resolver: &Resolver<'_>,
-    extern_public: &mut DefIdSet,
-    def_id: DefId,
-    is_mod: bool,
-) {
-    assert!(!def_id.is_local());
-    EarlyLibEmbargoVisitor { resolver, extern_public, visited_mods: Default::default() }
-        .visit_item(def_id, is_mod)
-}
-
 /// Similar to `librustc_privacy::EmbargoVisitor`, but also takes
 /// specific rustdoc annotations into account (i.e., `doc(hidden)`)
 struct LibEmbargoVisitor<'a, 'tcx> {
@@ -63,14 +47,6 @@ struct LibEmbargoVisitor<'a, 'tcx> {
     visited_mods: DefIdSet,
 }
 
-struct EarlyLibEmbargoVisitor<'r, 'ra> {
-    resolver: &'r Resolver<'ra>,
-    // Effective visibilities for reachable nodes
-    extern_public: &'r mut DefIdSet,
-    // Keeps track of already visited modules, in case a module re-exports its parent
-    visited_mods: DefIdSet,
-}
-
 impl LibEmbargoVisitor<'_, '_> {
     fn visit_mod(&mut self, def_id: DefId) {
         if !self.visited_mods.insert(def_id) {
@@ -95,28 +71,3 @@ impl LibEmbargoVisitor<'_, '_> {
         }
     }
 }
-
-impl EarlyLibEmbargoVisitor<'_, '_> {
-    fn visit_mod(&mut self, def_id: DefId) {
-        if !self.visited_mods.insert(def_id) {
-            return;
-        }
-
-        for item in self.resolver.cstore().module_children_untracked(def_id, self.resolver.sess()) {
-            if let Some(def_id) = item.res.opt_def_id() {
-                if item.vis.is_public() {
-                    self.visit_item(def_id, matches!(item.res, Res::Def(DefKind::Mod, _)));
-                }
-            }
-        }
-    }
-
-    fn visit_item(&mut self, def_id: DefId, is_mod: bool) {
-        if !self.resolver.cstore().is_doc_hidden_untracked(def_id) {
-            self.extern_public.insert(def_id);
-            if is_mod {
-                self.visit_mod(def_id);
-            }
-        }
-    }
-}