about summary refs log tree commit diff
diff options
context:
space:
mode:
authorbors <bors@rust-lang.org>2022-04-20 13:34:48 +0000
committerbors <bors@rust-lang.org>2022-04-20 13:34:48 +0000
commitd39864d64e6e0762d418f6beeedb4510942fc828 (patch)
tree2106cdb1ff078a58995e08633bbed36b189efd95
parent0034bbca260bfa00798d70150970924221688ede (diff)
parentca5c752a7a04f9a5149df1399106f103c6e5ba17 (diff)
downloadrust-d39864d64e6e0762d418f6beeedb4510942fc828.tar.gz
rust-d39864d64e6e0762d418f6beeedb4510942fc828.zip
Auto merge of #96135 - petrochenkov:doclink6, r=GuillaumeGomez
rustdoc: Optimize and refactor doc link resolution

One more subset of https://github.com/rust-lang/rust/pull/94857 that should bring perf improvements rather than regressions + a couple more optimizations on top of it.
It's better to read individual commits and their descriptions to understand the changes.
The `may_have_doc_links` optimization is not *very* useful here, but it's much more important for https://github.com/rust-lang/rust/pull/94857.

Closes https://github.com/rust-lang/rust/issues/96079
-rw-r--r--Cargo.lock1
-rw-r--r--compiler/rustc_ast/src/attr/mod.rs5
-rw-r--r--compiler/rustc_ast/src/util/comments.rs8
-rw-r--r--compiler/rustc_metadata/src/rmeta/decoder.rs4
-rw-r--r--compiler/rustc_metadata/src/rmeta/decoder/cstore_impl.rs4
-rw-r--r--compiler/rustc_metadata/src/rmeta/encoder.rs14
-rw-r--r--compiler/rustc_metadata/src/rmeta/mod.rs1
-rw-r--r--compiler/rustc_metadata/src/rmeta/table.rs14
-rw-r--r--library/proc_macro/Cargo.toml4
-rw-r--r--src/librustdoc/clean/types.rs70
-rw-r--r--src/librustdoc/core.rs5
-rw-r--r--src/librustdoc/html/markdown.rs10
-rw-r--r--src/librustdoc/passes/collect_intra_doc_links.rs112
-rw-r--r--src/librustdoc/passes/collect_intra_doc_links/early.rs113
-rw-r--r--src/test/rustdoc-ui/intra-doc/email-address-localhost.rs2
-rw-r--r--src/test/rustdoc-ui/intra-doc/email-address-localhost.stderr16
-rw-r--r--src/test/rustdoc/early-unindent.rs26
17 files changed, 275 insertions, 134 deletions
diff --git a/Cargo.lock b/Cargo.lock
index e3a9eb34936..fbc9ca7051f 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -2894,6 +2894,7 @@ dependencies = [
 name = "proc_macro"
 version = "0.0.0"
 dependencies = [
+ "core",
  "std",
 ]
 
diff --git a/compiler/rustc_ast/src/attr/mod.rs b/compiler/rustc_ast/src/attr/mod.rs
index 80caf37d709..9a6d12faa60 100644
--- a/compiler/rustc_ast/src/attr/mod.rs
+++ b/compiler/rustc_ast/src/attr/mod.rs
@@ -9,6 +9,7 @@ use crate::token::{self, CommentKind, Token};
 use crate::tokenstream::{AttrAnnotatedTokenStream, AttrAnnotatedTokenTree};
 use crate::tokenstream::{DelimSpan, Spacing, TokenTree, TreeAndSpacing};
 use crate::tokenstream::{LazyTokenStream, TokenStream};
+use crate::util::comments;
 
 use rustc_index::bit_set::GrowableBitSet;
 use rustc_span::source_map::BytePos;
@@ -262,6 +263,10 @@ impl Attribute {
         }
     }
 
+    pub fn may_have_doc_links(&self) -> bool {
+        self.doc_str().map_or(false, |s| comments::may_have_doc_links(s.as_str()))
+    }
+
     pub fn get_normal_item(&self) -> &AttrItem {
         match self.kind {
             AttrKind::Normal(ref item, _) => item,
diff --git a/compiler/rustc_ast/src/util/comments.rs b/compiler/rustc_ast/src/util/comments.rs
index 8730aeb0f3b..b4fff0022e2 100644
--- a/compiler/rustc_ast/src/util/comments.rs
+++ b/compiler/rustc_ast/src/util/comments.rs
@@ -24,6 +24,14 @@ pub struct Comment {
     pub pos: BytePos,
 }
 
+/// A fast conservative estimate on whether the string can contain documentation links.
+/// A pair of square brackets `[]` must exist in the string, but we only search for the
+/// opening bracket because brackets always go in pairs in practice.
+#[inline]
+pub fn may_have_doc_links(s: &str) -> bool {
+    s.contains('[')
+}
+
 /// Makes a doc string more presentable to users.
 /// Used by rustdoc and perhaps other tools, but not by rustc.
 pub fn beautify_doc_string(data: Symbol, kind: CommentKind) -> Symbol {
diff --git a/compiler/rustc_metadata/src/rmeta/decoder.rs b/compiler/rustc_metadata/src/rmeta/decoder.rs
index c0f2319f003..77afa7b4a5c 100644
--- a/compiler/rustc_metadata/src/rmeta/decoder.rs
+++ b/compiler/rustc_metadata/src/rmeta/decoder.rs
@@ -1744,6 +1744,10 @@ impl<'a, 'tcx> CrateMetadataRef<'a> {
                 adjustments: generator_data.adjustments,
             })
     }
