diff --git a/Cargo.lock b/Cargo.lock
index ad01ef5e41f16..ed5065d7611a5 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -4613,6 +4613,7 @@ name = "rustc_resolve"
 version = "0.0.0"
 dependencies = [
  "bitflags",
+ "pulldown-cmark 0.9.2",
  "rustc_arena",
  "rustc_ast",
  "rustc_ast_pretty",
@@ -4878,7 +4879,6 @@ dependencies = [
  "itertools",
  "minifier",
  "once_cell",
- "pulldown-cmark 0.9.2",
  "rayon",
  "regex",
  "rustdoc-json-types",
diff --git a/compiler/rustc_data_structures/src/stable_hasher.rs b/compiler/rustc_data_structures/src/stable_hasher.rs
index ae4836645fa41..e0d77cdaebb36 100644
--- a/compiler/rustc_data_structures/src/stable_hasher.rs
+++ b/compiler/rustc_data_structures/src/stable_hasher.rs
@@ -486,6 +486,14 @@ impl<HCX> ToStableHashKey<HCX> for String {
     }
 }
 
+impl<HCX, T1: ToStableHashKey<HCX>, T2: ToStableHashKey<HCX>> ToStableHashKey<HCX> for (T1, T2) {
+    type KeyType = (T1::KeyType, T2::KeyType);
+    #[inline]
+    fn to_stable_hash_key(&self, hcx: &HCX) -> Self::KeyType {
+        (self.0.to_stable_hash_key(hcx), self.1.to_stable_hash_key(hcx))
+    }
+}
+
 impl<CTX> HashStable<CTX> for bool {
     #[inline]
     fn hash_stable(&self, ctx: &mut CTX, hasher: &mut StableHasher) {
diff --git a/compiler/rustc_hir/src/def.rs b/compiler/rustc_hir/src/def.rs
index cca5ead0f8395..f1801a0f844f7 100644
--- a/compiler/rustc_hir/src/def.rs
+++ b/compiler/rustc_hir/src/def.rs
@@ -2,6 +2,8 @@ use crate::hir;
 
 use rustc_ast as ast;
 use rustc_ast::NodeId;
+use rustc_data_structures::fx::FxHashMap;
+use rustc_data_structures::stable_hasher::ToStableHashKey;
 use rustc_macros::HashStable_Generic;
 use rustc_span::def_id::{DefId, LocalDefId};
 use rustc_span::hygiene::MacroKind;
@@ -472,7 +474,8 @@ impl PartialRes {
 
 /// Different kinds of symbols can coexist even if they share the same textual name.
 /// Therefore, they each have a separate universe (known as a "namespace").
-#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)]
+#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Encodable, Decodable)]
+#[derive(HashStable_Generic)]
 pub enum Namespace {
     /// The type namespace includes `struct`s, `enum`s, `union`s, `trait`s, and `mod`s
     /// (and, by extension, crates).
@@ -499,6 +502,15 @@ impl Namespace {
     }
 }
 
+impl<CTX: crate::HashStableContext> ToStableHashKey<CTX> for Namespace {
+    type KeyType = Namespace;
+
+    #[inline]
+    fn to_stable_hash_key(&self, _: &CTX) -> Namespace {
+        *self
+    }
+}
+
 /// Just a helper ‒ separate structure for each namespace.
 #[derive(Copy, Clone, Default, Debug)]
 pub struct PerNS<T> {
@@ -760,3 +772,5 @@ pub enum LifetimeRes {
     /// HACK: This is used to recover the NodeId of an elided lifetime.
     ElidedAnchor { start: NodeId, end: NodeId },
 }
+
+pub type DocLinkResMap = FxHashMap<(Symbol, Namespace), Option<Res<NodeId>>>;
diff --git a/compiler/rustc_metadata/src/rmeta/decoder.rs b/compiler/rustc_metadata/src/rmeta/decoder.rs
index e2b07fad6e782..800f85063c41a 100644
--- a/compiler/rustc_metadata/src/rmeta/decoder.rs
+++ b/compiler/rustc_metadata/src/rmeta/decoder.rs
@@ -11,7 +11,7 @@ use rustc_data_structures::sync::{Lock, LockGuard, Lrc, OnceCell};
 use rustc_data_structures::unhash::UnhashMap;
 use rustc_expand::base::{SyntaxExtension, SyntaxExtensionKind};
 use rustc_expand::proc_macro::{AttrProcMacro, BangProcMacro, DeriveProcMacro};
-use rustc_hir::def::{CtorKind, DefKind, Res};
+use rustc_hir::def::{CtorKind, DefKind, DocLinkResMap, Res};
 use rustc_hir::def_id::{CrateNum, DefId, DefIndex, CRATE_DEF_INDEX, LOCAL_CRATE};
 use rustc_hir::definitions::{DefKey, DefPath, DefPathData, DefPathHash};
 use rustc_hir::diagnostic_items::DiagnosticItems;
@@ -1163,20 +1163,6 @@ impl<'a, 'tcx> CrateMetadataRef<'a> {
         )
     }
 
-    /// Decodes all inherent impls in the crate (for rustdoc).
-    fn get_inherent_impls(self) -> impl Iterator<Item = (DefId, DefId)> + 'a {
-        (0..self.root.tables.inherent_impls.size()).flat_map(move |i| {
-            let ty_index = DefIndex::from_usize(i);
-            let ty_def_id = self.local_def_id(ty_index);
-            self.root
-                .tables
-                .inherent_impls
-                .get(self, ty_index)
-                .decode(self)
-                .map(move |impl_index| (ty_def_id, self.local_def_id(impl_index)))
-        })
-    }
-
     /// Decodes all traits in the crate (for rustdoc and rustc diagnostics).
     fn get_traits(self) -> impl Iterator<Item = DefId> + 'a {
         self.root.traits.decode(self).map(move |index| self.local_def_id(index))
@@ -1195,13 +1181,6 @@ impl<'a, 'tcx> CrateMetadataRef<'a> {
         })
     }
 
