From 73fc00ff8c497af386cce222fe467371dc335885 Mon Sep 17 00:00:00 2001 From: Guillaume Gomez Date: Mon, 14 Oct 2024 17:51:08 +0200 Subject: [PATCH] Delay ambiguous intra-doc link resolution after `Cache` has been populated --- src/librustdoc/clean/types.rs | 13 +- src/librustdoc/core.rs | 24 +- .../passes/calculate_doc_coverage.rs | 2 +- .../passes/check_doc_test_visibility.rs | 2 +- .../passes/collect_intra_doc_links.rs | 260 +++++++++++++++--- src/librustdoc/passes/collect_trait_impls.rs | 2 +- src/librustdoc/passes/lint.rs | 2 +- src/librustdoc/passes/mod.rs | 2 +- src/librustdoc/passes/propagate_doc_cfg.rs | 2 +- src/librustdoc/passes/propagate_stability.rs | 2 +- .../passes/strip_aliased_non_local.rs | 2 +- src/librustdoc/passes/strip_hidden.rs | 2 +- src/librustdoc/passes/strip_priv_imports.rs | 2 +- src/librustdoc/passes/strip_private.rs | 2 +- 14 files changed, 263 insertions(+), 56 deletions(-) diff --git a/src/librustdoc/clean/types.rs b/src/librustdoc/clean/types.rs index bc5bf4c0583..44d9a5fdbe8 100644 --- a/src/librustdoc/clean/types.rs +++ b/src/librustdoc/clean/types.rs @@ -1228,15 +1228,14 @@ pub(crate) fn get_doc_aliases(&self) -> Box<[Symbol]> { for attr in self.other_attrs.lists(sym::doc).filter(|a| a.has_name(sym::alias)) { if let Some(values) = attr.meta_item_list() { for l in values { - match l.lit().unwrap().kind { - ast::LitKind::Str(s, _) => { - aliases.insert(s); - } - _ => unreachable!(), + if let Some(lit) = l.lit() + && let ast::LitKind::Str(s, _) = lit.kind + { + aliases.insert(s); } } - } else { - aliases.insert(attr.value_str().unwrap()); + } else if let Some(value) = attr.value_str() { + aliases.insert(value); } } aliases.into_iter().collect::>().into() diff --git a/src/librustdoc/core.rs b/src/librustdoc/core.rs index aaf4c80f997..9bebe1fb4c1 100644 --- a/src/librustdoc/core.rs +++ b/src/librustdoc/core.rs @@ -29,8 +29,9 @@ use crate::clean::{self, ItemId}; use crate::config::{Options as RustdocOptions, OutputFormat, RenderOptions}; use crate::formats::cache::Cache; +use crate::passes; use crate::passes::Condition::*; -use crate::passes::{self}; +use crate::passes::collect_intra_doc_links::LinkCollector; pub(crate) struct DocContext<'tcx> { pub(crate) tcx: TyCtxt<'tcx>, @@ -427,6 +428,9 @@ fn report_deprecated_attr(name: &str, dcx: DiagCtxtHandle<'_>, sp: Span) { info!("Executing passes"); + let mut visited = FxHashMap::default(); + let mut ambiguous = FxIndexMap::default(); + for p in passes::defaults(show_coverage) { let run = match p.condition { Always => true, @@ -436,18 +440,30 @@ fn report_deprecated_attr(name: &str, dcx: DiagCtxtHandle<'_>, sp: Span) { }; if run { debug!("running pass {}", p.pass.name); - krate = tcx.sess.time(p.pass.name, || (p.pass.run)(krate, &mut ctxt)); + if let Some(run_fn) = p.pass.run { + krate = tcx.sess.time(p.pass.name, || run_fn(krate, &mut ctxt)); + } else { + let (k, LinkCollector { visited_links, ambiguous_links, .. }) = + passes::collect_intra_doc_links::collect_intra_doc_links(krate, &mut ctxt); + krate = k; + visited = visited_links; + ambiguous = ambiguous_links; + } } } tcx.sess.time("check_lint_expectations", || tcx.check_expectations(Some(sym::rustdoc))); + krate = tcx.sess.time("create_format_cache", || Cache::populate(&mut ctxt, krate)); + + let mut collector = + LinkCollector { cx: &mut ctxt, visited_links: visited, ambiguous_links: ambiguous }; + collector.resolve_ambiguities(); + if let Some(guar) = tcx.dcx().has_errors() { return Err(guar); } - krate = tcx.sess.time("create_format_cache", || Cache::populate(&mut ctxt, krate)); - Ok((krate, ctxt.render_options, ctxt.cache)) } diff --git a/src/librustdoc/passes/calculate_doc_coverage.rs b/src/librustdoc/passes/calculate_doc_coverage.rs index abea5bcbc51..d27e737764d 100644 --- a/src/librustdoc/passes/calculate_doc_coverage.rs +++ b/src/librustdoc/passes/calculate_doc_coverage.rs @@ -20,7 +20,7 @@ pub(crate) const CALCULATE_DOC_COVERAGE: Pass = Pass { name: "calculate-doc-coverage", - run: calculate_doc_coverage, + run: Some(calculate_doc_coverage), description: "counts the number of items with and without documentation", }; diff --git a/src/librustdoc/passes/check_doc_test_visibility.rs b/src/librustdoc/passes/check_doc_test_visibility.rs index 1dc9af7ebe5..f4579d85531 100644 --- a/src/librustdoc/passes/check_doc_test_visibility.rs +++ b/src/librustdoc/passes/check_doc_test_visibility.rs @@ -20,7 +20,7 @@ pub(crate) const CHECK_DOC_TEST_VISIBILITY: Pass = Pass { name: "check_doc_test_visibility", - run: check_doc_test_visibility, + run: Some(check_doc_test_visibility), description: "run various visibility-related lints on doctests", }; diff --git a/src/librustdoc/passes/collect_intra_doc_links.rs b/src/librustdoc/passes/collect_intra_doc_links.rs index db235786cf4..81e7bcbd7d8 100644 --- a/src/librustdoc/passes/collect_intra_doc_links.rs +++ b/src/librustdoc/passes/collect_intra_doc_links.rs @@ -9,12 +9,12 @@ use pulldown_cmark::LinkType; use rustc_ast::util::comments::may_have_doc_links; -use rustc_data_structures::fx::{FxHashMap, FxHashSet, FxIndexSet}; +use rustc_data_structures::fx::{FxHashMap, FxHashSet, FxIndexMap, FxIndexSet}; use rustc_data_structures::intern::Interned; use rustc_errors::{Applicability, Diag, DiagMessage}; use rustc_hir::def::Namespace::*; use rustc_hir::def::{DefKind, Namespace, PerNS}; -use rustc_hir::def_id::{CRATE_DEF_ID, DefId}; +use rustc_hir::def_id::{CRATE_DEF_ID, DefId, LOCAL_CRATE}; use rustc_hir::{Mutability, Safety}; use rustc_middle::ty::{Ty, TyCtxt}; use rustc_middle::{bug, span_bug, ty}; @@ -30,23 +30,27 @@ use tracing::{debug, info, instrument, trace}; use crate::clean::utils::find_nearest_parent_module; -use crate::clean::{self, Crate, Item, ItemLink, PrimitiveType}; +use crate::clean::{self, Crate, Item, ItemId, ItemLink, PrimitiveType}; use crate::core::DocContext; use crate::html::markdown::{MarkdownLink, MarkdownLinkRange, markdown_links}; use crate::lint::{BROKEN_INTRA_DOC_LINKS, PRIVATE_INTRA_DOC_LINKS}; use crate::passes::Pass; use crate::visit::DocVisitor; -pub(crate) const COLLECT_INTRA_DOC_LINKS: Pass = Pass { - name: "collect-intra-doc-links", - run: collect_intra_doc_links, - description: "resolves intra-doc links", -}; +pub(crate) const COLLECT_INTRA_DOC_LINKS: Pass = + Pass { name: "collect-intra-doc-links", run: None, description: "resolves intra-doc links" }; -fn collect_intra_doc_links(krate: Crate, cx: &mut DocContext<'_>) -> Crate { - let mut collector = LinkCollector { cx, visited_links: FxHashMap::default() }; +pub(crate) fn collect_intra_doc_links<'a, 'tcx>( + krate: Crate, + cx: &'a mut DocContext<'tcx>, +) -> (Crate, LinkCollector<'a, 'tcx>) { + let mut collector = LinkCollector { + cx, + visited_links: FxHashMap::default(), + ambiguous_links: FxIndexMap::default(), + }; collector.visit_crate(&krate); - krate + (krate, collector) } fn filter_assoc_items_by_name_and_namespace<'a>( @@ -61,7 +65,7 @@ fn filter_assoc_items_by_name_and_namespace<'a>( } #[derive(Copy, Clone, Debug, Hash, PartialEq)] -enum Res { +pub(crate) enum Res { Def(DefKind, DefId), Primitive(PrimitiveType), } @@ -234,7 +238,7 @@ pub(crate) fn render(&self, s: &mut String, tcx: TyCtxt<'_>) { } #[derive(Clone, Debug, Hash, PartialEq, Eq)] -struct ResolutionInfo { +pub(crate) struct ResolutionInfo { item_id: DefId, module_id: DefId, dis: Option, @@ -243,18 +247,63 @@ struct ResolutionInfo { } #[derive(Clone)] -struct DiagnosticInfo<'a> { +pub(crate) struct DiagnosticInfo<'a> { item: &'a Item, dox: &'a str, ori_link: &'a str, link_range: MarkdownLinkRange, } -struct LinkCollector<'a, 'tcx> { - cx: &'a mut DocContext<'tcx>, +pub(crate) struct OwnedDiagnosticInfo { + item: Item, + dox: String, + ori_link: String, + link_range: MarkdownLinkRange, +} + +impl From> for OwnedDiagnosticInfo { + fn from(f: DiagnosticInfo<'_>) -> Self { + Self { + item: f.item.clone(), + dox: f.dox.to_string(), + ori_link: f.ori_link.to_string(), + link_range: f.link_range.clone(), + } + } +} + +impl OwnedDiagnosticInfo { + pub(crate) fn into_info(&self) -> DiagnosticInfo<'_> { + DiagnosticInfo { + item: &self.item, + ori_link: &self.ori_link, + dox: &self.dox, + link_range: self.link_range.clone(), + } + } +} + +pub(crate) struct LinkCollector<'a, 'tcx> { + pub(crate) cx: &'a mut DocContext<'tcx>, /// Cache the resolved links so we can avoid resolving (and emitting errors for) the same link. /// The link will be `None` if it could not be resolved (i.e. the error was cached). - visited_links: FxHashMap)>>, + pub(crate) visited_links: FxHashMap)>>, + /// These links are ambiguous. We need for the cache to have its paths filled. Unfortunately, + /// if we run the `LinkCollector` pass after `Cache::populate`, a lot of items that we need + /// to go through will be removed, making a lot of intra-doc links to not be inferred. + /// + /// So instead, we store the ambiguous links and we wait for cache paths to be filled before + /// inferring them (if possible). + /// + /// Key is `(item ID, path str)`. + pub(crate) ambiguous_links: FxIndexMap<(ItemId, String), Vec>, +} + +pub(crate) struct AmbiguousLinks { + disambiguator: Option, + link_text: Box, + diag_info: OwnedDiagnosticInfo, + resolved: Vec<(Res, Option)>, } impl<'a, 'tcx> LinkCollector<'a, 'tcx> { @@ -1001,6 +1050,10 @@ fn resolve_links(&mut self, item: &Item) { } } + pub(crate) fn save_link(&mut self, item_id: ItemId, link: ItemLink) { + self.cx.cache.intra_doc_links.entry(item_id).or_default().insert(link); + } + /// This is the entry point for resolving an intra-doc link. /// /// FIXME(jynelson): this is way too many arguments @@ -1024,7 +1077,7 @@ fn resolve_link( pp_link.as_ref().map_err(|err| err.report(self.cx, diag_info.clone())).ok()?; let disambiguator = *disambiguator; - let (mut res, fragment) = self.resolve_with_disambiguator_cached( + let mut resolved = self.resolve_with_disambiguator_cached( ResolutionInfo { item_id, module_id, @@ -1040,6 +1093,142 @@ fn resolve_link( false, )?; + if resolved.len() > 1 { + let links = AmbiguousLinks { + disambiguator, + link_text: link_text.clone(), + diag_info: diag_info.into(), + resolved, + }; + + self.ambiguous_links + .entry((item.item_id, path_str.to_string())) + .or_default() + .push(links); + None + } else if let Some((res, fragment)) = resolved.pop() { + self.compute_link(res, fragment, path_str, disambiguator, diag_info, link_text) + } else { + None + } + } + + /// Returns `true` if a link could be generated from the given intra-doc information. + /// + /// This is a very light version of `format::href_with_root_path` since we're only interested + /// about whether we can generate a link to an item or not. + /// + /// * If `original_did` is local, then we check if the item is reexported or public. + /// * If `original_did` is not local, then we check if the crate it comes from is a direct + /// public dependency. + fn validate_link(&self, original_did: DefId) -> bool { + let tcx = self.cx.tcx; + let def_kind = tcx.def_kind(original_did); + let did = match def_kind { + DefKind::AssocTy | DefKind::AssocFn | DefKind::AssocConst | DefKind::Variant => { + // documented on their parent's page + tcx.parent(original_did) + } + // If this a constructor, we get the parent (either a struct or a variant) and then + // generate the link for this item. + DefKind::Ctor(..) => return self.validate_link(tcx.parent(original_did)), + DefKind::ExternCrate => { + // Link to the crate itself, not the `extern crate` item. + if let Some(local_did) = original_did.as_local() { + tcx.extern_mod_stmt_cnum(local_did).unwrap_or(LOCAL_CRATE).as_def_id() + } else { + original_did + } + } + _ => original_did, + }; + + let cache = &self.cx.cache; + if !original_did.is_local() + && !cache.effective_visibilities.is_directly_public(tcx, did) + && !cache.document_private + && !cache.primitive_locations.values().any(|&id| id == did) + { + return false; + } + + cache.paths.get(&did).is_some() + || cache.external_paths.get(&did).is_some() + || !did.is_local() + } + + #[allow(rustc::potential_query_instability)] + pub(crate) fn resolve_ambiguities(&mut self) { + let mut ambiguous_links = mem::take(&mut self.ambiguous_links); + + for ((item_id, path_str), info_items) in ambiguous_links.iter_mut() { + for info in info_items { + info.resolved.retain(|(res, _)| match res { + Res::Def(_, def_id) => self.validate_link(*def_id), + // Primitive types are always valid. + Res::Primitive(_) => true, + }); + let diag_info = info.diag_info.into_info(); + match info.resolved.len() { + 1 => { + let (res, fragment) = info.resolved.pop().unwrap(); + if let Some(link) = self.compute_link( + res, + fragment, + path_str, + info.disambiguator, + diag_info, + &info.link_text, + ) { + self.save_link(*item_id, link); + } + } + 0 => { + report_diagnostic( + self.cx.tcx, + BROKEN_INTRA_DOC_LINKS, + format!( + "all items matching `{path_str}` are either private or doc(hidden)" + ), + &diag_info, + |diag, sp, _| { + if let Some(sp) = sp { + diag.span_label(sp, "unresolved link"); + } else { + diag.note("unresolved link"); + } + }, + ); + } + _ => { + let candidates = info + .resolved + .iter() + .map(|(res, fragment)| { + let def_id = if let Some(UrlFragment::Item(def_id)) = fragment { + Some(*def_id) + } else { + None + }; + (*res, def_id) + }) + .collect::>(); + ambiguity_error(self.cx, &diag_info, path_str, &candidates, true); + } + } + } + } + } + + fn compute_link( + &mut self, + mut res: Res, + fragment: Option, + path_str: &str, + disambiguator: Option, + diag_info: DiagnosticInfo<'_>, + link_text: &Box, + ) -> Option { // Check for a primitive which might conflict with a module // Report the ambiguity and require that the user specify which one they meant. // FIXME: could there ever be a primitive not in the type namespace? @@ -1055,7 +1244,7 @@ fn resolve_link( } else { // `[char]` when a `char` module is in scope let candidates = &[(res, res.def_id(self.cx.tcx)), (prim, None)]; - ambiguity_error(self.cx, &diag_info, path_str, candidates); + ambiguity_error(self.cx, &diag_info, path_str, candidates, true); return None; } } @@ -1085,7 +1274,7 @@ fn resolve_link( } res.def_id(self.cx.tcx).map(|page_id| ItemLink { - link: Box::::from(&*ori_link.link), + link: Box::::from(&*diag_info.ori_link), link_text: link_text.clone(), page_id, fragment, @@ -1107,7 +1296,7 @@ fn resolve_link( let page_id = clean::register_res(self.cx, rustc_hir::def::Res::Def(kind, id)); Some(ItemLink { - link: Box::::from(&*ori_link.link), + link: Box::::from(&*diag_info.ori_link), link_text: link_text.clone(), page_id, fragment, @@ -1220,10 +1409,10 @@ fn resolve_with_disambiguator_cached( // If this call is intended to be recoverable, then pass true to silence. // This is only recoverable when path is failed to resolved. recoverable: bool, - ) -> Option<(Res, Option)> { + ) -> Option)>> { if let Some(res) = self.visited_links.get(&key) { if res.is_some() || cache_errors { - return res.clone(); + return res.clone().map(|r| vec![r]); } } @@ -1248,13 +1437,14 @@ fn resolve_with_disambiguator_cached( // and after removing duplicated kinds, only one remains, the `ambiguity_error` function // won't emit an error. So at this point, we can just take the first candidate as it was // the first retrieved and use it to generate the link. - if let [candidate, _candidate2, ..] = *candidates - && !ambiguity_error(self.cx, &diag, &key.path_str, &candidates) - { - candidates = vec![candidate]; + if let [candidate, _candidate2, ..] = *candidates { + if !ambiguity_error(self.cx, &diag, &key.path_str, &candidates, false) { + candidates = vec![candidate]; + } } - if let &[(res, def_id)] = candidates.as_slice() { + let mut out = Vec::with_capacity(candidates.len()); + for (res, def_id) in candidates { let fragment = match (&key.extra_fragment, def_id) { (Some(_), Some(def_id)) => { report_anchor_conflict(self.cx, diag, def_id); @@ -1264,15 +1454,14 @@ fn resolve_with_disambiguator_cached( (None, Some(def_id)) => Some(UrlFragment::Item(def_id)), (None, None) => None, }; - let r = Some((res, fragment)); - self.visited_links.insert(key, r.clone()); - return r; + out.push((res, fragment)); } - - if cache_errors { + if let [r] = out.as_slice() { + self.visited_links.insert(key, Some(r.clone())); + } else if cache_errors { self.visited_links.insert(key, None); } - None + Some(out) } /// After parsing the disambiguator, resolve the main part of the link. @@ -2046,6 +2235,7 @@ fn ambiguity_error( diag_info: &DiagnosticInfo<'_>, path_str: &str, candidates: &[(Res, Option)], + emit_error: bool, ) -> bool { let mut descrs = FxHashSet::default(); let kinds = candidates @@ -2061,6 +2251,8 @@ fn ambiguity_error( // There is no way for users to disambiguate at this point, so better return the first // candidate and not show a warning. return false; + } else if !emit_error { + return true; } let mut msg = format!("`{path_str}` is "); diff --git a/src/librustdoc/passes/collect_trait_impls.rs b/src/librustdoc/passes/collect_trait_impls.rs index d1a1f0df3e7..f3589080322 100644 --- a/src/librustdoc/passes/collect_trait_impls.rs +++ b/src/librustdoc/passes/collect_trait_impls.rs @@ -16,7 +16,7 @@ pub(crate) const COLLECT_TRAIT_IMPLS: Pass = Pass { name: "collect-trait-impls", - run: collect_trait_impls, + run: Some(collect_trait_impls), description: "retrieves trait impls for items in the crate", }; diff --git a/src/librustdoc/passes/lint.rs b/src/librustdoc/passes/lint.rs index 593027ef7d2..35b62370abb 100644 --- a/src/librustdoc/passes/lint.rs +++ b/src/librustdoc/passes/lint.rs @@ -14,7 +14,7 @@ use crate::visit::DocVisitor; pub(crate) const RUN_LINTS: Pass = - Pass { name: "run-lints", run: run_lints, description: "runs some of rustdoc's lints" }; + Pass { name: "run-lints", run: Some(run_lints), description: "runs some of rustdoc's lints" }; struct Linter<'a, 'tcx> { cx: &'a mut DocContext<'tcx>, diff --git a/src/librustdoc/passes/mod.rs b/src/librustdoc/passes/mod.rs index f5b78023721..9ba63d34144 100644 --- a/src/librustdoc/passes/mod.rs +++ b/src/librustdoc/passes/mod.rs @@ -47,7 +47,7 @@ #[derive(Copy, Clone)] pub(crate) struct Pass { pub(crate) name: &'static str, - pub(crate) run: fn(clean::Crate, &mut DocContext<'_>) -> clean::Crate, + pub(crate) run: Option) -> clean::Crate>, pub(crate) description: &'static str, } diff --git a/src/librustdoc/passes/propagate_doc_cfg.rs b/src/librustdoc/passes/propagate_doc_cfg.rs index 6be51dd1560..350be37f553 100644 --- a/src/librustdoc/passes/propagate_doc_cfg.rs +++ b/src/librustdoc/passes/propagate_doc_cfg.rs @@ -13,7 +13,7 @@ pub(crate) const PROPAGATE_DOC_CFG: Pass = Pass { name: "propagate-doc-cfg", - run: propagate_doc_cfg, + run: Some(propagate_doc_cfg), description: "propagates `#[doc(cfg(...))]` to child items", }; diff --git a/src/librustdoc/passes/propagate_stability.rs b/src/librustdoc/passes/propagate_stability.rs index f51e993bfa5..f55479687f8 100644 --- a/src/librustdoc/passes/propagate_stability.rs +++ b/src/librustdoc/passes/propagate_stability.rs @@ -16,7 +16,7 @@ pub(crate) const PROPAGATE_STABILITY: Pass = Pass { name: "propagate-stability", - run: propagate_stability, + run: Some(propagate_stability), description: "propagates stability to child items", }; diff --git a/src/librustdoc/passes/strip_aliased_non_local.rs b/src/librustdoc/passes/strip_aliased_non_local.rs index 155ad543831..a078eec048e 100644 --- a/src/librustdoc/passes/strip_aliased_non_local.rs +++ b/src/librustdoc/passes/strip_aliased_non_local.rs @@ -8,7 +8,7 @@ pub(crate) const STRIP_ALIASED_NON_LOCAL: Pass = Pass { name: "strip-aliased-non-local", - run: strip_aliased_non_local, + run: Some(strip_aliased_non_local), description: "strips all non-local private aliased items from the output", }; diff --git a/src/librustdoc/passes/strip_hidden.rs b/src/librustdoc/passes/strip_hidden.rs index 430f3d8a574..aba04283e59 100644 --- a/src/librustdoc/passes/strip_hidden.rs +++ b/src/librustdoc/passes/strip_hidden.rs @@ -16,7 +16,7 @@ pub(crate) const STRIP_HIDDEN: Pass = Pass { name: "strip-hidden", - run: strip_hidden, + run: Some(strip_hidden), description: "strips all `#[doc(hidden)]` items from the output", }; diff --git a/src/librustdoc/passes/strip_priv_imports.rs b/src/librustdoc/passes/strip_priv_imports.rs index 7b05756ae21..2e9f06bd0a3 100644 --- a/src/librustdoc/passes/strip_priv_imports.rs +++ b/src/librustdoc/passes/strip_priv_imports.rs @@ -8,7 +8,7 @@ pub(crate) const STRIP_PRIV_IMPORTS: Pass = Pass { name: "strip-priv-imports", - run: strip_priv_imports, + run: Some(strip_priv_imports), description: "strips all private import statements (`use`, `extern crate`) from a crate", }; diff --git a/src/librustdoc/passes/strip_private.rs b/src/librustdoc/passes/strip_private.rs index 1bafa450be9..78f0ad27740 100644 --- a/src/librustdoc/passes/strip_private.rs +++ b/src/librustdoc/passes/strip_private.rs @@ -8,7 +8,7 @@ pub(crate) const STRIP_PRIVATE: Pass = Pass { name: "strip-private", - run: strip_private, + run: Some(strip_private), description: "strips all private items from a crate which cannot be seen externally, \ implies strip-priv-imports", };