From 52d6fa3065c3ae3cd39d03b683771fa89795024e Mon Sep 17 00:00:00 2001 From: Guillaume Gomez Date: Fri, 8 Nov 2024 22:36:05 +0100 Subject: [PATCH 1/5] Add new `example` disambiguator for intra-doc links --- src/librustdoc/clean/types.rs | 57 +++-- src/librustdoc/config.rs | 8 +- src/librustdoc/html/render/context.rs | 3 + src/librustdoc/html/sources.rs | 11 +- src/librustdoc/json/conversions.rs | 24 ++- .../passes/collect_intra_doc_links.rs | 198 ++++++++++++------ src/librustdoc/scrape_examples.rs | 24 ++- 7 files changed, 226 insertions(+), 99 deletions(-) diff --git a/src/librustdoc/clean/types.rs b/src/librustdoc/clean/types.rs index e5c9539b5e7a3..fc0f73df7993a 100644 --- a/src/librustdoc/clean/types.rs +++ b/src/librustdoc/clean/types.rs @@ -487,21 +487,38 @@ impl Item { let Some(links) = cx.cache().intra_doc_links.get(&self.item_id) else { return vec![] }; links .iter() - .filter_map(|ItemLink { link: s, link_text, page_id: id, ref fragment }| { - debug!(?id); - if let Ok((mut href, ..)) = href(*id, cx) { - debug!(?href); - if let Some(ref fragment) = *fragment { - fragment.render(&mut href, cx.tcx()) + .filter_map(|ItemLink { link: s, link_text, ref kind, ref fragment }| match kind { + ItemLinkKind::Item { page_id: id } => { + debug!(?id); + if let Some(id) = id { + if let Ok((mut href, ..)) = href(*id, cx) { + debug!(?href); + if let Some(ref fragment) = *fragment { + fragment.render(&mut href, cx.tcx()) + } + return Some(RenderedLink { + original_text: s.clone(), + new_text: link_text.clone(), + tooltip: link_tooltip(*id, fragment, cx), + href, + }); + } } + None + } + ItemLinkKind::Example { file_path } => { + let example_name = file_path.split('/').next().unwrap_or(file_path); + let mut href = + std::iter::repeat("../").take(cx.current.len()).collect::(); + href.push_str("src/"); + href.push_str(file_path); + href.push_str(".html"); Some(RenderedLink { original_text: s.clone(), new_text: link_text.clone(), - tooltip: link_tooltip(*id, fragment, cx), + tooltip: format!("Example {example_name}"), href, }) - } else { - None } }) .collect() @@ -1110,6 +1127,23 @@ impl> NestedAttributesExt for I { } } +/// The kind of a link that has not yet been rendered. +/// +/// It is used in [`ItemLink`]. +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub(crate) enum ItemLinkKind { + Item { + /// The `DefId` of the Item whose **HTML Page** contains the item being + /// linked to. This will be different to `item_id` on item's that don't + /// have their own page, such as struct fields and enum variants. + page_id: Option, + }, + Example { + /// The path of the example file. + file_path: String, + }, +} + /// A link that has not yet been rendered. /// /// This link will be turned into a rendered link by [`Item::links`]. @@ -1122,12 +1156,9 @@ pub(crate) struct ItemLink { /// This may not be the same as `link` if there was a disambiguator /// in an intra-doc link (e.g. \[`fn@f`\]) pub(crate) link_text: Box, - /// The `DefId` of the Item whose **HTML Page** contains the item being - /// linked to. This will be different to `item_id` on item's that don't - /// have their own page, such as struct fields and enum variants. - pub(crate) page_id: DefId, /// The url fragment to append to the link pub(crate) fragment: Option, + pub(crate) kind: ItemLinkKind, } pub struct RenderedLink { diff --git a/src/librustdoc/config.rs b/src/librustdoc/config.rs index 5071ed1c47faa..efd35b8b6b191 100644 --- a/src/librustdoc/config.rs +++ b/src/librustdoc/config.rs @@ -25,7 +25,7 @@ use crate::html::markdown::IdMap; use crate::html::render::StylePath; use crate::html::static_files; use crate::passes::{self, Condition}; -use crate::scrape_examples::{AllCallLocations, ScrapeExamplesOptions}; +use crate::scrape_examples::{AllCallLocations, AllExampleFiles, ScrapeExamplesOptions}; use crate::{html, opts, theme}; #[derive(Clone, Copy, PartialEq, Eq, Debug, Default)] @@ -287,6 +287,8 @@ pub(crate) struct RenderOptions { pub(crate) generate_link_to_definition: bool, /// Set of function-call locations to include as examples pub(crate) call_locations: AllCallLocations, + /// Set of function-call locations to include as examples + pub(crate) examples_files: AllExampleFiles, /// If `true`, Context::init will not emit shared files. pub(crate) no_emit_shared: bool, /// If `true`, HTML source code pages won't be generated. @@ -773,7 +775,8 @@ impl Options { let scrape_examples_options = ScrapeExamplesOptions::new(matches, dcx); let with_examples = matches.opt_strs("with-examples"); - let call_locations = crate::scrape_examples::load_call_locations(with_examples, dcx); + let (call_locations, examples_files) = + crate::scrape_examples::load_call_locations(with_examples, dcx); let unstable_features = rustc_feature::UnstableFeatures::from_environment(crate_name.as_deref()); @@ -846,6 +849,7 @@ impl Options { emit, generate_link_to_definition, call_locations, + examples_files, no_emit_shared: false, html_no_source, output_to_stdout, diff --git a/src/librustdoc/html/render/context.rs b/src/librustdoc/html/render/context.rs index dc4d45e592eb7..dd9b3332de4db 100644 --- a/src/librustdoc/html/render/context.rs +++ b/src/librustdoc/html/render/context.rs @@ -132,6 +132,8 @@ pub(crate) struct SharedContext<'tcx> { /// Controls whether we read / write to cci files in the doc root. Defaults read=true, /// write=true should_merge: ShouldMerge, + /// Paths of generated files. + pub(crate) emitted_local_sources: RefCell>, } impl SharedContext<'_> { @@ -554,6 +556,7 @@ impl<'tcx> FormatRenderer<'tcx> for Context<'tcx> { cache, call_locations, should_merge: options.should_merge, + emitted_local_sources: Default::default(), }; let dst = output; diff --git a/src/librustdoc/html/sources.rs b/src/librustdoc/html/sources.rs index f4a0ef01c253b..6c969dbd7f3cf 100644 --- a/src/librustdoc/html/sources.rs +++ b/src/librustdoc/html/sources.rs @@ -30,7 +30,7 @@ pub(crate) fn render(cx: &mut Context<'_>, krate: &clean::Crate) -> Result<(), E let crate_name = crate_name.as_str(); let mut collector = - SourceCollector { dst, cx, emitted_local_sources: FxHashSet::default(), crate_name }; + SourceCollector { dst, cx, crate_name, emitted_paths: FxHashSet::default() }; collector.visit_crate(krate); Ok(()) } @@ -117,9 +117,10 @@ struct SourceCollector<'a, 'tcx> { /// Root destination to place all HTML output into dst: PathBuf, - emitted_local_sources: FxHashSet, crate_name: &'a str, + + emitted_paths: FxHashSet, } impl DocVisitor<'_> for SourceCollector<'_, '_> { @@ -182,7 +183,7 @@ impl SourceCollector<'_, '_> { } _ => return Ok(()), }; - if self.emitted_local_sources.contains(&*p) { + if self.emitted_paths.contains(&*p) { // We've already emitted this source return Ok(()); } @@ -245,6 +246,7 @@ impl SourceCollector<'_, '_> { resource_suffix: &shared.resource_suffix, rust_logo: has_doc_flag(self.cx.tcx(), LOCAL_CRATE.as_def_id(), sym::rust_logo), }; + let file_path_s = file_path.display().to_string(); let v = layout::render( &shared.layout, &page, @@ -264,7 +266,8 @@ impl SourceCollector<'_, '_> { &shared.style_files, ); shared.fs.write(cur, v)?; - self.emitted_local_sources.insert(p); + self.emitted_paths.insert(p); + self.cx.shared.emitted_local_sources.borrow_mut().insert(file_path_s); Ok(()) } } diff --git a/src/librustdoc/json/conversions.rs b/src/librustdoc/json/conversions.rs index 1c8303d4c2087..6192eb12cd596 100644 --- a/src/librustdoc/json/conversions.rs +++ b/src/librustdoc/json/conversions.rs @@ -15,7 +15,7 @@ use rustc_span::{Pos, Symbol, sym}; use rustdoc_json_types::*; use super::FullItemId; -use crate::clean::{self, ItemId}; +use crate::clean::{self, ItemId, ItemLink, ItemLinkKind}; use crate::formats::FormatRenderer; use crate::formats::item_type::ItemType; use crate::json::JsonRenderer; @@ -30,14 +30,20 @@ impl JsonRenderer<'_> { .get(&item.item_id) .into_iter() .flatten() - .map(|clean::ItemLink { link, page_id, fragment, .. }| { - let id = match fragment { - Some(UrlFragment::Item(frag_id)) => *frag_id, - // FIXME: Pass the `UserWritten` segment to JSON consumer. - Some(UrlFragment::UserWritten(_)) | None => *page_id, - }; - - (String::from(&**link), self.id_from_item_default(id.into())) + .filter_map(|ItemLink { link, kind, fragment, .. }| { + if let ItemLinkKind::Item { page_id } = kind + && let Some(page_id) = page_id + { + let id = match fragment { + Some(UrlFragment::Item(frag_id)) => *frag_id, + // FIXME: Pass the `UserWritten` segment to JSON consumer. + Some(UrlFragment::UserWritten(_)) | None => *page_id, + }; + + Some((String::from(&**link), self.id_from_item_default(id.into()))) + } else { + None + } }) .collect(); let docs = item.opt_doc_value(); diff --git a/src/librustdoc/passes/collect_intra_doc_links.rs b/src/librustdoc/passes/collect_intra_doc_links.rs index 140fda7091885..b7de93d08c299 100644 --- a/src/librustdoc/passes/collect_intra_doc_links.rs +++ b/src/librustdoc/passes/collect_intra_doc_links.rs @@ -30,7 +30,7 @@ use smallvec::{SmallVec, smallvec}; use tracing::{debug, info, instrument, trace}; use crate::clean::utils::find_nearest_parent_module; -use crate::clean::{self, Crate, Item, ItemId, ItemLink, PrimitiveType}; +use crate::clean::{self, Crate, Item, ItemId, ItemLink, ItemLinkKind, PrimitiveType}; use crate::core::DocContext; use crate::html::markdown::{MarkdownLink, MarkdownLinkRange, markdown_links}; use crate::lint::{BROKEN_INTRA_DOC_LINKS, PRIVATE_INTRA_DOC_LINKS}; @@ -64,40 +64,45 @@ fn filter_assoc_items_by_name_and_namespace<'a>( }) } -#[derive(Copy, Clone, Debug, Hash, PartialEq)] +#[derive(Clone, Debug, Hash, PartialEq)] pub(crate) enum Res { Def(DefKind, DefId), Primitive(PrimitiveType), + Example(String), } type ResolveRes = rustc_hir::def::Res; impl Res { - fn descr(self) -> &'static str { + fn descr(&self) -> &'static str { match self { - Res::Def(kind, id) => ResolveRes::Def(kind, id).descr(), + Res::Def(kind, id) => ResolveRes::Def(*kind, *id).descr(), Res::Primitive(_) => "primitive type", + Res::Example(_) => "example", } } - fn article(self) -> &'static str { + fn article(&self) -> &'static str { match self { - Res::Def(kind, id) => ResolveRes::Def(kind, id).article(), + Res::Def(kind, id) => ResolveRes::Def(*kind, *id).article(), Res::Primitive(_) => "a", + Res::Example(_) => "an", } } - fn name(self, tcx: TyCtxt<'_>) -> Symbol { + fn name(&self, tcx: TyCtxt<'_>) -> Symbol { match self { - Res::Def(_, id) => tcx.item_name(id), + Res::Def(_, id) => tcx.item_name(*id), Res::Primitive(prim) => prim.as_sym(), + Res::Example(_) => panic!("no name"), } } - fn def_id(self, tcx: TyCtxt<'_>) -> Option { + fn def_id(&self, tcx: TyCtxt<'_>) -> Option { match self { - Res::Def(_, id) => Some(id), - Res::Primitive(prim) => PrimitiveType::primitive_locations(tcx).get(&prim).copied(), + Res::Def(_, id) => Some(*id), + Res::Primitive(prim) => PrimitiveType::primitive_locations(tcx).get(prim).copied(), + Res::Example(_) => None, } } @@ -106,9 +111,10 @@ impl Res { } /// Used for error reporting. - fn disambiguator_suggestion(self) -> Suggestion { + fn disambiguator_suggestion(&self) -> Suggestion { let kind = match self { Res::Primitive(_) => return Suggestion::Prefix("prim"), + Res::Example(_) => return Suggestion::Prefix("example"), Res::Def(kind, _) => kind, }; @@ -625,7 +631,7 @@ impl<'a, 'tcx> LinkCollector<'a, 'tcx> { .map(|ty| { resolve_associated_trait_item(ty, module_id, item_name, ns, self.cx) .iter() - .map(|item| (root_res, item.def_id)) + .map(|item| (root_res.clone(), item.def_id)) .collect::>() }) .unwrap_or(Vec::new()) @@ -682,7 +688,7 @@ impl<'a, 'tcx> LinkCollector<'a, 'tcx> { .fields .iter() .filter(|field| field.name == item_name) - .map(|field| (root_res, field.did)) + .map(|field| (root_res.clone(), field.did)) .collect::>() }; @@ -702,7 +708,7 @@ impl<'a, 'tcx> LinkCollector<'a, 'tcx> { ns, ) }) - .map(|item| (root_res, item.def_id)) + .map(|item| (root_res.clone(), item.def_id)) .collect(); if assoc_items.is_empty() { @@ -719,7 +725,7 @@ impl<'a, 'tcx> LinkCollector<'a, 'tcx> { self.cx, ) .into_iter() - .map(|item| (root_res, item.def_id)) + .map(|item| (root_res.clone(), item.def_id)) .collect::>(); } @@ -751,8 +757,8 @@ impl<'a, 'tcx> LinkCollector<'a, 'tcx> { } } -fn full_res(tcx: TyCtxt<'_>, (base, assoc_item): (Res, Option)) -> Res { - assoc_item.map_or(base, |def_id| Res::from_def_id(tcx, def_id)) +fn full_res(tcx: TyCtxt<'_>, (base, assoc_item): &(Res, Option)) -> Res { + assoc_item.map_or_else(|| base.clone(), |def_id| Res::from_def_id(tcx, def_id)) } /// Look to see if a resolved item has an associated item named `item_name`. @@ -930,13 +936,14 @@ fn preprocess_link( ori_link: &MarkdownLink, dox: &str, ) -> Option> { - // [] is mostly likely not supposed to be a link + // `[]` is most likely not supposed to be a link if ori_link.link.is_empty() { return None; } + let has_example_disambiguator = ori_link.link.starts_with("example@"); // Bail early for real links. - if ori_link.link.contains('/') { + if ori_link.link.contains('/') && !has_example_disambiguator { return None; } @@ -978,26 +985,31 @@ fn preprocess_link( } }; - if should_ignore_link(path_str) { - return None; - } - - // Strip generics from the path. - 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()))); + let path_str = if !matches!(disambiguator, Some(Disambiguator::Example)) { + if should_ignore_link(path_str) { + return None; } - }; - // Sanity check to make sure we don't have any angle brackets after stripping generics. - assert!(!path_str.contains(['<', '>'].as_slice())); + // Strip generics from the path. + 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()))); + } + }; - // The link is not an intra-doc link if it still contains spaces after stripping generics. - if path_str.contains(' ') { - return None; - } + // Sanity check to make sure we don't have any angle brackets after stripping generics. + assert!(!path_str.contains(['<', '>'].as_slice())); + + // The link is not an intra-doc link if it still contains spaces after stripping generics. + if path_str.contains(' ') { + return None; + } + path_str + } else { + path_str.into() + }; Some(Ok(PreprocessingInfo { path_str, @@ -1164,8 +1176,8 @@ impl LinkCollector<'_, '_> { 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, + // Primitive types and examples are always valid. + Res::Primitive(_) | Res::Example(_) => true, }); let diag_info = info.diag_info.into_info(); match info.resolved.len() { @@ -1207,7 +1219,7 @@ impl LinkCollector<'_, '_> { } else { None }; - (*res, def_id) + (res.clone(), def_id) }) .collect::>(); ambiguity_error(self.cx, &diag_info, path_str, &candidates, true); @@ -1240,7 +1252,8 @@ impl LinkCollector<'_, '_> { res = prim; } else { // `[char]` when a `char` module is in scope - let candidates = &[(res, res.def_id(self.cx.tcx)), (prim, None)]; + let def_id = res.def_id(self.cx.tcx); + let candidates = &[(res, def_id), (prim, None)]; ambiguity_error(self.cx, &diag_info, path_str, candidates, true); return None; } @@ -1273,8 +1286,8 @@ impl LinkCollector<'_, '_> { res.def_id(self.cx.tcx).map(|page_id| ItemLink { link: Box::::from(&*diag_info.ori_link), link_text: link_text.clone(), - page_id, fragment, + kind: ItemLinkKind::Item { page_id: Some(page_id) }, }) } Res::Def(kind, id) => { @@ -1291,14 +1304,21 @@ impl LinkCollector<'_, '_> { &diag_info, )?; - let page_id = clean::register_res(self.cx, rustc_hir::def::Res::Def(kind, id)); + let page_id = + Some(clean::register_res(self.cx, rustc_hir::def::Res::Def(kind, id))); Some(ItemLink { link: Box::::from(&*diag_info.ori_link), link_text: link_text.clone(), - page_id, fragment, + kind: ItemLinkKind::Item { page_id }, }) } + Res::Example(path) => Some(ItemLink { + link: Box::::from(&*diag_info.ori_link), + link_text: link_text.clone(), + fragment, + kind: ItemLinkKind::Example { file_path: path.into() }, + }), } } @@ -1330,6 +1350,7 @@ impl LinkCollector<'_, '_> { self.report_disambiguator_mismatch(path_str, specified, Res::Def(kind, id), diag_info); return None; } + (_, Some(Disambiguator::Example)) => unreachable!(), } // item can be non-local e.g. when using `#[rustc_doc_primitive = "pointer"]` @@ -1368,7 +1389,7 @@ impl LinkCollector<'_, '_> { } else { diag.note(note); } - suggest_disambiguator(resolved, diag, path_str, link_range, sp, diag_info); + suggest_disambiguator(&resolved, diag, path_str, link_range, sp, diag_info); }; report_diagnostic(self.cx.tcx, BROKEN_INTRA_DOC_LINKS, msg, diag_info, callback); } @@ -1423,7 +1444,7 @@ impl LinkCollector<'_, '_> { self.report_rawptr_assoc_feature_gate(diag.dox, &diag.link_range, diag.item); return None; } else { - candidates = vec![*candidate]; + candidates = vec![candidate.clone()]; } } @@ -1431,9 +1452,9 @@ impl LinkCollector<'_, '_> { // 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 { + if let [ref candidate, ref _candidate2, ..] = *candidates { if !ambiguity_error(self.cx, &diag, &key.path_str, &candidates, false) { - candidates = vec![candidate]; + candidates = vec![candidate.clone()]; } } @@ -1458,6 +1479,43 @@ impl LinkCollector<'_, '_> { Some(out) } + fn get_example_file( + &self, + path_str: &str, + diag: DiagnosticInfo<'_>, + ) -> Vec<(Res, Option)> { + // If the user is referring to the example by its name: + if let Some(files) = self.cx.render_options.examples_files.get(path_str) + && let Some(file_path) = files.iter().next() + { + return vec![(Res::Example(file_path.clone()), None)]; + } + // If the user is referring to a specific file of the example, it'll be of this form: + // + // CRATE_NAME/PATH + if let Some(crate_name) = path_str.split('/').next() + && let Some(files) = self.cx.render_options.examples_files.get(crate_name) + && let Some(file_path) = files.get(path_str) + { + return vec![(Res::Example(file_path.clone()), None)]; + } + report_diagnostic( + self.cx.tcx, + BROKEN_INTRA_DOC_LINKS, + format!("unresolved link to `{path_str}`"), + &diag, + |diag, sp, _link_range| { + let note = "unknown example"; + if let Some(span) = sp { + diag.span_label(span, note); + } else { + diag.note(note); + } + }, + ); + Vec::new() + } + /// After parsing the disambiguator, resolve the main part of the link. fn resolve_with_disambiguator( &mut self, @@ -1465,10 +1523,13 @@ impl LinkCollector<'_, '_> { diag: DiagnosticInfo<'_>, ) -> Vec<(Res, Option)> { let disambiguator = key.dis; - let path_str = &key.path_str; + let path_str: &str = &key.path_str; let item_id = key.item_id; let module_id = key.module_id; + if matches!(disambiguator, Some(Disambiguator::Example)) { + return self.get_example_file(path_str, diag); + } match disambiguator.map(Disambiguator::ns) { Some(expected_ns) => { match self.resolve(path_str, expected_ns, disambiguator, item_id, module_id) { @@ -1480,7 +1541,7 @@ impl LinkCollector<'_, '_> { let mut err = ResolutionFailure::NotResolved(err); for other_ns in [TypeNS, ValueNS, MacroNS] { if other_ns != expected_ns { - if let Ok(&[res, ..]) = self + if let Ok(&[ref res, ..]) = self .resolve(path_str, other_ns, None, item_id, module_id) .as_deref() { @@ -1513,7 +1574,7 @@ impl LinkCollector<'_, '_> { // Constructors are picked up in the type namespace. Res::Def(DefKind::Ctor(..), _) => { return Err(ResolutionFailure::WrongNamespace { - res: *res, + res: res.clone(), expected_ns: TypeNS, }); } @@ -1613,6 +1674,8 @@ enum Disambiguator { Kind(DefKind), /// `type@` Namespace(Namespace), + /// `example@` + Example, } impl Disambiguator { @@ -1622,7 +1685,7 @@ impl Disambiguator { /// `Ok(None)` if no disambiguator was found, or `Err(...)` /// if there was a problem with the disambiguator. fn from_str(link: &str) -> Result, (String, Range)> { - use Disambiguator::{Kind, Namespace as NS, Primitive}; + use Disambiguator::{Example, Kind, Namespace as NS, Primitive}; let suffixes = [ // If you update this list, please also update the relevant rustdoc book section! @@ -1656,6 +1719,7 @@ impl Disambiguator { "value" => NS(Namespace::ValueNS), "macro" => NS(Namespace::MacroNS), "prim" | "primitive" => Primitive, + "example" => Example, _ => return Err((format!("unknown disambiguator `{prefix}`"), 0..idx)), }; @@ -1696,6 +1760,7 @@ impl Disambiguator { k.ns().expect("only DefKinds with a valid namespace can be disambiguators") } Self::Primitive => TypeNS, + Self::Example => panic!("examples don't have namespace"), } } @@ -1704,6 +1769,7 @@ impl Disambiguator { Self::Namespace(_) => panic!("article() doesn't make sense for namespaces"), Self::Kind(k) => k.article(), Self::Primitive => "a", + Self::Example => "an", } } @@ -1714,6 +1780,7 @@ impl Disambiguator { // printing "module" vs "crate" so using the wrong ID is not a huge problem Self::Kind(k) => k.descr(CRATE_DEF_ID.to_def_id()), Self::Primitive => "builtin type", + Self::Example => "example", } } } @@ -1899,8 +1966,8 @@ fn resolution_failure( format!("unresolved link to `{path_str}`"), &diag_info, |diag, sp, link_range| { - let item = |res: Res| format!("the {} `{}`", res.descr(), res.name(tcx)); - let assoc_item_not_allowed = |res: Res| { + let item = |res: &Res| format!("the {} `{}`", res.descr(), res.name(tcx)); + let assoc_item_not_allowed = |res: &Res| { let name = res.name(tcx); format!( "`{name}` is {} {}, not a module or type, and cannot have associated items", @@ -1947,7 +2014,7 @@ fn resolution_failure( collector.resolve(start, ns, None, item_id, module_id) { debug!("found partial_res={v_res:?}"); - if let Some(&res) = v_res.first() { + if let Some(ref res) = v_res.first() { *partial_res = Some(full_res(tcx, res)); *unresolved = end.into(); break 'outer; @@ -2006,10 +2073,11 @@ fn resolution_failure( } // Otherwise, it must be an associated item or variant - let res = partial_res.expect("None case was handled by `last_found_module`"); + let res = + partial_res.as_ref().expect("None case was handled by `last_found_module`"); let kind_did = match res { Res::Def(kind, did) => Some((kind, did)), - Res::Primitive(_) => None, + Res::Primitive(_) | Res::Example(_) => None, }; let is_struct_variant = |did| { if let ty::Adt(def, _) = tcx.type_of(did).instantiate_identity().kind() @@ -2092,9 +2160,9 @@ fn resolution_failure( } let note = match failure { ResolutionFailure::NotResolved { .. } => unreachable!("handled above"), - ResolutionFailure::WrongNamespace { res, expected_ns } => { + ResolutionFailure::WrongNamespace { ref res, expected_ns } => { suggest_disambiguator( - res, + &res, diag, path_str, link_range.clone(), @@ -2226,11 +2294,9 @@ fn ambiguity_error( let mut descrs = FxHashSet::default(); let kinds = candidates .iter() - .map( - |(res, def_id)| { - if let Some(def_id) = def_id { Res::from_def_id(cx.tcx, *def_id) } else { *res } - }, - ) + .map(|(res, def_id)| { + if let Some(def_id) = def_id { Res::from_def_id(cx.tcx, *def_id) } else { res.clone() } + }) .filter(|res| descrs.insert(res.descr())) .collect::>(); if descrs.len() == 1 { @@ -2272,7 +2338,7 @@ fn ambiguity_error( } for res in kinds { - suggest_disambiguator(res, diag, path_str, link_range.clone(), sp, diag_info); + suggest_disambiguator(&res, diag, path_str, link_range.clone(), sp, diag_info); } }); true @@ -2281,7 +2347,7 @@ fn ambiguity_error( /// In case of an ambiguity or mismatched disambiguator, suggest the correct /// disambiguator. fn suggest_disambiguator( - res: Res, + res: &Res, diag: &mut Diag<'_, ()>, path_str: &str, link_range: MarkdownLinkRange, diff --git a/src/librustdoc/scrape_examples.rs b/src/librustdoc/scrape_examples.rs index 980a9f7a47cd3..115645087926a 100644 --- a/src/librustdoc/scrape_examples.rs +++ b/src/librustdoc/scrape_examples.rs @@ -3,7 +3,7 @@ use std::fs; use std::path::PathBuf; -use rustc_data_structures::fx::FxIndexMap; +use rustc_data_structures::fx::{FxIndexMap, FxIndexSet}; use rustc_errors::DiagCtxtHandle; use rustc_hir::intravisit::{self, Visitor}; use rustc_hir::{self as hir}; @@ -109,6 +109,14 @@ pub(crate) struct CallData { pub(crate) type FnCallLocations = FxIndexMap; pub(crate) type AllCallLocations = FxIndexMap; +pub(crate) type AllExampleFiles = FxIndexMap>; + +#[derive(Encodable, Decodable, Debug, Clone)] +pub(crate) struct ScrapedInfo { + calls: AllCallLocations, + files: FxIndexSet, + crate_name: String, +} /// Visitor for traversing a crate and finding instances of function calls. struct FindCalls<'a, 'tcx> { @@ -279,8 +287,11 @@ pub(crate) fn run( let inner = move || -> Result<(), String> { // Generates source files for examples renderopts.no_emit_shared = true; + let crate_name = krate.name(tcx).as_str().to_string(); let (cx, _) = Context::init(krate, renderopts, cache, tcx).map_err(|e| e.to_string())?; + let files = cx.shared.emitted_local_sources.take(); + // Collect CrateIds corresponding to provided target crates // If two different versions of the crate in the dependency tree, then examples will be // collected from both. @@ -320,7 +331,7 @@ pub(crate) fn run( // Save output to provided path let mut encoder = FileEncoder::new(options.output_path).map_err(|e| e.to_string())?; - calls.encode(&mut encoder); + ScrapedInfo { calls, files, crate_name }.encode(&mut encoder); encoder.finish().map_err(|(_path, e)| e.to_string())?; Ok(()) @@ -338,8 +349,10 @@ pub(crate) fn run( pub(crate) fn load_call_locations( with_examples: Vec, dcx: DiagCtxtHandle<'_>, -) -> AllCallLocations { +) -> (AllCallLocations, AllExampleFiles) { let mut all_calls: AllCallLocations = FxIndexMap::default(); + let mut crate_files: AllExampleFiles = FxIndexMap::default(); + for path in with_examples { let bytes = match fs::read(&path) { Ok(bytes) => bytes, @@ -348,12 +361,13 @@ pub(crate) fn load_call_locations( let Ok(mut decoder) = MemDecoder::new(&bytes, 0) else { dcx.fatal(format!("Corrupt metadata encountered in {path}")) }; - let calls = AllCallLocations::decode(&mut decoder); + let ScrapedInfo { calls, crate_name, files } = ScrapedInfo::decode(&mut decoder); for (function, fn_calls) in calls.into_iter() { all_calls.entry(function).or_default().extend(fn_calls.into_iter()); } + crate_files.entry(crate_name).or_default().extend(files.into_iter()); } - all_calls + (all_calls, crate_files) } From ae9f844db81ffd8e981f50293e2f80679baa8984 Mon Sep 17 00:00:00 2001 From: Guillaume Gomez Date: Fri, 8 Nov 2024 22:36:22 +0100 Subject: [PATCH 2/5] Prevent htmldocck to get content if not necessary --- src/etc/htmldocck.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/etc/htmldocck.py b/src/etc/htmldocck.py index 851b01a7458f2..a00561a771c67 100755 --- a/src/etc/htmldocck.py +++ b/src/etc/htmldocck.py @@ -350,7 +350,7 @@ def resolve_path(self, path): def get_absolute_path(self, path): return os.path.join(self.root, path) - def get_file(self, path): + def get_file(self, path, need_content): path = self.resolve_path(path) if path in self.files: return self.files[path] @@ -359,6 +359,9 @@ def get_file(self, path): if not(os.path.exists(abspath) and os.path.isfile(abspath)): raise FailedCheck('File does not exist {!r}'.format(path)) + if not need_content: + return None + with io.open(abspath, encoding='utf-8') as f: data = f.read() self.files[path] = data @@ -614,7 +617,7 @@ def check_command(c, cache): # has = file existence if len(c.args) == 1 and not regexp and 'raw' not in c.cmd: try: - cache.get_file(c.args[0]) + cache.get_file(c.args[0], False) ret = True except FailedCheck as err: cerr = str(err) @@ -622,7 +625,7 @@ def check_command(c, cache): # hasraw/matchesraw = string test elif len(c.args) == 2 and 'raw' in c.cmd: cerr = "`PATTERN` did not match" - ret = check_string(cache.get_file(c.args[0]), c.args[1], regexp) + ret = check_string(cache.get_file(c.args[0], True), c.args[1], regexp) # has/matches = XML tree test elif len(c.args) == 3 and 'raw' not in c.cmd: cerr = "`XPATH PATTERN` did not match" From 5d0a0448dd37932771ba917eff8995853c6f1689 Mon Sep 17 00:00:00 2001 From: Guillaume Gomez Date: Fri, 8 Nov 2024 22:55:40 +0100 Subject: [PATCH 3/5] Add GUI regression test for scraped examples intra doc links --- .../scrape-example-intra-doc-links.goml | 27 +++++++++++++++++++ .../src/scrape_examples/examples/check.rs | 4 +++ .../src/scrape_examples/examples/sub.rs | 1 + .../src/scrape_examples/src/lib.rs | 3 +++ 4 files changed, 35 insertions(+) create mode 100644 tests/rustdoc-gui/scrape-example-intra-doc-links.goml create mode 100644 tests/rustdoc-gui/src/scrape_examples/examples/sub.rs diff --git a/tests/rustdoc-gui/scrape-example-intra-doc-links.goml b/tests/rustdoc-gui/scrape-example-intra-doc-links.goml new file mode 100644 index 0000000000000..eed2865d6596e --- /dev/null +++ b/tests/rustdoc-gui/scrape-example-intra-doc-links.goml @@ -0,0 +1,27 @@ +// This test checks the intra doc links to code examples. +go-to: "file://" + |DOC_PATH| + "/scrape_examples/index.html" + +assert-attribute: ( + "//a[text()='check']", + { + "href": "../src/check/check.rs.html", + "title": "Example check", + }, +) +assert-attribute: ( + "//a[text()='check/check.rs']", + { + "href": "../src/check/check.rs.html", + "title": "Example check", + }, +) +assert-attribute: ( + "//a[text()='check/sub.rs']", + { + "href": "../src/check/sub.rs.html", + "title": "Example check", + }, +) +// We now click on the link to ensure it works. +click: "//a[text()='check/sub.rs']" +wait-for-text: (".main-heading h1", "check/sub.rs") diff --git a/tests/rustdoc-gui/src/scrape_examples/examples/check.rs b/tests/rustdoc-gui/src/scrape_examples/examples/check.rs index b3f682fe4973c..65ea825977c97 100644 --- a/tests/rustdoc-gui/src/scrape_examples/examples/check.rs +++ b/tests/rustdoc-gui/src/scrape_examples/examples/check.rs @@ -1,4 +1,8 @@ +mod sub; + fn main() { + sub::foo(); + for i in 0..9 { println!("hello world!"); println!("hello world!"); diff --git a/tests/rustdoc-gui/src/scrape_examples/examples/sub.rs b/tests/rustdoc-gui/src/scrape_examples/examples/sub.rs new file mode 100644 index 0000000000000..b76b4321d62aa --- /dev/null +++ b/tests/rustdoc-gui/src/scrape_examples/examples/sub.rs @@ -0,0 +1 @@ +pub fn foo() {} diff --git a/tests/rustdoc-gui/src/scrape_examples/src/lib.rs b/tests/rustdoc-gui/src/scrape_examples/src/lib.rs index d6351c53074d0..ab88fe9a30b6f 100644 --- a/tests/rustdoc-gui/src/scrape_examples/src/lib.rs +++ b/tests/rustdoc-gui/src/scrape_examples/src/lib.rs @@ -1,4 +1,7 @@ //@ run-flags:-Zrustdoc-scrape-examples + +//! [example@check] [example@check/check.rs] [example@check/sub.rs] + /// # Examples /// /// ``` From 918a4ee2d5e513fbc9c6974e7e1641c01b3182a1 Mon Sep 17 00:00:00 2001 From: Guillaume Gomez Date: Fri, 8 Nov 2024 23:00:09 +0100 Subject: [PATCH 4/5] Add documentation for new `example` intra-doc link disambiguator --- src/doc/rustdoc/src/scraped-examples.md | 18 ++++++++++++++++++ .../linking-to-items-by-name.md | 3 +++ 2 files changed, 21 insertions(+) diff --git a/src/doc/rustdoc/src/scraped-examples.md b/src/doc/rustdoc/src/scraped-examples.md index 7197e01c8e313..cf1e570b28e91 100644 --- a/src/doc/rustdoc/src/scraped-examples.md +++ b/src/doc/rustdoc/src/scraped-examples.md @@ -47,6 +47,24 @@ Rustdoc has a few techniques to ensure these examples don't overwhelm documentat For a given item, Rustdoc sorts its examples based on the size of the example — smaller ones are shown first. +## Linking to an example source code + +You can use intra-doc links to link to a scraped example source file with `example@` disambiguator: + +```rust +// If your example is named "foo": +/// [example@foo] +struct Item; +``` + +By default, the intra-doc link will link to the file containing the `main` function. If you want to +link to another file, you can specify its path: + +```rust +// If your example is named "foo": +/// [example@foo/another_file.rs] +struct Item; +``` ## FAQ diff --git a/src/doc/rustdoc/src/write-documentation/linking-to-items-by-name.md b/src/doc/rustdoc/src/write-documentation/linking-to-items-by-name.md index 5e7854834028a..ee3195e5876ab 100644 --- a/src/doc/rustdoc/src/write-documentation/linking-to-items-by-name.md +++ b/src/doc/rustdoc/src/write-documentation/linking-to-items-by-name.md @@ -92,6 +92,9 @@ rendered as `Foo`. The following prefixes are available: `struct`, `enum`, `trai `mod`, `module`, `const`, `constant`, `fn`, `function`, `field`, `variant`, `method`, `derive`, `type`, `value`, `macro`, `prim` or `primitive`. +There is another disambiguator available: `example`. If you want more information about this one, +take a look at the [scraped examples chapter](../scraped-examples.md). + You can also disambiguate for functions by adding `()` after the function name, or for macros by adding `!` after the macro name. The macro `!` can be followed by `()`, `{}`, or `[]`. Example: From 036079e43bad503f700521f3ea1120efdaba484a Mon Sep 17 00:00:00 2001 From: Guillaume Gomez Date: Tue, 12 Nov 2024 15:26:39 +0100 Subject: [PATCH 5/5] Migrate GUI example intra-doc link to into `run-make` --- .../rustdoc-link-to-examples/Cargo.toml | 7 +++++ .../examples/check.rs | 3 +++ .../rustdoc-link-to-examples/examples/sub.rs | 1 + .../run-make/rustdoc-link-to-examples/lib.rs | 1 + .../rustdoc-link-to-examples/rmake.rs | 25 +++++++++++++++++ .../scrape-example-intra-doc-links.goml | 27 ------------------- .../src/scrape_examples/src/lib.rs | 2 -- 7 files changed, 37 insertions(+), 29 deletions(-) create mode 100644 tests/run-make/rustdoc-link-to-examples/Cargo.toml create mode 100644 tests/run-make/rustdoc-link-to-examples/examples/check.rs create mode 100644 tests/run-make/rustdoc-link-to-examples/examples/sub.rs create mode 100644 tests/run-make/rustdoc-link-to-examples/lib.rs create mode 100644 tests/run-make/rustdoc-link-to-examples/rmake.rs delete mode 100644 tests/rustdoc-gui/scrape-example-intra-doc-links.goml diff --git a/tests/run-make/rustdoc-link-to-examples/Cargo.toml b/tests/run-make/rustdoc-link-to-examples/Cargo.toml new file mode 100644 index 0000000000000..f4f4ce25c9942 --- /dev/null +++ b/tests/run-make/rustdoc-link-to-examples/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "foo" +version = "0.1.0" +edition = "2021" + +[lib] +path = "lib.rs" diff --git a/tests/run-make/rustdoc-link-to-examples/examples/check.rs b/tests/run-make/rustdoc-link-to-examples/examples/check.rs new file mode 100644 index 0000000000000..36c565304fb9e --- /dev/null +++ b/tests/run-make/rustdoc-link-to-examples/examples/check.rs @@ -0,0 +1,3 @@ +mod sub; + +fn main() {} diff --git a/tests/run-make/rustdoc-link-to-examples/examples/sub.rs b/tests/run-make/rustdoc-link-to-examples/examples/sub.rs new file mode 100644 index 0000000000000..b76b4321d62aa --- /dev/null +++ b/tests/run-make/rustdoc-link-to-examples/examples/sub.rs @@ -0,0 +1 @@ +pub fn foo() {} diff --git a/tests/run-make/rustdoc-link-to-examples/lib.rs b/tests/run-make/rustdoc-link-to-examples/lib.rs new file mode 100644 index 0000000000000..056a9449fd093 --- /dev/null +++ b/tests/run-make/rustdoc-link-to-examples/lib.rs @@ -0,0 +1 @@ +//! [example@check] [example@check/check.rs] [example@check/sub.rs] diff --git a/tests/run-make/rustdoc-link-to-examples/rmake.rs b/tests/run-make/rustdoc-link-to-examples/rmake.rs new file mode 100644 index 0000000000000..f8c2bedba1c9e --- /dev/null +++ b/tests/run-make/rustdoc-link-to-examples/rmake.rs @@ -0,0 +1,25 @@ +// Test to ensure that intra-doc links work correctly with examples. + +use std::path::Path; + +use run_make_support::rfs::read_to_string; +use run_make_support::{assert_contains, cargo, path}; + +fn main() { + let target_dir = path("target"); + cargo().args(&["doc", "-Zunstable-options", "-Zrustdoc-scrape-examples"]).run(); + + let content = read_to_string(target_dir.join("doc/foo/index.html")); + assert_contains( + &content, + r#"check"#, + ); + assert_contains( + &content, + r#"check/check.rs"#, + ); + assert_contains( + &content, + r#"check/sub.rs"#, + ); +} diff --git a/tests/rustdoc-gui/scrape-example-intra-doc-links.goml b/tests/rustdoc-gui/scrape-example-intra-doc-links.goml deleted file mode 100644 index eed2865d6596e..0000000000000 --- a/tests/rustdoc-gui/scrape-example-intra-doc-links.goml +++ /dev/null @@ -1,27 +0,0 @@ -// This test checks the intra doc links to code examples. -go-to: "file://" + |DOC_PATH| + "/scrape_examples/index.html" - -assert-attribute: ( - "//a[text()='check']", - { - "href": "../src/check/check.rs.html", - "title": "Example check", - }, -) -assert-attribute: ( - "//a[text()='check/check.rs']", - { - "href": "../src/check/check.rs.html", - "title": "Example check", - }, -) -assert-attribute: ( - "//a[text()='check/sub.rs']", - { - "href": "../src/check/sub.rs.html", - "title": "Example check", - }, -) -// We now click on the link to ensure it works. -click: "//a[text()='check/sub.rs']" -wait-for-text: (".main-heading h1", "check/sub.rs") diff --git a/tests/rustdoc-gui/src/scrape_examples/src/lib.rs b/tests/rustdoc-gui/src/scrape_examples/src/lib.rs index ab88fe9a30b6f..1bfc875196901 100644 --- a/tests/rustdoc-gui/src/scrape_examples/src/lib.rs +++ b/tests/rustdoc-gui/src/scrape_examples/src/lib.rs @@ -1,7 +1,5 @@ //@ run-flags:-Zrustdoc-scrape-examples -//! [example@check] [example@check/check.rs] [example@check/sub.rs] - /// # Examples /// /// ```