-    fn get_all_incoherent_impls(self) -> impl Iterator<Item = DefId> + 'a {
-        self.cdata
-            .incoherent_impls
-            .values()
-            .flat_map(move |impls| impls.decode(self).map(move |idx| self.local_def_id(idx)))
-    }
-
     fn get_incoherent_impls(self, tcx: TyCtxt<'tcx>, simp: SimplifiedType) -> &'tcx [DefId] {
         if let Some(impls) = self.cdata.incoherent_impls.get(&simp) {
             tcx.arena.alloc_from_iter(impls.decode(self).map(|idx| self.local_def_id(idx)))
@@ -1598,6 +1577,24 @@ impl<'a, 'tcx> CrateMetadataRef<'a> {
     fn get_is_intrinsic(self, index: DefIndex) -> bool {
         self.root.tables.is_intrinsic.get(self, index)
     }
+
+    fn get_doc_link_resolutions(self, index: DefIndex) -> DocLinkResMap {
+        self.root
+            .tables
+            .doc_link_resolutions
+            .get(self, index)
+            .expect("no resolutions for a doc link")
+            .decode(self)
+    }
+
+    fn get_doc_link_traits_in_scope(self, index: DefIndex) -> impl Iterator<Item = DefId> + 'a {
+        self.root
+            .tables
+            .doc_link_traits_in_scope
+            .get(self, index)
+            .expect("no traits in scope for a doc link")
+            .decode(self)
+    }
 }
 
 impl CrateMetadata {
diff --git a/compiler/rustc_metadata/src/rmeta/decoder/cstore_impl.rs b/compiler/rustc_metadata/src/rmeta/decoder/cstore_impl.rs
index 07cc84ab95368..b12f9b5c917e8 100644
--- a/compiler/rustc_metadata/src/rmeta/decoder/cstore_impl.rs
+++ b/compiler/rustc_metadata/src/rmeta/decoder/cstore_impl.rs
@@ -345,6 +345,10 @@ provide! { tcx, def_id, other, cdata,
     expn_that_defined => { cdata.get_expn_that_defined(def_id.index, tcx.sess) }
     generator_diagnostic_data => { cdata.get_generator_diagnostic_data(tcx, def_id.index) }
     is_doc_hidden => { cdata.get_attr_flags(def_id.index).contains(AttrFlags::IS_DOC_HIDDEN) }
+    doc_link_resolutions => { tcx.arena.alloc(cdata.get_doc_link_resolutions(def_id.index)) }
+    doc_link_traits_in_scope => {
+        tcx.arena.alloc_from_iter(cdata.get_doc_link_traits_in_scope(def_id.index))
+    }
 }
 
 pub(in crate::rmeta) fn provide(providers: &mut Providers) {
@@ -613,36 +617,6 @@ impl CStore {
         self.get_crate_data(cnum).get_trait_impls()
     }
 
-    /// Decodes all inherent impls in the crate (for rustdoc).
-    pub fn inherent_impls_in_crate_untracked(
-        &self,
-        cnum: CrateNum,
-    ) -> impl Iterator<Item = (DefId, DefId)> + '_ {
-        self.get_crate_data(cnum).get_inherent_impls()
-    }
-
-    /// Decodes all incoherent inherent impls in the crate (for rustdoc).
-    pub fn incoherent_impls_in_crate_untracked(
-        &self,
-        cnum: CrateNum,
-    ) -> impl Iterator<Item = DefId> + '_ {
-        self.get_crate_data(cnum).get_all_incoherent_impls()
-    }
-
-    pub fn associated_item_def_ids_untracked<'a>(
-        &'a self,
-        def_id: DefId,
-        sess: &'a Session,
-    ) -> impl Iterator<Item = DefId> + 'a {
-        self.get_crate_data(def_id.krate).get_associated_item_def_ids(def_id.index, sess)
-    }
-
-    pub fn may_have_doc_links_untracked(&self, def_id: DefId) -> bool {
-        self.get_crate_data(def_id.krate)
-            .get_attr_flags(def_id.index)
-            .contains(AttrFlags::MAY_HAVE_DOC_LINKS)
-    }
-
     pub fn is_doc_hidden_untracked(&self, def_id: DefId) -> bool {
         self.get_crate_data(def_id.krate)
             .get_attr_flags(def_id.index)
diff --git a/compiler/rustc_metadata/src/rmeta/encoder.rs b/compiler/rustc_metadata/src/rmeta/encoder.rs
index 85e9ae9a98302..263c71ae70286 100644
--- a/compiler/rustc_metadata/src/rmeta/encoder.rs
+++ b/compiler/rustc_metadata/src/rmeta/encoder.rs
@@ -3,7 +3,6 @@ use crate::rmeta::def_path_hash_map::DefPathHashMapRef;
 use crate::rmeta::table::TableBuilder;
 use crate::rmeta::*;
 
-use rustc_ast::util::comments;
 use rustc_ast::Attribute;
 use rustc_data_structures::fingerprint::Fingerprint;
 use rustc_data_structures::fx::{FxHashMap, FxIndexSet};
@@ -772,7 +771,6 @@ impl<'a, 'tcx> EncodeContext<'a, 'tcx> {
 
 struct AnalyzeAttrState {
     is_exported: bool,
-    may_have_doc_links: bool,
     is_doc_hidden: bool,
 }
 
@@ -790,15 +788,12 @@ fn analyze_attr(attr: &Attribute, state: &mut AnalyzeAttrState) -> bool {
     let mut should_encode = false;
     if rustc_feature::is_builtin_only_local(attr.name_or_empty()) {
         // Attributes marked local-only don't need to be encoded for downstream crates.
-    } else if let Some(s) = attr.doc_str() {
+    } else if attr.doc_str().is_some() {
         // We keep all doc comments reachable to rustdoc because they might be "imported" into
         // downstream crates if they use `#[doc(inline)]` to copy an item's documentation into
         // their own.
         if state.is_exported {
             should_encode = true;
-            if comments::may_have_doc_links(s.as_str()) {
-                state.may_have_doc_links = true;
-            }
         }
     } else if attr.has_name(sym::doc) {
         // If this is a `doc` attribute that doesn't have anything except maybe `inline` (as in
@@ -1139,7 +1134,6 @@ impl<'a, 'tcx> EncodeContext<'a, 'tcx> {
         let tcx = self.tcx;
         let mut state = AnalyzeAttrState {
             is_exported: tcx.effective_visibilities(()).is_exported(def_id),
-            may_have_doc_links: false,
             is_doc_hidden: false,
         };
         let attr_iter = tcx
@@ -1151,9 +1145,6 @@ impl<'a, 'tcx> EncodeContext<'a, 'tcx> {
         record_array!(self.tables.attributes[def_id.to_def_id()] <- attr_iter);
 
         let mut attr_flags = AttrFlags::empty();
-        if state.may_have_doc_links {
-            attr_flags |= AttrFlags::MAY_HAVE_DOC_LINKS;
-        }
         if state.is_doc_hidden {
             attr_flags |= AttrFlags::IS_DOC_HIDDEN;
         }
@@ -1231,6 +1222,14 @@ impl<'a, 'tcx> EncodeContext<'a, 'tcx> {
                 def_id.index
             }));
         }
+
+        for (def_id, res_map) in &tcx.resolutions(()).doc_link_resolutions {
+            record!(self.tables.doc_link_resolutions[def_id.to_def_id()] <- res_map);
+        }
+
+        for (def_id, traits) in &tcx.resolutions(()).doc_link_traits_in_scope {
+            record_array!(self.tables.doc_link_traits_in_scope[def_id.to_def_id()] <- traits);
+        }
     }
 
     #[instrument(level = "trace", skip(self))]
@@ -1715,6 +1714,12 @@ impl<'a, 'tcx> EncodeContext<'a, 'tcx> {
                 record!(self.tables.lookup_stability[LOCAL_CRATE.as_def_id()] <- stability);
             }
             self.encode_deprecation(LOCAL_CRATE.as_def_id());
+            if let Some(res_map) = tcx.resolutions(()).doc_link_resolutions.get(&CRATE_DEF_ID) {
+                record!(self.tables.doc_link_resolutions[LOCAL_CRATE.as_def_id()] <- res_map);
+            }
+            if let Some(traits) = tcx.resolutions(()).doc_link_traits_in_scope.get(&CRATE_DEF_ID) {
+                record_array!(self.tables.doc_link_traits_in_scope[LOCAL_CRATE.as_def_id()] <- traits);
+            }
 
             // Normally, this information is encoded when we walk the items
             // defined in this crate. However, we skip doing that for proc-macro crates,
@@ -2225,6 +2230,18 @@ fn encode_metadata_impl(tcx: TyCtxt<'_>, path: &Path) {
 
 pub fn provide(providers: &mut Providers) {
     *providers = Providers {
+        doc_link_resolutions: |tcx, def_id| {
+            tcx.resolutions(())
+                .doc_link_resolutions
+                .get(&def_id.expect_local())
+                .expect("no resolutions for a doc link")
+        },
+        doc_link_traits_in_scope: |tcx, def_id| {
+            tcx.resolutions(())
+                .doc_link_traits_in_scope
+                .get(&def_id.expect_local())
+                .expect("no traits in scope for a doc link")
+        },
         traits_in_crate: |tcx, cnum| {
             assert_eq!(cnum, LOCAL_CRATE);
 
diff --git a/compiler/rustc_metadata/src/rmeta/mod.rs b/compiler/rustc_metadata/src/rmeta/mod.rs
index a74aa381d9eb8..9227609cc8b66 100644
--- a/compiler/rustc_metadata/src/rmeta/mod.rs
+++ b/compiler/rustc_metadata/src/rmeta/mod.rs
@@ -9,7 +9,7 @@ use rustc_attr as attr;
 use rustc_data_structures::svh::Svh;
 use rustc_data_structures::sync::MetadataRef;
 use rustc_hir as hir;
-use rustc_hir::def::{CtorKind, DefKind};
+use rustc_hir::def::{CtorKind, DefKind, DocLinkResMap};
 use rustc_hir::def_id::{CrateNum, DefId, DefIndex, DefPathHash, StableCrateId};
 use rustc_hir::definitions::DefKey;
 use rustc_hir::lang_items::LangItem;
@@ -413,6 +413,8 @@ define_tables! {
     module_reexports: Table<DefIndex, LazyArray<ModChild>>,
     deduced_param_attrs: Table<DefIndex, LazyArray<DeducedParamAttrs>>,
     trait_impl_trait_tys: Table<DefIndex, LazyValue<FxHashMap<DefId, Ty<'static>>>>,
+    doc_link_resolutions: Table<DefIndex, LazyValue<DocLinkResMap>>,
+    doc_link_traits_in_scope: Table<DefIndex, LazyArray<DefId>>,
 }
 
 #[derive(TyEncodable, TyDecodable)]
@@ -426,8 +428,7 @@ struct VariantData {
 bitflags::bitflags! {
     #[derive(Default)]
     pub struct AttrFlags: u8 {
-        const MAY_HAVE_DOC_LINKS = 1 << 0;
-        const IS_DOC_HIDDEN      = 1 << 1;
+        const IS_DOC_HIDDEN = 1 << 0;
     }
 }
 
diff --git a/compiler/rustc_middle/src/arena.rs b/compiler/rustc_middle/src/arena.rs
index 2ba7ec5b15192..9d2144c443b4a 100644
--- a/compiler/rustc_middle/src/arena.rs
+++ b/compiler/rustc_middle/src/arena.rs
@@ -113,6 +113,7 @@ macro_rules! arena_types {
             [decode] trait_impl_trait_tys: rustc_data_structures::fx::FxHashMap<rustc_hir::def_id::DefId, rustc_middle::ty::Ty<'tcx>>,
             [] bit_set_u32: rustc_index::bit_set::BitSet<u32>,
             [] external_constraints: rustc_middle::traits::solve::ExternalConstraintsData<'tcx>,
+            [decode] doc_link_resolutions: rustc_hir::def::DocLinkResMap,
         ]);
     )
 }
diff --git a/compiler/rustc_middle/src/query/mod.rs b/compiler/rustc_middle/src/query/mod.rs
index 0a16ede64991d..d37d6b37a37c3 100644
--- a/compiler/rustc_middle/src/query/mod.rs
+++ b/compiler/rustc_middle/src/query/mod.rs
@@ -2156,4 +2156,16 @@ rustc_queries! {
         desc { |tcx| "deducing parameter attributes for {}", tcx.def_path_str(def_id) }
         separate_provide_extern
     }
+
+    query doc_link_resolutions(def_id: DefId) -> &'tcx DocLinkResMap {
+        eval_always
+        desc { "resolutions for documentation links for a module" }
+        separate_provide_extern
+    }
+
+    query doc_link_traits_in_scope(def_id: DefId) -> &'tcx [DefId] {
+        eval_always
+        desc { "traits in scope for documentation links for a module" }
+        separate_provide_extern
+    }
 }
diff --git a/compiler/rustc_middle/src/ty/mod.rs b/compiler/rustc_middle/src/ty/mod.rs
index 09c3d5b736cf1..cff3ba194bdb2 100644
--- a/compiler/rustc_middle/src/ty/mod.rs
+++ b/compiler/rustc_middle/src/ty/mod.rs
@@ -36,7 +36,7 @@ use rustc_data_structures::intern::Interned;
 use rustc_data_structures::stable_hasher::{HashStable, StableHasher};
 use rustc_data_structures::tagged_ptr::CopyTaggedPtr;
 use rustc_hir as hir;
-use rustc_hir::def::{CtorKind, CtorOf, DefKind, LifetimeRes, Res};
+use rustc_hir::def::{CtorKind, CtorOf, DefKind, DocLinkResMap, LifetimeRes, Res};
 use rustc_hir::def_id::{CrateNum, DefId, DefIdMap, LocalDefId, LocalDefIdMap};
 use rustc_hir::Node;
 use rustc_index::vec::IndexVec;
@@ -181,6 +181,8 @@ pub struct ResolverGlobalCtxt {
     /// exist under `std`. For example, wrote `str::from_utf8` instead of `std::str::from_utf8`.
     pub confused_type_with_std_module: FxHashMap<Span, Span>,
     pub registered_tools: RegisteredTools,
+    pub doc_link_resolutions: FxHashMap<LocalDefId, DocLinkResMap>,
+    pub doc_link_traits_in_scope: FxHashMap<LocalDefId, Vec<DefId>>,
 }
 
 /// Resolutions that should only be used for lowering.
diff --git a/compiler/rustc_middle/src/ty/parameterized.rs b/compiler/rustc_middle/src/ty/parameterized.rs
index 84edb5f2a4288..303675d3ca5c1 100644
--- a/compiler/rustc_middle/src/ty/parameterized.rs
+++ b/compiler/rustc_middle/src/ty/parameterized.rs
@@ -81,6 +81,8 @@ trivially_parameterized_over_tcx! {
     rustc_hir::IsAsync,
     rustc_hir::LangItem,
     rustc_hir::def::DefKind,
+    rustc_hir::def::DocLinkResMap,
+    rustc_hir::def_id::DefId,
     rustc_hir::def_id::DefIndex,
     rustc_hir::definitions::DefKey,
     rustc_index::bit_set::BitSet<u32>,
diff --git a/compiler/rustc_middle/src/ty/query.rs b/compiler/rustc_middle/src/ty/query.rs
index 933aaadd62e1d..bec70974dde04 100644
--- a/compiler/rustc_middle/src/ty/query.rs
+++ b/compiler/rustc_middle/src/ty/query.rs
@@ -45,7 +45,7 @@ use rustc_data_structures::sync::Lrc;
 use rustc_data_structures::unord::UnordSet;
 use rustc_errors::ErrorGuaranteed;
 use rustc_hir as hir;
-use rustc_hir::def::DefKind;
+use rustc_hir::def::{DefKind, DocLinkResMap};
 use rustc_hir::def_id::{CrateNum, DefId, DefIdMap, DefIdSet, LocalDefId};
 use rustc_hir::hir_id::OwnerId;
 use rustc_hir::lang_items::{LangItem, LanguageItems};
diff --git a/compiler/rustc_resolve/Cargo.toml b/compiler/rustc_resolve/Cargo.toml
index 7c3a0f8f277b5..d4935b52b1044 100644
--- a/compiler/rustc_resolve/Cargo.toml
+++ b/compiler/rustc_resolve/Cargo.toml
@@ -7,6 +7,7 @@ edition = "2021"
 
 [dependencies]
 bitflags = "1.2.1"
+pulldown-cmark = { version = "0.9.2", default-features = false }
 rustc_arena = { path = "../rustc_arena" }
 rustc_ast = { path = "../rustc_ast" }
 rustc_ast_pretty = { path = "../rustc_ast_pretty" }
diff --git a/compiler/rustc_resolve/src/build_reduced_graph.rs b/compiler/rustc_resolve/src/build_reduced_graph.rs
index 2fb62ce53ba6e..e74bb0a9a4f32 100644
--- a/compiler/rustc_resolve/src/build_reduced_graph.rs
+++ b/compiler/rustc_resolve/src/build_reduced_graph.rs
@@ -95,7 +95,7 @@ impl<'a> Resolver<'a> {
     /// Reachable macros with block module parents exist due to `#[macro_export] macro_rules!`,
     /// but they cannot use def-site hygiene, so the assumption holds
     /// (<https://github.com/rust-lang/rust/pull/77984#issuecomment-712445508>).
-    pub fn get_nearest_non_block_module(&mut self, mut def_id: DefId) -> Module<'a> {
+    pub(crate) fn get_nearest_non_block_module(&mut self, mut def_id: DefId) -> Module<'a> {
         loop {
             match self.get_module(def_id) {
                 Some(module) => return module,
@@ -104,7 +104,7 @@ impl<'a> Resolver<'a> {
         }
     }
 
-    pub fn expect_module(&mut self, def_id: DefId) -> Module<'a> {
+    pub(crate) fn expect_module(&mut self, def_id: DefId) -> Module<'a> {
         self.get_module(def_id).expect("argument `DefId` is not a module")
     }
 
diff --git a/compiler/rustc_resolve/src/late.rs b/compiler/rustc_resolve/src/late.rs
index 3ca10ac50baa6..ca2a99971c316 100644
--- a/compiler/rustc_resolve/src/late.rs
+++ b/compiler/rustc_resolve/src/late.rs
@@ -8,7 +8,7 @@
 
 use RibKind::*;
 
-use crate::{path_names_to_string, BindingError, Finalize, LexicalScopeBinding};
+use crate::{path_names_to_string, rustdoc, BindingError, Finalize, LexicalScopeBinding};
 use crate::{Module, ModuleOrUniformRoot, NameBinding, ParentScope, PathResult};
 use crate::{ResolutionError, Resolver, Segment, UseError};
 
@@ -24,12 +24,13 @@ use rustc_hir::{BindingAnnotation, PrimTy, TraitCandidate};
 use rustc_middle::middle::resolve_lifetime::Set1;
 use rustc_middle::ty::DefIdTree;
 use rustc_middle::{bug, span_bug};
+use rustc_session::config::{CrateType, ResolveDocLinks};
 use rustc_session::lint;
+use rustc_span::source_map::{respan, Spanned};
 use rustc_span::symbol::{kw, sym, Ident, Symbol};
-use rustc_span::{BytePos, Span};
+use rustc_span::{BytePos, Span, SyntaxContext};
 use smallvec::{smallvec, SmallVec};
 
-use rustc_span::source_map::{respan, Spanned};
 use std::assert_matches::debug_assert_matches;
 use std::borrow::Cow;
 use std::collections::{hash_map::Entry, BTreeSet};
@@ -493,6 +494,30 @@ impl<'a> PathSource<'a> {
     }
 }
 
+/// At this point for most items we can answer whether that item is exported or not,
+/// but some items like impls require type information to determine exported-ness, so we make a
+/// conservative estimate for them (e.g. based on nominal visibility).
+#[derive(Clone, Copy)]
+enum MaybeExported<'a> {
+    Ok(NodeId),
+    Impl(Option<DefId>),
+    ImplItem(Result<DefId, &'a Visibility>),
+}
+
+impl MaybeExported<'_> {
+    fn eval(self, r: &Resolver<'_>) -> bool {
+        let def_id = match self {
+            MaybeExported::Ok(node_id) => Some(r.local_def_id(node_id)),
+            MaybeExported::Impl(Some(trait_def_id)) | MaybeExported::ImplItem(Ok(trait_def_id)) => {
+                trait_def_id.as_local()
+            }
+            MaybeExported::Impl(None) => return true,
+            MaybeExported::ImplItem(Err(vis)) => return vis.kind.is_pub(),
+        };
+        def_id.map_or(true, |def_id| r.effective_visibilities.is_exported(def_id))
+    }
+}
+
 #[derive(Default)]
 struct DiagnosticMetadata<'ast> {
     /// The current trait's associated items' ident, used for diagnostic suggestions.
@@ -620,7 +645,9 @@ impl<'a: 'ast, 'ast> Visitor<'ast> for LateResolutionVisitor<'a, '_, 'ast> {
         self.resolve_arm(arm);
     }
     fn visit_block(&mut self, block: &'ast Block) {
+        let old_macro_rules = self.parent_scope.macro_rules;
         self.resolve_block(block);
+        self.parent_scope.macro_rules = old_macro_rules;
     }
     fn visit_anon_const(&mut self, constant: &'ast AnonConst) {
         // We deal with repeat expressions explicitly in `resolve_expr`.
@@ -771,6 +798,7 @@ impl<'a: 'ast, 'ast> Visitor<'ast> for LateResolutionVisitor<'a, '_, 'ast> {
         );
     }
     fn visit_foreign_item(&mut self, foreign_item: &'ast ForeignItem) {
+        self.resolve_doc_links(&foreign_item.attrs, MaybeExported::Ok(foreign_item.id));
         match foreign_item.kind {
             ForeignItemKind::TyAlias(box TyAlias { ref generics, .. }) => {
                 self.with_generic_param_rib(
@@ -1159,6 +1187,16 @@ impl<'a: 'ast, 'ast> Visitor<'ast> for LateResolutionVisitor<'a, '_, 'ast> {
             })
         });
     }