+
+    fn get_may_have_doc_links(self, index: DefIndex) -> bool {
+        self.root.tables.may_have_doc_links.get(self, index).is_some()
+    }
 }
 
 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 6a8f1dec0c5..6b1f7d55026 100644
--- a/compiler/rustc_metadata/src/rmeta/decoder/cstore_impl.rs
+++ b/compiler/rustc_metadata/src/rmeta/decoder/cstore_impl.rs
@@ -531,6 +531,10 @@ impl CStore {
     ) -> impl Iterator<Item = DefId> + '_ {
         self.get_crate_data(cnum).get_all_incoherent_impls()
     }
+
+    pub fn may_have_doc_links_untracked(&self, def_id: DefId) -> bool {
+        self.get_crate_data(def_id.krate).get_may_have_doc_links(def_id.index)
+    }
 }
 
 impl CrateStore for CStore {
diff --git a/compiler/rustc_metadata/src/rmeta/encoder.rs b/compiler/rustc_metadata/src/rmeta/encoder.rs
index 066bcb428f6..f485e09913e 100644
--- a/compiler/rustc_metadata/src/rmeta/encoder.rs
+++ b/compiler/rustc_metadata/src/rmeta/encoder.rs
@@ -977,6 +977,14 @@ fn should_encode_generics(def_kind: DefKind) -> bool {
 }
 
 impl<'a, 'tcx> EncodeContext<'a, 'tcx> {
+    fn encode_attrs(&mut self, def_id: DefId) {
+        let attrs = self.tcx.get_attrs(def_id);
+        record!(self.tables.attributes[def_id] <- attrs);
+        if attrs.iter().any(|attr| attr.may_have_doc_links()) {
+            self.tables.may_have_doc_links.set(def_id.index, ());
+        }
+    }
+
     fn encode_def_ids(&mut self) {
         if self.is_proc_macro {
             return;
@@ -989,7 +997,7 @@ impl<'a, 'tcx> EncodeContext<'a, 'tcx> {
             let Some(def_kind) = def_kind else { continue };
             self.tables.opt_def_kind.set(def_id.index, def_kind);
             record!(self.tables.def_span[def_id] <- tcx.def_span(def_id));
-            record!(self.tables.attributes[def_id] <- tcx.get_attrs(def_id));
+            self.encode_attrs(def_id);
             record!(self.tables.expn_that_defined[def_id] <- self.tcx.expn_that_defined(def_id));
             if should_encode_visibility(def_kind) {
                 record!(self.tables.visibility[def_id] <- self.tcx.visibility(def_id));
@@ -1651,7 +1659,7 @@ impl<'a, 'tcx> EncodeContext<'a, 'tcx> {
 
             self.tables.opt_def_kind.set(LOCAL_CRATE.as_def_id().index, DefKind::Mod);
             record!(self.tables.def_span[LOCAL_CRATE.as_def_id()] <- tcx.def_span(LOCAL_CRATE.as_def_id()));
-            record!(self.tables.attributes[LOCAL_CRATE.as_def_id()] <- tcx.get_attrs(LOCAL_CRATE.as_def_id()));
+            self.encode_attrs(LOCAL_CRATE.as_def_id());
             record!(self.tables.visibility[LOCAL_CRATE.as_def_id()] <- tcx.visibility(LOCAL_CRATE.as_def_id()));
             if let Some(stability) = stability {
                 record!(self.tables.lookup_stability[LOCAL_CRATE.as_def_id()] <- stability);
@@ -1692,7 +1700,7 @@ impl<'a, 'tcx> EncodeContext<'a, 'tcx> {
                 let def_id = id.to_def_id();
                 self.tables.opt_def_kind.set(def_id.index, DefKind::Macro(macro_kind));
                 record!(self.tables.kind[def_id] <- EntryKind::ProcMacro(macro_kind));
-                record!(self.tables.attributes[def_id] <- attrs);
+                self.encode_attrs(def_id);
                 record!(self.tables.def_keys[def_id] <- def_key);
                 record!(self.tables.def_ident_span[def_id] <- span);
                 record!(self.tables.def_span[def_id] <- span);
diff --git a/compiler/rustc_metadata/src/rmeta/mod.rs b/compiler/rustc_metadata/src/rmeta/mod.rs
index e1a1589adb3..f1498665ff3 100644
--- a/compiler/rustc_metadata/src/rmeta/mod.rs
+++ b/compiler/rustc_metadata/src/rmeta/mod.rs
@@ -360,6 +360,7 @@ define_tables! {
     def_path_hashes: Table<DefIndex, DefPathHash>,
     proc_macro_quoted_spans: Table<usize, Lazy<Span>>,
     generator_diagnostic_data: Table<DefIndex, Lazy<GeneratorDiagnosticData<'tcx>>>,
+    may_have_doc_links: Table<DefIndex, ()>,
 }
 
 #[derive(Copy, Clone, MetadataEncodable, MetadataDecodable)]
diff --git a/compiler/rustc_metadata/src/rmeta/table.rs b/compiler/rustc_metadata/src/rmeta/table.rs
index 7a23cba536a..53fc2efe00b 100644
--- a/compiler/rustc_metadata/src/rmeta/table.rs
+++ b/compiler/rustc_metadata/src/rmeta/table.rs
@@ -186,6 +186,20 @@ impl FixedSizeEncoding for Option<RawDefId> {
     }
 }
 
+impl FixedSizeEncoding for Option<()> {
+    type ByteArray = [u8; 1];
+
+    #[inline]
+    fn from_bytes(b: &[u8; 1]) -> Self {
+        (b[0] != 0).then(|| ())
+    }
+
+    #[inline]
+    fn write_to_bytes(self, b: &mut [u8; 1]) {
+        b[0] = self.is_some() as u8
+    }
+}
+
 // NOTE(eddyb) there could be an impl for `usize`, which would enable a more
 // generic `Lazy<T>` impl, but in the general case we might not need / want to
 // fit every `usize` in `u32`.
diff --git a/library/proc_macro/Cargo.toml b/library/proc_macro/Cargo.toml
index db5e2e4e245..e54a50aa15c 100644
--- a/library/proc_macro/Cargo.toml
+++ b/library/proc_macro/Cargo.toml
@@ -5,3 +5,7 @@ edition = "2021"
 
 [dependencies]
 std = { path = "../std" }
+# Workaround: when documenting this crate rustdoc will try to load crate named
+# `core` when resolving doc links. Without this line a different `core` will be
+# loaded from sysroot causing duplicate lang items and other similar errors.
+core = { path = "../core" }
diff --git a/src/librustdoc/clean/types.rs b/src/librustdoc/clean/types.rs
index e30bc6e0a97..95ac3ab622a 100644
--- a/src/librustdoc/clean/types.rs
+++ b/src/librustdoc/clean/types.rs
@@ -1089,35 +1089,35 @@ impl Attributes {
         attrs: &[ast::Attribute],
         additional_attrs: Option<(&[ast::Attribute], DefId)>,
     ) -> Attributes {
-        let mut doc_strings: Vec<DocFragment> = vec![];
-        let clean_attr = |(attr, parent_module): (&ast::Attribute, Option<DefId>)| {
-            if let Some((value, kind)) = attr.doc_str_and_comment_kind() {
-                trace!("got doc_str={:?}", value);
-                let value = beautify_doc_string(value, kind);
+        // Additional documentation should be shown before the original documentation.
+        let attrs1 = additional_attrs
+            .into_iter()
+            .flat_map(|(attrs, def_id)| attrs.iter().map(move |attr| (attr, Some(def_id))));
+        let attrs2 = attrs.iter().map(|attr| (attr, None));
+        Attributes::from_ast_iter(attrs1.chain(attrs2), false)
+    }
+
+    crate fn from_ast_iter<'a>(
+        attrs: impl Iterator<Item = (&'a ast::Attribute, Option<DefId>)>,
+        doc_only: bool,
+    ) -> Attributes {
+        let mut doc_strings = Vec::new();
+        let mut other_attrs = Vec::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 frag =
-                    DocFragment { span: attr.span, doc: value, kind, parent_module, indent: 0 };
-
-                doc_strings.push(frag);
-
-                None
-            } else {
-                Some(attr.clone())
+                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());
             }
-        };
-
-        // Additional documentation should be shown before the original documentation
-        let other_attrs = additional_attrs
-            .into_iter()
-            .flat_map(|(attrs, id)| attrs.iter().map(move |attr| (attr, Some(id))))
-            .chain(attrs.iter().map(|attr| (attr, None)))
-            .filter_map(clean_attr)
-            .collect();
+        }
 
         Attributes { doc_strings, other_attrs }
     }
@@ -1138,23 +1138,17 @@ impl Attributes {
     }
 
     /// 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.
-    crate fn collapsed_doc_value_by_module_level(&self) -> FxHashMap<Option<DefId>, String> {
-        let mut ret = FxHashMap::default();
-        if self.doc_strings.len() == 0 {
-            return ret;
-        }
-        let last_index = self.doc_strings.len() - 1;
-
-        for (i, new_frag) in self.doc_strings.iter().enumerate() {
-            let out = ret.entry(new_frag.parent_module).or_default();
-            add_doc_fragment(out, new_frag);
-            if i == last_index {
-                out.pop();
-            }
+    ///
+    /// The last newline is not trimmed so the produced strings are reusable between
+    /// early and late doc link resolution regardless of their position.
+    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);
         }
-        ret
+        res
     }
 
     /// Finds all `doc` attributes as NameValues and returns their corresponding values, joined
diff --git a/src/librustdoc/core.rs b/src/librustdoc/core.rs
index b9e20c41b68..1db6064551c 100644
--- a/src/librustdoc/core.rs
+++ b/src/librustdoc/core.rs
@@ -4,7 +4,7 @@ use rustc_data_structures::sync::{self, Lrc};
 use rustc_errors::emitter::{Emitter, EmitterWriter};
 use rustc_errors::json::JsonEmitter;
 use rustc_feature::UnstableFeatures;
-use rustc_hir::def::Res;
+use rustc_hir::def::{Namespace, Res};
 use rustc_hir::def_id::{DefId, DefIdMap, LocalDefId};
 use rustc_hir::intravisit::{self, Visitor};
 use rustc_hir::{HirId, Path, TraitCandidate};
@@ -29,11 +29,14 @@ use crate::clean::inline::build_external_trait;
 use crate::clean::{self, ItemId, TraitWithExtraInfo};
 use crate::config::{Options as RustdocOptions, OutputFormat, RenderOptions};
 use crate::formats::cache::Cache;
+use crate::passes::collect_intra_doc_links::PreprocessedMarkdownLink;
 use crate::passes::{self, Condition::*};
 
 crate use rustc_session::config::{DebuggingOptions, Input, Options};
 
 crate struct ResolverCaches {
+    crate markdown_links: Option<FxHashMap<String, Vec<PreprocessedMarkdownLink>>>,
+    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.
     crate traits_in_scope: DefIdMap<Vec<TraitCandidate>>,
diff --git a/src/librustdoc/html/markdown.rs b/src/librustdoc/html/markdown.rs
index 1ebb41b5933..eafe6f17d44 100644
--- a/src/librustdoc/html/markdown.rs
+++ b/src/librustdoc/html/markdown.rs
@@ -1255,7 +1255,7 @@ crate struct MarkdownLink {
     pub range: Range<usize>,
 }
 
-crate fn markdown_links(md: &str) -> Vec<MarkdownLink> {
+crate fn markdown_links<R>(md: &str, filter_map: impl Fn(MarkdownLink) -> Option<R>) -> Vec<R> {
     if md.is_empty() {
         return vec![];
     }
@@ -1295,11 +1295,12 @@ crate fn markdown_links(md: &str) -> Vec<MarkdownLink> {
 
     let mut push = |link: BrokenLink<'_>| {
         let span = span_for_link(&link.reference, link.span);
-        links.borrow_mut().push(MarkdownLink {
+        filter_map(MarkdownLink {
             kind: LinkType::ShortcutUnknown,
             link: link.reference.to_string(),
             range: span,
-        });
+        })
+        .map(|link| links.borrow_mut().push(link));
         None
     };
     let p = Parser::new_with_broken_link_callback(md, main_body_opts(), Some(&mut push))
@@ -1314,7 +1315,8 @@ crate fn markdown_links(md: &str) -> Vec<MarkdownLink> {
         if let Event::Start(Tag::Link(kind, dest, _)) = ev.0 {
             debug!("found link: {dest}");
             let span = span_for_link(&dest, ev.1);
-            links.borrow_mut().push(MarkdownLink { kind, link: dest.into_string(), range: span });
+            filter_map(MarkdownLink { kind, link: dest.into_string(), range: span })
+                .map(|link| links.borrow_mut().push(link));
         }
     }
 
diff --git a/src/librustdoc/passes/collect_intra_doc_links.rs b/src/librustdoc/passes/collect_intra_doc_links.rs
index c48f8bd0c7c..42e87f3f961 100644
--- a/src/librustdoc/passes/collect_intra_doc_links.rs
+++ b/src/librustdoc/passes/collect_intra_doc_links.rs
@@ -3,6 +3,7 @@
 //! [RFC 1946]: https://github.com/rust-lang/rfcs/blob/master/text/1946-intra-rustdoc-links.md
 
 use pulldown_cmark::LinkType;
+use rustc_ast::util::comments::may_have_doc_links;
 use rustc_data_structures::{fx::FxHashMap, intern::Interned, stable_set::FxHashSet};
 use rustc_errors::{Applicability, Diagnostic};
 use rustc_hir::def::Namespace::*;
@@ -159,7 +160,7 @@ impl TryFrom<ResolveRes> for Res {
 }
 
 /// A link failed to resolve.
-#[derive(Debug)]
+#[derive(Clone, Debug)]
 enum ResolutionFailure<'a> {
     /// This resolved, but with the wrong namespace.
     WrongNamespace {
@@ -199,7 +200,7 @@ enum ResolutionFailure<'a> {
     Dummy,
 }
 
-#[derive(Debug)]
+#[derive(Clone, Debug)]
 enum MalformedGenerics {
     /// This link has unbalanced angle brackets.
     ///
@@ -252,6 +253,7 @@ impl ResolutionFailure<'_> {
     }
 }
 
+#[derive(Clone, Copy)]
 enum AnchorFailure {
     /// User error: `[std#x#y]` is not valid
     MultipleAnchors,
@@ -556,7 +558,15 @@ 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
-            .enter_resolver(|resolver| resolver.resolve_rustdoc_path(path_str, ns, module_id))
+            .resolver_caches
+            .doc_link_resolutions
+            .get(&(Symbol::intern(path_str), ns, module_id))
+            .copied()
+            .unwrap_or_else(|| {
+                self.cx.enter_resolver(|resolver| {
+                    resolver.resolve_rustdoc_path(path_str, ns, module_id)
+                })
+            })
             .and_then(|res| res.try_into().ok())
             .or_else(|| resolve_primitive(path_str, ns))
             .or_else(|| self.resolve_macro_rules(path_str, ns));
@@ -1040,17 +1050,30 @@ 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.collapsed_doc_value_by_module_level() {
+        for (parent_module, doc) in item.attrs.prepare_to_doc_link_resolution() {
+            if !may_have_doc_links(&doc) {
+                continue;
+            }
             debug!("combined_docs={}", doc);
             // NOTE: if there are links that start in one crate and end in another, this will not resolve them.
             // This is a degenerate case and it's not supported by rustdoc.
             let parent_node = parent_module.or(parent_node);
-            for md_link in markdown_links(&doc) {
+            let mut tmp_links = self
+                .cx
+                .resolver_caches
+                .markdown_links
+                .take()
+                .expect("`markdown_links` are already borrowed");
+            if !tmp_links.contains_key(&doc) {
+                tmp_links.insert(doc.clone(), preprocessed_markdown_links(&doc));
+            }
+            for md_link in &tmp_links[&doc] {
                 let link = self.resolve_link(&item, &doc, parent_node, md_link);
                 if let Some(link) = link {
                     self.cx.cache.intra_doc_links.entry(item.item_id).or_default().push(link);
                 }
             }
+            self.cx.resolver_caches.markdown_links = Some(tmp_links);
         }
 
         if item.is_mod() {
@@ -1066,18 +1089,19 @@ impl<'a, 'tcx> DocVisitor for LinkCollector<'a, 'tcx> {
     }
 }
 
-enum PreprocessingError<'a> {
+enum PreprocessingError {
     Anchor(AnchorFailure),
     Disambiguator(Range<usize>, String),
-    Resolution(ResolutionFailure<'a>, String, Option<Disambiguator>),
+    Resolution(ResolutionFailure<'static>, String, Option<Disambiguator>),
 }
 
-impl From<AnchorFailure> for PreprocessingError<'_> {
+impl From<AnchorFailure> for PreprocessingError {
     fn from(err: AnchorFailure) -> Self {
         Self::Anchor(err)
     }
 }
 
+#[derive(Clone)]
 struct PreprocessingInfo {
     path_str: String,
     disambiguator: Option<Disambiguator>,
@@ -1085,15 +1109,18 @@ struct PreprocessingInfo {
     link_text: String,
 }
 
+// Not a typedef to avoid leaking several private structures from this module.
+crate struct PreprocessedMarkdownLink(Result<PreprocessingInfo, PreprocessingError>, MarkdownLink);
+
 /// Returns:
 /// - `None` if the link should be ignored.
 /// - `Some(Err)` if the link should emit an error
 /// - `Some(Ok)` if the link is valid
 ///
 /// `link_buffer` is needed for lifetime reasons; it will always be overwritten and the contents ignored.
-fn preprocess_link<'a>(
-    ori_link: &'a MarkdownLink,
-) -> Option<Result<PreprocessingInfo, PreprocessingError<'a>>> {
+fn preprocess_link(
+    ori_link: &MarkdownLink,
+) -> Option<Result<PreprocessingInfo, PreprocessingError>> {
     // [] is mostly likely not supposed to be a link
     if ori_link.link.is_empty() {
         return None;
@@ -1172,6 +1199,12 @@ fn preprocess_link<'a>(
     }))
 }
 
+fn preprocessed_markdown_links(s: &str) -> Vec<PreprocessedMarkdownLink> {
+    markdown_links(s, |link| {
+        preprocess_link(&link).map(|pp_link| PreprocessedMarkdownLink(pp_link, link))
+    })
+}
+
 impl LinkCollector<'_, '_> {
     /// This is the entry point for resolving an intra-doc link.
     ///
@@ -1181,8 +1214,9 @@ impl LinkCollector<'_, '_> {
         item: &Item,
         dox: &str,
         parent_node: Option<DefId>,
-        ori_link: MarkdownLink,
+        link: &PreprocessedMarkdownLink,
     ) -> Option<ItemLink> {
+        let PreprocessedMarkdownLink(pp_link, ori_link) = link;
         trace!("considering link '{}'", ori_link.link);
 
         let diag_info = DiagnosticInfo {
@@ -1192,28 +1226,29 @@ impl LinkCollector<'_, '_> {
             link_range: ori_link.range.clone(),
         };
 
-        let PreprocessingInfo { ref path_str, disambiguator, extra_fragment, link_text } =
-            match preprocess_link(&ori_link)? {
-                Ok(x) => x,
-                Err(err) => {
-                    match err {
-                        PreprocessingError::Anchor(err) => anchor_failure(self.cx, diag_info, err),
-                        PreprocessingError::Disambiguator(range, msg) => {
-                            disambiguator_error(self.cx, diag_info, range, &msg)
-                        }
-                        PreprocessingError::Resolution(err, path_str, disambiguator) => {
-                            resolution_failure(
-                                self,
-                                diag_info,
-                                &path_str,
-                                disambiguator,
-                                smallvec![err],
-                            );
-                        }
+        let PreprocessingInfo { path_str, disambiguator, extra_fragment, link_text } = match pp_link
+        {
+            Ok(x) => x,
+            Err(err) => {
+                match err {
+                    PreprocessingError::Anchor(err) => anchor_failure(self.cx, diag_info, *err),
+                    PreprocessingError::Disambiguator(range, msg) => {
+                        disambiguator_error(self.cx, diag_info, range.clone(), msg)
+                    }
+                    PreprocessingError::Resolution(err, path_str, disambiguator) => {
+                        resolution_failure(
+                            self,
+                            diag_info,
+                            path_str,
+                            *disambiguator,
+                            smallvec![err.clone()],
+                        );
                     }
-                    return None;
                 }
-            };
+                return None;
+            }
+        };
+        let disambiguator = *disambiguator;
 
         let inner_docs = item.inner_docs(self.cx.tcx);
 
@@ -1250,7 +1285,7 @@ impl LinkCollector<'_, '_> {
                 module_id,
                 dis: disambiguator,
                 path_str: path_str.to_owned(),
-                extra_fragment,
+                extra_fragment: extra_fragment.clone(),
             },
             diag_info.clone(), // this struct should really be Copy, but Range is not :(
             matches!(ori_link.kind, LinkType::Reference | LinkType::Shortcut),
@@ -1320,8 +1355,8 @@ impl LinkCollector<'_, '_> {
                 }
 
                 Some(ItemLink {
-                    link: ori_link.link,
-                    link_text,
+                    link: ori_link.link.clone(),
+                    link_text: link_text.clone(),
                     did: res.def_id(self.cx.tcx),
                     fragment,
                 })
@@ -1343,7 +1378,12 @@ impl LinkCollector<'_, '_> {
                     &diag_info,
                 )?;
                 let id = clean::register_res(self.cx, rustc_hir::def::Res::Def(kind, id));
-                Some(ItemLink { link: ori_link.link, link_text, did: id, fragment })
+                Some(ItemLink {
+                    link: ori_link.link.clone(),
+                    link_text: link_text.clone(),
+                    did: id,
+                    fragment,
+                })
             }
         }
     }
diff --git a/src/librustdoc/passes/collect_intra_doc_links/early.rs b/src/librustdoc/passes/collect_intra_doc_links/early.rs
index dffceff045d..e2359da870e 100644
--- a/src/librustdoc/passes/collect_intra_doc_links/early.rs
+++ b/src/librustdoc/passes/collect_intra_doc_links/early.rs
@@ -1,19 +1,20 @@
 use crate::clean::Attributes;
 use crate::core::ResolverCaches;
-use crate::html::markdown::markdown_links;
-use crate::passes::collect_intra_doc_links::preprocess_link;
+use crate::passes::collect_intra_doc_links::preprocessed_markdown_links;
+use crate::passes::collect_intra_doc_links::PreprocessedMarkdownLink;
 
 use rustc_ast::visit::{self, AssocCtxt, Visitor};
 use rustc_ast::{self as ast, ItemKind};
 use rustc_ast_lowering::ResolverAstLowering;
-use rustc_hir::def::Namespace::TypeNS;
-use rustc_hir::def::{DefKind, Res};
+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, LocalDefId, CRATE_DEF_ID};
 use rustc_hir::TraitCandidate;
 use rustc_middle::ty::{DefIdTree, Visibility};
 use rustc_resolve::{ParentScope, Resolver};
 use rustc_session::config::Externs;
-use rustc_span::SyntaxContext;
+use rustc_span::{Symbol, SyntaxContext};
 
 use std::collections::hash_map::Entry;
 use std::mem;
@@ -28,6 +29,8 @@ crate fn early_resolve_intra_doc_links(
         resolver,
         current_mod: CRATE_DEF_ID,
         visited_mods: Default::default(),
+        markdown_links: Default::default(),
+        doc_link_resolutions: Default::default(),
         traits_in_scope: Default::default(),
         all_traits: Default::default(),
         all_trait_impls: Default::default(),
@@ -36,7 +39,7 @@ crate fn early_resolve_intra_doc_links(
 
     // Overridden `visit_item` below doesn't apply to the crate root,
     // so we have to visit its attributes and reexports separately.
-    link_resolver.load_links_in_attrs(&krate.attrs);
+    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);
     link_resolver.process_extern_impls();
@@ -50,6 +53,8 @@ crate fn early_resolve_intra_doc_links(
     }
 
     ResolverCaches {
+        markdown_links: Some(link_resolver.markdown_links),
+        doc_link_resolutions: link_resolver.doc_link_resolutions,
         traits_in_scope: link_resolver.traits_in_scope,
         all_traits: Some(link_resolver.all_traits),
         all_trait_impls: Some(link_resolver.all_trait_impls),
@@ -57,10 +62,18 @@ crate fn early_resolve_intra_doc_links(
     }
 }
 
+fn doc_attrs<'a>(attrs: impl Iterator<Item = &'a ast::Attribute>) -> Attributes {
+    let mut attrs = Attributes::from_ast_iter(attrs.map(|attr| (attr, None)), true);
+    attrs.unindent_doc_comments();
+    attrs
+}
+
 struct EarlyDocLinkResolver<'r, 'ra> {
     resolver: &'r mut Resolver<'ra>,
     current_mod: LocalDefId,
     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_traits: Vec<DefId>,
     all_trait_impls: Vec<DefId>,
@@ -92,18 +105,11 @@ impl EarlyDocLinkResolver<'_, '_> {
         }
     }
 
-    fn add_traits_in_parent_scope(&mut self, def_id: DefId) {
-        if let Some(module_id) = self.resolver.parent(def_id) {
-            self.add_traits_in_scope(module_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) {
-        // FIXME: Need to resolve doc links on all these impl and trait items below.
         // Resolving links in already existing crates may trigger loading of new crates.
         let mut start_cnum = 0;
         loop {
@@ -124,7 +130,7 @@ impl EarlyDocLinkResolver<'_, '_> {
                 // the current crate, and links in their doc comments are not resolved.
                 for &def_id in &all_traits {
                     if self.resolver.cstore().visibility_untracked(def_id) == Visibility::Public {
-                        self.add_traits_in_parent_scope(def_id);
+                        self.resolve_doc_links_extern_impl(def_id, false);
                     }
                 }
                 for &(trait_def_id, impl_def_id, simplified_self_ty) in &all_trait_impls {
@@ -135,17 +141,17 @@ impl EarlyDocLinkResolver<'_, '_> {
                                 == Visibility::Public
                         })
                     {
-                        self.add_traits_in_parent_scope(impl_def_id);
+                        self.resolve_doc_links_extern_impl(impl_def_id, false);
                     }
                 }
                 for (ty_def_id, impl_def_id) in all_inherent_impls {
                     if self.resolver.cstore().visibility_untracked(ty_def_id) == Visibility::Public
                     {
-                        self.add_traits_in_parent_scope(impl_def_id);
+                        self.resolve_doc_links_extern_impl(impl_def_id, true);
                     }
                 }
-                for def_id in all_incoherent_impls {
-                    self.add_traits_in_parent_scope(def_id);
+                for impl_def_id in all_incoherent_impls {
+                    self.resolve_doc_links_extern_impl(impl_def_id, true);
                 }
 
                 self.all_traits.extend(all_traits);
@@ -161,16 +167,55 @@ impl EarlyDocLinkResolver<'_, '_> {
         }
     }
 
-    fn load_links_in_attrs(&mut self, attrs: &[ast::Attribute]) {
+    fn resolve_doc_links_extern_impl(&mut self, def_id: DefId, _is_inherent: bool) {
+        // FIXME: Resolve links in associated items in addition to traits themselves,
+        // `force` is used to provide traits in scope for the associated items.
+        self.resolve_doc_links_extern_outer(def_id, def_id, true);
+    }
+
+    fn resolve_doc_links_extern_outer(&mut self, def_id: DefId, scope_id: DefId, force: bool) {
+        if !force && !self.resolver.cstore().may_have_doc_links_untracked(def_id) {
+            return;
+        }
+        // FIXME: actually resolve links, not just add traits in scope.
+        if let Some(parent_id) = self.resolver.parent(scope_id) {
+            self.add_traits_in_scope(parent_id);
+        }
+    }
+
+    fn resolve_doc_links_extern_inner(&mut self, def_id: DefId) {
+        if !self.resolver.cstore().may_have_doc_links_untracked(def_id) {
+            return;
+        }
+        // FIXME: actually resolve links, not just add traits in scope.
+        self.add_traits_in_scope(def_id);
+    }
+
+    fn resolve_doc_links_local(&mut self, attrs: &[ast::Attribute]) {
+        if !attrs.iter().any(|attr| attr.may_have_doc_links()) {
+            return;
+        }
         let module_id = self.current_mod.to_def_id();
+        self.resolve_doc_links(doc_attrs(attrs.iter()), module_id);
+    }
+
+    fn resolve_doc_links(&mut self, attrs: Attributes, module_id: DefId) {
         let mut need_traits_in_scope = false;
-        for (doc_module, doc) in
-            Attributes::from_ast(attrs, None).collapsed_doc_value_by_module_level()
-        {
+        for (doc_module, doc) in attrs.prepare_to_doc_link_resolution() {
             assert_eq!(doc_module, None);
-            for link in markdown_links(&doc.as_str()) {
-                if let Some(Ok(pinfo)) = preprocess_link(&link) {
-                    self.resolver.resolve_rustdoc_path(&pinfo.path_str, TypeNS, module_id);
+            let links = self
+                .markdown_links
+                .entry(doc)
+                .or_insert_with_key(|doc| preprocessed_markdown_links(doc));
+            for PreprocessedMarkdownLink(pp_link, _) in links {
+                if let Ok(pinfo) = pp_link {
+                    // FIXME: Resolve the path in all namespaces and resolve its prefixes too.
+                    let ns = TypeNS;
+                    self.doc_link_resolutions
+                        .entry((Symbol::intern(&pinfo.path_str), ns, module_id))
+                        .or_insert_with_key(|(path, ns, module_id)| {
+                            self.resolver.resolve_rustdoc_path(path.as_str(), *ns, *module_id)
+                        });
                     need_traits_in_scope = true;
                 }
             }
@@ -197,15 +242,13 @@ impl EarlyDocLinkResolver<'_, '_> {
                     && module_id.is_local()
             {
                 if let Some(def_id) = child.res.opt_def_id() && !def_id.is_local() {
-                    // FIXME: Need to resolve doc links on all these extern items
-                    // reached through reexports.
                     let scope_id = match child.res {
                         Res::Def(DefKind::Variant, ..) => self.resolver.parent(def_id).unwrap(),
                         _ => def_id,
                     };
-                    self.add_traits_in_parent_scope(scope_id); // Outer attribute scope
+                    self.resolve_doc_links_extern_outer(def_id, scope_id, false); // Outer attribute scope
                     if let Res::Def(DefKind::Mod, ..) = child.res {
-                        self.add_traits_in_scope(def_id); // Inner attribute scope
+                        self.resolve_doc_links_extern_inner(def_id); // Inner attribute scope
                     }
                     // Traits are processed in `add_extern_traits_in_scope`.
                     if let Res::Def(DefKind::Mod | DefKind::Enum, ..) = child.res {
@@ -219,10 +262,10 @@ impl EarlyDocLinkResolver<'_, '_> {
 
 impl Visitor<'_> for EarlyDocLinkResolver<'_, '_> {
     fn visit_item(&mut self, item: &ast::Item) {
-        self.load_links_in_attrs(&item.attrs); // Outer attribute scope
+        self.resolve_doc_links_local(&item.attrs); // Outer attribute scope
         if let ItemKind::Mod(..) = item.kind {
             let old_mod = mem::replace(&mut self.current_mod, self.resolver.local_def_id(item.id));
-            self.load_links_in_attrs(&item.attrs); // Inner attribute scope
+            self.resolve_doc_links_local(&item.attrs); // Inner attribute scope
             self.process_module_children_or_reexports(self.current_mod.to_def_id());
             visit::walk_item(self, item);
             self.current_mod = old_mod;
@@ -241,22 +284,22 @@ impl Visitor<'_> for EarlyDocLinkResolver<'_, '_> {
     }
 
     fn visit_assoc_item(&mut self, item: &ast::AssocItem, ctxt: AssocCtxt) {
-        self.load_links_in_attrs(&item.attrs);
+        self.resolve_doc_links_local(&item.attrs);
         visit::walk_assoc_item(self, item, ctxt)
     }
 
     fn visit_foreign_item(&mut self, item: &ast::ForeignItem) {
-        self.load_links_in_attrs(&item.attrs);
+        self.resolve_doc_links_local(&item.attrs);
         visit::walk_foreign_item(self, item)
     }
 
     fn visit_variant(&mut self, v: &ast::Variant) {
-        self.load_links_in_attrs(&v.attrs);
+        self.resolve_doc_links_local(&v.attrs);
         visit::walk_variant(self, v)
     }
 
     fn visit_field_def(&mut self, field: &ast::FieldDef) {
-        self.load_links_in_attrs(&field.attrs);
+        self.resolve_doc_links_local(&field.attrs);
         visit::walk_field_def(self, field)
     }
 
diff --git a/src/test/rustdoc-ui/intra-doc/email-address-localhost.rs b/src/test/rustdoc-ui/intra-doc/email-address-localhost.rs
index 9465e8e7ab9..7a5156e81c4 100644
--- a/src/test/rustdoc-ui/intra-doc/email-address-localhost.rs
+++ b/src/test/rustdoc-ui/intra-doc/email-address-localhost.rs
@@ -1,7 +1,7 @@
 // normalize-stderr-test: "nightly|beta|1\.[0-9][0-9]\.[0-9]" -> "$$CHANNEL"
+// check-pass
 #![deny(warnings)]
 
 //! Email me at <hello@localhost>.
-//~^ ERROR unknown disambiguator `hello`
 
 //! This should *not* warn: <hello@example.com>.
diff --git a/src/test/rustdoc-ui/intra-doc/email-address-localhost.stderr b/src/test/rustdoc-ui/intra-doc/email-address-localhost.stderr
deleted file mode 100644
index 1b07828fc6e..00000000000
--- a/src/test/rustdoc-ui/intra-doc/email-address-localhost.stderr
+++ /dev/null
@@ -1,16 +0,0 @@
-error: unknown disambiguator `hello`
-  --> $DIR/email-address-localhost.rs:4:18
-   |
-LL | //! Email me at <hello@localhost>.
-   |                  ^^^^^
-   |
-note: the lint level is defined here
-  --> $DIR/email-address-localhost.rs:2:9
-   |
-LL | #![deny(warnings)]
-   |         ^^^^^^^^
-   = note: `#[deny(rustdoc::broken_intra_doc_links)]` implied by `#[deny(warnings)]`
-   = note: see https://doc.rust-lang.org/$CHANNEL/rustdoc/linking-to-items-by-name.html#namespaces-and-disambiguators for more info about disambiguators
-
-error: aborting due to previous error
-
diff --git a/src/test/rustdoc/early-unindent.rs b/src/test/rustdoc/early-unindent.rs
new file mode 100644
index 00000000000..791a452c957
--- /dev/null
+++ b/src/test/rustdoc/early-unindent.rs
@@ -0,0 +1,26 @@
+// This is a regression for https://github.com/rust-lang/rust/issues/96079.
+
+#![crate_name = "foo"]
+
+pub mod app {
+    pub struct S;
+
+    impl S {
+        // @has 'foo/app/struct.S.html'
+        // @has - '//a[@href="../enums/enum.Foo.html#method.by_name"]' 'Foo::by_name'
+        /**
+        Doc comment hello! [`Foo::by_name`](`crate::enums::Foo::by_name`).
+        */
+        pub fn whatever(&self) {}
+    }
+}
+
+pub mod enums {
+    pub enum Foo {
+        Bar,
+    }
+
+    impl Foo {
+        pub fn by_name(&self) {}
+    }
+}