+
+    fn visit_variant(&mut self, v: &'ast Variant) {
+        self.resolve_doc_links(&v.attrs, MaybeExported::Ok(v.id));
+        visit::walk_variant(self, v)
+    }
+
+    fn visit_field_def(&mut self, f: &'ast FieldDef) {
+        self.resolve_doc_links(&f.attrs, MaybeExported::Ok(f.id));
+        visit::walk_field_def(self, f)
+    }
 }
 
 impl<'a: 'ast, 'b, 'ast> LateResolutionVisitor<'a, 'b, 'ast> {
@@ -2185,6 +2223,12 @@ impl<'a: 'ast, 'b, 'ast> LateResolutionVisitor<'a, 'b, 'ast> {
     }
 
     fn resolve_item(&mut self, item: &'ast Item) {
+        let mod_inner_docs =
+            matches!(item.kind, ItemKind::Mod(..)) && rustdoc::inner_docs(&item.attrs);
+        if !mod_inner_docs && !matches!(item.kind, ItemKind::Impl(..)) {
+            self.resolve_doc_links(&item.attrs, MaybeExported::Ok(item.id));
+        }
+
         let name = item.ident.name;
         debug!("(resolving item) resolving {} ({:?})", name, item.kind);
 
@@ -2229,7 +2273,14 @@ impl<'a: 'ast, 'b, 'ast> LateResolutionVisitor<'a, 'b, 'ast> {
                 ..
             }) => {
                 self.diagnostic_metadata.current_impl_items = Some(impl_items);
-                self.resolve_implementation(generics, of_trait, &self_ty, item.id, impl_items);
+                self.resolve_implementation(
+                    &item.attrs,
+                    generics,
+                    of_trait,
+                    &self_ty,
+                    item.id,
+                    impl_items,
+                );
                 self.diagnostic_metadata.current_impl_items = None;
             }
 
@@ -2274,9 +2325,20 @@ impl<'a: 'ast, 'b, 'ast> LateResolutionVisitor<'a, 'b, 'ast> {
                 );
             }
 
-            ItemKind::Mod(..) | ItemKind::ForeignMod(_) => {
+            ItemKind::Mod(..) => {
                 self.with_scope(item.id, |this| {
+                    if mod_inner_docs {
+                        this.resolve_doc_links(&item.attrs, MaybeExported::Ok(item.id));
+                    }
+                    let old_macro_rules = this.parent_scope.macro_rules;
                     visit::walk_item(this, item);
+                    // Maintain macro_rules scopes in the same way as during early resolution
+                    // for diagnostics and doc links.
+                    if item.attrs.iter().all(|attr| {
+                        !attr.has_name(sym::macro_use) && !attr.has_name(sym::macro_escape)
+                    }) {
+                        this.parent_scope.macro_rules = old_macro_rules;
+                    }
                 });
             }
 
@@ -2309,14 +2371,22 @@ impl<'a: 'ast, 'b, 'ast> LateResolutionVisitor<'a, 'b, 'ast> {
                 self.future_proof_import(use_tree);
             }
 
-            ItemKind::ExternCrate(..) | ItemKind::MacroDef(..) => {
-                // do nothing, these are just around to be encoded
+            ItemKind::MacroDef(ref macro_def) => {
+                // Maintain macro_rules scopes in the same way as during early resolution
+                // for diagnostics and doc links.
+                if macro_def.macro_rules {
+                    let (macro_rules_scope, _) =
+                        self.r.macro_rules_scope(self.r.local_def_id(item.id));
+                    self.parent_scope.macro_rules = macro_rules_scope;
+                }
             }
 
-            ItemKind::GlobalAsm(_) => {
+            ItemKind::ForeignMod(_) | ItemKind::GlobalAsm(_) => {
                 visit::walk_item(self, item);
             }
 
+            ItemKind::ExternCrate(..) => {}
+
             ItemKind::MacCall(_) => panic!("unexpanded macro in resolve!"),
         }
     }
@@ -2544,6 +2614,7 @@ impl<'a: 'ast, 'b, 'ast> LateResolutionVisitor<'a, 'b, 'ast> {
             };
 
         for item in trait_items {
+            self.resolve_doc_links(&item.attrs, MaybeExported::Ok(item.id));
             match &item.kind {
                 AssocItemKind::Const(_, ty, default) => {
                     self.visit_ty(ty);
@@ -2631,6 +2702,7 @@ impl<'a: 'ast, 'b, 'ast> LateResolutionVisitor<'a, 'b, 'ast> {
 
     fn resolve_implementation(
         &mut self,
+        attrs: &[ast::Attribute],
         generics: &'ast Generics,
         opt_trait_reference: &'ast Option<TraitRef>,
         self_type: &'ast Ty,
@@ -2661,6 +2733,8 @@ impl<'a: 'ast, 'b, 'ast> LateResolutionVisitor<'a, 'b, 'ast> {
                                 opt_trait_reference.as_ref(),
                                 self_type,
                                 |this, trait_id| {
+                                    this.resolve_doc_links(attrs, MaybeExported::Impl(trait_id));
+
                                     let item_def_id = this.r.local_def_id(item_id);
 
                                     // Register the trait definitions from here.
@@ -2694,7 +2768,7 @@ impl<'a: 'ast, 'b, 'ast> LateResolutionVisitor<'a, 'b, 'ast> {
                                                 debug!("resolve_implementation with_self_rib_ns(ValueNS, ...)");
                                                 let mut seen_trait_items = Default::default();
                                                 for item in impl_items {
-                                                    this.resolve_impl_item(&**item, &mut seen_trait_items);
+                                                    this.resolve_impl_item(&**item, &mut seen_trait_items, trait_id);
                                                 }
                                             });
                                         });
@@ -2712,8 +2786,10 @@ impl<'a: 'ast, 'b, 'ast> LateResolutionVisitor<'a, 'b, 'ast> {
         &mut self,
         item: &'ast AssocItem,
         seen_trait_items: &mut FxHashMap<DefId, Span>,
+        trait_id: Option<DefId>,
     ) {
         use crate::ResolutionError::*;
+        self.resolve_doc_links(&item.attrs, MaybeExported::ImplItem(trait_id.ok_or(&item.vis)));
         match &item.kind {
             AssocItemKind::Const(_, ty, default) => {
                 debug!("resolve_implementation AssocItemKind::Const");
@@ -4116,6 +4192,102 @@ impl<'a: 'ast, 'b, 'ast> LateResolutionVisitor<'a, 'b, 'ast> {
             self.r.extra_lifetime_params_map.insert(async_node_id, extra_lifetime_params);
         }
     }
+
+    fn resolve_and_cache_rustdoc_path(&mut self, path_str: &str, ns: Namespace) -> bool {
+        // FIXME: This caching may be incorrect in case of multiple `macro_rules`
+        // items with the same name in the same module.
+        // Also hygiene is not considered.
+        let mut doc_link_resolutions = std::mem::take(&mut self.r.doc_link_resolutions);
+        let res = doc_link_resolutions
+            .entry(self.parent_scope.module.nearest_parent_mod().expect_local())
+            .or_default()
+            .entry((Symbol::intern(path_str), ns))
+            .or_insert_with_key(|(path, ns)| {
+                let res = self.r.resolve_rustdoc_path(path.as_str(), *ns, self.parent_scope);
+                if let Some(res) = res
+                    && let Some(def_id) = res.opt_def_id()
+                    && !def_id.is_local()
+                    && self.r.session.crate_types().contains(&CrateType::ProcMacro) {
+                    // Encoding foreign def ids in proc macro crate metadata will ICE.
+                    return None;
+                }
+                res
+            })
+            .is_some();
+        self.r.doc_link_resolutions = doc_link_resolutions;
+        res
+    }
+
+    fn resolve_doc_links(&mut self, attrs: &[Attribute], maybe_exported: MaybeExported<'_>) {
+        match self.r.session.opts.resolve_doc_links {
+            ResolveDocLinks::None => return,
+            ResolveDocLinks::ExportedMetadata
+                if !self.r.session.crate_types().iter().copied().any(CrateType::has_metadata)
+                    || !maybe_exported.eval(self.r) =>
+            {
+                return;
+            }
+            ResolveDocLinks::Exported if !maybe_exported.eval(self.r) => {
+                return;
+            }
+            ResolveDocLinks::ExportedMetadata
+            | ResolveDocLinks::Exported
+            | ResolveDocLinks::All => {}
+        }
+
+        if !attrs.iter().any(|attr| attr.may_have_doc_links()) {
+            return;
+        }
+
+        let mut need_traits_in_scope = false;
+        for path_str in rustdoc::attrs_to_preprocessed_links(attrs) {
+            // 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_rustdoc_path(&path_str, ns) {
+                    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 = &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_rustdoc_path(path, ns);
+                    }
+                }
+            }
+        }
+
+        if need_traits_in_scope {
+            // FIXME: hygiene is not considered.
+            let mut doc_link_traits_in_scope = std::mem::take(&mut self.r.doc_link_traits_in_scope);
+            doc_link_traits_in_scope
+                .entry(self.parent_scope.module.nearest_parent_mod().expect_local())
+                .or_insert_with(|| {
+                    self.r
+                        .traits_in_scope(None, &self.parent_scope, SyntaxContext::root(), None)
+                        .into_iter()
+                        .filter_map(|tr| {
+                            if !tr.def_id.is_local()
+                                && self.r.session.crate_types().contains(&CrateType::ProcMacro)
+                            {
+                                // Encoding foreign def ids in proc macro crate metadata will ICE.
+                                return None;
+                            }
+                            Some(tr.def_id)
+                        })
+                        .collect()
+                });
+            self.r.doc_link_traits_in_scope = doc_link_traits_in_scope;
+        }
+    }
 }
 
 struct LifetimeCountVisitor<'a, 'b> {
@@ -4162,6 +4334,7 @@ impl<'a> Resolver<'a> {
     pub(crate) fn late_resolve_crate(&mut self, krate: &Crate) {
         visit::walk_crate(&mut LifetimeCountVisitor { r: self }, krate);
         let mut late_resolution_visitor = LateResolutionVisitor::new(self);
+        late_resolution_visitor.resolve_doc_links(&krate.attrs, MaybeExported::Ok(CRATE_NODE_ID));
         visit::walk_crate(&mut late_resolution_visitor, krate);
         for (id, span) in late_resolution_visitor.diagnostic_metadata.unused_labels.iter() {
             self.lint_buffer.buffer_lint(lint::builtin::UNUSED_LABELS, *id, *span, "unused label");
diff --git a/compiler/rustc_resolve/src/lib.rs b/compiler/rustc_resolve/src/lib.rs
index 1b181b714005b..e61e83189c384 100644
--- a/compiler/rustc_resolve/src/lib.rs
+++ b/compiler/rustc_resolve/src/lib.rs
@@ -33,7 +33,7 @@ use rustc_data_structures::sync::{Lrc, RwLock};
 use rustc_errors::{Applicability, DiagnosticBuilder, ErrorGuaranteed};
 use rustc_expand::base::{DeriveResolutions, SyntaxExtension, SyntaxExtensionKind};
 use rustc_hir::def::Namespace::*;
-use rustc_hir::def::{self, CtorOf, DefKind, LifetimeRes, PartialRes};
+use rustc_hir::def::{self, CtorOf, DefKind, DocLinkResMap, LifetimeRes, PartialRes};
 use rustc_hir::def_id::{CrateNum, DefId, DefIdMap, LocalDefId};
 use rustc_hir::def_id::{CRATE_DEF_ID, LOCAL_CRATE};
 use rustc_hir::definitions::{DefPathData, Definitions};
@@ -78,6 +78,7 @@ mod ident;
 mod imports;
 mod late;
 mod macros;
+pub mod rustdoc;
 
 enum Weak {
     Yes,
@@ -138,17 +139,17 @@ enum ScopeSet<'a> {
 /// This struct is currently used only for early resolution (imports and macros),
 /// but not for late resolution yet.
 #[derive(Clone, Copy, Debug)]
-pub struct ParentScope<'a> {
-    pub module: Module<'a>,
+struct ParentScope<'a> {
+    module: Module<'a>,
     expansion: LocalExpnId,
-    pub macro_rules: MacroRulesScopeRef<'a>,
+    macro_rules: MacroRulesScopeRef<'a>,
     derives: &'a [ast::Path],
 }
 
 impl<'a> ParentScope<'a> {
     /// Creates a parent scope with the passed argument used as the module scope component,
     /// and other scope components set to default empty values.
-    pub fn module(module: Module<'a>, resolver: &Resolver<'a>) -> ParentScope<'a> {
+    fn module(module: Module<'a>, resolver: &Resolver<'a>) -> ParentScope<'a> {
         ParentScope {
             module,
             expansion: LocalExpnId::ROOT,
@@ -1046,6 +1047,8 @@ pub struct Resolver<'a> {
     lifetime_elision_allowed: FxHashSet<NodeId>,
 
     effective_visibilities: EffectiveVisibilities,
+    doc_link_resolutions: FxHashMap<LocalDefId, DocLinkResMap>,
+    doc_link_traits_in_scope: FxHashMap<LocalDefId, Vec<DefId>>,
 }
 
 /// Nothing really interesting here; it just provides memory for the rest of the crate.
@@ -1374,6 +1377,8 @@ impl<'a> Resolver<'a> {
             confused_type_with_std_module: Default::default(),
             lifetime_elision_allowed: Default::default(),
             effective_visibilities: Default::default(),
+            doc_link_resolutions: Default::default(),
+            doc_link_traits_in_scope: Default::default(),
         };
 
         let root_parent_scope = ParentScope::module(graph_root, &resolver);
@@ -1450,6 +1455,8 @@ impl<'a> Resolver<'a> {
             proc_macros,
             confused_type_with_std_module,
             registered_tools: self.registered_tools,
+            doc_link_resolutions: self.doc_link_resolutions,
+            doc_link_traits_in_scope: self.doc_link_traits_in_scope,
         };
         let ast_lowering = ty::ResolverAstLowering {
             legacy_const_generic_args: self.legacy_const_generic_args,
@@ -1494,6 +1501,8 @@ impl<'a> Resolver<'a> {
             confused_type_with_std_module: self.confused_type_with_std_module.clone(),
             registered_tools: self.registered_tools.clone(),
             effective_visibilities: self.effective_visibilities.clone(),
+            doc_link_resolutions: self.doc_link_resolutions.clone(),
+            doc_link_traits_in_scope: self.doc_link_traits_in_scope.clone(),
         };
         let ast_lowering = ty::ResolverAstLowering {
             legacy_const_generic_args: self.legacy_const_generic_args.clone(),
@@ -1575,7 +1584,7 @@ impl<'a> Resolver<'a> {
         });
     }
 
-    pub fn traits_in_scope(
+    fn traits_in_scope(
         &mut self,
         current_trait: Option<Module<'a>>,
         parent_scope: &ParentScope<'a>,
@@ -1927,7 +1936,7 @@ impl<'a> Resolver<'a> {
     /// isn't something that can be returned because it can't be made to live that long,
     /// and also it's a private type. Fortunately rustdoc doesn't need to know the error,
     /// just that an error occurred.
-    pub fn resolve_rustdoc_path(
+    fn resolve_rustdoc_path(
         &mut self,
         path_str: &str,
         ns: Namespace,
@@ -1959,16 +1968,6 @@ impl<'a> Resolver<'a> {
         }
     }
 
-    /// For rustdoc.
-    /// For local modules returns only reexports, for external modules returns all children.
-    pub fn module_children_or_reexports(&self, def_id: DefId) -> Vec<ModChild> {
-        if let Some(def_id) = def_id.as_local() {
-            self.reexport_map.get(&def_id).cloned().unwrap_or_default()
-        } else {
-            self.cstore().module_children_untracked(def_id, self.session).collect()
-        }
-    }
-
     /// For rustdoc.
     pub fn macro_rules_scope(&self, def_id: LocalDefId) -> (MacroRulesScopeRef<'a>, Res) {
         let scope = *self.macro_rules_scopes.get(&def_id).expect("not a `macro_rules` item");
@@ -1978,11 +1977,6 @@ impl<'a> Resolver<'a> {
         }
     }
 
-    /// For rustdoc.
-    pub fn get_partial_res(&self, node_id: NodeId) -> Option<PartialRes> {
-        self.partial_res_map.get(&node_id).copied()
-    }
-
     /// Retrieves the span of the given `DefId` if `DefId` is in the local crate.
     #[inline]
     pub fn opt_span(&self, def_id: DefId) -> Option<Span> {
diff --git a/compiler/rustc_resolve/src/macros.rs b/compiler/rustc_resolve/src/macros.rs
index b5b1602c5e0d3..0c2e8be049884 100644
--- a/compiler/rustc_resolve/src/macros.rs
+++ b/compiler/rustc_resolve/src/macros.rs
@@ -568,7 +568,7 @@ impl<'a> Resolver<'a> {
         Ok((ext, res))
     }
 
-    pub fn resolve_macro_path(
+    pub(crate) fn resolve_macro_path(
         &mut self,
         path: &ast::Path,
         kind: Option<MacroKind>,
diff --git a/compiler/rustc_resolve/src/rustdoc.rs b/compiler/rustc_resolve/src/rustdoc.rs
new file mode 100644
index 0000000000000..a967f4b940c80
--- /dev/null
+++ b/compiler/rustc_resolve/src/rustdoc.rs
@@ -0,0 +1,369 @@
+use pulldown_cmark::{BrokenLink, Event, Options, Parser, Tag};
+use rustc_ast as ast;
+use rustc_ast::util::comments::beautify_doc_string;
+use rustc_data_structures::fx::FxHashMap;
+use rustc_span::def_id::DefId;
+use rustc_span::symbol::{kw, Symbol};
+use rustc_span::Span;
+use std::cell::RefCell;
+use std::{cmp, mem};
+
+#[derive(Clone, Copy, PartialEq, Eq, Debug)]
+pub enum DocFragmentKind {
+    /// A doc fragment created from a `///` or `//!` doc comment.
+    SugaredDoc,
+    /// A doc fragment created from a "raw" `#[doc=""]` attribute.
+    RawDoc,
+}
+
+/// 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 struct DocFragment {
+    pub 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 parent_module: Option<DefId>,
+    pub doc: Symbol,
+    pub kind: DocFragmentKind,
+    pub indent: usize,
+}
+
+#[derive(Clone, Copy, Debug)]
+pub 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,
+}
+
+/// 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.
+pub fn unindent_doc_fragments(docs: &mut [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;
+    }
+}
+
+/// 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
+pub 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');
+    }
+}
+
+pub fn attrs_to_doc_fragments<'a>(
+    attrs: impl Iterator<Item = (&'a ast::Attribute, Option<DefId>)>,
+    doc_only: bool,
+) -> (Vec<DocFragment>, ast::AttrVec) {
+    let mut doc_fragments = 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() {
+            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_fragments.push(fragment);
+        } else if !doc_only {
+            other_attrs.push(attr.clone());
+        }
+    }
+
+    unindent_doc_fragments(&mut doc_fragments);
+
+    (doc_fragments, other_attrs)
+}
+
+/// 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 fn prepare_to_doc_link_resolution(
+    doc_fragments: &[DocFragment],
+) -> FxHashMap<Option<DefId>, String> {
+    let mut res = FxHashMap::default();
+    for fragment in doc_fragments {
+        let out_str = res.entry(fragment.parent_module).or_default();
+        add_doc_fragment(out_str, fragment);
+    }
+    res
+}
+
+/// Options for rendering Markdown in the main body of documentation.
+pub fn main_body_opts() -> Options {
+    Options::ENABLE_TABLES
+        | Options::ENABLE_FOOTNOTES
+        | Options::ENABLE_STRIKETHROUGH
+        | Options::ENABLE_TASKLISTS
+        | Options::ENABLE_SMART_PUNCTUATION
+}
+
+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)
+    }
+}
+
+pub fn strip_generics_from_path(path_str: &str) -> Result<String, MalformedGenerics> {
+    if !path_str.contains(['<', '>']) {
+        return Ok(path_str.to_string());
+    }
+    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) }
+}
+
+/// 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.
+pub fn inner_docs(attrs: &[ast::Attribute]) -> bool {
+    attrs.iter().find(|a| a.doc_str().is_some()).map_or(true, |a| a.style == ast::AttrStyle::Inner)
+}
+
+/// Simplified version of the corresponding function in rustdoc.
+/// If the rustdoc version returns a successful result, this function must return the same result.
+/// Otherwise this function may return anything.
+fn preprocess_link(link: &str) -> String {
+    let link = link.replace('`', "");
+    let link = link.split('#').next().unwrap();
+    let link = link.rsplit('@').next().unwrap();
+    let link = link.strip_suffix("()").unwrap_or(link);
+    let link = link.strip_suffix("{}").unwrap_or(link);
+    let link = link.strip_suffix("[]").unwrap_or(link);
+    let link = if link != "!" { link.strip_suffix("!").unwrap_or(link) } else { link };
+    strip_generics_from_path(link).unwrap_or_else(|_| link.to_string())
+}
+
+/// Simplified version of `preprocessed_markdown_links` from rustdoc.
+/// Must return at least the same links as it, but may add some more links on top of that.
+pub(crate) fn attrs_to_preprocessed_links(attrs: &[ast::Attribute]) -> Vec<String> {
+    let (doc_fragments, _) = attrs_to_doc_fragments(attrs.iter().map(|attr| (attr, None)), true);
+    let doc = prepare_to_doc_link_resolution(&doc_fragments).into_values().next().unwrap();
+
+    let links = RefCell::new(Vec::new());
+    let mut callback = |link: BrokenLink<'_>| {
+        links.borrow_mut().push(preprocess_link(&link.reference));
+        None
+    };
+    for event in Parser::new_with_broken_link_callback(&doc, main_body_opts(), Some(&mut callback))
+    {
+        if let Event::Start(Tag::Link(_, dest, _)) = event {
+            links.borrow_mut().push(preprocess_link(&dest));
+        }
+    }
+    links.into_inner()
+}
diff --git a/compiler/rustc_session/src/config.rs b/compiler/rustc_session/src/config.rs
index 7d2fdf94baa36..e8bc19f88e3e3 100644
--- a/compiler/rustc_session/src/config.rs
+++ b/compiler/rustc_session/src/config.rs
@@ -419,6 +419,18 @@ pub enum TrimmedDefPaths {
     GoodPath,
 }
 
+#[derive(Clone, Hash)]
+pub enum ResolveDocLinks {
+    /// Do not resolve doc links.
+    None,
+    /// Resolve doc links on exported items only for crate types that have metadata.
+    ExportedMetadata,
+    /// Resolve doc links on exported items.
+    Exported,
+    /// Resolve doc links on all items.
+    All,
+}
+
 /// Use tree-based collections to cheaply get a deterministic `Hash` implementation.
 /// *Do not* switch `BTreeMap` out for an unsorted container type! That would break
 /// dependency tracking for command-line arguments. Also only hash keys, since tracking
@@ -788,6 +800,7 @@ impl Default for Options {
             unstable_features: UnstableFeatures::Disallow,
             debug_assertions: true,
             actually_rustdoc: false,
+            resolve_doc_links: ResolveDocLinks::None,
             trimmed_def_paths: TrimmedDefPaths::default(),
             cli_forced_codegen_units: None,
             cli_forced_local_thinlto_off: false,
@@ -883,6 +896,15 @@ pub enum CrateType {
     ProcMacro,
 }
 
+impl CrateType {
+    pub fn has_metadata(self) -> bool {
+        match self {
+            CrateType::Rlib | CrateType::Dylib | CrateType::ProcMacro => true,
+            CrateType::Executable | CrateType::Cdylib | CrateType::Staticlib => false,
+        }
+    }
+}
+
 #[derive(Clone, Hash, Debug, PartialEq, Eq)]
 pub enum Passes {
     Some(Vec<String>),
@@ -2562,6 +2584,7 @@ pub fn build_session_options(matches: &getopts::Matches) -> Options {
         libs,
         debug_assertions,
         actually_rustdoc: false,
+        resolve_doc_links: ResolveDocLinks::ExportedMetadata,
         trimmed_def_paths: TrimmedDefPaths::default(),
         cli_forced_codegen_units: codegen_units,
         cli_forced_local_thinlto_off: disable_local_thinlto,
@@ -2825,8 +2848,9 @@ pub(crate) mod dep_tracking {
     use super::{
         BranchProtection, CFGuard, CFProtection, CrateType, DebugInfo, ErrorOutputType,
         InstrumentCoverage, InstrumentXRay, LdImpl, LinkerPluginLto, LocationDetail, LtoCli,
-        OomStrategy, OptLevel, OutputType, OutputTypes, Passes, SourceFileHashAlgorithm,
-        SplitDwarfKind, SwitchWithOptPath, SymbolManglingVersion, TraitSolver, TrimmedDefPaths,
+        OomStrategy, OptLevel, OutputType, OutputTypes, Passes, ResolveDocLinks,
+        SourceFileHashAlgorithm, SplitDwarfKind, SwitchWithOptPath, SymbolManglingVersion,
+        TraitSolver, TrimmedDefPaths,
     };
     use crate::lint;
     use crate::options::WasiExecModel;
@@ -2913,6 +2937,7 @@ pub(crate) mod dep_tracking {
         TargetTriple,
         Edition,
         LinkerPluginLto,
+        ResolveDocLinks,
         SplitDebuginfo,
         SplitDwarfKind,
         StackProtector,
diff --git a/compiler/rustc_session/src/options.rs b/compiler/rustc_session/src/options.rs
index b9723d35e5891..2305ac19a331f 100644
--- a/compiler/rustc_session/src/options.rs
+++ b/compiler/rustc_session/src/options.rs
@@ -169,6 +169,8 @@ top_level_options!(
         /// is currently just a hack and will be removed eventually, so please
         /// try to not rely on this too much.
         actually_rustdoc: bool [TRACKED],
+        /// Whether name resolver should resolve documentation links.
+        resolve_doc_links: ResolveDocLinks [TRACKED],
 
         /// Control path trimming.
         trimmed_def_paths: TrimmedDefPaths [TRACKED],
diff --git a/src/librustdoc/Cargo.toml b/src/librustdoc/Cargo.toml
index 0271c27b4f522..5e592227d49c5 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/types.rs b/src/librustdoc/clean/types.rs
index 85dd3881593ac..ffe6fea7ea447 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)
     }
 
@@ -874,8 +874,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 +892,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 +1000,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 +1011,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`].
@@ -1231,26 +1089,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 +1108,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 71eddf4348f1e..20627c2cfc164 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 0ce43f7db8e8b..d85749cadbd76 100644
--- a/src/librustdoc/core.rs
+++ b/src/librustdoc/core.rs
@@ -5,15 +5,14 @@ use rustc_data_structures::unord::UnordSet;
 use rustc_errors::emitter::{Emitter, EmitterWriter};
 use rustc_errors::json::JsonEmitter;
 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;
@@ -35,10 +34,6 @@ 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,
@@ -46,12 +41,6 @@ pub(crate) struct ResolverCaches {
 
 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.
     ///
@@ -100,13 +89,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
@@ -218,6 +200,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 +228,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 +248,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,7 +304,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,
@@ -348,7 +338,6 @@ pub(crate) fn run_global_ctxt(
 
     let mut ctxt = DocContext {
         tcx,
-        resolver,
         resolver_caches,
         param_env: ParamEnv::empty(),
         external_traits: Default::default(),
diff --git a/src/librustdoc/html/markdown.rs b/src/librustdoc/html/markdown.rs
index 94de93e7a9916..dee0a01a65413 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};
 
@@ -58,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
diff --git a/src/librustdoc/lib.rs b/src/librustdoc/lib.rs
index 64108c8828518..b22c12fa81054 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;
@@ -741,7 +742,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 +775,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,22 +793,13 @@ 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 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,
-                    )
+                    collect_intra_doc_links::early_resolve_intra_doc_links(resolver, krate)
                 });
-                (resolver.clone(), resolver_caches)
+                resolver_caches
             };
 
             if sess.diagnostic().has_errors_or_lint_errors().is_some() {
@@ -820,7 +812,6 @@ fn main_args(at_args: &[String]) -> MainResult {
                 let (krate, render_opts, mut cache) = sess.time("run_global_ctxt", || {
                     core::run_global_ctxt(
                         tcx,
-                        resolver,
                         resolver_caches,
                         show_coverage,
                         render_options,
diff --git a/src/librustdoc/passes/collect_intra_doc_links.rs b/src/librustdoc/passes/collect_intra_doc_links.rs
index 8435972bb11f2..692adcf0a8091 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};
@@ -23,7 +24,6 @@ use rustc_span::BytePos;
 use smallvec::{smallvec, SmallVec};
 
 use std::borrow::Cow;
-use std::mem;
 use std::ops::Range;
 
 use crate::clean::{self, utils::find_nearest_parent_module};
@@ -179,47 +179,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),
@@ -407,10 +366,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 +385,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);
@@ -779,8 +732,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 +798,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;
             }
@@ -975,16 +927,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.
@@ -2064,94 +2012,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
index f690c49005d9c..75c3380ee9bb9 100644
--- a/src/librustdoc/passes/collect_intra_doc_links/early.rs
+++ b/src/librustdoc/passes/collect_intra_doc_links/early.rs
@@ -1,357 +1,56 @@
-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::visit::{self, 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;
+use rustc_hir::def::Res;
+use rustc_hir::def_id::{DefId, DefIdSet};
+use rustc_resolve::Resolver;
+use rustc_span::Symbol;
 
 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,
+        markdown_links: Some(Default::default()),
         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);
-                        }
-                    }
-                }
+        for cnum in self.resolver.cstore().crates_untracked() {
+            early_lib_embargo_visit_item(
+                self.resolver,
+                &mut self.extern_doc_reachable,
+                cnum.as_def_id(),
+                true,
+            );
+            for (_, impl_def_id, _) in self.resolver.cstore().trait_impls_in_crate_untracked(cnum) {
+                self.all_trait_impls.push(impl_def_id);
             }
         }
     }
@@ -359,73 +58,16 @@ impl<'ra> EarlyDocLinkResolver<'_, 'ra> {
 
 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;
+        match &item.kind {
+            ItemKind::Impl(impl_) if impl_.of_trait.is_some() => {
+                self.all_trait_impls.push(self.resolver.local_def_id(item.id).to_def_id());
             }
-            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);
-                }
-                _ => {}
+            ItemKind::MacroDef(macro_def) if macro_def.macro_rules => {
+                let (_, res) = self.resolver.macro_rules_scope(self.resolver.local_def_id(item.id));
+                self.all_macro_rules.insert(item.ident.name, res);
             }
-            visit::walk_item(self, item);
+            _ => {}
         }
+        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/mod.rs b/src/librustdoc/passes/mod.rs
index 634e70ec97a0d..4b1ff68df502f 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/tools/tidy/src/deps.rs b/src/tools/tidy/src/deps.rs
index 8ce19c8b5145b..c4b994af13bfe 100644
--- a/src/tools/tidy/src/deps.rs
+++ b/src/tools/tidy/src/deps.rs
@@ -178,6 +178,7 @@ const PERMITTED_RUSTC_DEPENDENCIES: &[&str] = &[
     "ppv-lite86",
     "proc-macro-hack",
     "proc-macro2",
+    "pulldown-cmark",
     "psm",
     "punycode",
     "quote",
@@ -246,6 +247,7 @@ const PERMITTED_RUSTC_DEPENDENCIES: &[&str] = &[
     "unic-langid-macros",
     "unic-langid-macros-impl",
     "unic-ucd-version",
+    "unicase",
     "unicode-ident",
     "unicode-normalization",
     "unicode-script",