From 4b3f82ad0321b8f2e2630b74bbc526ffb8fa5bda Mon Sep 17 00:00:00 2001 From: Will Crichton Date: Sun, 9 May 2021 16:22:22 -0700 Subject: [PATCH 01/24] Add updated support for example-analyzer Move rendering of examples into Finalize design Cleanup, rename found -> scraped Softer yellow Clean up dead code Document scrape_examples More simplification and documentation Remove extra css Test --- src/librustdoc/clean/inline.rs | 1 + src/librustdoc/clean/mod.rs | 41 +++-- src/librustdoc/clean/types.rs | 12 ++ src/librustdoc/config.rs | 11 ++ src/librustdoc/html/render/context.rs | 9 +- src/librustdoc/html/render/mod.rs | 95 ++++++++++ src/librustdoc/html/sources.rs | 2 +- src/librustdoc/html/static/css/rustdoc.css | 107 +++++++++++ src/librustdoc/html/static/js/main.js | 196 +++++++++++++++++++++ src/librustdoc/json/conversions.rs | 4 +- src/librustdoc/lib.rs | 14 +- src/librustdoc/scrape_examples.rs | 138 +++++++++++++++ 12 files changed, 609 insertions(+), 21 deletions(-) create mode 100644 src/librustdoc/scrape_examples.rs diff --git a/src/librustdoc/clean/inline.rs b/src/librustdoc/clean/inline.rs index 4a888b22332ee..09622d721f7f0 100644 --- a/src/librustdoc/clean/inline.rs +++ b/src/librustdoc/clean/inline.rs @@ -235,6 +235,7 @@ fn build_external_function(cx: &mut DocContext<'_>, did: DefId) -> clean::Functi decl, generics, header: hir::FnHeader { unsafety: sig.unsafety(), abi: sig.abi(), constness, asyncness }, + call_locations: None, } } diff --git a/src/librustdoc/clean/mod.rs b/src/librustdoc/clean/mod.rs index a55d85f5841d2..e2b1ff4547ba5 100644 --- a/src/librustdoc/clean/mod.rs +++ b/src/librustdoc/clean/mod.rs @@ -801,7 +801,10 @@ impl<'a> Clean for (&'a hir::FnSig<'a>, &'a hir::Generics<'a>, hir::Bo fn clean(&self, cx: &mut DocContext<'_>) -> Function { let (generics, decl) = enter_impl_trait(cx, |cx| (self.1.clean(cx), (&*self.0.decl, self.2).clean(cx))); - Function { decl, generics, header: self.0.header } + let mut function = Function { decl, generics, header: self.0.header, call_locations: None }; + let def_id = self.2.hir_id.owner.to_def_id(); + function.load_call_locations(def_id, cx); + function } } @@ -933,12 +936,14 @@ impl Clean for hir::TraitItem<'_> { let (generics, decl) = enter_impl_trait(cx, |cx| { (self.generics.clean(cx), (&*sig.decl, &names[..]).clean(cx)) }); - let mut t = Function { header: sig.header, decl, generics }; + let mut t = + Function { header: sig.header, decl, generics, call_locations: None }; if t.header.constness == hir::Constness::Const && is_unstable_const_fn(cx.tcx, local_did).is_some() { t.header.constness = hir::Constness::NotConst; } + t.load_call_locations(self.def_id.to_def_id(), cx); TyMethodItem(t) } hir::TraitItemKind::Type(ref bounds, ref default) => { @@ -1057,21 +1062,21 @@ impl Clean for ty::AssocItem { ty::ImplContainer(_) => Some(self.defaultness), ty::TraitContainer(_) => None, }; - MethodItem( - Function { - generics, - decl, - header: hir::FnHeader { - unsafety: sig.unsafety(), - abi: sig.abi(), - constness, - asyncness, - }, + let mut function = Function { + generics, + decl, + header: hir::FnHeader { + unsafety: sig.unsafety(), + abi: sig.abi(), + constness, + asyncness, }, - defaultness, - ) + call_locations: None, + }; + function.load_call_locations(self.def_id, cx); + MethodItem(function, defaultness) } else { - TyMethodItem(Function { + let mut function = Function { generics, decl, header: hir::FnHeader { @@ -1080,7 +1085,10 @@ impl Clean for ty::AssocItem { constness: hir::Constness::NotConst, asyncness: hir::IsAsync::NotAsync, }, - }) + call_locations: None, + }; + function.load_call_locations(self.def_id, cx); + TyMethodItem(function) } } ty::AssocKind::Type => { @@ -2098,6 +2106,7 @@ impl Clean for (&hir::ForeignItem<'_>, Option) { constness: hir::Constness::NotConst, asyncness: hir::IsAsync::NotAsync, }, + call_locations: None, }) } hir::ForeignItemKind::Static(ref ty, mutability) => { diff --git a/src/librustdoc/clean/types.rs b/src/librustdoc/clean/types.rs index 0e78fe7aec357..2e0be44d93224 100644 --- a/src/librustdoc/clean/types.rs +++ b/src/librustdoc/clean/types.rs @@ -42,6 +42,7 @@ use crate::formats::cache::Cache; use crate::formats::item_type::ItemType; use crate::html::render::cache::ExternalLocation; use crate::html::render::Context; +use crate::scrape_examples::FnCallLocations; use self::FnRetTy::*; use self::ItemKind::*; @@ -1254,6 +1255,17 @@ crate struct Function { crate decl: FnDecl, crate generics: Generics, crate header: hir::FnHeader, + crate call_locations: Option, +} + +impl Function { + crate fn load_call_locations(&mut self, def_id: hir::def_id::DefId, cx: &DocContext<'_>) { + if let Some(call_locations) = cx.render_options.call_locations.as_ref() { + let key = cx.tcx.def_path(def_id).to_string_no_crate_verbose(); + self.call_locations = call_locations.get(&key).cloned(); + debug!("call_locations: {} -- {:?}", key, self.call_locations); + } + } } #[derive(Clone, PartialEq, Eq, Debug, Hash)] diff --git a/src/librustdoc/config.rs b/src/librustdoc/config.rs index ac440a395155c..f34f773ea56dc 100644 --- a/src/librustdoc/config.rs +++ b/src/librustdoc/config.rs @@ -25,6 +25,7 @@ use crate::html::render::StylePath; use crate::html::static_files; use crate::opts; use crate::passes::{self, Condition, DefaultPassOption}; +use crate::scrape_examples::AllCallLocations; use crate::theme; #[derive(Clone, Copy, PartialEq, Eq, Debug)] @@ -158,6 +159,8 @@ crate struct Options { crate json_unused_externs: bool, /// Whether to skip capturing stdout and stderr of tests. crate nocapture: bool, + + crate scrape_examples: Vec, } impl fmt::Debug for Options { @@ -280,6 +283,8 @@ crate struct RenderOptions { crate emit: Vec, /// If `true`, HTML source pages will generate links for items to their definition. crate generate_link_to_definition: bool, + crate call_locations: Option, + crate repository_url: Option, } #[derive(Copy, Clone, Debug, PartialEq, Eq)] @@ -671,6 +676,9 @@ impl Options { return Err(1); } + let repository_url = matches.opt_str("repository-url"); + let scrape_examples = matches.opt_strs("scrape-examples"); + let (lint_opts, describe_lints, lint_cap) = get_cmd_lint_options(matches, error_format); Ok(Options { @@ -737,10 +745,13 @@ impl Options { ), emit, generate_link_to_definition, + call_locations: None, + repository_url, }, crate_name, output_format, json_unused_externs, + scrape_examples, }) } diff --git a/src/librustdoc/html/render/context.rs b/src/librustdoc/html/render/context.rs index b99d2fe5aa0d1..49bf760c29cc5 100644 --- a/src/librustdoc/html/render/context.rs +++ b/src/librustdoc/html/render/context.rs @@ -124,6 +124,7 @@ crate struct SharedContext<'tcx> { crate span_correspondance_map: FxHashMap, /// The [`Cache`] used during rendering. crate cache: Cache, + pub(super) repository_url: Option, } impl SharedContext<'_> { @@ -140,7 +141,11 @@ impl SharedContext<'_> { /// Returns the `collapsed_doc_value` of the given item if this is the main crate, otherwise /// returns the `doc_value`. crate fn maybe_collapsed_doc_value<'a>(&self, item: &'a clean::Item) -> Option { - if self.collapsed { item.collapsed_doc_value() } else { item.doc_value() } + if self.collapsed { + item.collapsed_doc_value() + } else { + item.doc_value() + } } crate fn edition(&self) -> Edition { @@ -389,6 +394,7 @@ impl<'tcx> FormatRenderer<'tcx> for Context<'tcx> { generate_redirect_map, show_type_layout, generate_link_to_definition, + repository_url, .. } = options; @@ -480,6 +486,7 @@ impl<'tcx> FormatRenderer<'tcx> for Context<'tcx> { templates, span_correspondance_map: matches, cache, + repository_url, }; // Add the default themes to the `Vec` of stylepaths diff --git a/src/librustdoc/html/render/mod.rs b/src/librustdoc/html/render/mod.rs index 11682afdf899b..0fb7723b68bf1 100644 --- a/src/librustdoc/html/render/mod.rs +++ b/src/librustdoc/html/render/mod.rs @@ -39,6 +39,7 @@ crate use span_map::{collect_spans_and_sources, LinkFromSrc}; use std::collections::VecDeque; use std::default::Default; use std::fmt; +use std::fs; use std::path::PathBuf; use std::str; use std::string::ToString; @@ -68,6 +69,8 @@ use crate::html::format::{ print_generic_bounds, print_where_clause, Buffer, HrefError, PrintWithSpace, }; use crate::html::markdown::{HeadingOffset, Markdown, MarkdownHtml, MarkdownSummaryLine}; +use crate::html::sources; +use crate::scrape_examples::FnCallLocations; /// A pair of name and its optional document. crate type NameDoc = (String, Option); @@ -584,6 +587,13 @@ fn document_full_inner( render_markdown(w, cx, &s, item.links(cx), heading_offset); } } + + match &*item.kind { + clean::ItemKind::FunctionItem(f) | clean::ItemKind::MethodItem(f, _) => { + render_call_locations(w, cx, &f.call_locations); + } + _ => {} + } } /// Add extra information about an item such as: @@ -2440,3 +2450,88 @@ fn collect_paths_for_type(first_ty: clean::Type, cache: &Cache) -> Vec { } out } + +fn render_call_locations( + w: &mut Buffer, + cx: &Context<'_>, + call_locations: &Option, +) { + let call_locations = match call_locations.as_ref() { + Some(call_locations) => call_locations, + None => { + return; + } + }; + + let filtered_locations: Vec<_> = call_locations + .iter() + .filter_map(|(file, locs)| { + // TODO(wcrichto): file I/O should be cached + let mut contents = match fs::read_to_string(&file) { + Ok(contents) => contents, + Err(e) => { + eprintln!("Failed to read file {}", e); + return None; + } + }; + + // Remove the utf-8 BOM if any + if contents.starts_with('\u{feff}') { + contents.drain(..3); + } + + Some((file, contents, locs)) + }) + .collect(); + + let n_examples = filtered_locations.len(); + if n_examples == 0 { + return; + } + + let id = cx.id_map.borrow_mut().derive("scraped-examples"); + write!( + w, + r##"
+

+ Uses found in examples/ +

"##, + id + ); + + let write_example = |w: &mut Buffer, (file, contents, locs): (&String, String, _)| { + let ex_title = match cx.shared.repository_url.as_ref() { + Some(url) => format!( + r#"{file}"#, + file = file, + url = url + ), + None => file.clone(), + }; + let edition = cx.shared.edition(); + write!( + w, + r#"
+ {title} +
"#, + code = contents.replace("\"", """), + locations = serde_json::to_string(&locs).unwrap(), + title = ex_title, + ); + write!(w, r#" "#); + write!(w, r#""#); + sources::print_src(w, &contents, edition); + write!(w, "
"); + }; + + let mut it = filtered_locations.into_iter(); + write_example(w, it.next().unwrap()); + + if n_examples > 1 { + write!(w, r#""); + } + + write!(w, "
"); +} diff --git a/src/librustdoc/html/sources.rs b/src/librustdoc/html/sources.rs index 71c64231a210e..d6dead152051d 100644 --- a/src/librustdoc/html/sources.rs +++ b/src/librustdoc/html/sources.rs @@ -243,7 +243,7 @@ where /// Wrapper struct to render the source code of a file. This will do things like /// adding line numbers to the left-hand side. -fn print_src( +crate fn print_src( buf: &mut Buffer, s: &str, edition: Edition, diff --git a/src/librustdoc/html/static/css/rustdoc.css b/src/librustdoc/html/static/css/rustdoc.css index 5d33681847a41..ca8db4530f3ff 100644 --- a/src/librustdoc/html/static/css/rustdoc.css +++ b/src/librustdoc/html/static/css/rustdoc.css @@ -1970,3 +1970,110 @@ details.undocumented[open] > summary::before { margin-left: 12px; } } + +/* This part is for the new "examples" components */ + +.scraped-example:not(.expanded) .code-wrapper pre.line-numbers, .scraped-example:not(.expanded) .code-wrapper .example-wrap pre.rust { + overflow: hidden; + height: 240px; +} + +.scraped-example .code-wrapper .prev { + position: absolute; + top: 0.25em; + right: 2.25em; + z-index: 100; + cursor: pointer; +} + +.scraped-example .code-wrapper .next { + position: absolute; + top: 0.25em; + right: 1.25em; + z-index: 100; + cursor: pointer; +} + +.scraped-example .code-wrapper .expand { + position: absolute; + top: 0.25em; + right: 0.25em; + z-index: 100; + cursor: pointer; +} + +.scraped-example .code-wrapper { + position: relative; + display: flex; + flex-direction: row; + flex-wrap: wrap; + width: 100%; +} + +.scraped-example:not(.expanded) .code-wrapper:before { + content: " "; + width: 100%; + height: 20px; + position: absolute; + z-index: 100; + top: 0; + background: linear-gradient(to bottom, rgba(255, 255, 255, 1), rgba(255, 255, 255, 0)); +} + +.scraped-example:not(.expanded) .code-wrapper:after { + content: " "; + width: 100%; + height: 20px; + position: absolute; + z-index: 100; + bottom: 0; + background: linear-gradient(to top, rgba(255, 255, 255, 1), rgba(255, 255, 255, 0)); +} + +.scraped-example:not(.expanded) .code-wrapper { + overflow: hidden; + height: 240px; +} + +.scraped-example .code-wrapper .line-numbers { + margin: 0; + padding: 14px 0; +} + +.scraped-example .code-wrapper .line-numbers span { + padding: 0 14px; +} + +.scraped-example .code-wrapper .example-wrap { + flex: 1; + overflow-x: auto; + overflow-y: hidden; + margin-bottom: 0; +} + +.scraped-example .code-wrapper .example-wrap pre.rust { + overflow-x: inherit; + width: inherit; + overflow-y: hidden; +} + +.scraped-example .line-numbers span.highlight { + background: #f6fdb0; +} + +.scraped-example .example-wrap .rust span.highlight { + background: #f6fdb0; +} + +.more-scraped-examples { + padding-left: 10px; + border-left: 1px solid #ccc; +} + +.toggle-examples .collapse-toggle { + position: relative; +} + +.toggle-examples a { + color: #999 !important; // FIXME(wcrichto): why is important needed +} diff --git a/src/librustdoc/html/static/js/main.js b/src/librustdoc/html/static/js/main.js index e396fd9d288db..5ac00ff244ab2 100644 --- a/src/librustdoc/html/static/js/main.js +++ b/src/librustdoc/html/static/js/main.js @@ -979,6 +979,202 @@ function hideThemeButtonState() { onHashChange(null); window.addEventListener("hashchange", onHashChange); searchState.setup(); + + /////// EXAMPLE ANALYZER + + // Merge the full set of [from, to] offsets into a minimal set of non-overlapping + // [from, to] offsets. + // NB: This is such a archetypal software engineering interview question that + // I can't believe I actually had to write it. Yes, it's O(N) in the input length -- + // but it does assume a sorted input! + function distinctRegions(locs) { + var start = -1; + var end = -1; + var output = []; + for (var i = 0; i < locs.length; i++) { + var loc = locs[i]; + if (loc[0] > end) { + if (end > 0) { + output.push([start, end]); + } + start = loc[0]; + end = loc[1]; + } else { + end = Math.max(end, loc[1]); + } + } + if (end > 0) { + output.push([start, end]); + } + return output; + } + + function convertLocsStartsToLineOffsets(code, locs) { + locs = distinctRegions(locs.slice(0).sort(function (a, b) { + return a[0] === b[0] ? a[1] - b[1] : a[0] - b[0]; + })); // sort by start; use end if start is equal. + var codeLines = code.split("\n"); + var lineIndex = 0; + var totalOffset = 0; + var output = []; + + while (locs.length > 0 && lineIndex < codeLines.length) { + var lineLength = codeLines[lineIndex].length + 1; // +1 here and later is due to omitted \n + while (locs.length > 0 && totalOffset + lineLength > locs[0][0]) { + var endIndex = lineIndex; + var charsRemaining = locs[0][1] - totalOffset; + while (endIndex < codeLines.length && charsRemaining > codeLines[endIndex].length + 1) { + charsRemaining -= codeLines[endIndex].length + 1; + endIndex += 1; + } + output.push({ + from: [lineIndex, locs[0][0] - totalOffset], + to: [endIndex, charsRemaining] + }); + locs.shift(); + } + lineIndex++; + totalOffset += lineLength; + } + return output; + } + + // inserts str into html, *but* calculates idx by eliding anything in html that's not in raw. + // ideally this would work by walking the element tree...but this is good enough for now. + function insertStrAtRawIndex(raw, html, idx, str) { + if (idx > raw.length) { + return html; + } + if (idx == raw.length) { + return html + str; + } + var rawIdx = 0; + var htmlIdx = 0; + while (rawIdx < idx && rawIdx < raw.length) { + while (raw[rawIdx] !== html[htmlIdx] && htmlIdx < html.length) { + htmlIdx++; + } + rawIdx++; + htmlIdx++; + } + return html.substring(0, htmlIdx) + str + html.substr(htmlIdx); + } + + // Scroll code block to put the given code location in the middle of the viewer + function scrollToLoc(elt, loc) { + var wrapper = elt.querySelector(".code-wrapper"); + var halfHeight = wrapper.offsetHeight / 2; + var lines = elt.querySelector('.line-numbers'); + var offsetMid = (lines.children[loc.from[0]].offsetTop + lines.children[loc.to[0]].offsetTop) / 2; + var scrollOffset = offsetMid - halfHeight; + lines.scrollTo(0, scrollOffset); + elt.querySelector(".rust").scrollTo(0, scrollOffset); + } + + function updateScrapedExample(example) { + var code = example.attributes.getNamedItem("data-code").textContent; + var codeLines = code.split("\n"); + var locs = JSON.parse(example.attributes.getNamedItem("data-locs").textContent); + locs = convertLocsStartsToLineOffsets(code, locs); + + // Add call-site highlights to code listings + var litParent = example.querySelector('.example-wrap pre.rust'); + var litHtml = litParent.innerHTML.split("\n"); + onEach(locs, function (loc) { + for (var i = loc.from[0]; i < loc.to[0] + 1; i++) { + addClass(example.querySelector('.line-numbers').children[i], "highlight"); + } + litHtml[loc.to[0]] = insertStrAtRawIndex( + codeLines[loc.to[0]], + litHtml[loc.to[0]], + loc.to[1], + ""); + litHtml[loc.from[0]] = insertStrAtRawIndex( + codeLines[loc.from[0]], + litHtml[loc.from[0]], + loc.from[1], + ''); + }, true); // do this backwards to avoid shifting later offsets + litParent.innerHTML = litHtml.join('\n'); + + // Toggle through list of examples in a given file + var locIndex = 0; + if (locs.length > 1) { + example.querySelector('.prev') + .addEventListener('click', function () { + locIndex = (locIndex - 1 + locs.length) % locs.length; + scrollToLoc(example, locs[locIndex]); + }); + example.querySelector('.next') + .addEventListener('click', function () { + locIndex = (locIndex + 1) % locs.length; + scrollToLoc(example, locs[locIndex]); + }); + } else { + example.querySelector('.prev').remove(); + example.querySelector('.next').remove(); + } + + // Show full code on expansion + example.querySelector('.expand').addEventListener('click', function () { + if (hasClass(example, "expanded")) { + removeClass(example, "expanded"); + scrollToLoc(example, locs[0]); + } else { + addClass(example, "expanded"); + } + }); + + // Start with the first example in view + scrollToLoc(example, locs[0]); + } + + function updateScrapedExamples() { + onEach(document.getElementsByClassName('scraped-example-list'), function (exampleSet) { + updateScrapedExample(exampleSet.querySelector(".small-section-header + .scraped-example")); + }); + + onEach(document.getElementsByClassName("more-scraped-examples"), function (more) { + var toggle = createSimpleToggle(true); + var label = "More examples"; + var wrapper = createToggle(toggle, label, 14, "toggle-examples", false); + more.parentNode.insertBefore(wrapper, more); + var examples_init = false; + + // Show additional examples on click + wrapper.onclick = function () { + if (hasClass(this, "collapsed")) { + removeClass(this, "collapsed"); + onEachLazy(this.parentNode.getElementsByClassName("hidden"), function (x) { + if (hasClass(x, "content") === false) { + removeClass(x, "hidden"); + addClass(x, "x") + } + }, true); + this.querySelector('.toggle-label').innerHTML = "Hide examples"; + this.querySelector('.inner').innerHTML = labelForToggleButton(false); + if (!examples_init) { + examples_init = true; + onEach(more.getElementsByClassName('scraped-example'), updateScrapedExample); + } + } else { + addClass(this, "collapsed"); + onEachLazy(this.parentNode.getElementsByClassName("x"), function (x) { + if (hasClass(x, "content") === false) { + addClass(x, "hidden"); + removeClass(x, "x") + } + }, true); + this.querySelector('.toggle-label').innerHTML = label; + this.querySelector('.inner').innerHTML = labelForToggleButton(true); + } + }; + }); + } + + var start = Date.now(); + updateScrapedExamples(); + console.log("updated examples took", Date.now() - start, "ms"); }()); (function () { diff --git a/src/librustdoc/json/conversions.rs b/src/librustdoc/json/conversions.rs index 731fc4ff3ce00..866514d7c9b5b 100644 --- a/src/librustdoc/json/conversions.rs +++ b/src/librustdoc/json/conversions.rs @@ -289,7 +289,7 @@ crate fn from_fn_header(header: &rustc_hir::FnHeader) -> HashSet { impl FromWithTcx for Function { fn from_tcx(function: clean::Function, tcx: TyCtxt<'_>) -> Self { - let clean::Function { decl, generics, header } = function; + let clean::Function { decl, generics, header, call_locations: _ } = function; Function { decl: decl.into_tcx(tcx), generics: generics.into_tcx(tcx), @@ -530,7 +530,7 @@ crate fn from_function_method( has_body: bool, tcx: TyCtxt<'_>, ) -> Method { - let clean::Function { header, decl, generics } = function; + let clean::Function { header, decl, generics, call_locations: _ } = function; Method { decl: decl.into_tcx(tcx), generics: generics.into_tcx(tcx), diff --git a/src/librustdoc/lib.rs b/src/librustdoc/lib.rs index efc8e31498a9c..df0e309c94a0c 100644 --- a/src/librustdoc/lib.rs +++ b/src/librustdoc/lib.rs @@ -119,6 +119,7 @@ mod json; crate mod lint; mod markdown; mod passes; +mod scrape_examples; mod theme; mod visit_ast; mod visit_lib; @@ -618,6 +619,8 @@ fn opts() -> Vec { "Make the identifiers in the HTML source code pages navigable", ) }), + unstable("scrape-examples", |o| o.optmulti("", "scrape-examples", "", "")), + unstable("repository-url", |o| o.optopt("", "repository-url", "", "TODO")), ] } @@ -697,7 +700,7 @@ fn run_renderer<'tcx, T: formats::FormatRenderer<'tcx>>( } } -fn main_options(options: config::Options) -> MainResult { +fn main_options(mut options: config::Options) -> MainResult { let diag = core::new_handler(options.error_format, None, &options.debugging_opts); match (options.should_test, options.markdown_input()) { @@ -712,6 +715,15 @@ fn main_options(options: config::Options) -> MainResult { (false, false) => {} } + if options.scrape_examples.len() > 0 { + if let Some(crate_name) = &options.crate_name { + options.render_options.call_locations = + Some(scrape_examples::scrape(&options.scrape_examples, crate_name)?); + } else { + // raise an error? + } + } + // need to move these items separately because we lose them by the time the closure is called, // but we can't create the Handler ahead of time because it's not Send let show_coverage = options.show_coverage; diff --git a/src/librustdoc/scrape_examples.rs b/src/librustdoc/scrape_examples.rs new file mode 100644 index 0000000000000..da076f87a9c04 --- /dev/null +++ b/src/librustdoc/scrape_examples.rs @@ -0,0 +1,138 @@ +//! This module analyzes provided crates to find examples of uses for items in the +//! current crate being documented. + +use rayon::prelude::*; +use rustc_data_structures::fx::FxHashMap; +use rustc_hir::{ + self as hir, + intravisit::{self, Visitor}, +}; +use rustc_interface::interface; +use rustc_middle::hir::map::Map; +use rustc_middle::ty::{TyCtxt, TyKind}; +use rustc_span::symbol::Symbol; + +crate type FnCallLocations = FxHashMap>; +crate type AllCallLocations = FxHashMap; + +/// Visitor for traversing a crate and finding instances of function calls. +struct FindCalls<'a, 'tcx> { + tcx: TyCtxt<'tcx>, + map: Map<'tcx>, + + /// Workspace-relative path to the root of the crate. Used to remember + /// which example a particular call came from. + file_name: String, + + /// Name of the crate being documented, to filter out calls to irrelevant + /// functions. + krate: Symbol, + + /// Data structure to accumulate call sites across all examples. + calls: &'a mut AllCallLocations, +} + +impl<'a, 'tcx> Visitor<'tcx> for FindCalls<'a, 'tcx> +where + 'tcx: 'a, +{ + type Map = Map<'tcx>; + + fn nested_visit_map(&mut self) -> intravisit::NestedVisitorMap { + intravisit::NestedVisitorMap::OnlyBodies(self.map) + } + + fn visit_expr(&mut self, ex: &'tcx hir::Expr<'tcx>) { + intravisit::walk_expr(self, ex); + + // Get type of function if expression is a function call + let types = self.tcx.typeck(ex.hir_id.owner); + let (ty, span) = match ex.kind { + hir::ExprKind::Call(f, _) => (types.node_type(f.hir_id), ex.span), + hir::ExprKind::MethodCall(_, _, _, span) => { + let types = self.tcx.typeck(ex.hir_id.owner); + let def_id = types.type_dependent_def_id(ex.hir_id).unwrap(); + (self.tcx.type_of(def_id), span) + } + _ => { + return; + } + }; + + // Save call site if the function resovles to a concrete definition + if let TyKind::FnDef(def_id, _) = ty.kind() { + if self.tcx.crate_name(def_id.krate) == self.krate { + let key = self.tcx.def_path(*def_id).to_string_no_crate_verbose(); + let entries = self.calls.entry(key).or_insert_with(FxHashMap::default); + entries + .entry(self.file_name.clone()) + .or_insert_with(Vec::new) + .push((span.lo().0 as usize, span.hi().0 as usize)); + } + } + } +} + +struct Callbacks { + calls: AllCallLocations, + krate: String, + file_name: String, +} + +impl rustc_driver::Callbacks for Callbacks { + fn after_analysis<'tcx>( + &mut self, + _compiler: &rustc_interface::interface::Compiler, + queries: &'tcx rustc_interface::Queries<'tcx>, + ) -> rustc_driver::Compilation { + queries.global_ctxt().unwrap().take().enter(|tcx| { + let mut finder = FindCalls { + calls: &mut self.calls, + tcx, + map: tcx.hir(), + file_name: self.file_name.clone(), + krate: Symbol::intern(&self.krate), + }; + tcx.hir().krate().visit_all_item_likes(&mut finder.as_deep_visitor()); + }); + + rustc_driver::Compilation::Stop + } +} + +/// Executes rustc on each example and collects call locations into a single structure. +/// +/// # Arguments: +/// * `examples` is an array of invocations to rustc, generated by Cargo. +/// * `krate` is the name of the crate being documented. +pub fn scrape(examples: &[String], krate: &str) -> interface::Result { + // Scrape each crate in parallel + // TODO(wcrichto): do we need optional support for no rayon? + let maps = examples + .par_iter() + .map(|example| { + // TODO(wcrichto): is there a more robust way to get arguments than split(" ")? + let mut args = example.split(" ").map(|s| s.to_owned()).collect::>(); + let file_name = args[0].clone(); + args.insert(0, "_".to_string()); + + // TODO(wcrichto): is there any setup / cleanup that needs to be performed + // here upon the invocation of rustc_driver? + debug!("Scraping examples from krate {} with args:\n{:?}", krate, args); + let mut callbacks = + Callbacks { calls: FxHashMap::default(), file_name, krate: krate.to_string() }; + rustc_driver::RunCompiler::new(&args, &mut callbacks).run()?; + Ok(callbacks.calls) + }) + .collect::>>()?; + + // Merge the call locations into a single result + let mut all_map = FxHashMap::default(); + for map in maps { + for (function, calls) in map.into_iter() { + all_map.entry(function).or_insert_with(FxHashMap::default).extend(calls.into_iter()); + } + } + + Ok(all_map) +} From 7831fee9f8c4409c80380c6ebed52ae267971438 Mon Sep 17 00:00:00 2001 From: Will Crichton Date: Sun, 30 May 2021 10:00:44 -0700 Subject: [PATCH 02/24] Fix check issue Clean up tidy checks --- src/librustdoc/html/render/mod.rs | 2 +- src/librustdoc/html/static/css/rustdoc.css | 3 ++- src/librustdoc/html/static/js/main.js | 21 +++++++++++++++------ src/librustdoc/lib.rs | 2 +- src/librustdoc/scrape_examples.rs | 10 +++++----- 5 files changed, 24 insertions(+), 14 deletions(-) diff --git a/src/librustdoc/html/render/mod.rs b/src/librustdoc/html/render/mod.rs index 0fb7723b68bf1..e83e085dc1169 100644 --- a/src/librustdoc/html/render/mod.rs +++ b/src/librustdoc/html/render/mod.rs @@ -2466,7 +2466,7 @@ fn render_call_locations( let filtered_locations: Vec<_> = call_locations .iter() .filter_map(|(file, locs)| { - // TODO(wcrichto): file I/O should be cached + // FIXME(wcrichto): file I/O should be cached let mut contents = match fs::read_to_string(&file) { Ok(contents) => contents, Err(e) => { diff --git a/src/librustdoc/html/static/css/rustdoc.css b/src/librustdoc/html/static/css/rustdoc.css index ca8db4530f3ff..212362b94e00c 100644 --- a/src/librustdoc/html/static/css/rustdoc.css +++ b/src/librustdoc/html/static/css/rustdoc.css @@ -1973,7 +1973,8 @@ details.undocumented[open] > summary::before { /* This part is for the new "examples" components */ -.scraped-example:not(.expanded) .code-wrapper pre.line-numbers, .scraped-example:not(.expanded) .code-wrapper .example-wrap pre.rust { +.scraped-example:not(.expanded) .code-wrapper pre.line-numbers, +.scraped-example:not(.expanded) .code-wrapper .example-wrap pre.rust { overflow: hidden; height: 240px; } diff --git a/src/librustdoc/html/static/js/main.js b/src/librustdoc/html/static/js/main.js index 5ac00ff244ab2..c6671623c2340 100644 --- a/src/librustdoc/html/static/js/main.js +++ b/src/librustdoc/html/static/js/main.js @@ -1019,11 +1019,14 @@ function hideThemeButtonState() { var output = []; while (locs.length > 0 && lineIndex < codeLines.length) { - var lineLength = codeLines[lineIndex].length + 1; // +1 here and later is due to omitted \n + // +1 here and later is due to omitted \n + var lineLength = codeLines[lineIndex].length + 1; while (locs.length > 0 && totalOffset + lineLength > locs[0][0]) { var endIndex = lineIndex; var charsRemaining = locs[0][1] - totalOffset; - while (endIndex < codeLines.length && charsRemaining > codeLines[endIndex].length + 1) { + while (endIndex < codeLines.length && + charsRemaining > codeLines[endIndex].length + 1) + { charsRemaining -= codeLines[endIndex].length + 1; endIndex += 1; } @@ -1065,7 +1068,8 @@ function hideThemeButtonState() { var wrapper = elt.querySelector(".code-wrapper"); var halfHeight = wrapper.offsetHeight / 2; var lines = elt.querySelector('.line-numbers'); - var offsetMid = (lines.children[loc.from[0]].offsetTop + lines.children[loc.to[0]].offsetTop) / 2; + var offsetMid = (lines.children[loc.from[0]].offsetTop + + lines.children[loc.to[0]].offsetTop) / 2; var scrollOffset = offsetMid - halfHeight; lines.scrollTo(0, scrollOffset); elt.querySelector(".rust").scrollTo(0, scrollOffset); @@ -1093,7 +1097,9 @@ function hideThemeButtonState() { codeLines[loc.from[0]], litHtml[loc.from[0]], loc.from[1], - ''); + ''); }, true); // do this backwards to avoid shifting later offsets litParent.innerHTML = litHtml.join('\n'); @@ -1131,7 +1137,9 @@ function hideThemeButtonState() { function updateScrapedExamples() { onEach(document.getElementsByClassName('scraped-example-list'), function (exampleSet) { - updateScrapedExample(exampleSet.querySelector(".small-section-header + .scraped-example")); + updateScrapedExample( + exampleSet.querySelector(".small-section-header + .scraped-example") + ); }); onEach(document.getElementsByClassName("more-scraped-examples"), function (more) { @@ -1155,7 +1163,8 @@ function hideThemeButtonState() { this.querySelector('.inner').innerHTML = labelForToggleButton(false); if (!examples_init) { examples_init = true; - onEach(more.getElementsByClassName('scraped-example'), updateScrapedExample); + onEach(more.getElementsByClassName('scraped-example'), + updateScrapedExample); } } else { addClass(this, "collapsed"); diff --git a/src/librustdoc/lib.rs b/src/librustdoc/lib.rs index df0e309c94a0c..ed7f656f4068f 100644 --- a/src/librustdoc/lib.rs +++ b/src/librustdoc/lib.rs @@ -620,7 +620,7 @@ fn opts() -> Vec { ) }), unstable("scrape-examples", |o| o.optmulti("", "scrape-examples", "", "")), - unstable("repository-url", |o| o.optopt("", "repository-url", "", "TODO")), + unstable("repository-url", |o| o.optopt("", "repository-url", "", "")), ] } diff --git a/src/librustdoc/scrape_examples.rs b/src/librustdoc/scrape_examples.rs index da076f87a9c04..bc1626977b3e1 100644 --- a/src/librustdoc/scrape_examples.rs +++ b/src/librustdoc/scrape_examples.rs @@ -9,7 +9,7 @@ use rustc_hir::{ }; use rustc_interface::interface; use rustc_middle::hir::map::Map; -use rustc_middle::ty::{TyCtxt, TyKind}; +use rustc_middle::ty::{self, TyCtxt}; use rustc_span::symbol::Symbol; crate type FnCallLocations = FxHashMap>; @@ -60,7 +60,7 @@ where }; // Save call site if the function resovles to a concrete definition - if let TyKind::FnDef(def_id, _) = ty.kind() { + if let ty::FnDef(def_id, _) = ty.kind() { if self.tcx.crate_name(def_id.krate) == self.krate { let key = self.tcx.def_path(*def_id).to_string_no_crate_verbose(); let entries = self.calls.entry(key).or_insert_with(FxHashMap::default); @@ -107,16 +107,16 @@ impl rustc_driver::Callbacks for Callbacks { /// * `krate` is the name of the crate being documented. pub fn scrape(examples: &[String], krate: &str) -> interface::Result { // Scrape each crate in parallel - // TODO(wcrichto): do we need optional support for no rayon? + // FIXME(wcrichto): do we need optional support for no rayon? let maps = examples .par_iter() .map(|example| { - // TODO(wcrichto): is there a more robust way to get arguments than split(" ")? + // FIXME(wcrichto): is there a more robust way to get arguments than split(" ")? let mut args = example.split(" ").map(|s| s.to_owned()).collect::>(); let file_name = args[0].clone(); args.insert(0, "_".to_string()); - // TODO(wcrichto): is there any setup / cleanup that needs to be performed + // FIXME(wcrichto): is there any setup / cleanup that needs to be performed // here upon the invocation of rustc_driver? debug!("Scraping examples from krate {} with args:\n{:?}", krate, args); let mut callbacks = From 2855bf039a574865c13c67a43cefbf8cead49c1b Mon Sep 17 00:00:00 2001 From: Will Crichton Date: Tue, 1 Jun 2021 14:02:09 -0700 Subject: [PATCH 03/24] Factor scraping and rendering into separate calls to rustdoc Simplify toggle UI logic, add workspace root for URLs --- src/librustdoc/clean/types.rs | 6 +- src/librustdoc/config.rs | 38 +++++- src/librustdoc/doctest.rs | 10 +- src/librustdoc/html/render/mod.rs | 11 +- src/librustdoc/html/static/css/rustdoc.css | 13 +- src/librustdoc/html/static/js/main.js | 55 ++------ src/librustdoc/lib.rs | 26 ++-- src/librustdoc/scrape_examples.rs | 142 +++++++++------------ 8 files changed, 143 insertions(+), 158 deletions(-) diff --git a/src/librustdoc/clean/types.rs b/src/librustdoc/clean/types.rs index 2e0be44d93224..ab5b6000a1859 100644 --- a/src/librustdoc/clean/types.rs +++ b/src/librustdoc/clean/types.rs @@ -42,7 +42,7 @@ use crate::formats::cache::Cache; use crate::formats::item_type::ItemType; use crate::html::render::cache::ExternalLocation; use crate::html::render::Context; -use crate::scrape_examples::FnCallLocations; +use crate::scrape_examples::{self, FnCallLocations}; use self::FnRetTy::*; use self::ItemKind::*; @@ -1261,9 +1261,9 @@ crate struct Function { impl Function { crate fn load_call_locations(&mut self, def_id: hir::def_id::DefId, cx: &DocContext<'_>) { if let Some(call_locations) = cx.render_options.call_locations.as_ref() { - let key = cx.tcx.def_path(def_id).to_string_no_crate_verbose(); + let key = scrape_examples::def_id_call_key(cx.tcx, def_id); self.call_locations = call_locations.get(&key).cloned(); - debug!("call_locations: {} -- {:?}", key, self.call_locations); + debug!("call_locations: {:?} -- {:?}", key, self.call_locations); } } } diff --git a/src/librustdoc/config.rs b/src/librustdoc/config.rs index f34f773ea56dc..65c38566a058e 100644 --- a/src/librustdoc/config.rs +++ b/src/librustdoc/config.rs @@ -2,6 +2,7 @@ use std::collections::BTreeMap; use std::convert::TryFrom; use std::ffi::OsStr; use std::fmt; +use std::fs; use std::path::PathBuf; use std::str::FromStr; @@ -160,7 +161,12 @@ crate struct Options { /// Whether to skip capturing stdout and stderr of tests. crate nocapture: bool, - crate scrape_examples: Vec, + // Options for scraping call sites from examples/ directory + /// Path to output file to write JSON of call sites. If this option is Some(..) then + /// the compiler will scrape examples and not generate documentation. + crate scrape_examples: Option, + /// Path to the root of the workspace, used to generate workspace-relative file paths. + crate workspace_root: Option, } impl fmt::Debug for Options { @@ -677,7 +683,32 @@ impl Options { } let repository_url = matches.opt_str("repository-url"); - let scrape_examples = matches.opt_strs("scrape-examples"); + let scrape_examples = matches.opt_str("scrape-examples").map(PathBuf::from); + let workspace_root = matches.opt_str("workspace-root").map(PathBuf::from); + let with_examples = matches.opt_strs("with-examples"); + let each_call_locations = with_examples + .into_iter() + .map(|path| { + let bytes = fs::read(&path).map_err(|e| format!("{} (for path {})", e, path))?; + let calls: AllCallLocations = + serde_json::from_slice(&bytes).map_err(|e| format!("{}", e))?; + Ok(calls) + }) + .collect::, _>>() + .map_err(|e: String| { + diag.err(&format!("failed to load examples with error: {}", e)); + 1 + })?; + let call_locations = (each_call_locations.len() > 0).then(move || { + each_call_locations.into_iter().fold(FxHashMap::default(), |mut acc, map| { + for (function, calls) in map.into_iter() { + acc.entry(function) + .or_insert_with(FxHashMap::default) + .extend(calls.into_iter()); + } + acc + }) + }); let (lint_opts, describe_lints, lint_cap) = get_cmd_lint_options(matches, error_format); @@ -745,13 +776,14 @@ impl Options { ), emit, generate_link_to_definition, - call_locations: None, + call_locations, repository_url, }, crate_name, output_format, json_unused_externs, scrape_examples, + workspace_root, }) } diff --git a/src/librustdoc/doctest.rs b/src/librustdoc/doctest.rs index 43abcf095d858..f8f5e6be9d519 100644 --- a/src/librustdoc/doctest.rs +++ b/src/librustdoc/doctest.rs @@ -45,7 +45,7 @@ crate struct TestOptions { crate attrs: Vec, } -crate fn run(options: Options) -> Result<(), ErrorReported> { +crate fn make_rustc_config(options: &Options) -> interface::Config { let input = config::Input::File(options.input.clone()); let invalid_codeblock_attributes_name = crate::lint::INVALID_CODEBLOCK_ATTRIBUTES.name; @@ -87,7 +87,7 @@ crate fn run(options: Options) -> Result<(), ErrorReported> { let mut cfgs = options.cfgs.clone(); cfgs.push("doc".to_owned()); cfgs.push("doctest".to_owned()); - let config = interface::Config { + interface::Config { opts: sessopts, crate_cfg: interface::parse_cfgspecs(cfgs), input, @@ -103,7 +103,11 @@ crate fn run(options: Options) -> Result<(), ErrorReported> { override_queries: None, make_codegen_backend: None, registry: rustc_driver::diagnostics_registry(), - }; + } +} + +crate fn run(options: Options) -> Result<(), ErrorReported> { + let config = make_rustc_config(&options); let test_args = options.test_args.clone(); let display_doctest_warnings = options.display_doctest_warnings; diff --git a/src/librustdoc/html/render/mod.rs b/src/librustdoc/html/render/mod.rs index e83e085dc1169..aa65ca474deee 100644 --- a/src/librustdoc/html/render/mod.rs +++ b/src/librustdoc/html/render/mod.rs @@ -2528,9 +2528,16 @@ fn render_call_locations( write_example(w, it.next().unwrap()); if n_examples > 1 { - write!(w, r#""); } write!(w, ""); diff --git a/src/librustdoc/html/static/css/rustdoc.css b/src/librustdoc/html/static/css/rustdoc.css index 212362b94e00c..557a1d1194808 100644 --- a/src/librustdoc/html/static/css/rustdoc.css +++ b/src/librustdoc/html/static/css/rustdoc.css @@ -2066,15 +2066,12 @@ details.undocumented[open] > summary::before { background: #f6fdb0; } +.more-examples-toggle summary { + color: #999; +} + .more-scraped-examples { padding-left: 10px; border-left: 1px solid #ccc; -} - -.toggle-examples .collapse-toggle { - position: relative; -} - -.toggle-examples a { - color: #999 !important; // FIXME(wcrichto): why is important needed + margin-left: 24px; } diff --git a/src/librustdoc/html/static/js/main.js b/src/librustdoc/html/static/js/main.js index c6671623c2340..fea1b4ecbf1e6 100644 --- a/src/librustdoc/html/static/js/main.js +++ b/src/librustdoc/html/static/js/main.js @@ -1136,54 +1136,21 @@ function hideThemeButtonState() { } function updateScrapedExamples() { - onEach(document.getElementsByClassName('scraped-example-list'), function (exampleSet) { - updateScrapedExample( - exampleSet.querySelector(".small-section-header + .scraped-example") - ); - }); - - onEach(document.getElementsByClassName("more-scraped-examples"), function (more) { - var toggle = createSimpleToggle(true); - var label = "More examples"; - var wrapper = createToggle(toggle, label, 14, "toggle-examples", false); - more.parentNode.insertBefore(wrapper, more); - var examples_init = false; - - // Show additional examples on click - wrapper.onclick = function () { - if (hasClass(this, "collapsed")) { - removeClass(this, "collapsed"); - onEachLazy(this.parentNode.getElementsByClassName("hidden"), function (x) { - if (hasClass(x, "content") === false) { - removeClass(x, "hidden"); - addClass(x, "x") - } - }, true); - this.querySelector('.toggle-label').innerHTML = "Hide examples"; - this.querySelector('.inner').innerHTML = labelForToggleButton(false); - if (!examples_init) { - examples_init = true; - onEach(more.getElementsByClassName('scraped-example'), - updateScrapedExample); - } - } else { - addClass(this, "collapsed"); - onEachLazy(this.parentNode.getElementsByClassName("x"), function (x) { - if (hasClass(x, "content") === false) { - addClass(x, "hidden"); - removeClass(x, "x") - } - }, true); - this.querySelector('.toggle-label').innerHTML = label; - this.querySelector('.inner').innerHTML = labelForToggleButton(true); - } - }; + var firstExamples = document.querySelectorAll('.scraped-example-list > .scraped-example'); + onEach(firstExamples, updateScrapedExample); + onEach(document.querySelectorAll('.more-examples-toggle'), function(toggle) { + var moreExamples = toggle.querySelectorAll('.scraped-example'); + toggle.querySelector('summary').addEventListener('click', function() { + // Wrapping in setTimeout ensures the update happens after the elements are actually + // visible. This is necessary since updateScrapedExample calls scrollToLoc which + // depends on offsetHeight, a property that requires an element to be visible to + // compute correctly. + setTimeout(function() { onEach(moreExamples, updateScrapedExample); }); + }, {once: true}); }); } - var start = Date.now(); updateScrapedExamples(); - console.log("updated examples took", Date.now() - start, "ms"); }()); (function () { diff --git a/src/librustdoc/lib.rs b/src/librustdoc/lib.rs index ed7f656f4068f..9bcdbc406a6f7 100644 --- a/src/librustdoc/lib.rs +++ b/src/librustdoc/lib.rs @@ -619,8 +619,10 @@ fn opts() -> Vec { "Make the identifiers in the HTML source code pages navigable", ) }), - unstable("scrape-examples", |o| o.optmulti("", "scrape-examples", "", "")), + unstable("scrape-examples", |o| o.optopt("", "scrape-examples", "", "")), + unstable("workspace-root", |o| o.optopt("", "workspace-root", "", "")), unstable("repository-url", |o| o.optopt("", "repository-url", "", "")), + unstable("with-examples", |o| o.optmulti("", "with-examples", "", "")), ] } @@ -700,28 +702,20 @@ fn run_renderer<'tcx, T: formats::FormatRenderer<'tcx>>( } } -fn main_options(mut options: config::Options) -> MainResult { +fn main_options(options: config::Options) -> MainResult { let diag = core::new_handler(options.error_format, None, &options.debugging_opts); - match (options.should_test, options.markdown_input()) { - (true, true) => return wrap_return(&diag, markdown::test(options)), - (true, false) => return doctest::run(options), - (false, true) => { + match (options.should_test, options.markdown_input(), options.scrape_examples.is_some()) { + (_, _, true) => return scrape_examples::run(options), + (true, true, false) => return wrap_return(&diag, markdown::test(options)), + (true, false, false) => return doctest::run(options), + (false, true, false) => { return wrap_return( &diag, markdown::render(&options.input, options.render_options, options.edition), ); } - (false, false) => {} - } - - if options.scrape_examples.len() > 0 { - if let Some(crate_name) = &options.crate_name { - options.render_options.call_locations = - Some(scrape_examples::scrape(&options.scrape_examples, crate_name)?); - } else { - // raise an error? - } + (false, false, false) => {} } // need to move these items separately because we lose them by the time the closure is called, diff --git a/src/librustdoc/scrape_examples.rs b/src/librustdoc/scrape_examples.rs index bc1626977b3e1..67d80d01be759 100644 --- a/src/librustdoc/scrape_examples.rs +++ b/src/librustdoc/scrape_examples.rs @@ -1,7 +1,8 @@ //! This module analyzes provided crates to find examples of uses for items in the //! current crate being documented. -use rayon::prelude::*; +use crate::config::Options; +use crate::doctest::make_rustc_config; use rustc_data_structures::fx::FxHashMap; use rustc_hir::{ self as hir, @@ -10,10 +11,12 @@ use rustc_hir::{ use rustc_interface::interface; use rustc_middle::hir::map::Map; use rustc_middle::ty::{self, TyCtxt}; -use rustc_span::symbol::Symbol; +use rustc_span::def_id::DefId; +use std::fs; +crate type DefIdCallKey = String; crate type FnCallLocations = FxHashMap>; -crate type AllCallLocations = FxHashMap; +crate type AllCallLocations = FxHashMap; /// Visitor for traversing a crate and finding instances of function calls. struct FindCalls<'a, 'tcx> { @@ -22,16 +25,20 @@ struct FindCalls<'a, 'tcx> { /// Workspace-relative path to the root of the crate. Used to remember /// which example a particular call came from. - file_name: String, - - /// Name of the crate being documented, to filter out calls to irrelevant - /// functions. - krate: Symbol, + file_path: String, /// Data structure to accumulate call sites across all examples. calls: &'a mut AllCallLocations, } +crate fn def_id_call_key(tcx: TyCtxt<'_>, def_id: DefId) -> DefIdCallKey { + format!( + "{}{}", + tcx.crate_name(def_id.krate).to_ident_string(), + tcx.def_path(def_id).to_string_no_crate_verbose() + ) +} + impl<'a, 'tcx> Visitor<'tcx> for FindCalls<'a, 'tcx> where 'tcx: 'a, @@ -46,9 +53,11 @@ where intravisit::walk_expr(self, ex); // Get type of function if expression is a function call - let types = self.tcx.typeck(ex.hir_id.owner); let (ty, span) = match ex.kind { - hir::ExprKind::Call(f, _) => (types.node_type(f.hir_id), ex.span), + hir::ExprKind::Call(f, _) => { + let types = self.tcx.typeck(ex.hir_id.owner); + (types.node_type(f.hir_id), ex.span) + } hir::ExprKind::MethodCall(_, _, _, span) => { let types = self.tcx.typeck(ex.hir_id.owner); let def_id = types.type_dependent_def_id(ex.hir_id).unwrap(); @@ -59,80 +68,55 @@ where } }; - // Save call site if the function resovles to a concrete definition + // Save call site if the function resolves to a concrete definition if let ty::FnDef(def_id, _) = ty.kind() { - if self.tcx.crate_name(def_id.krate) == self.krate { - let key = self.tcx.def_path(*def_id).to_string_no_crate_verbose(); - let entries = self.calls.entry(key).or_insert_with(FxHashMap::default); - entries - .entry(self.file_name.clone()) - .or_insert_with(Vec::new) - .push((span.lo().0 as usize, span.hi().0 as usize)); - } + let key = def_id_call_key(self.tcx, *def_id); + let entries = self.calls.entry(key).or_insert_with(FxHashMap::default); + entries + .entry(self.file_path.clone()) + .or_insert_with(Vec::new) + .push((span.lo().0 as usize, span.hi().0 as usize)); } } } -struct Callbacks { - calls: AllCallLocations, - krate: String, - file_name: String, -} - -impl rustc_driver::Callbacks for Callbacks { - fn after_analysis<'tcx>( - &mut self, - _compiler: &rustc_interface::interface::Compiler, - queries: &'tcx rustc_interface::Queries<'tcx>, - ) -> rustc_driver::Compilation { - queries.global_ctxt().unwrap().take().enter(|tcx| { - let mut finder = FindCalls { - calls: &mut self.calls, - tcx, - map: tcx.hir(), - file_name: self.file_name.clone(), - krate: Symbol::intern(&self.krate), - }; - tcx.hir().krate().visit_all_item_likes(&mut finder.as_deep_visitor()); - }); - - rustc_driver::Compilation::Stop - } -} - -/// Executes rustc on each example and collects call locations into a single structure. -/// -/// # Arguments: -/// * `examples` is an array of invocations to rustc, generated by Cargo. -/// * `krate` is the name of the crate being documented. -pub fn scrape(examples: &[String], krate: &str) -> interface::Result { - // Scrape each crate in parallel - // FIXME(wcrichto): do we need optional support for no rayon? - let maps = examples - .par_iter() - .map(|example| { - // FIXME(wcrichto): is there a more robust way to get arguments than split(" ")? - let mut args = example.split(" ").map(|s| s.to_owned()).collect::>(); - let file_name = args[0].clone(); - args.insert(0, "_".to_string()); - - // FIXME(wcrichto): is there any setup / cleanup that needs to be performed - // here upon the invocation of rustc_driver? - debug!("Scraping examples from krate {} with args:\n{:?}", krate, args); - let mut callbacks = - Callbacks { calls: FxHashMap::default(), file_name, krate: krate.to_string() }; - rustc_driver::RunCompiler::new(&args, &mut callbacks).run()?; - Ok(callbacks.calls) +crate fn run(options: Options) -> interface::Result<()> { + let inner = move || { + let config = make_rustc_config(&options); + + // Get input file path as relative to workspace root + let file_path = options + .input + .strip_prefix(options.workspace_root.as_ref().unwrap()) + .map_err(|e| format!("{}", e))?; + + interface::run_compiler(config, |compiler| { + compiler.enter(|queries| { + let mut global_ctxt = queries.global_ctxt().unwrap().take(); + global_ctxt.enter(|tcx| { + // Run call-finder on all items + let mut calls = FxHashMap::default(); + let mut finder = FindCalls { + calls: &mut calls, + tcx, + map: tcx.hir(), + file_path: file_path.display().to_string(), + }; + tcx.hir().krate().visit_all_item_likes(&mut finder.as_deep_visitor()); + + // Save output JSON to provided path + let calls_json = serde_json::to_string(&calls).map_err(|e| format!("{}", e))?; + fs::write(options.scrape_examples.as_ref().unwrap(), &calls_json) + .map_err(|e| format!("{}", e))?; + + Ok(()) + }) + }) }) - .collect::>>()?; - - // Merge the call locations into a single result - let mut all_map = FxHashMap::default(); - for map in maps { - for (function, calls) in map.into_iter() { - all_map.entry(function).or_insert_with(FxHashMap::default).extend(calls.into_iter()); - } - } + }; - Ok(all_map) + inner().map_err(|e: String| { + eprintln!("{}", e); + rustc_errors::ErrorReported + }) } From b6338e7792fab06e015cdf3a3d1c30ff9797673e Mon Sep 17 00:00:00 2001 From: Will Crichton Date: Wed, 2 Jun 2021 17:21:48 -0700 Subject: [PATCH 04/24] Generate example source files with corresponding links Add display name Fix remaining merge conflicts Only embed code for items containing examples --- src/librustdoc/config.rs | 8 -- src/librustdoc/html/render/context.rs | 1 + src/librustdoc/html/render/mod.rs | 91 +++++++------- src/librustdoc/html/sources.rs | 14 ++- src/librustdoc/html/static/css/rustdoc.css | 2 +- src/librustdoc/lib.rs | 18 +-- src/librustdoc/scrape_examples.rs | 134 +++++++++++++-------- 7 files changed, 160 insertions(+), 108 deletions(-) diff --git a/src/librustdoc/config.rs b/src/librustdoc/config.rs index 65c38566a058e..2f8bae5ded0ad 100644 --- a/src/librustdoc/config.rs +++ b/src/librustdoc/config.rs @@ -161,12 +161,9 @@ crate struct Options { /// Whether to skip capturing stdout and stderr of tests. crate nocapture: bool, - // Options for scraping call sites from examples/ directory /// Path to output file to write JSON of call sites. If this option is Some(..) then /// the compiler will scrape examples and not generate documentation. crate scrape_examples: Option, - /// Path to the root of the workspace, used to generate workspace-relative file paths. - crate workspace_root: Option, } impl fmt::Debug for Options { @@ -290,7 +287,6 @@ crate struct RenderOptions { /// If `true`, HTML source pages will generate links for items to their definition. crate generate_link_to_definition: bool, crate call_locations: Option, - crate repository_url: Option, } #[derive(Copy, Clone, Debug, PartialEq, Eq)] @@ -682,9 +678,7 @@ impl Options { return Err(1); } - let repository_url = matches.opt_str("repository-url"); let scrape_examples = matches.opt_str("scrape-examples").map(PathBuf::from); - let workspace_root = matches.opt_str("workspace-root").map(PathBuf::from); let with_examples = matches.opt_strs("with-examples"); let each_call_locations = with_examples .into_iter() @@ -777,13 +771,11 @@ impl Options { emit, generate_link_to_definition, call_locations, - repository_url, }, crate_name, output_format, json_unused_externs, scrape_examples, - workspace_root, }) } diff --git a/src/librustdoc/html/render/context.rs b/src/librustdoc/html/render/context.rs index 49bf760c29cc5..fd53a3d7bfbc6 100644 --- a/src/librustdoc/html/render/context.rs +++ b/src/librustdoc/html/render/context.rs @@ -351,6 +351,7 @@ impl<'tcx> Context<'tcx> { let hiline = span.hi(self.sess()).line; let lines = if loline == hiline { loline.to_string() } else { format!("{}-{}", loline, hiline) }; + Some(format!( "{root}src/{krate}/{path}#{lines}", root = Escape(&root), diff --git a/src/librustdoc/html/render/mod.rs b/src/librustdoc/html/render/mod.rs index aa65ca474deee..693a9d7b8a3e3 100644 --- a/src/librustdoc/html/render/mod.rs +++ b/src/librustdoc/html/render/mod.rs @@ -70,7 +70,7 @@ use crate::html::format::{ }; use crate::html::markdown::{HeadingOffset, Markdown, MarkdownHtml, MarkdownSummaryLine}; use crate::html::sources; -use crate::scrape_examples::FnCallLocations; +use crate::scrape_examples::{CallData, FnCallLocations}; /// A pair of name and its optional document. crate type NameDoc = (String, Option); @@ -2451,6 +2451,8 @@ fn collect_paths_for_type(first_ty: clean::Type, cache: &Cache) -> Vec { out } +const MAX_FULL_EXAMPLES: usize = 5; + fn render_call_locations( w: &mut Buffer, cx: &Context<'_>, @@ -2463,29 +2465,7 @@ fn render_call_locations( } }; - let filtered_locations: Vec<_> = call_locations - .iter() - .filter_map(|(file, locs)| { - // FIXME(wcrichto): file I/O should be cached - let mut contents = match fs::read_to_string(&file) { - Ok(contents) => contents, - Err(e) => { - eprintln!("Failed to read file {}", e); - return None; - } - }; - - // Remove the utf-8 BOM if any - if contents.starts_with('\u{feff}') { - contents.drain(..3); - } - - Some((file, contents, locs)) - }) - .collect(); - - let n_examples = filtered_locations.len(); - if n_examples == 0 { + if call_locations.len() == 0 { return; } @@ -2499,35 +2479,55 @@ fn render_call_locations( id ); - let write_example = |w: &mut Buffer, (file, contents, locs): (&String, String, _)| { - let ex_title = match cx.shared.repository_url.as_ref() { - Some(url) => format!( - r#"{file}"#, - file = file, - url = url - ), - None => file.clone(), - }; + let example_url = |call_data: &CallData| -> String { + format!( + r#"{name}"#, + root = cx.root_path(), + url = call_data.url, + name = call_data.display_name + ) + }; + + let write_example = |w: &mut Buffer, (path, call_data): (&PathBuf, &CallData)| { + let mut contents = + fs::read_to_string(&path).expect(&format!("Failed to read file: {}", path.display())); + + let min_loc = + call_data.locations.iter().min_by_key(|loc| loc.enclosing_item_span.0).unwrap(); + let min_byte = min_loc.enclosing_item_span.0; + let min_line = min_loc.enclosing_item_lines.0; + let max_byte = + call_data.locations.iter().map(|loc| loc.enclosing_item_span.1).max().unwrap(); + contents = contents[min_byte..max_byte].to_string(); + + let locations = call_data + .locations + .iter() + .map(|loc| (loc.call_span.0 - min_byte, loc.call_span.1 - min_byte)) + .collect::>(); + let edition = cx.shared.edition(); write!( w, r#"
- {title} -
"#, + {title} +
"#, code = contents.replace("\"", """), - locations = serde_json::to_string(&locs).unwrap(), - title = ex_title, + locations = serde_json::to_string(&locations).unwrap(), + title = example_url(call_data), ); write!(w, r#" "#); write!(w, r#""#); - sources::print_src(w, &contents, edition); + let file_span = rustc_span::DUMMY_SP; + let root_path = "".to_string(); + sources::print_src(w, &contents, edition, file_span, cx, &root_path, Some(min_line)); write!(w, "
"); }; - let mut it = filtered_locations.into_iter(); + let mut it = call_locations.into_iter().peekable(); write_example(w, it.next().unwrap()); - if n_examples > 1 { + if it.peek().is_some() { write!( w, r#"
@@ -2536,7 +2536,16 @@ fn render_call_locations(
"# ); - it.for_each(|ex| write_example(w, ex)); + (&mut it).take(MAX_FULL_EXAMPLES).for_each(|ex| write_example(w, ex)); + + if it.peek().is_some() { + write!(w, "Additional examples can be found in:
    "); + it.for_each(|(_, call_data)| { + write!(w, "
  • {}
  • ", example_url(call_data)); + }); + write!(w, "
"); + } + write!(w, "
"); } diff --git a/src/librustdoc/html/sources.rs b/src/librustdoc/html/sources.rs index d6dead152051d..6bd335a9b96bb 100644 --- a/src/librustdoc/html/sources.rs +++ b/src/librustdoc/html/sources.rs @@ -204,7 +204,15 @@ impl SourceCollector<'_, 'tcx> { &page, "", |buf: &mut _| { - print_src(buf, contents, self.cx.shared.edition(), file_span, &self.cx, &root_path) + print_src( + buf, + contents, + self.cx.shared.edition(), + file_span, + &self.cx, + &root_path, + None, + ) }, &self.cx.shared.style_files, ); @@ -250,6 +258,7 @@ crate fn print_src( file_span: rustc_span::Span, context: &Context<'_>, root_path: &str, + offset: Option, ) { let lines = s.lines().count(); let mut line_numbers = Buffer::empty_from(buf); @@ -260,8 +269,9 @@ crate fn print_src( tmp /= 10; } line_numbers.write_str("
");
+    let offset = offset.unwrap_or(0);
     for i in 1..=lines {
-        writeln!(line_numbers, "{0:1$}", i, cols);
+        writeln!(line_numbers, "{0:1$}", i + offset, cols);
     }
     line_numbers.write_str("
"); highlight::render_with_highlighting( diff --git a/src/librustdoc/html/static/css/rustdoc.css b/src/librustdoc/html/static/css/rustdoc.css index 557a1d1194808..89a205be023af 100644 --- a/src/librustdoc/html/static/css/rustdoc.css +++ b/src/librustdoc/html/static/css/rustdoc.css @@ -453,7 +453,7 @@ nav.sub { text-decoration: underline; } -.rustdoc:not(.source) .example-wrap > pre:not(.line-number) { +.rustdoc:not(.source) .example-wrap > pre:not(.line-numbers) { width: 100%; overflow-x: auto; } diff --git a/src/librustdoc/lib.rs b/src/librustdoc/lib.rs index 9bcdbc406a6f7..b7407ee409f34 100644 --- a/src/librustdoc/lib.rs +++ b/src/librustdoc/lib.rs @@ -620,8 +620,6 @@ fn opts() -> Vec { ) }), unstable("scrape-examples", |o| o.optopt("", "scrape-examples", "", "")), - unstable("workspace-root", |o| o.optopt("", "workspace-root", "", "")), - unstable("repository-url", |o| o.optopt("", "repository-url", "", "")), unstable("with-examples", |o| o.optmulti("", "with-examples", "", "")), ] } @@ -705,17 +703,16 @@ fn run_renderer<'tcx, T: formats::FormatRenderer<'tcx>>( fn main_options(options: config::Options) -> MainResult { let diag = core::new_handler(options.error_format, None, &options.debugging_opts); - match (options.should_test, options.markdown_input(), options.scrape_examples.is_some()) { - (_, _, true) => return scrape_examples::run(options), - (true, true, false) => return wrap_return(&diag, markdown::test(options)), - (true, false, false) => return doctest::run(options), - (false, true, false) => { + match (options.should_test, options.markdown_input()) { + (true, true) => return wrap_return(&diag, markdown::test(options)), + (true, false) => return doctest::run(options), + (false, true) => { return wrap_return( &diag, markdown::render(&options.input, options.render_options, options.edition), ); } - (false, false, false) => {} + (false, false) => {} } // need to move these items separately because we lose them by the time the closure is called, @@ -737,6 +734,7 @@ fn main_options(options: config::Options) -> MainResult { // FIXME: fix this clone (especially render_options) let manual_passes = options.manual_passes.clone(); let render_options = options.render_options.clone(); + let scrape_examples = options.scrape_examples.clone(); let config = core::create_config(options); interface::create_compiler_and_run(config, |compiler| { @@ -773,6 +771,10 @@ fn main_options(options: config::Options) -> MainResult { }); info!("finished with rustc"); + if let Some(example_path) = scrape_examples { + return scrape_examples::run(krate, render_opts, cache, tcx, example_path); + } + cache.crate_version = crate_version; if show_coverage { diff --git a/src/librustdoc/scrape_examples.rs b/src/librustdoc/scrape_examples.rs index 67d80d01be759..950af8fbb6328 100644 --- a/src/librustdoc/scrape_examples.rs +++ b/src/librustdoc/scrape_examples.rs @@ -1,8 +1,12 @@ //! This module analyzes provided crates to find examples of uses for items in the //! current crate being documented. -use crate::config::Options; -use crate::doctest::make_rustc_config; +use crate::clean; +use crate::config; +use crate::formats; +use crate::formats::renderer::FormatRenderer; +use crate::html::render::Context; + use rustc_data_structures::fx::FxHashMap; use rustc_hir::{ self as hir, @@ -11,23 +15,33 @@ use rustc_hir::{ use rustc_interface::interface; use rustc_middle::hir::map::Map; use rustc_middle::ty::{self, TyCtxt}; -use rustc_span::def_id::DefId; +use rustc_span::{def_id::DefId, FileName}; +use serde::{Deserialize, Serialize}; use std::fs; +use std::path::PathBuf; + +#[derive(Serialize, Deserialize, Debug, Clone)] +crate struct CallLocation { + crate call_span: (usize, usize), + crate enclosing_item_span: (usize, usize), + crate enclosing_item_lines: (usize, usize), +} +#[derive(Serialize, Deserialize, Debug, Clone)] +crate struct CallData { + crate locations: Vec, + crate url: String, + crate display_name: String, +} crate type DefIdCallKey = String; -crate type FnCallLocations = FxHashMap>; +crate type FnCallLocations = FxHashMap; crate type AllCallLocations = FxHashMap; /// Visitor for traversing a crate and finding instances of function calls. struct FindCalls<'a, 'tcx> { tcx: TyCtxt<'tcx>, map: Map<'tcx>, - - /// Workspace-relative path to the root of the crate. Used to remember - /// which example a particular call came from. - file_path: String, - - /// Data structure to accumulate call sites across all examples. + cx: Context<'tcx>, calls: &'a mut AllCallLocations, } @@ -68,51 +82,75 @@ where } }; + if span.from_expansion() { + return; + } + // Save call site if the function resolves to a concrete definition if let ty::FnDef(def_id, _) = ty.kind() { - let key = def_id_call_key(self.tcx, *def_id); - let entries = self.calls.entry(key).or_insert_with(FxHashMap::default); - entries - .entry(self.file_path.clone()) - .or_insert_with(Vec::new) - .push((span.lo().0 as usize, span.hi().0 as usize)); + let fn_key = def_id_call_key(self.tcx, *def_id); + let entries = self.calls.entry(fn_key).or_insert_with(FxHashMap::default); + let file = self.tcx.sess.source_map().lookup_char_pos(span.lo()).file; + let file_path = match file.name.clone() { + FileName::Real(real_filename) => real_filename.into_local_path(), + _ => None, + }; + + let get_pos = + |bytepos: rustc_span::BytePos| file.original_relative_byte_pos(bytepos).0 as usize; + let get_range = |span: rustc_span::Span| (get_pos(span.lo()), get_pos(span.hi())); + let get_line = |bytepos: rustc_span::BytePos| file.lookup_line(bytepos).unwrap(); + let get_lines = |span: rustc_span::Span| (get_line(span.lo()), get_line(span.hi())); + + if let Some(file_path) = file_path { + let abs_path = fs::canonicalize(file_path.clone()).unwrap(); + let cx = &self.cx; + let enclosing_item_span = + self.tcx.hir().span_with_body(self.tcx.hir().get_parent_item(ex.hir_id)); + assert!(enclosing_item_span.contains(span)); + + let location = CallLocation { + call_span: get_range(span), + enclosing_item_span: get_range(enclosing_item_span), + enclosing_item_lines: get_lines(enclosing_item_span), + }; + + entries + .entry(abs_path) + .or_insert_with(|| { + let clean_span = crate::clean::types::Span::new(span); + let url = cx.href_from_span(clean_span).unwrap(); + let display_name = file_path.display().to_string(); + CallData { locations: Vec::new(), url, display_name } + }) + .locations + .push(location); + } } } } -crate fn run(options: Options) -> interface::Result<()> { +crate fn run( + krate: clean::Crate, + renderopts: config::RenderOptions, + cache: formats::cache::Cache, + tcx: TyCtxt<'tcx>, + example_path: PathBuf, +) -> interface::Result<()> { let inner = move || { - let config = make_rustc_config(&options); - - // Get input file path as relative to workspace root - let file_path = options - .input - .strip_prefix(options.workspace_root.as_ref().unwrap()) - .map_err(|e| format!("{}", e))?; - - interface::run_compiler(config, |compiler| { - compiler.enter(|queries| { - let mut global_ctxt = queries.global_ctxt().unwrap().take(); - global_ctxt.enter(|tcx| { - // Run call-finder on all items - let mut calls = FxHashMap::default(); - let mut finder = FindCalls { - calls: &mut calls, - tcx, - map: tcx.hir(), - file_path: file_path.display().to_string(), - }; - tcx.hir().krate().visit_all_item_likes(&mut finder.as_deep_visitor()); - - // Save output JSON to provided path - let calls_json = serde_json::to_string(&calls).map_err(|e| format!("{}", e))?; - fs::write(options.scrape_examples.as_ref().unwrap(), &calls_json) - .map_err(|e| format!("{}", e))?; - - Ok(()) - }) - }) - }) + // Generates source files for examples + let (cx, _) = Context::init(krate, renderopts, cache, tcx).map_err(|e| format!("{}", e))?; + + // Run call-finder on all items + let mut calls = FxHashMap::default(); + let mut finder = FindCalls { calls: &mut calls, tcx, map: tcx.hir(), cx }; + tcx.hir().krate().visit_all_item_likes(&mut finder.as_deep_visitor()); + + // Save output JSON to provided path + let calls_json = serde_json::to_string(&calls).map_err(|e| format!("{}", e))?; + fs::write(example_path, &calls_json).map_err(|e| format!("{}", e))?; + + Ok(()) }; inner().map_err(|e: String| { From eea8f0a39a2423cc7a4acd31e3a7309853f22509 Mon Sep 17 00:00:00 2001 From: Will Crichton Date: Wed, 25 Aug 2021 20:15:46 -0700 Subject: [PATCH 05/24] Sort examples by size Improve styling Start to clean up code, add comments --- src/librustdoc/clean/types.rs | 2 + src/librustdoc/config.rs | 25 +------- src/librustdoc/html/render/mod.rs | 75 ++++++++++++++++------ src/librustdoc/html/static/css/rustdoc.css | 58 ++++++++++++++--- src/librustdoc/html/static/js/main.js | 29 ++++++--- src/librustdoc/scrape_examples.rs | 30 ++++++++- 6 files changed, 158 insertions(+), 61 deletions(-) diff --git a/src/librustdoc/clean/types.rs b/src/librustdoc/clean/types.rs index ab5b6000a1859..eb507e4eecabb 100644 --- a/src/librustdoc/clean/types.rs +++ b/src/librustdoc/clean/types.rs @@ -1259,6 +1259,8 @@ crate struct Function { } impl Function { + /// If --scrape-examples is used, then this function attempts to find call locations + /// for `self` within `RenderOptions::call_locations` and store them in `Function::call_locations`. crate fn load_call_locations(&mut self, def_id: hir::def_id::DefId, cx: &DocContext<'_>) { if let Some(call_locations) = cx.render_options.call_locations.as_ref() { let key = scrape_examples::def_id_call_key(cx.tcx, def_id); diff --git a/src/librustdoc/config.rs b/src/librustdoc/config.rs index 2f8bae5ded0ad..4e019d4e15d3b 100644 --- a/src/librustdoc/config.rs +++ b/src/librustdoc/config.rs @@ -2,7 +2,6 @@ use std::collections::BTreeMap; use std::convert::TryFrom; use std::ffi::OsStr; use std::fmt; -use std::fs; use std::path::PathBuf; use std::str::FromStr; @@ -680,29 +679,7 @@ impl Options { let scrape_examples = matches.opt_str("scrape-examples").map(PathBuf::from); let with_examples = matches.opt_strs("with-examples"); - let each_call_locations = with_examples - .into_iter() - .map(|path| { - let bytes = fs::read(&path).map_err(|e| format!("{} (for path {})", e, path))?; - let calls: AllCallLocations = - serde_json::from_slice(&bytes).map_err(|e| format!("{}", e))?; - Ok(calls) - }) - .collect::, _>>() - .map_err(|e: String| { - diag.err(&format!("failed to load examples with error: {}", e)); - 1 - })?; - let call_locations = (each_call_locations.len() > 0).then(move || { - each_call_locations.into_iter().fold(FxHashMap::default(), |mut acc, map| { - for (function, calls) in map.into_iter() { - acc.entry(function) - .or_insert_with(FxHashMap::default) - .extend(calls.into_iter()); - } - acc - }) - }); + let call_locations = crate::scrape_examples::load_call_locations(with_examples, &diag)?; let (lint_opts, describe_lints, lint_cap) = get_cmd_lint_options(matches, error_format); diff --git a/src/librustdoc/html/render/mod.rs b/src/librustdoc/html/render/mod.rs index 693a9d7b8a3e3..b50aab6351c02 100644 --- a/src/librustdoc/html/render/mod.rs +++ b/src/librustdoc/html/render/mod.rs @@ -2453,32 +2453,31 @@ fn collect_paths_for_type(first_ty: clean::Type, cache: &Cache) -> Vec { const MAX_FULL_EXAMPLES: usize = 5; +/// Generates the HTML for example call locations generated via the --scrape-examples flag. fn render_call_locations( w: &mut Buffer, cx: &Context<'_>, call_locations: &Option, ) { let call_locations = match call_locations.as_ref() { - Some(call_locations) => call_locations, - None => { + Some(call_locations) if call_locations.len() > 0 => call_locations, + _ => { return; } }; - if call_locations.len() == 0 { - return; - } - + // Generate a unique ID so users can link to this section for a given method let id = cx.id_map.borrow_mut().derive("scraped-examples"); write!( w, r##"

- Uses found in examples/ + Examples found in repository

"##, id ); + // Link to the source file containing a given example let example_url = |call_data: &CallData| -> String { format!( r#"{name}"#, @@ -2488,18 +2487,27 @@ fn render_call_locations( ) }; + // Generate the HTML for a single example, being the title and code block let write_example = |w: &mut Buffer, (path, call_data): (&PathBuf, &CallData)| { - let mut contents = + // FIXME(wcrichto): is there a better way to handle an I/O error than a panic? + // When would such an error arise? + let contents = fs::read_to_string(&path).expect(&format!("Failed to read file: {}", path.display())); + // To reduce file sizes, we only want to embed the source code needed to understand the example, not + // the entire file. So we find the smallest byte range that covers all items enclosing examples. let min_loc = call_data.locations.iter().min_by_key(|loc| loc.enclosing_item_span.0).unwrap(); let min_byte = min_loc.enclosing_item_span.0; let min_line = min_loc.enclosing_item_lines.0; let max_byte = call_data.locations.iter().map(|loc| loc.enclosing_item_span.1).max().unwrap(); - contents = contents[min_byte..max_byte].to_string(); + // The output code is limited to that byte range. + let contents_subset = &contents[min_byte..max_byte]; + + // The call locations need to be updated to reflect that the size of the program has changed. + // Specifically, the ranges are all subtracted by `min_byte` since that's the new zero point. let locations = call_data .locations .iter() @@ -2510,23 +2518,44 @@ fn render_call_locations( write!( w, r#"
- {title} +
{title}
"#, - code = contents.replace("\"", """), - locations = serde_json::to_string(&locations).unwrap(), title = example_url(call_data), + // The code and locations are encoded as data attributes, so they can be read + // later by the JS for interactions. + code = contents_subset.replace("\"", """), + locations = serde_json::to_string(&locations).unwrap(), ); write!(w, r#" "#); write!(w, r#""#); + + // FIXME(wcrichto): where should file_span and root_path come from? let file_span = rustc_span::DUMMY_SP; let root_path = "".to_string(); - sources::print_src(w, &contents, edition, file_span, cx, &root_path, Some(min_line)); + sources::print_src(w, contents_subset, edition, file_span, cx, &root_path, Some(min_line)); write!(w, "
"); }; - let mut it = call_locations.into_iter().peekable(); + // The call locations are output in sequence, so that sequence needs to be determined. + // Ideally the most "relevant" examples would be shown first, but there's no general algorithm + // for determining relevance. Instead, we prefer the smallest examples being likely the easiest to + // understand at a glance. + let ordered_locations = { + let sort_criterion = |(_, call_data): &(_, &CallData)| { + let (lo, hi) = call_data.locations[0].enclosing_item_span; + hi - lo + }; + + let mut locs = call_locations.into_iter().collect::>(); + locs.sort_by_key(|x| sort_criterion(x)); + locs + }; + + // Write just one example that's visible by default in the method's description. + let mut it = ordered_locations.into_iter().peekable(); write_example(w, it.next().unwrap()); + // Then add the remaining examples in a hidden section. if it.peek().is_some() { write!( w, @@ -2534,19 +2563,29 @@ fn render_call_locations( More examples -
"# +
+
+
+"# ); + + // Only generate inline code for MAX_FULL_EXAMPLES number of examples. Otherwise we could + // make the page arbitrarily huge! (&mut it).take(MAX_FULL_EXAMPLES).for_each(|ex| write_example(w, ex)); + // For the remaining examples, generate a
    containing links to the source files. if it.peek().is_some() { - write!(w, "Additional examples can be found in:
      "); + write!( + w, + r#"
"); } - write!(w, "
"); + write!(w, "
"); } write!(w, "
"); diff --git a/src/librustdoc/html/static/css/rustdoc.css b/src/librustdoc/html/static/css/rustdoc.css index 89a205be023af..2767f6468fb51 100644 --- a/src/librustdoc/html/static/css/rustdoc.css +++ b/src/librustdoc/html/static/css/rustdoc.css @@ -137,7 +137,7 @@ h1.fqn { margin-top: 0; /* workaround to keep flex from breaking below 700 px width due to the float: right on the nav - above the h1 */ + above the h1 */ padding-left: 1px; } h1.fqn > .in-band > a:hover { @@ -974,7 +974,7 @@ body.blur > :not(#help) { text-shadow: 1px 0 0 black, -1px 0 0 black, - 0 1px 0 black, + 0 1px 0 black, 0 -1px 0 black; } @@ -1214,8 +1214,8 @@ a.test-arrow:hover{ .notable-traits-tooltip::after { /* The margin on the tooltip does not capture hover events, - this extends the area of hover enough so that mouse hover is not - lost when moving the mouse to the tooltip */ + this extends the area of hover enough so that mouse hover is not + lost when moving the mouse to the tooltip */ content: "\00a0\00a0\00a0"; } @@ -1715,7 +1715,7 @@ details.undocumented[open] > summary::before { } /* We do NOT hide this element so that alternative device readers still have this information - available. */ + available. */ .sidebar-elems { position: fixed; z-index: 1; @@ -1973,10 +1973,15 @@ details.undocumented[open] > summary::before { /* This part is for the new "examples" components */ +.scraped-example-title { + font-family: 'Fira Sans'; + font-weight: 500; +} + .scraped-example:not(.expanded) .code-wrapper pre.line-numbers, .scraped-example:not(.expanded) .code-wrapper .example-wrap pre.rust { overflow: hidden; - height: 240px; + max-height: 240px; } .scraped-example .code-wrapper .prev { @@ -2033,7 +2038,7 @@ details.undocumented[open] > summary::before { .scraped-example:not(.expanded) .code-wrapper { overflow: hidden; - height: 240px; + max-height: 240px; } .scraped-example .code-wrapper .line-numbers { @@ -2072,6 +2077,41 @@ details.undocumented[open] > summary::before { .more-scraped-examples { padding-left: 10px; - border-left: 1px solid #ccc; - margin-left: 24px; + margin-left: 15px; + display: flex; + flex-direction: row; +} + +.toggle-line { + align-self: stretch; + margin-right: 10px; + margin-top: 5px; + padding: 0 4px; + cursor: pointer; +} + +.toggle-line:hover .toggle-line-inner { + background: #aaa; +} + +.toggle-line-inner { + min-width: 2px; + background: #ddd; + height: 100%; +} + +h1 + .scraped-example { + margin-bottom: 10px; +} + +.more-scraped-examples .scraped-example { + margin-bottom: 20px; +} + +.example-links a { + font-family: 'Fira Sans'; +} + +.example-links ul { + margin-bottom: 0; } diff --git a/src/librustdoc/html/static/js/main.js b/src/librustdoc/html/static/js/main.js index fea1b4ecbf1e6..a52e539fbd327 100644 --- a/src/librustdoc/html/static/js/main.js +++ b/src/librustdoc/html/static/js/main.js @@ -1121,15 +1121,22 @@ function hideThemeButtonState() { example.querySelector('.next').remove(); } - // Show full code on expansion - example.querySelector('.expand').addEventListener('click', function () { - if (hasClass(example, "expanded")) { - removeClass(example, "expanded"); - scrollToLoc(example, locs[0]); - } else { - addClass(example, "expanded"); - } - }); + let codeEl = example.querySelector('.rust'); + let expandButton = example.querySelector('.expand'); + if (codeEl.scrollHeight == codeEl.clientHeight) { + addClass(example, 'expanded'); + expandButton.remove(); + } else { + // Show full code on expansion + expandButton.addEventListener('click', function () { + if (hasClass(example, "expanded")) { + removeClass(example, "expanded"); + scrollToLoc(example, locs[0]); + } else { + addClass(example, "expanded"); + } + }); + } // Start with the first example in view scrollToLoc(example, locs[0]); @@ -1139,6 +1146,10 @@ function hideThemeButtonState() { var firstExamples = document.querySelectorAll('.scraped-example-list > .scraped-example'); onEach(firstExamples, updateScrapedExample); onEach(document.querySelectorAll('.more-examples-toggle'), function(toggle) { + toggle.querySelector('.toggle-line').addEventListener('click', function() { + toggle.open = false; + }); + var moreExamples = toggle.querySelectorAll('.scraped-example'); toggle.querySelector('summary').addEventListener('click', function() { // Wrapping in setTimeout ensures the update happens after the elements are actually diff --git a/src/librustdoc/scrape_examples.rs b/src/librustdoc/scrape_examples.rs index 950af8fbb6328..16a40ed1cb31b 100644 --- a/src/librustdoc/scrape_examples.rs +++ b/src/librustdoc/scrape_examples.rs @@ -1,4 +1,4 @@ -//! This module analyzes provided crates to find examples of uses for items in the +//! This module analyzes crates to find examples of uses for items in the //! current crate being documented. use crate::clean; @@ -158,3 +158,31 @@ crate fn run( rustc_errors::ErrorReported }) } + +crate fn load_call_locations( + with_examples: Vec, + diag: &rustc_errors::Handler, +) -> Result, i32> { + let each_call_locations = with_examples + .into_iter() + .map(|path| { + let bytes = fs::read(&path).map_err(|e| format!("{} (for path {})", e, path))?; + let calls: AllCallLocations = + serde_json::from_slice(&bytes).map_err(|e| format!("{}", e))?; + Ok(calls) + }) + .collect::, _>>() + .map_err(|e: String| { + diag.err(&format!("failed to load examples with error: {}", e)); + 1 + })?; + + Ok((each_call_locations.len() > 0).then(|| { + each_call_locations.into_iter().fold(FxHashMap::default(), |mut acc, map| { + for (function, calls) in map.into_iter() { + acc.entry(function).or_insert_with(FxHashMap::default).extend(calls.into_iter()); + } + acc + }) + })) +} From 55bb51786e56a0096a550cf3f26b6c1aed83c872 Mon Sep 17 00:00:00 2001 From: Will Crichton Date: Thu, 26 Aug 2021 14:43:12 -0700 Subject: [PATCH 06/24] Move highlighting logic from JS to Rust Continue migrating JS functionality Cleanup Fix compile error Clean up the diff Set toggle font to sans-serif --- src/librustdoc/doctest.rs | 10 +- src/librustdoc/html/highlight.rs | 75 ++++++++-- src/librustdoc/html/highlight/tests.rs | 6 +- src/librustdoc/html/markdown.rs | 1 + src/librustdoc/html/render/mod.rs | 41 ++++-- src/librustdoc/html/render/print_item.rs | 1 + src/librustdoc/html/sources.rs | 3 + src/librustdoc/html/static/css/rustdoc.css | 22 +-- src/librustdoc/html/static/js/main.js | 155 +++++---------------- src/librustdoc/scrape_examples.rs | 62 ++++++--- 10 files changed, 189 insertions(+), 187 deletions(-) diff --git a/src/librustdoc/doctest.rs b/src/librustdoc/doctest.rs index f8f5e6be9d519..43abcf095d858 100644 --- a/src/librustdoc/doctest.rs +++ b/src/librustdoc/doctest.rs @@ -45,7 +45,7 @@ crate struct TestOptions { crate attrs: Vec, } -crate fn make_rustc_config(options: &Options) -> interface::Config { +crate fn run(options: Options) -> Result<(), ErrorReported> { let input = config::Input::File(options.input.clone()); let invalid_codeblock_attributes_name = crate::lint::INVALID_CODEBLOCK_ATTRIBUTES.name; @@ -87,7 +87,7 @@ crate fn make_rustc_config(options: &Options) -> interface::Config { let mut cfgs = options.cfgs.clone(); cfgs.push("doc".to_owned()); cfgs.push("doctest".to_owned()); - interface::Config { + let config = interface::Config { opts: sessopts, crate_cfg: interface::parse_cfgspecs(cfgs), input, @@ -103,11 +103,7 @@ crate fn make_rustc_config(options: &Options) -> interface::Config { override_queries: None, make_codegen_backend: None, registry: rustc_driver::diagnostics_registry(), - } -} - -crate fn run(options: Options) -> Result<(), ErrorReported> { - let config = make_rustc_config(&options); + }; let test_args = options.test_args.clone(); let display_doctest_warnings = options.display_doctest_warnings; diff --git a/src/librustdoc/html/highlight.rs b/src/librustdoc/html/highlight.rs index 43d1b8f794c30..b0e907cb0590d 100644 --- a/src/librustdoc/html/highlight.rs +++ b/src/librustdoc/html/highlight.rs @@ -12,6 +12,7 @@ use crate::html::render::Context; use std::collections::VecDeque; use std::fmt::{Display, Write}; +use rustc_data_structures::fx::FxHashMap; use rustc_lexer::{LiteralKind, TokenKind}; use rustc_span::edition::Edition; use rustc_span::symbol::Symbol; @@ -30,6 +31,8 @@ crate struct ContextInfo<'a, 'b, 'c> { crate root_path: &'c str, } +crate type DecorationInfo = FxHashMap<&'static str, Vec<(u32, u32)>>; + /// Highlights `src`, returning the HTML output. crate fn render_with_highlighting( src: &str, @@ -40,6 +43,7 @@ crate fn render_with_highlighting( edition: Edition, extra_content: Option, context_info: Option>, + decoration_info: Option, ) { debug!("highlighting: ================\n{}\n==============", src); if let Some((edition_info, class)) = tooltip { @@ -56,7 +60,7 @@ crate fn render_with_highlighting( } write_header(out, class, extra_content); - write_code(out, &src, edition, context_info); + write_code(out, &src, edition, context_info, decoration_info); write_footer(out, playground_button); } @@ -89,17 +93,23 @@ fn write_code( src: &str, edition: Edition, context_info: Option>, + decoration_info: Option, ) { // This replace allows to fix how the code source with DOS backline characters is displayed. let src = src.replace("\r\n", "\n"); - Classifier::new(&src, edition, context_info.as_ref().map(|c| c.file_span).unwrap_or(DUMMY_SP)) - .highlight(&mut |highlight| { - match highlight { - Highlight::Token { text, class } => string(out, Escape(text), class, &context_info), - Highlight::EnterSpan { class } => enter_span(out, class), - Highlight::ExitSpan => exit_span(out), - }; - }); + Classifier::new( + &src, + edition, + context_info.as_ref().map(|c| c.file_span).unwrap_or(DUMMY_SP), + decoration_info, + ) + .highlight(&mut |highlight| { + match highlight { + Highlight::Token { text, class } => string(out, Escape(text), class, &context_info), + Highlight::EnterSpan { class } => enter_span(out, class), + Highlight::ExitSpan => exit_span(out), + }; + }); } fn write_footer(out: &mut Buffer, playground_button: Option<&str>) { @@ -127,6 +137,7 @@ enum Class { PreludeTy, PreludeVal, QuestionMark, + Decoration(&'static str), } impl Class { @@ -150,6 +161,7 @@ impl Class { Class::PreludeTy => "prelude-ty", Class::PreludeVal => "prelude-val", Class::QuestionMark => "question-mark", + Class::Decoration(kind) => kind, } } @@ -244,7 +256,28 @@ impl Iterator for PeekIter<'a> { type Item = (TokenKind, &'a str); fn next(&mut self) -> Option { self.peek_pos = 0; - if let Some(first) = self.stored.pop_front() { Some(first) } else { self.iter.next() } + if let Some(first) = self.stored.pop_front() { + Some(first) + } else { + self.iter.next() + } + } +} + +/// Custom spans inserted into the source. Eg --scrape-examples uses this to highlight function calls +struct Decorations { + starts: Vec<(u32, &'static str)>, + ends: Vec, +} + +impl Decorations { + fn new(info: DecorationInfo) -> Self { + let (starts, ends) = info + .into_iter() + .map(|(kind, ranges)| ranges.into_iter().map(move |(lo, hi)| ((lo, kind), hi))) + .flatten() + .unzip(); + Decorations { starts, ends } } } @@ -259,12 +292,18 @@ struct Classifier<'a> { byte_pos: u32, file_span: Span, src: &'a str, + decorations: Option, } impl<'a> Classifier<'a> { /// Takes as argument the source code to HTML-ify, the rust edition to use and the source code /// file span which will be used later on by the `span_correspondance_map`. - fn new(src: &str, edition: Edition, file_span: Span) -> Classifier<'_> { + fn new( + src: &str, + edition: Edition, + file_span: Span, + decoration_info: Option, + ) -> Classifier<'_> { let tokens = PeekIter::new(TokenIter { src }); Classifier { tokens, @@ -275,6 +314,7 @@ impl<'a> Classifier<'a> { byte_pos: 0, file_span, src, + decorations, } } @@ -356,6 +396,19 @@ impl<'a> Classifier<'a> { /// token is used. fn highlight(mut self, sink: &mut dyn FnMut(Highlight<'a>)) { loop { + if let Some(decs) = self.decorations.as_mut() { + let byte_pos = self.byte_pos; + let n_starts = decs.starts.iter().filter(|(i, _)| byte_pos >= *i).count(); + for (_, kind) in decs.starts.drain(0..n_starts) { + sink(Highlight::EnterSpan { class: Class::Decoration(kind) }); + } + + let n_ends = decs.ends.iter().filter(|i| byte_pos >= **i).count(); + for _ in decs.ends.drain(0..n_ends) { + sink(Highlight::ExitSpan); + } + } + if self .tokens .peek() diff --git a/src/librustdoc/html/highlight/tests.rs b/src/librustdoc/html/highlight/tests.rs index 450bbfea1ea86..405bdf0d8108e 100644 --- a/src/librustdoc/html/highlight/tests.rs +++ b/src/librustdoc/html/highlight/tests.rs @@ -22,7 +22,7 @@ fn test_html_highlighting() { let src = include_str!("fixtures/sample.rs"); let html = { let mut out = Buffer::new(); - write_code(&mut out, src, Edition::Edition2018, None); + write_code(&mut out, src, Edition::Edition2018, None, None); format!("{}
{}
\n", STYLE, out.into_inner()) }; expect_file!["fixtures/sample.html"].assert_eq(&html); @@ -36,7 +36,7 @@ fn test_dos_backline() { println!(\"foo\");\r\n\ }\r\n"; let mut html = Buffer::new(); - write_code(&mut html, src, Edition::Edition2018, None); + write_code(&mut html, src, Edition::Edition2018, None, None); expect_file!["fixtures/dos_line.html"].assert_eq(&html.into_inner()); }); } @@ -50,7 +50,7 @@ let x = super::b::foo; let y = Self::whatever;"; let mut html = Buffer::new(); - write_code(&mut html, src, Edition::Edition2018, None); + write_code(&mut html, src, Edition::Edition2018, None, None); expect_file!["fixtures/highlight.html"].assert_eq(&html.into_inner()); }); } diff --git a/src/librustdoc/html/markdown.rs b/src/librustdoc/html/markdown.rs index 9f2e282fce1c3..a0f13dd71a5b6 100644 --- a/src/librustdoc/html/markdown.rs +++ b/src/librustdoc/html/markdown.rs @@ -360,6 +360,7 @@ impl<'a, I: Iterator>> Iterator for CodeBlocks<'_, 'a, I> { edition, None, None, + None, ); Some(Event::Html(s.into_inner().into())) } diff --git a/src/librustdoc/html/render/mod.rs b/src/librustdoc/html/render/mod.rs index b50aab6351c02..24e50ef91ab91 100644 --- a/src/librustdoc/html/render/mod.rs +++ b/src/librustdoc/html/render/mod.rs @@ -46,7 +46,7 @@ use std::string::ToString; use rustc_ast_pretty::pprust; use rustc_attr::{ConstStability, Deprecation, StabilityLevel}; -use rustc_data_structures::fx::FxHashSet; +use rustc_data_structures::fx::{FxHashMap, FxHashSet}; use rustc_hir as hir; use rustc_hir::def::CtorKind; use rustc_hir::def_id::DefId; @@ -2496,23 +2496,28 @@ fn render_call_locations( // To reduce file sizes, we only want to embed the source code needed to understand the example, not // the entire file. So we find the smallest byte range that covers all items enclosing examples. + assert!(call_data.locations.len() > 0); let min_loc = - call_data.locations.iter().min_by_key(|loc| loc.enclosing_item_span.0).unwrap(); - let min_byte = min_loc.enclosing_item_span.0; - let min_line = min_loc.enclosing_item_lines.0; + call_data.locations.iter().min_by_key(|loc| loc.enclosing_item.byte_span.0).unwrap(); + let min_byte = min_loc.enclosing_item.byte_span.0; + let min_line = min_loc.enclosing_item.line_span.0; let max_byte = - call_data.locations.iter().map(|loc| loc.enclosing_item_span.1).max().unwrap(); + call_data.locations.iter().map(|loc| loc.enclosing_item.byte_span.1).max().unwrap(); // The output code is limited to that byte range. - let contents_subset = &contents[min_byte..max_byte]; + let contents_subset = &contents[(min_byte as usize)..(max_byte as usize)]; // The call locations need to be updated to reflect that the size of the program has changed. // Specifically, the ranges are all subtracted by `min_byte` since that's the new zero point. - let locations = call_data + let (byte_ranges, line_ranges): (Vec<_>, Vec<_>) = call_data .locations .iter() - .map(|loc| (loc.call_span.0 - min_byte, loc.call_span.1 - min_byte)) - .collect::>(); + .map(|loc| { + let (byte_lo, byte_hi) = loc.call_expr.byte_span; + let (line_lo, line_hi) = loc.call_expr.line_span; + ((byte_lo - min_byte, byte_hi - min_byte), (line_lo - min_line, line_hi - min_line)) + }) + .unzip(); let edition = cx.shared.edition(); write!( @@ -2524,7 +2529,7 @@ fn render_call_locations( // The code and locations are encoded as data attributes, so they can be read // later by the JS for interactions. code = contents_subset.replace("\"", """), - locations = serde_json::to_string(&locations).unwrap(), + locations = serde_json::to_string(&line_ranges).unwrap(), ); write!(w, r#" "#); write!(w, r#""#); @@ -2532,7 +2537,18 @@ fn render_call_locations( // FIXME(wcrichto): where should file_span and root_path come from? let file_span = rustc_span::DUMMY_SP; let root_path = "".to_string(); - sources::print_src(w, contents_subset, edition, file_span, cx, &root_path, Some(min_line)); + let mut decoration_info = FxHashMap::default(); + decoration_info.insert("highlight", byte_ranges); + sources::print_src( + w, + contents_subset, + edition, + file_span, + cx, + &root_path, + Some(min_line), + Some(decoration_info), + ); write!(w, ""); }; @@ -2542,7 +2558,8 @@ fn render_call_locations( // understand at a glance. let ordered_locations = { let sort_criterion = |(_, call_data): &(_, &CallData)| { - let (lo, hi) = call_data.locations[0].enclosing_item_span; + // Use the first location because that's what the user will see initially + let (lo, hi) = call_data.locations[0].enclosing_item.byte_span; hi - lo }; diff --git a/src/librustdoc/html/render/print_item.rs b/src/librustdoc/html/render/print_item.rs index 1275fa4e15617..a9dce1be0d817 100644 --- a/src/librustdoc/html/render/print_item.rs +++ b/src/librustdoc/html/render/print_item.rs @@ -1117,6 +1117,7 @@ fn item_macro(w: &mut Buffer, cx: &Context<'_>, it: &clean::Item, t: &clean::Mac it.span(cx.tcx()).inner().edition(), None, None, + None, ); }); document(w, cx, it, None, HeadingOffset::H2) diff --git a/src/librustdoc/html/sources.rs b/src/librustdoc/html/sources.rs index 6bd335a9b96bb..c3441036d503b 100644 --- a/src/librustdoc/html/sources.rs +++ b/src/librustdoc/html/sources.rs @@ -212,6 +212,7 @@ impl SourceCollector<'_, 'tcx> { &self.cx, &root_path, None, + None, ) }, &self.cx.shared.style_files, @@ -259,6 +260,7 @@ crate fn print_src( context: &Context<'_>, root_path: &str, offset: Option, + decoration_info: Option, ) { let lines = s.lines().count(); let mut line_numbers = Buffer::empty_from(buf); @@ -283,5 +285,6 @@ crate fn print_src( edition, Some(line_numbers), Some(highlight::ContextInfo { context, file_span, root_path }), + decoration_info, ); } diff --git a/src/librustdoc/html/static/css/rustdoc.css b/src/librustdoc/html/static/css/rustdoc.css index 2767f6468fb51..ccb6bb7986849 100644 --- a/src/librustdoc/html/static/css/rustdoc.css +++ b/src/librustdoc/html/static/css/rustdoc.css @@ -137,7 +137,7 @@ h1.fqn { margin-top: 0; /* workaround to keep flex from breaking below 700 px width due to the float: right on the nav - above the h1 */ + above the h1 */ padding-left: 1px; } h1.fqn > .in-band > a:hover { @@ -974,7 +974,7 @@ body.blur > :not(#help) { text-shadow: 1px 0 0 black, -1px 0 0 black, - 0 1px 0 black, + 0 1px 0 black, 0 -1px 0 black; } @@ -1214,8 +1214,8 @@ a.test-arrow:hover{ .notable-traits-tooltip::after { /* The margin on the tooltip does not capture hover events, - this extends the area of hover enough so that mouse hover is not - lost when moving the mouse to the tooltip */ + this extends the area of hover enough so that mouse hover is not + lost when moving the mouse to the tooltip */ content: "\00a0\00a0\00a0"; } @@ -1715,7 +1715,7 @@ details.undocumented[open] > summary::before { } /* We do NOT hide this element so that alternative device readers still have this information - available. */ + available. */ .sidebar-elems { position: fixed; z-index: 1; @@ -1971,7 +1971,8 @@ details.undocumented[open] > summary::before { } } -/* This part is for the new "examples" components */ + +/* Begin: styles for --scrape-examples feature */ .scraped-example-title { font-family: 'Fira Sans'; @@ -2063,16 +2064,17 @@ details.undocumented[open] > summary::before { overflow-y: hidden; } -.scraped-example .line-numbers span.highlight { - background: #f6fdb0; +.scraped-example .example-wrap .rust span.highlight { + background: #fcffd6; } -.scraped-example .example-wrap .rust span.highlight { +.scraped-example .example-wrap .rust span.highlight.focus { background: #f6fdb0; } .more-examples-toggle summary { color: #999; + font-family: 'Fira Sans'; } .more-scraped-examples { @@ -2115,3 +2117,5 @@ h1 + .scraped-example { .example-links ul { margin-bottom: 0; } + +/* End: styles for --scrape-examples feature */ diff --git a/src/librustdoc/html/static/js/main.js b/src/librustdoc/html/static/js/main.js index a52e539fbd327..1b924991139b4 100644 --- a/src/librustdoc/html/static/js/main.js +++ b/src/librustdoc/html/static/js/main.js @@ -980,154 +980,55 @@ function hideThemeButtonState() { window.addEventListener("hashchange", onHashChange); searchState.setup(); - /////// EXAMPLE ANALYZER - - // Merge the full set of [from, to] offsets into a minimal set of non-overlapping - // [from, to] offsets. - // NB: This is such a archetypal software engineering interview question that - // I can't believe I actually had to write it. Yes, it's O(N) in the input length -- - // but it does assume a sorted input! - function distinctRegions(locs) { - var start = -1; - var end = -1; - var output = []; - for (var i = 0; i < locs.length; i++) { - var loc = locs[i]; - if (loc[0] > end) { - if (end > 0) { - output.push([start, end]); - } - start = loc[0]; - end = loc[1]; - } else { - end = Math.max(end, loc[1]); - } - } - if (end > 0) { - output.push([start, end]); - } - return output; - } - - function convertLocsStartsToLineOffsets(code, locs) { - locs = distinctRegions(locs.slice(0).sort(function (a, b) { - return a[0] === b[0] ? a[1] - b[1] : a[0] - b[0]; - })); // sort by start; use end if start is equal. - var codeLines = code.split("\n"); - var lineIndex = 0; - var totalOffset = 0; - var output = []; - - while (locs.length > 0 && lineIndex < codeLines.length) { - // +1 here and later is due to omitted \n - var lineLength = codeLines[lineIndex].length + 1; - while (locs.length > 0 && totalOffset + lineLength > locs[0][0]) { - var endIndex = lineIndex; - var charsRemaining = locs[0][1] - totalOffset; - while (endIndex < codeLines.length && - charsRemaining > codeLines[endIndex].length + 1) - { - charsRemaining -= codeLines[endIndex].length + 1; - endIndex += 1; - } - output.push({ - from: [lineIndex, locs[0][0] - totalOffset], - to: [endIndex, charsRemaining] - }); - locs.shift(); - } - lineIndex++; - totalOffset += lineLength; - } - return output; - } - - // inserts str into html, *but* calculates idx by eliding anything in html that's not in raw. - // ideally this would work by walking the element tree...but this is good enough for now. - function insertStrAtRawIndex(raw, html, idx, str) { - if (idx > raw.length) { - return html; - } - if (idx == raw.length) { - return html + str; - } - var rawIdx = 0; - var htmlIdx = 0; - while (rawIdx < idx && rawIdx < raw.length) { - while (raw[rawIdx] !== html[htmlIdx] && htmlIdx < html.length) { - htmlIdx++; - } - rawIdx++; - htmlIdx++; - } - return html.substring(0, htmlIdx) + str + html.substr(htmlIdx); - } + /////// --scrape-examples interactions // Scroll code block to put the given code location in the middle of the viewer function scrollToLoc(elt, loc) { var wrapper = elt.querySelector(".code-wrapper"); var halfHeight = wrapper.offsetHeight / 2; var lines = elt.querySelector('.line-numbers'); - var offsetMid = (lines.children[loc.from[0]].offsetTop - + lines.children[loc.to[0]].offsetTop) / 2; + var offsetMid = (lines.children[loc[0]].offsetTop + + lines.children[loc[1]].offsetTop) / 2; var scrollOffset = offsetMid - halfHeight; lines.scrollTo(0, scrollOffset); elt.querySelector(".rust").scrollTo(0, scrollOffset); } function updateScrapedExample(example) { - var code = example.attributes.getNamedItem("data-code").textContent; - var codeLines = code.split("\n"); var locs = JSON.parse(example.attributes.getNamedItem("data-locs").textContent); - locs = convertLocsStartsToLineOffsets(code, locs); - - // Add call-site highlights to code listings - var litParent = example.querySelector('.example-wrap pre.rust'); - var litHtml = litParent.innerHTML.split("\n"); - onEach(locs, function (loc) { - for (var i = loc.from[0]; i < loc.to[0] + 1; i++) { - addClass(example.querySelector('.line-numbers').children[i], "highlight"); - } - litHtml[loc.to[0]] = insertStrAtRawIndex( - codeLines[loc.to[0]], - litHtml[loc.to[0]], - loc.to[1], - "
"); - litHtml[loc.from[0]] = insertStrAtRawIndex( - codeLines[loc.from[0]], - litHtml[loc.from[0]], - loc.from[1], - ''); - }, true); // do this backwards to avoid shifting later offsets - litParent.innerHTML = litHtml.join('\n'); - - // Toggle through list of examples in a given file + var locIndex = 0; + var highlights = example.querySelectorAll('.highlight'); + addClass(highlights[0], 'focus'); if (locs.length > 1) { + // Toggle through list of examples in a given file + var onChangeLoc = function(f) { + removeClass(highlights[locIndex], 'focus'); + f(); + scrollToLoc(example, locs[locIndex]); + addClass(highlights[locIndex], 'focus'); + }; example.querySelector('.prev') - .addEventListener('click', function () { - locIndex = (locIndex - 1 + locs.length) % locs.length; - scrollToLoc(example, locs[locIndex]); + .addEventListener('click', function() { + onChangeLoc(function() { + locIndex = (locIndex - 1 + locs.length) % locs.length; + }); }); example.querySelector('.next') - .addEventListener('click', function () { - locIndex = (locIndex + 1) % locs.length; - scrollToLoc(example, locs[locIndex]); + .addEventListener('click', function() { + onChangeLoc(function() { locIndex = (locIndex + 1) % locs.length; }); }); } else { + // Remove buttons if there's only one example in the file example.querySelector('.prev').remove(); example.querySelector('.next').remove(); } - let codeEl = example.querySelector('.rust'); - let expandButton = example.querySelector('.expand'); - if (codeEl.scrollHeight == codeEl.clientHeight) { - addClass(example, 'expanded'); - expandButton.remove(); - } else { - // Show full code on expansion + var codeEl = example.querySelector('.rust'); + var codeOverflows = codeEl.scrollHeight > codeEl.clientHeight; + var expandButton = example.querySelector('.expand'); + if (codeOverflows) { + // If file is larger than default height, give option to expand the viewer expandButton.addEventListener('click', function () { if (hasClass(example, "expanded")) { removeClass(example, "expanded"); @@ -1136,6 +1037,10 @@ function hideThemeButtonState() { addClass(example, "expanded"); } }); + } else { + // Otherwise remove expansion buttons + addClass(example, 'expanded'); + expandButton.remove(); } // Start with the first example in view @@ -1146,6 +1051,8 @@ function hideThemeButtonState() { var firstExamples = document.querySelectorAll('.scraped-example-list > .scraped-example'); onEach(firstExamples, updateScrapedExample); onEach(document.querySelectorAll('.more-examples-toggle'), function(toggle) { + // Allow users to click the left border of the
section to close it, + // since the section can be large and finding the [+] button is annoying. toggle.querySelector('.toggle-line').addEventListener('click', function() { toggle.open = false; }); diff --git a/src/librustdoc/scrape_examples.rs b/src/librustdoc/scrape_examples.rs index 16a40ed1cb31b..3887647ca0a4c 100644 --- a/src/librustdoc/scrape_examples.rs +++ b/src/librustdoc/scrape_examples.rs @@ -1,5 +1,4 @@ -//! This module analyzes crates to find examples of uses for items in the -//! current crate being documented. +//! This module analyzes crates to find call sites that can serve as examples in the documentation. use crate::clean; use crate::config; @@ -11,20 +10,55 @@ use rustc_data_structures::fx::FxHashMap; use rustc_hir::{ self as hir, intravisit::{self, Visitor}, + HirId, }; use rustc_interface::interface; use rustc_middle::hir::map::Map; use rustc_middle::ty::{self, TyCtxt}; -use rustc_span::{def_id::DefId, FileName}; +use rustc_span::{def_id::DefId, BytePos, FileName, SourceFile}; use serde::{Deserialize, Serialize}; use std::fs; use std::path::PathBuf; +#[derive(Serialize, Deserialize, Debug, Clone)] +crate struct SyntaxRange { + crate byte_span: (u32, u32), + crate line_span: (usize, usize), +} + +impl SyntaxRange { + fn new(span: rustc_span::Span, file: &SourceFile) -> Self { + let get_pos = |bytepos: BytePos| file.original_relative_byte_pos(bytepos).0; + let get_line = |bytepos: BytePos| file.lookup_line(bytepos).unwrap(); + + SyntaxRange { + byte_span: (get_pos(span.lo()), get_pos(span.hi())), + line_span: (get_line(span.lo()), get_line(span.hi())), + } + } +} + #[derive(Serialize, Deserialize, Debug, Clone)] crate struct CallLocation { - crate call_span: (usize, usize), - crate enclosing_item_span: (usize, usize), - crate enclosing_item_lines: (usize, usize), + crate call_expr: SyntaxRange, + crate enclosing_item: SyntaxRange, +} + +impl CallLocation { + fn new( + tcx: TyCtxt<'_>, + expr_span: rustc_span::Span, + expr_id: HirId, + source_file: &rustc_span::SourceFile, + ) -> Self { + let enclosing_item_span = tcx.hir().span_with_body(tcx.hir().get_parent_item(expr_id)); + assert!(enclosing_item_span.contains(expr_span)); + + CallLocation { + call_expr: SyntaxRange::new(expr_span, source_file), + enclosing_item: SyntaxRange::new(enclosing_item_span, source_file), + } + } } #[derive(Serialize, Deserialize, Debug, Clone)] @@ -96,24 +130,10 @@ where _ => None, }; - let get_pos = - |bytepos: rustc_span::BytePos| file.original_relative_byte_pos(bytepos).0 as usize; - let get_range = |span: rustc_span::Span| (get_pos(span.lo()), get_pos(span.hi())); - let get_line = |bytepos: rustc_span::BytePos| file.lookup_line(bytepos).unwrap(); - let get_lines = |span: rustc_span::Span| (get_line(span.lo()), get_line(span.hi())); - if let Some(file_path) = file_path { let abs_path = fs::canonicalize(file_path.clone()).unwrap(); let cx = &self.cx; - let enclosing_item_span = - self.tcx.hir().span_with_body(self.tcx.hir().get_parent_item(ex.hir_id)); - assert!(enclosing_item_span.contains(span)); - - let location = CallLocation { - call_span: get_range(span), - enclosing_item_span: get_range(enclosing_item_span), - enclosing_item_lines: get_lines(enclosing_item_span), - }; + let location = CallLocation::new(self.tcx, span, ex.hir_id, &file); entries .entry(abs_path) From 18edcf86d2870de7975f1142f272b9a1236cdef4 Mon Sep 17 00:00:00 2001 From: Will Crichton Date: Mon, 13 Sep 2021 18:08:14 -0700 Subject: [PATCH 07/24] Reduce blur size, fix example width bug, add better error handling for I/O issues Remove repository url Fix formatting Fix file_span in print_src Formatting --- src/librustdoc/html/render/context.rs | 10 +-- src/librustdoc/html/render/mod.rs | 74 +++++++++++++++++----- src/librustdoc/html/static/css/rustdoc.css | 13 ++-- 3 files changed, 68 insertions(+), 29 deletions(-) diff --git a/src/librustdoc/html/render/context.rs b/src/librustdoc/html/render/context.rs index fd53a3d7bfbc6..b99d2fe5aa0d1 100644 --- a/src/librustdoc/html/render/context.rs +++ b/src/librustdoc/html/render/context.rs @@ -124,7 +124,6 @@ crate struct SharedContext<'tcx> { crate span_correspondance_map: FxHashMap, /// The [`Cache`] used during rendering. crate cache: Cache, - pub(super) repository_url: Option, } impl SharedContext<'_> { @@ -141,11 +140,7 @@ impl SharedContext<'_> { /// Returns the `collapsed_doc_value` of the given item if this is the main crate, otherwise /// returns the `doc_value`. crate fn maybe_collapsed_doc_value<'a>(&self, item: &'a clean::Item) -> Option { - if self.collapsed { - item.collapsed_doc_value() - } else { - item.doc_value() - } + if self.collapsed { item.collapsed_doc_value() } else { item.doc_value() } } crate fn edition(&self) -> Edition { @@ -351,7 +346,6 @@ impl<'tcx> Context<'tcx> { let hiline = span.hi(self.sess()).line; let lines = if loline == hiline { loline.to_string() } else { format!("{}-{}", loline, hiline) }; - Some(format!( "{root}src/{krate}/{path}#{lines}", root = Escape(&root), @@ -395,7 +389,6 @@ impl<'tcx> FormatRenderer<'tcx> for Context<'tcx> { generate_redirect_map, show_type_layout, generate_link_to_definition, - repository_url, .. } = options; @@ -487,7 +480,6 @@ impl<'tcx> FormatRenderer<'tcx> for Context<'tcx> { templates, span_correspondance_map: matches, cache, - repository_url, }; // Add the default themes to the `Vec` of stylepaths diff --git a/src/librustdoc/html/render/mod.rs b/src/librustdoc/html/render/mod.rs index 24e50ef91ab91..24eb4e88c3bde 100644 --- a/src/librustdoc/html/render/mod.rs +++ b/src/librustdoc/html/render/mod.rs @@ -40,6 +40,7 @@ use std::collections::VecDeque; use std::default::Default; use std::fmt; use std::fs; +use std::iter::Peekable; use std::path::PathBuf; use std::str; use std::string::ToString; @@ -53,7 +54,10 @@ use rustc_hir::def_id::DefId; use rustc_hir::Mutability; use rustc_middle::middle::stability; use rustc_middle::ty::TyCtxt; -use rustc_span::symbol::{kw, sym, Symbol}; +use rustc_span::{ + symbol::{kw, sym, Symbol}, + BytePos, FileName, RealFileName, +}; use serde::ser::SerializeSeq; use serde::{Serialize, Serializer}; @@ -590,7 +594,7 @@ fn document_full_inner( match &*item.kind { clean::ItemKind::FunctionItem(f) | clean::ItemKind::MethodItem(f, _) => { - render_call_locations(w, cx, &f.call_locations); + render_call_locations(w, cx, &f.call_locations, item); } _ => {} } @@ -2458,6 +2462,7 @@ fn render_call_locations( w: &mut Buffer, cx: &Context<'_>, call_locations: &Option, + item: &clean::Item, ) { let call_locations = match call_locations.as_ref() { Some(call_locations) if call_locations.len() > 0 => call_locations, @@ -2488,11 +2493,17 @@ fn render_call_locations( }; // Generate the HTML for a single example, being the title and code block - let write_example = |w: &mut Buffer, (path, call_data): (&PathBuf, &CallData)| { - // FIXME(wcrichto): is there a better way to handle an I/O error than a panic? - // When would such an error arise? - let contents = - fs::read_to_string(&path).expect(&format!("Failed to read file: {}", path.display())); + let tcx = cx.tcx(); + let write_example = |w: &mut Buffer, (path, call_data): (&PathBuf, &CallData)| -> bool { + let contents = match fs::read_to_string(&path) { + Ok(contents) => contents, + Err(err) => { + let span = item.span(tcx).inner(); + tcx.sess + .span_err(span, &format!("failed to read file {}: {}", path.display(), err)); + return false; + } + }; // To reduce file sizes, we only want to embed the source code needed to understand the example, not // the entire file. So we find the smallest byte range that covers all items enclosing examples. @@ -2522,23 +2533,42 @@ fn render_call_locations( let edition = cx.shared.edition(); write!( w, - r#"
+ r#"
{title}
"#, title = example_url(call_data), - // The code and locations are encoded as data attributes, so they can be read + // The locations are encoded as a data attribute, so they can be read // later by the JS for interactions. - code = contents_subset.replace("\"", """), locations = serde_json::to_string(&line_ranges).unwrap(), ); write!(w, r#" "#); write!(w, r#""#); - // FIXME(wcrichto): where should file_span and root_path come from? - let file_span = rustc_span::DUMMY_SP; - let root_path = "".to_string(); + // Look for the example file in the source map if it exists, otherwise return a dummy span + let file_span = (|| { + let source_map = tcx.sess.source_map(); + let crate_src = tcx.sess.local_crate_source_file.as_ref()?; + let abs_crate_src = crate_src.canonicalize().ok()?; + let crate_root = abs_crate_src.parent()?.parent()?; + let rel_path = path.strip_prefix(crate_root).ok()?; + let files = source_map.files(); + let file = files.iter().find(|file| match &file.name { + FileName::Real(RealFileName::LocalPath(other_path)) => rel_path == other_path, + _ => false, + })?; + Some(rustc_span::Span::with_root_ctxt( + file.start_pos + BytePos(min_byte), + file.start_pos + BytePos(max_byte), + )) + })() + .unwrap_or(rustc_span::DUMMY_SP); + + // The root path is the inverse of Context::current + let root_path = vec!["../"; cx.current.len() - 1].join(""); + let mut decoration_info = FxHashMap::default(); decoration_info.insert("highlight", byte_ranges); + sources::print_src( w, contents_subset, @@ -2550,6 +2580,8 @@ fn render_call_locations( Some(decoration_info), ); write!(w, "
"); + + true }; // The call locations are output in sequence, so that sequence needs to be determined. @@ -2570,7 +2602,15 @@ fn render_call_locations( // Write just one example that's visible by default in the method's description. let mut it = ordered_locations.into_iter().peekable(); - write_example(w, it.next().unwrap()); + let write_and_skip_failure = |w: &mut Buffer, it: &mut Peekable<_>| { + while let Some(example) = it.next() { + if write_example(&mut *w, example) { + break; + } + } + }; + + write_and_skip_failure(w, &mut it); // Then add the remaining examples in a hidden section. if it.peek().is_some() { @@ -2582,13 +2622,15 @@ fn render_call_locations(
-
+
"# ); // Only generate inline code for MAX_FULL_EXAMPLES number of examples. Otherwise we could // make the page arbitrarily huge! - (&mut it).take(MAX_FULL_EXAMPLES).for_each(|ex| write_example(w, ex)); + for _ in 0..MAX_FULL_EXAMPLES { + write_and_skip_failure(w, &mut it); + } // For the remaining examples, generate a
    containing links to the source files. if it.peek().is_some() { diff --git a/src/librustdoc/html/static/css/rustdoc.css b/src/librustdoc/html/static/css/rustdoc.css index ccb6bb7986849..a9f7113c88166 100644 --- a/src/librustdoc/html/static/css/rustdoc.css +++ b/src/librustdoc/html/static/css/rustdoc.css @@ -2020,7 +2020,7 @@ details.undocumented[open] > summary::before { .scraped-example:not(.expanded) .code-wrapper:before { content: " "; width: 100%; - height: 20px; + height: 10px; position: absolute; z-index: 100; top: 0; @@ -2030,7 +2030,7 @@ details.undocumented[open] > summary::before { .scraped-example:not(.expanded) .code-wrapper:after { content: " "; width: 100%; - height: 20px; + height: 10px; position: absolute; z-index: 100; bottom: 0; @@ -2078,10 +2078,15 @@ details.undocumented[open] > summary::before { } .more-scraped-examples { - padding-left: 10px; - margin-left: 15px; + margin-left: 25px; display: flex; flex-direction: row; + width: calc(100% - 25px); +} + +.more-scraped-examples-inner { + /* 20px is width of toggle-line + toggle-line-inner */ + width: calc(100% - 20px); } .toggle-line { From a1cb19444f27db434183daa003bad89be9491564 Mon Sep 17 00:00:00 2001 From: Will Crichton Date: Tue, 14 Sep 2021 09:50:47 -0700 Subject: [PATCH 08/24] Add styles for non-white themes Tweak colors Tabs New link heading style --- src/librustdoc/html/render/mod.rs | 28 +++++++++---------- src/librustdoc/html/static/css/rustdoc.css | 10 ++++++- src/librustdoc/html/static/css/themes/ayu.css | 19 +++++++++++++ .../html/static/css/themes/dark.css | 19 +++++++++++++ 4 files changed, 61 insertions(+), 15 deletions(-) diff --git a/src/librustdoc/html/render/mod.rs b/src/librustdoc/html/render/mod.rs index 24eb4e88c3bde..6b30abb8260d2 100644 --- a/src/librustdoc/html/render/mod.rs +++ b/src/librustdoc/html/render/mod.rs @@ -2482,16 +2482,6 @@ fn render_call_locations( id ); - // Link to the source file containing a given example - let example_url = |call_data: &CallData| -> String { - format!( - r#"{name}"#, - root = cx.root_path(), - url = call_data.url, - name = call_data.display_name - ) - }; - // Generate the HTML for a single example, being the title and code block let tcx = cx.tcx(); let write_example = |w: &mut Buffer, (path, call_data): (&PathBuf, &CallData)| -> bool { @@ -2534,9 +2524,13 @@ fn render_call_locations( write!( w, r#"
    -
    {title}
    -
    "#, - title = example_url(call_data), +
    + {name} [src] +
    +
    "#, + root = cx.root_path(), + url = call_data.url, + name = call_data.display_name, // The locations are encoded as a data attribute, so they can be read // later by the JS for interactions. locations = serde_json::to_string(&line_ranges).unwrap(), @@ -2639,7 +2633,13 @@ fn render_call_locations( r#""); } diff --git a/src/librustdoc/html/static/css/rustdoc.css b/src/librustdoc/html/static/css/rustdoc.css index a9f7113c88166..b35412bdc2a90 100644 --- a/src/librustdoc/html/static/css/rustdoc.css +++ b/src/librustdoc/html/static/css/rustdoc.css @@ -1976,7 +1976,10 @@ details.undocumented[open] > summary::before { .scraped-example-title { font-family: 'Fira Sans'; - font-weight: 500; +} + +.scraped-example-title a { + margin-left: 5px; } .scraped-example:not(.expanded) .code-wrapper pre.line-numbers, @@ -2115,7 +2118,12 @@ h1 + .scraped-example { margin-bottom: 20px; } +.more-scraped-examples .scraped-example:last-child { + margin-bottom: 0; +} + .example-links a { + margin-top: 20px; font-family: 'Fira Sans'; } diff --git a/src/librustdoc/html/static/css/themes/ayu.css b/src/librustdoc/html/static/css/themes/ayu.css index 0fd6462a8f5dd..4ce7a372e3956 100644 --- a/src/librustdoc/html/static/css/themes/ayu.css +++ b/src/librustdoc/html/static/css/themes/ayu.css @@ -613,3 +613,22 @@ div.files > .selected { input:checked + .slider { background-color: #ffb454 !important; } + +.scraped-example .example-wrap .rust span.highlight { + background: rgb(91, 59, 1); +} +.scraped-example .example-wrap .rust span.highlight.focus { + background: rgb(124, 75, 15); +} +.scraped-example:not(.expanded) .code-wrapper:before { + background: linear-gradient(to bottom, rgba(15, 20, 25, 1), rgba(15, 20, 25, 0)); +} +.scraped-example:not(.expanded) .code-wrapper:after { + background: linear-gradient(to top, rgba(15, 20, 25, 1), rgba(15, 20, 25, 0)); +} +.toggle-line-inner { + background: #616161; +} +.toggle-line:hover .toggle-line-inner { + background: ##898989; +} diff --git a/src/librustdoc/html/static/css/themes/dark.css b/src/librustdoc/html/static/css/themes/dark.css index d863701dd73c7..aa6e7b2763adc 100644 --- a/src/librustdoc/html/static/css/themes/dark.css +++ b/src/librustdoc/html/static/css/themes/dark.css @@ -485,3 +485,22 @@ div.files > .selected { .setting-line > .title { border-bottom-color: #ddd; } + +.scraped-example .example-wrap .rust span.highlight { + background: rgb(91, 59, 1); +} +.scraped-example .example-wrap .rust span.highlight.focus { + background: rgb(124, 75, 15); +} +.scraped-example:not(.expanded) .code-wrapper:before { + background: linear-gradient(to bottom, rgba(53, 53, 53, 1), rgba(53, 53, 53, 0)); +} +.scraped-example:not(.expanded) .code-wrapper:after { + background: linear-gradient(to top, rgba(53, 53, 53, 1), rgba(53, 53, 53, 0)); +} +.toggle-line-inner { + background: #616161; +} +.toggle-line:hover .toggle-line-inner { + background: ##898989; +} From 829b1a9dd99f9f08faec8b1774798b9025bdf13b Mon Sep 17 00:00:00 2001 From: Will Crichton Date: Thu, 16 Sep 2021 18:12:45 -0700 Subject: [PATCH 09/24] Incorporate jyn's feedback * Move call location logic from function constructor to rendering * Fix issue with macro spans in scraping examples * Clean up example loading logic Documentation / newtype for DecorationInfo Fix line number display Serialize edition of call site, other small cleanup --- src/librustdoc/clean/inline.rs | 2 +- src/librustdoc/clean/mod.rs | 24 ++-- src/librustdoc/clean/types.rs | 15 +-- src/librustdoc/config.rs | 3 +- src/librustdoc/html/highlight.rs | 6 +- src/librustdoc/html/render/context.rs | 5 + src/librustdoc/html/render/mod.rs | 77 +++++++----- src/librustdoc/html/static/css/rustdoc.css | 4 - src/librustdoc/html/static/js/main.js | 25 ++++ src/librustdoc/json/conversions.rs | 4 +- src/librustdoc/lib.rs | 20 +++- src/librustdoc/scrape_examples.rs | 130 +++++++++++---------- 12 files changed, 183 insertions(+), 132 deletions(-) diff --git a/src/librustdoc/clean/inline.rs b/src/librustdoc/clean/inline.rs index 09622d721f7f0..9d5a95a88a2df 100644 --- a/src/librustdoc/clean/inline.rs +++ b/src/librustdoc/clean/inline.rs @@ -235,7 +235,7 @@ fn build_external_function(cx: &mut DocContext<'_>, did: DefId) -> clean::Functi decl, generics, header: hir::FnHeader { unsafety: sig.unsafety(), abi: sig.abi(), constness, asyncness }, - call_locations: None, + def_id: did, } } diff --git a/src/librustdoc/clean/mod.rs b/src/librustdoc/clean/mod.rs index e2b1ff4547ba5..dded5c9934b83 100644 --- a/src/librustdoc/clean/mod.rs +++ b/src/librustdoc/clean/mod.rs @@ -801,10 +801,8 @@ impl<'a> Clean for (&'a hir::FnSig<'a>, &'a hir::Generics<'a>, hir::Bo fn clean(&self, cx: &mut DocContext<'_>) -> Function { let (generics, decl) = enter_impl_trait(cx, |cx| (self.1.clean(cx), (&*self.0.decl, self.2).clean(cx))); - let mut function = Function { decl, generics, header: self.0.header, call_locations: None }; let def_id = self.2.hir_id.owner.to_def_id(); - function.load_call_locations(def_id, cx); - function + Function { decl, generics, header: self.0.header, def_id } } } @@ -936,14 +934,13 @@ impl Clean for hir::TraitItem<'_> { let (generics, decl) = enter_impl_trait(cx, |cx| { (self.generics.clean(cx), (&*sig.decl, &names[..]).clean(cx)) }); - let mut t = - Function { header: sig.header, decl, generics, call_locations: None }; + let def_id = self.def_id.to_def_id(); + let mut t = Function { header: sig.header, decl, generics, def_id }; if t.header.constness == hir::Constness::Const && is_unstable_const_fn(cx.tcx, local_did).is_some() { t.header.constness = hir::Constness::NotConst; } - t.load_call_locations(self.def_id.to_def_id(), cx); TyMethodItem(t) } hir::TraitItemKind::Type(ref bounds, ref default) => { @@ -1062,7 +1059,7 @@ impl Clean for ty::AssocItem { ty::ImplContainer(_) => Some(self.defaultness), ty::TraitContainer(_) => None, }; - let mut function = Function { + let function = Function { generics, decl, header: hir::FnHeader { @@ -1071,12 +1068,11 @@ impl Clean for ty::AssocItem { constness, asyncness, }, - call_locations: None, + def_id: self.def_id, }; - function.load_call_locations(self.def_id, cx); MethodItem(function, defaultness) } else { - let mut function = Function { + let function = Function { generics, decl, header: hir::FnHeader { @@ -1085,9 +1081,8 @@ impl Clean for ty::AssocItem { constness: hir::Constness::NotConst, asyncness: hir::IsAsync::NotAsync, }, - call_locations: None, + def_id: self.def_id, }; - function.load_call_locations(self.def_id, cx); TyMethodItem(function) } } @@ -2086,7 +2081,8 @@ fn clean_use_statement( impl Clean for (&hir::ForeignItem<'_>, Option) { fn clean(&self, cx: &mut DocContext<'_>) -> Item { let (item, renamed) = self; - cx.with_param_env(item.def_id.to_def_id(), |cx| { + let def_id = item.def_id.to_def_id(); + cx.with_param_env(def_id, |cx| { let kind = match item.kind { hir::ForeignItemKind::Fn(ref decl, ref names, ref generics) => { let abi = cx.tcx.hir().get_foreign_abi(item.hir_id()); @@ -2106,7 +2102,7 @@ impl Clean for (&hir::ForeignItem<'_>, Option) { constness: hir::Constness::NotConst, asyncness: hir::IsAsync::NotAsync, }, - call_locations: None, + def_id, }) } hir::ForeignItemKind::Static(ref ty, mutability) => { diff --git a/src/librustdoc/clean/types.rs b/src/librustdoc/clean/types.rs index eb507e4eecabb..043ffbfd187c3 100644 --- a/src/librustdoc/clean/types.rs +++ b/src/librustdoc/clean/types.rs @@ -42,7 +42,6 @@ use crate::formats::cache::Cache; use crate::formats::item_type::ItemType; use crate::html::render::cache::ExternalLocation; use crate::html::render::Context; -use crate::scrape_examples::{self, FnCallLocations}; use self::FnRetTy::*; use self::ItemKind::*; @@ -1255,19 +1254,7 @@ crate struct Function { crate decl: FnDecl, crate generics: Generics, crate header: hir::FnHeader, - crate call_locations: Option, -} - -impl Function { - /// If --scrape-examples is used, then this function attempts to find call locations - /// for `self` within `RenderOptions::call_locations` and store them in `Function::call_locations`. - crate fn load_call_locations(&mut self, def_id: hir::def_id::DefId, cx: &DocContext<'_>) { - if let Some(call_locations) = cx.render_options.call_locations.as_ref() { - let key = scrape_examples::def_id_call_key(cx.tcx, def_id); - self.call_locations = call_locations.get(&key).cloned(); - debug!("call_locations: {:?} -- {:?}", key, self.call_locations); - } - } + crate def_id: DefId, } #[derive(Clone, PartialEq, Eq, Debug, Hash)] diff --git a/src/librustdoc/config.rs b/src/librustdoc/config.rs index 4e019d4e15d3b..54b62db791748 100644 --- a/src/librustdoc/config.rs +++ b/src/librustdoc/config.rs @@ -207,6 +207,7 @@ impl fmt::Debug for Options { .field("run_check", &self.run_check) .field("no_run", &self.no_run) .field("nocapture", &self.nocapture) + .field("scrape_examples", &self.scrape_examples) .finish() } } @@ -285,7 +286,7 @@ crate struct RenderOptions { crate emit: Vec, /// If `true`, HTML source pages will generate links for items to their definition. crate generate_link_to_definition: bool, - crate call_locations: Option, + crate call_locations: AllCallLocations, } #[derive(Copy, Clone, Debug, PartialEq, Eq)] diff --git a/src/librustdoc/html/highlight.rs b/src/librustdoc/html/highlight.rs index b0e907cb0590d..a97dd95dcb661 100644 --- a/src/librustdoc/html/highlight.rs +++ b/src/librustdoc/html/highlight.rs @@ -31,7 +31,9 @@ crate struct ContextInfo<'a, 'b, 'c> { crate root_path: &'c str, } -crate type DecorationInfo = FxHashMap<&'static str, Vec<(u32, u32)>>; +/// Decorations are represented as a map from CSS class to vector of character ranges. +/// Each range will be wrapped in a span with that class. +crate struct DecorationInfo(crate FxHashMap<&'static str, Vec<(u32, u32)>>); /// Highlights `src`, returning the HTML output. crate fn render_with_highlighting( @@ -273,6 +275,7 @@ struct Decorations { impl Decorations { fn new(info: DecorationInfo) -> Self { let (starts, ends) = info + .0 .into_iter() .map(|(kind, ranges)| ranges.into_iter().map(move |(lo, hi)| ((lo, kind), hi))) .flatten() @@ -305,6 +308,7 @@ impl<'a> Classifier<'a> { decoration_info: Option, ) -> Classifier<'_> { let tokens = PeekIter::new(TokenIter { src }); + let decorations = decoration_info.map(Decorations::new); Classifier { tokens, in_attribute: false, diff --git a/src/librustdoc/html/render/context.rs b/src/librustdoc/html/render/context.rs index b99d2fe5aa0d1..fe684fd79a1f5 100644 --- a/src/librustdoc/html/render/context.rs +++ b/src/librustdoc/html/render/context.rs @@ -35,6 +35,7 @@ use crate::html::format::Buffer; use crate::html::markdown::{self, plain_text_summary, ErrorCodes, IdMap}; use crate::html::static_files::PAGE; use crate::html::{layout, sources}; +use crate::scrape_examples::AllCallLocations; /// Major driving force in all rustdoc rendering. This contains information /// about where in the tree-like hierarchy rendering is occurring and controls @@ -124,6 +125,8 @@ crate struct SharedContext<'tcx> { crate span_correspondance_map: FxHashMap, /// The [`Cache`] used during rendering. crate cache: Cache, + + crate call_locations: AllCallLocations, } impl SharedContext<'_> { @@ -389,6 +392,7 @@ impl<'tcx> FormatRenderer<'tcx> for Context<'tcx> { generate_redirect_map, show_type_layout, generate_link_to_definition, + call_locations, .. } = options; @@ -480,6 +484,7 @@ impl<'tcx> FormatRenderer<'tcx> for Context<'tcx> { templates, span_correspondance_map: matches, cache, + call_locations, }; // Add the default themes to the `Vec` of stylepaths diff --git a/src/librustdoc/html/render/mod.rs b/src/librustdoc/html/render/mod.rs index 6b30abb8260d2..cd5316240d00f 100644 --- a/src/librustdoc/html/render/mod.rs +++ b/src/librustdoc/html/render/mod.rs @@ -73,8 +73,9 @@ use crate::html::format::{ print_generic_bounds, print_where_clause, Buffer, HrefError, PrintWithSpace, }; use crate::html::markdown::{HeadingOffset, Markdown, MarkdownHtml, MarkdownSummaryLine}; +use crate::html::highlight; use crate::html::sources; -use crate::scrape_examples::{CallData, FnCallLocations}; +use crate::scrape_examples::CallData; /// A pair of name and its optional document. crate type NameDoc = (String, Option); @@ -592,9 +593,13 @@ fn document_full_inner( } } - match &*item.kind { + let kind = match &*item.kind { + clean::ItemKind::StrippedItem(box kind) | kind => kind, + }; + + match kind { clean::ItemKind::FunctionItem(f) | clean::ItemKind::MethodItem(f, _) => { - render_call_locations(w, cx, &f.call_locations, item); + render_call_locations(w, cx, f.def_id, item); } _ => {} } @@ -2458,14 +2463,11 @@ fn collect_paths_for_type(first_ty: clean::Type, cache: &Cache) -> Vec { const MAX_FULL_EXAMPLES: usize = 5; /// Generates the HTML for example call locations generated via the --scrape-examples flag. -fn render_call_locations( - w: &mut Buffer, - cx: &Context<'_>, - call_locations: &Option, - item: &clean::Item, -) { - let call_locations = match call_locations.as_ref() { - Some(call_locations) if call_locations.len() > 0 => call_locations, +fn render_call_locations(w: &mut Buffer, cx: &Context<'_>, def_id: DefId, item: &clean::Item) { + let tcx = cx.tcx(); + let key = crate::scrape_examples::def_id_call_key(tcx, def_id); + let call_locations = match cx.shared.call_locations.get(&key) { + Some(call_locations) => call_locations, _ => { return; } @@ -2483,7 +2485,6 @@ fn render_call_locations( ); // Generate the HTML for a single example, being the title and code block - let tcx = cx.tcx(); let write_example = |w: &mut Buffer, (path, call_data): (&PathBuf, &CallData)| -> bool { let contents = match fs::read_to_string(&path) { Ok(contents) => contents, @@ -2497,40 +2498,51 @@ fn render_call_locations( // To reduce file sizes, we only want to embed the source code needed to understand the example, not // the entire file. So we find the smallest byte range that covers all items enclosing examples. - assert!(call_data.locations.len() > 0); + assert!(!call_data.locations.is_empty()); let min_loc = call_data.locations.iter().min_by_key(|loc| loc.enclosing_item.byte_span.0).unwrap(); - let min_byte = min_loc.enclosing_item.byte_span.0; - let min_line = min_loc.enclosing_item.line_span.0; - let max_byte = + let (byte_offset, _) = min_loc.enclosing_item.byte_span; + let (line_offset, _) = min_loc.enclosing_item.line_span; + let byte_ceiling = call_data.locations.iter().map(|loc| loc.enclosing_item.byte_span.1).max().unwrap(); // The output code is limited to that byte range. - let contents_subset = &contents[(min_byte as usize)..(max_byte as usize)]; + let contents_subset = &contents[(byte_offset as usize)..(byte_ceiling as usize)]; // The call locations need to be updated to reflect that the size of the program has changed. - // Specifically, the ranges are all subtracted by `min_byte` since that's the new zero point. + // Specifically, the ranges are all subtracted by `byte_offset` since that's the new zero point. let (byte_ranges, line_ranges): (Vec<_>, Vec<_>) = call_data .locations .iter() .map(|loc| { let (byte_lo, byte_hi) = loc.call_expr.byte_span; let (line_lo, line_hi) = loc.call_expr.line_span; - ((byte_lo - min_byte, byte_hi - min_byte), (line_lo - min_line, line_hi - min_line)) + ( + (byte_lo - byte_offset, byte_hi - byte_offset), + (line_lo - line_offset, line_hi - line_offset), + ) }) .unzip(); - let edition = cx.shared.edition(); + let (init_min, init_max) = line_ranges[0]; + let line_range = if init_min == init_max { + format!("line {}", init_min + line_offset + 1) + } else { + format!("lines {}-{}", init_min + line_offset + 1, init_max + line_offset + 1) + }; + write!( w, - r#"
    + r#"
    - {name} [src] + {name} ({line_range})
    "#, root = cx.root_path(), url = call_data.url, name = call_data.display_name, + line_range = line_range, + offset = line_offset, // The locations are encoded as a data attribute, so they can be read // later by the JS for interactions. locations = serde_json::to_string(&line_ranges).unwrap(), @@ -2551,8 +2563,8 @@ fn render_call_locations( _ => false, })?; Some(rustc_span::Span::with_root_ctxt( - file.start_pos + BytePos(min_byte), - file.start_pos + BytePos(max_byte), + file.start_pos + BytePos(byte_offset), + file.start_pos + BytePos(byte_ceiling), )) })() .unwrap_or(rustc_span::DUMMY_SP); @@ -2566,12 +2578,12 @@ fn render_call_locations( sources::print_src( w, contents_subset, - edition, + call_data.edition, file_span, cx, &root_path, - Some(min_line), - Some(decoration_info), + Some(line_offset), + Some(highlight::DecorationInfo(decoration_info)), ); write!(w, "
    "); @@ -2590,12 +2602,14 @@ fn render_call_locations( }; let mut locs = call_locations.into_iter().collect::>(); - locs.sort_by_key(|x| sort_criterion(x)); + locs.sort_by_key(sort_criterion); locs }; - // Write just one example that's visible by default in the method's description. let mut it = ordered_locations.into_iter().peekable(); + + // An example may fail to write if its source can't be read for some reason, so this method + // continues iterating until a write suceeds let write_and_skip_failure = |w: &mut Buffer, it: &mut Peekable<_>| { while let Some(example) = it.next() { if write_example(&mut *w, example) { @@ -2604,6 +2618,7 @@ fn render_call_locations( } }; + // Write just one example that's visible by default in the method's description. write_and_skip_failure(w, &mut it); // Then add the remaining examples in a hidden section. @@ -2626,7 +2641,7 @@ fn render_call_locations( write_and_skip_failure(w, &mut it); } - // For the remaining examples, generate a
      containing links to the source files. + // For the remaining examples, generate a
        containing links to the source files. if it.peek().is_some() { write!( w, @@ -2635,7 +2650,7 @@ fn render_call_locations( it.for_each(|(_, call_data)| { write!( w, - r#"
      • {}
      • "#, + r#"
      • {name}
      • "#, root = cx.root_path(), url = call_data.url, name = call_data.display_name diff --git a/src/librustdoc/html/static/css/rustdoc.css b/src/librustdoc/html/static/css/rustdoc.css index b35412bdc2a90..b7d9a30beb169 100644 --- a/src/librustdoc/html/static/css/rustdoc.css +++ b/src/librustdoc/html/static/css/rustdoc.css @@ -1978,10 +1978,6 @@ details.undocumented[open] > summary::before { font-family: 'Fira Sans'; } -.scraped-example-title a { - margin-left: 5px; -} - .scraped-example:not(.expanded) .code-wrapper pre.line-numbers, .scraped-example:not(.expanded) .code-wrapper .example-wrap pre.rust { overflow: hidden; diff --git a/src/librustdoc/html/static/js/main.js b/src/librustdoc/html/static/js/main.js index 1b924991139b4..be63e129f3d91 100644 --- a/src/librustdoc/html/static/js/main.js +++ b/src/librustdoc/html/static/js/main.js @@ -996,9 +996,11 @@ function hideThemeButtonState() { function updateScrapedExample(example) { var locs = JSON.parse(example.attributes.getNamedItem("data-locs").textContent); + var offset = parseInt(example.attributes.getNamedItem("data-offset").textContent); var locIndex = 0; var highlights = example.querySelectorAll('.highlight'); + var link = example.querySelector('.scraped-example-title a'); addClass(highlights[0], 'focus'); if (locs.length > 1) { // Toggle through list of examples in a given file @@ -1007,13 +1009,36 @@ function hideThemeButtonState() { f(); scrollToLoc(example, locs[locIndex]); addClass(highlights[locIndex], 'focus'); + + var curLoc = locs[locIndex]; + var minLine = curLoc[0] + offset + 1; + var maxLine = curLoc[1] + offset + 1; + + var text; + var anchor; + if (minLine == maxLine) { + text = 'line ' + minLine.toString(); + anchor = minLine.toString(); + } else { + var range = minLine.toString() + '-' + maxLine.toString(); + text = 'lines ' + range; + anchor = range; + } + + var url = new URL(link.href); + url.hash = anchor; + + link.href = url.toString(); + link.innerHTML = text; }; + example.querySelector('.prev') .addEventListener('click', function() { onChangeLoc(function() { locIndex = (locIndex - 1 + locs.length) % locs.length; }); }); + example.querySelector('.next') .addEventListener('click', function() { onChangeLoc(function() { locIndex = (locIndex + 1) % locs.length; }); diff --git a/src/librustdoc/json/conversions.rs b/src/librustdoc/json/conversions.rs index 866514d7c9b5b..b151f62c1bff2 100644 --- a/src/librustdoc/json/conversions.rs +++ b/src/librustdoc/json/conversions.rs @@ -289,7 +289,7 @@ crate fn from_fn_header(header: &rustc_hir::FnHeader) -> HashSet { impl FromWithTcx for Function { fn from_tcx(function: clean::Function, tcx: TyCtxt<'_>) -> Self { - let clean::Function { decl, generics, header, call_locations: _ } = function; + let clean::Function { decl, generics, header, def_id: _ } = function; Function { decl: decl.into_tcx(tcx), generics: generics.into_tcx(tcx), @@ -530,7 +530,7 @@ crate fn from_function_method( has_body: bool, tcx: TyCtxt<'_>, ) -> Method { - let clean::Function { header, decl, generics, call_locations: _ } = function; + let clean::Function { header, decl, generics, def_id: _ } = function; Method { decl: decl.into_tcx(tcx), generics: generics.into_tcx(tcx), diff --git a/src/librustdoc/lib.rs b/src/librustdoc/lib.rs index b7407ee409f34..71a62ec5f0db6 100644 --- a/src/librustdoc/lib.rs +++ b/src/librustdoc/lib.rs @@ -47,11 +47,13 @@ extern crate rustc_interface; extern crate rustc_lexer; extern crate rustc_lint; extern crate rustc_lint_defs; +extern crate rustc_macros; extern crate rustc_metadata; extern crate rustc_middle; extern crate rustc_parse; extern crate rustc_passes; extern crate rustc_resolve; +extern crate rustc_serialize; extern crate rustc_session; extern crate rustc_span; extern crate rustc_target; @@ -619,8 +621,22 @@ fn opts() -> Vec { "Make the identifiers in the HTML source code pages navigable", ) }), - unstable("scrape-examples", |o| o.optopt("", "scrape-examples", "", "")), - unstable("with-examples", |o| o.optmulti("", "with-examples", "", "")), + unstable("scrape-examples", |o| { + o.optopt( + "", + "scrape-examples", + "", + "collect function call information (for use with `--with-examples`)", + ) + }), + unstable("with-examples", |o| { + o.optmulti( + "", + "with-examples", + "", + "path to function call information (for displaying examples in the documentation)", + ) + }), ] } diff --git a/src/librustdoc/scrape_examples.rs b/src/librustdoc/scrape_examples.rs index 3887647ca0a4c..a3d61fa4d2c5e 100644 --- a/src/librustdoc/scrape_examples.rs +++ b/src/librustdoc/scrape_examples.rs @@ -13,14 +13,19 @@ use rustc_hir::{ HirId, }; use rustc_interface::interface; +use rustc_macros::{Decodable, Encodable}; use rustc_middle::hir::map::Map; use rustc_middle::ty::{self, TyCtxt}; -use rustc_span::{def_id::DefId, BytePos, FileName, SourceFile}; -use serde::{Deserialize, Serialize}; +use rustc_serialize::{ + opaque::{Decoder, FileEncoder}, + Decodable, Encodable, +}; +use rustc_span::{def_id::DefId, edition::Edition, BytePos, FileName, SourceFile}; + use std::fs; use std::path::PathBuf; -#[derive(Serialize, Deserialize, Debug, Clone)] +#[derive(Encodable, Decodable, Debug, Clone)] crate struct SyntaxRange { crate byte_span: (u32, u32), crate line_span: (usize, usize), @@ -38,7 +43,7 @@ impl SyntaxRange { } } -#[derive(Serialize, Deserialize, Debug, Clone)] +#[derive(Encodable, Decodable, Debug, Clone)] crate struct CallLocation { crate call_expr: SyntaxRange, crate enclosing_item: SyntaxRange, @@ -49,9 +54,10 @@ impl CallLocation { tcx: TyCtxt<'_>, expr_span: rustc_span::Span, expr_id: HirId, - source_file: &rustc_span::SourceFile, + source_file: &SourceFile, ) -> Self { - let enclosing_item_span = tcx.hir().span_with_body(tcx.hir().get_parent_item(expr_id)); + let enclosing_item_span = + tcx.hir().span_with_body(tcx.hir().get_parent_item(expr_id)).source_callsite(); assert!(enclosing_item_span.contains(expr_span)); CallLocation { @@ -61,15 +67,16 @@ impl CallLocation { } } -#[derive(Serialize, Deserialize, Debug, Clone)] +#[derive(Encodable, Decodable, Debug, Clone)] crate struct CallData { crate locations: Vec, crate url: String, crate display_name: String, + crate edition: Edition, } -crate type DefIdCallKey = String; + crate type FnCallLocations = FxHashMap; -crate type AllCallLocations = FxHashMap; +crate type AllCallLocations = FxHashMap; /// Visitor for traversing a crate and finding instances of function calls. struct FindCalls<'a, 'tcx> { @@ -79,7 +86,7 @@ struct FindCalls<'a, 'tcx> { calls: &'a mut AllCallLocations, } -crate fn def_id_call_key(tcx: TyCtxt<'_>, def_id: DefId) -> DefIdCallKey { +crate fn def_id_call_key(tcx: TyCtxt<'_>, def_id: DefId) -> String { format!( "{}{}", tcx.crate_name(def_id.krate).to_ident_string(), @@ -101,30 +108,29 @@ where intravisit::walk_expr(self, ex); // Get type of function if expression is a function call + let tcx = self.tcx; let (ty, span) = match ex.kind { hir::ExprKind::Call(f, _) => { - let types = self.tcx.typeck(ex.hir_id.owner); + let types = tcx.typeck(ex.hir_id.owner); (types.node_type(f.hir_id), ex.span) } hir::ExprKind::MethodCall(_, _, _, span) => { - let types = self.tcx.typeck(ex.hir_id.owner); + let types = tcx.typeck(ex.hir_id.owner); let def_id = types.type_dependent_def_id(ex.hir_id).unwrap(); - (self.tcx.type_of(def_id), span) + (tcx.type_of(def_id), span) } _ => { return; } }; - if span.from_expansion() { - return; - } + // We need to get the file the example originates in. If the call is contained + // in a macro, then trace the span back to the macro source (rather than macro definition). + let span = span.source_callsite(); // Save call site if the function resolves to a concrete definition if let ty::FnDef(def_id, _) = ty.kind() { - let fn_key = def_id_call_key(self.tcx, *def_id); - let entries = self.calls.entry(fn_key).or_insert_with(FxHashMap::default); - let file = self.tcx.sess.source_map().lookup_char_pos(span.lo()).file; + let file = tcx.sess.source_map().lookup_char_pos(span.lo()).file; let file_path = match file.name.clone() { FileName::Real(real_filename) => real_filename.into_local_path(), _ => None, @@ -133,18 +139,19 @@ where if let Some(file_path) = file_path { let abs_path = fs::canonicalize(file_path.clone()).unwrap(); let cx = &self.cx; - let location = CallLocation::new(self.tcx, span, ex.hir_id, &file); - - entries - .entry(abs_path) - .or_insert_with(|| { - let clean_span = crate::clean::types::Span::new(span); - let url = cx.href_from_span(clean_span).unwrap(); - let display_name = file_path.display().to_string(); - CallData { locations: Vec::new(), url, display_name } - }) - .locations - .push(location); + let mk_call_data = || { + let clean_span = crate::clean::types::Span::new(span); + let url = cx.href_from_span(clean_span).unwrap(); + let display_name = file_path.display().to_string(); + let edition = tcx.sess.edition(); + CallData { locations: Vec::new(), url, display_name, edition } + }; + + let fn_key = def_id_call_key(tcx, *def_id); + let fn_entries = self.calls.entry(fn_key).or_default(); + + let location = CallLocation::new(tcx, span, ex.hir_id, &file); + fn_entries.entry(abs_path).or_insert_with(mk_call_data).locations.push(location); } } } @@ -154,55 +161,54 @@ crate fn run( krate: clean::Crate, renderopts: config::RenderOptions, cache: formats::cache::Cache, - tcx: TyCtxt<'tcx>, + tcx: TyCtxt<'_>, example_path: PathBuf, ) -> interface::Result<()> { - let inner = move || { + let inner = move || -> Result<(), String> { // Generates source files for examples - let (cx, _) = Context::init(krate, renderopts, cache, tcx).map_err(|e| format!("{}", e))?; + let (cx, _) = Context::init(krate, renderopts, cache, tcx).map_err(|e| e.to_string())?; // Run call-finder on all items let mut calls = FxHashMap::default(); let mut finder = FindCalls { calls: &mut calls, tcx, map: tcx.hir(), cx }; tcx.hir().krate().visit_all_item_likes(&mut finder.as_deep_visitor()); - // Save output JSON to provided path - let calls_json = serde_json::to_string(&calls).map_err(|e| format!("{}", e))?; - fs::write(example_path, &calls_json).map_err(|e| format!("{}", e))?; + // Save output to provided path + let mut encoder = FileEncoder::new(example_path).map_err(|e| e.to_string())?; + calls.encode(&mut encoder).map_err(|e| e.to_string())?; + encoder.flush().map_err(|e| e.to_string())?; Ok(()) }; - inner().map_err(|e: String| { - eprintln!("{}", e); - rustc_errors::ErrorReported - }) + if let Err(e) = inner() { + tcx.sess.fatal(&e); + } + + Ok(()) } crate fn load_call_locations( with_examples: Vec, diag: &rustc_errors::Handler, -) -> Result, i32> { - let each_call_locations = with_examples - .into_iter() - .map(|path| { +) -> Result { + let inner = || { + let mut all_calls: AllCallLocations = FxHashMap::default(); + for path in with_examples { let bytes = fs::read(&path).map_err(|e| format!("{} (for path {})", e, path))?; - let calls: AllCallLocations = - serde_json::from_slice(&bytes).map_err(|e| format!("{}", e))?; - Ok(calls) - }) - .collect::, _>>() - .map_err(|e: String| { - diag.err(&format!("failed to load examples with error: {}", e)); - 1 - })?; - - Ok((each_call_locations.len() > 0).then(|| { - each_call_locations.into_iter().fold(FxHashMap::default(), |mut acc, map| { - for (function, calls) in map.into_iter() { - acc.entry(function).or_insert_with(FxHashMap::default).extend(calls.into_iter()); + let mut decoder = Decoder::new(&bytes, 0); + let calls = AllCallLocations::decode(&mut decoder)?; + + for (function, fn_calls) in calls.into_iter() { + all_calls.entry(function).or_default().extend(fn_calls.into_iter()); } - acc - }) - })) + } + + Ok(all_calls) + }; + + inner().map_err(|e: String| { + diag.err(&format!("failed to load examples: {}", e)); + 1 + }) } From 5c05b3c03da5d37e544589e42fe9738728b27b40 Mon Sep 17 00:00:00 2001 From: Will Crichton Date: Mon, 20 Sep 2021 14:08:33 -0700 Subject: [PATCH 10/24] Add target crates as inputs to reduce size of intermediates Add tests for module-path remapping and scrape example options Find all crates with a given name --- src/librustdoc/clean/mod.rs | 29 ++++----- src/librustdoc/config.rs | 12 ++-- src/librustdoc/lib.rs | 20 ++++-- src/librustdoc/scrape_examples.rs | 63 +++++++++++++++++-- .../rustdoc-scrape-examples-remap/Makefile | 22 +++++++ .../examples/ex.rs | 4 ++ .../rustdoc-scrape-examples-remap/src/a.rs | 1 + .../rustdoc-scrape-examples-remap/src/lib.rs | 8 +++ .../rustdoc/scrape-examples-wrong-options.rs | 2 + 9 files changed, 130 insertions(+), 31 deletions(-) create mode 100644 src/test/run-make/rustdoc-scrape-examples-remap/Makefile create mode 100644 src/test/run-make/rustdoc-scrape-examples-remap/examples/ex.rs create mode 100644 src/test/run-make/rustdoc-scrape-examples-remap/src/a.rs create mode 100644 src/test/run-make/rustdoc-scrape-examples-remap/src/lib.rs create mode 100644 src/test/rustdoc/scrape-examples-wrong-options.rs diff --git a/src/librustdoc/clean/mod.rs b/src/librustdoc/clean/mod.rs index dded5c9934b83..409dc914597f7 100644 --- a/src/librustdoc/clean/mod.rs +++ b/src/librustdoc/clean/mod.rs @@ -1059,20 +1059,22 @@ impl Clean for ty::AssocItem { ty::ImplContainer(_) => Some(self.defaultness), ty::TraitContainer(_) => None, }; - let function = Function { - generics, - decl, - header: hir::FnHeader { - unsafety: sig.unsafety(), - abi: sig.abi(), - constness, - asyncness, + MethodItem( + Function { + generics, + decl, + header: hir::FnHeader { + unsafety: sig.unsafety(), + abi: sig.abi(), + constness, + asyncness, + }, + def_id: self.def_id, }, - def_id: self.def_id, - }; - MethodItem(function, defaultness) + defaultness, + ) } else { - let function = Function { + TyMethodItem(Function { generics, decl, header: hir::FnHeader { @@ -1082,8 +1084,7 @@ impl Clean for ty::AssocItem { asyncness: hir::IsAsync::NotAsync, }, def_id: self.def_id, - }; - TyMethodItem(function) + }) } } ty::AssocKind::Type => { diff --git a/src/librustdoc/config.rs b/src/librustdoc/config.rs index 54b62db791748..7342478c3ec0d 100644 --- a/src/librustdoc/config.rs +++ b/src/librustdoc/config.rs @@ -25,7 +25,7 @@ use crate::html::render::StylePath; use crate::html::static_files; use crate::opts; use crate::passes::{self, Condition, DefaultPassOption}; -use crate::scrape_examples::AllCallLocations; +use crate::scrape_examples::{AllCallLocations, ScrapeExamplesOptions}; use crate::theme; #[derive(Clone, Copy, PartialEq, Eq, Debug)] @@ -160,9 +160,9 @@ crate struct Options { /// Whether to skip capturing stdout and stderr of tests. crate nocapture: bool, - /// Path to output file to write JSON of call sites. If this option is Some(..) then + /// Configuration for scraping examples from the current crate. If this option is Some(..) then /// the compiler will scrape examples and not generate documentation. - crate scrape_examples: Option, + crate scrape_examples_options: Option, } impl fmt::Debug for Options { @@ -207,7 +207,7 @@ impl fmt::Debug for Options { .field("run_check", &self.run_check) .field("no_run", &self.no_run) .field("nocapture", &self.nocapture) - .field("scrape_examples", &self.scrape_examples) + .field("scrape_examples_options", &self.scrape_examples_options) .finish() } } @@ -678,7 +678,7 @@ impl Options { return Err(1); } - let scrape_examples = matches.opt_str("scrape-examples").map(PathBuf::from); + let scrape_examples_options = ScrapeExamplesOptions::new(&matches, &diag)?; let with_examples = matches.opt_strs("with-examples"); let call_locations = crate::scrape_examples::load_call_locations(with_examples, &diag)?; @@ -753,7 +753,7 @@ impl Options { crate_name, output_format, json_unused_externs, - scrape_examples, + scrape_examples_options, }) } diff --git a/src/librustdoc/lib.rs b/src/librustdoc/lib.rs index 71a62ec5f0db6..1efe0c7a54c5a 100644 --- a/src/librustdoc/lib.rs +++ b/src/librustdoc/lib.rs @@ -621,12 +621,20 @@ fn opts() -> Vec { "Make the identifiers in the HTML source code pages navigable", ) }), - unstable("scrape-examples", |o| { + unstable("scrape-examples-output-path", |o| { o.optopt( "", - "scrape-examples", + "scrape-examples-output-path", "", - "collect function call information (for use with `--with-examples`)", + "collect function call information and output at the given path", + ) + }), + unstable("scrape-examples-target-crate", |o| { + o.optmulti( + "", + "scrape-examples-target-crate", + "", + "collect function call information for functions from the target crate", ) }), unstable("with-examples", |o| { @@ -750,7 +758,7 @@ fn main_options(options: config::Options) -> MainResult { // FIXME: fix this clone (especially render_options) let manual_passes = options.manual_passes.clone(); let render_options = options.render_options.clone(); - let scrape_examples = options.scrape_examples.clone(); + let scrape_examples_options = options.scrape_examples_options.clone(); let config = core::create_config(options); interface::create_compiler_and_run(config, |compiler| { @@ -787,8 +795,8 @@ fn main_options(options: config::Options) -> MainResult { }); info!("finished with rustc"); - if let Some(example_path) = scrape_examples { - return scrape_examples::run(krate, render_opts, cache, tcx, example_path); + if let Some(options) = scrape_examples_options { + return scrape_examples::run(krate, render_opts, cache, tcx, options); } cache.crate_version = crate_version; diff --git a/src/librustdoc/scrape_examples.rs b/src/librustdoc/scrape_examples.rs index a3d61fa4d2c5e..62557fa5c7dca 100644 --- a/src/librustdoc/scrape_examples.rs +++ b/src/librustdoc/scrape_examples.rs @@ -20,11 +20,43 @@ use rustc_serialize::{ opaque::{Decoder, FileEncoder}, Decodable, Encodable, }; -use rustc_span::{def_id::DefId, edition::Edition, BytePos, FileName, SourceFile}; +use rustc_session::getopts; +use rustc_span::{ + def_id::{CrateNum, DefId}, + edition::Edition, + BytePos, FileName, SourceFile, +}; use std::fs; use std::path::PathBuf; +#[derive(Debug, Clone)] +crate struct ScrapeExamplesOptions { + output_path: PathBuf, + target_crates: Vec, +} + +impl ScrapeExamplesOptions { + crate fn new( + matches: &getopts::Matches, + diag: &rustc_errors::Handler, + ) -> Result, i32> { + let output_path = matches.opt_str("scrape-examples-output-path"); + let target_crates = matches.opt_strs("scrape-examples-target-crate"); + match (output_path, !target_crates.is_empty()) { + (Some(output_path), true) => Ok(Some(ScrapeExamplesOptions { + output_path: PathBuf::from(output_path), + target_crates, + })), + (Some(_), false) | (None, true) => { + diag.err(&format!("must use --scrape-examples-output-path and --scrape-examples-target-crate together")); + Err(1) + } + (None, false) => Ok(None), + } + } +} + #[derive(Encodable, Decodable, Debug, Clone)] crate struct SyntaxRange { crate byte_span: (u32, u32), @@ -83,6 +115,7 @@ struct FindCalls<'a, 'tcx> { tcx: TyCtxt<'tcx>, map: Map<'tcx>, cx: Context<'tcx>, + target_crates: Vec, calls: &'a mut AllCallLocations, } @@ -130,6 +163,11 @@ where // Save call site if the function resolves to a concrete definition if let ty::FnDef(def_id, _) = ty.kind() { + // Ignore functions not from the crate being documented + if self.target_crates.iter().all(|krate| *krate != def_id.krate) { + return; + } + let file = tcx.sess.source_map().lookup_char_pos(span.lo()).file; let file_path = match file.name.clone() { FileName::Real(real_filename) => real_filename.into_local_path(), @@ -143,7 +181,7 @@ where let clean_span = crate::clean::types::Span::new(span); let url = cx.href_from_span(clean_span).unwrap(); let display_name = file_path.display().to_string(); - let edition = tcx.sess.edition(); + let edition = span.edition(); CallData { locations: Vec::new(), url, display_name, edition } }; @@ -162,19 +200,34 @@ crate fn run( renderopts: config::RenderOptions, cache: formats::cache::Cache, tcx: TyCtxt<'_>, - example_path: PathBuf, + options: ScrapeExamplesOptions, ) -> interface::Result<()> { let inner = move || -> Result<(), String> { // Generates source files for examples let (cx, _) = Context::init(krate, renderopts, cache, tcx).map_err(|e| e.to_string())?; + // Collect CrateIds corresponding to provided target crates + // If two different versions of the crate in the dependency tree, then examples will be collcted from both. + let find_crates_with_name = |target_crate: String| { + tcx.crates(()) + .iter() + .filter(move |crate_num| tcx.crate_name(**crate_num).as_str() == target_crate) + .copied() + }; + let target_crates = options + .target_crates + .into_iter() + .map(find_crates_with_name) + .flatten() + .collect::>(); + // Run call-finder on all items let mut calls = FxHashMap::default(); - let mut finder = FindCalls { calls: &mut calls, tcx, map: tcx.hir(), cx }; + let mut finder = FindCalls { calls: &mut calls, tcx, map: tcx.hir(), cx, target_crates }; tcx.hir().krate().visit_all_item_likes(&mut finder.as_deep_visitor()); // Save output to provided path - let mut encoder = FileEncoder::new(example_path).map_err(|e| e.to_string())?; + let mut encoder = FileEncoder::new(options.output_path).map_err(|e| e.to_string())?; calls.encode(&mut encoder).map_err(|e| e.to_string())?; encoder.flush().map_err(|e| e.to_string())?; diff --git a/src/test/run-make/rustdoc-scrape-examples-remap/Makefile b/src/test/run-make/rustdoc-scrape-examples-remap/Makefile new file mode 100644 index 0000000000000..823ae37f6efe1 --- /dev/null +++ b/src/test/run-make/rustdoc-scrape-examples-remap/Makefile @@ -0,0 +1,22 @@ +-include ../../run-make-fulldeps/tools.mk + +OUTPUT_DIR := "$(TMPDIR)/rustdoc" + +all: +# 1. compile the library crate and emit an rmeta + $(RUSTC) src/lib.rs --crate-name foobar --crate-type lib --emit=metadata + +# 2. scrape examples from the reverse-dependency into an ex.calls file + $(RUSTDOC) examples/ex.rs --crate-name ex --crate-type bin \ + --extern foobar=$(TMPDIR)/libfoobar.rmeta \ + -Z unstable-options \ + --scrape-examples-output-path $(TMPDIR)/ex.calls \ + --scrape-examples-target-crate foobar + +# 3. pass those examples to rustdoc when documenting the library crate + $(RUSTDOC) src/lib.rs --crate-name foobar --crate-type lib --output $(OUTPUT_DIR) \ + -Z unstable-options \ + --with-examples $(TMPDIR)/ex.calls + +# 4. check that the examples were scraped successfully + $(HTMLDOCCK) $(OUTPUT_DIR) src/lib.rs diff --git a/src/test/run-make/rustdoc-scrape-examples-remap/examples/ex.rs b/src/test/run-make/rustdoc-scrape-examples-remap/examples/ex.rs new file mode 100644 index 0000000000000..1438fdba7072c --- /dev/null +++ b/src/test/run-make/rustdoc-scrape-examples-remap/examples/ex.rs @@ -0,0 +1,4 @@ +fn main() { + foobar::b::foo(); + foobar::c::foo(); +} diff --git a/src/test/run-make/rustdoc-scrape-examples-remap/src/a.rs b/src/test/run-make/rustdoc-scrape-examples-remap/src/a.rs new file mode 100644 index 0000000000000..b76b4321d62aa --- /dev/null +++ b/src/test/run-make/rustdoc-scrape-examples-remap/src/a.rs @@ -0,0 +1 @@ +pub fn foo() {} diff --git a/src/test/run-make/rustdoc-scrape-examples-remap/src/lib.rs b/src/test/run-make/rustdoc-scrape-examples-remap/src/lib.rs new file mode 100644 index 0000000000000..b021f27da56da --- /dev/null +++ b/src/test/run-make/rustdoc-scrape-examples-remap/src/lib.rs @@ -0,0 +1,8 @@ +// @has foobar/b/fn.foo.html '//*[@class="scraped-example"]' 'ex.rs' +// @has foobar/c/fn.foo.html '//*[@class="scraped-example"]' 'ex.rs' + +#[path = "a.rs"] +pub mod b; + +#[path = "a.rs"] +pub mod c; diff --git a/src/test/rustdoc/scrape-examples-wrong-options.rs b/src/test/rustdoc/scrape-examples-wrong-options.rs new file mode 100644 index 0000000000000..419234d21b162 --- /dev/null +++ b/src/test/rustdoc/scrape-examples-wrong-options.rs @@ -0,0 +1,2 @@ +// should-fail +// compile-flags: --scrape-examples-target-crate foobar From df5e3a6e40b5e70f3e869f97a7ce800385913c28 Mon Sep 17 00:00:00 2001 From: Will Crichton Date: Tue, 21 Sep 2021 15:49:36 -0700 Subject: [PATCH 11/24] Change serialized format to use DefPathHash instead of custom String Move test to rustdoc-ui Fix test writing to wrong directory Formatting Fix test Add FIXME Remove raw multiline strings --- src/librustdoc/html/highlight.rs | 6 +-- src/librustdoc/html/highlight/tests.rs | 2 +- src/librustdoc/html/render/mod.rs | 41 +++++++++---------- src/librustdoc/scrape_examples.rs | 14 ++----- .../rustdoc-scrape-examples-remap/Makefile | 2 +- .../scrape-examples-wrong-options.rs | 1 - .../scrape-examples-wrong-options.stderr | 2 + 7 files changed, 27 insertions(+), 41 deletions(-) rename src/test/{rustdoc => rustdoc-ui}/scrape-examples-wrong-options.rs (78%) create mode 100644 src/test/rustdoc-ui/scrape-examples-wrong-options.stderr diff --git a/src/librustdoc/html/highlight.rs b/src/librustdoc/html/highlight.rs index a97dd95dcb661..8ef5fabb9017f 100644 --- a/src/librustdoc/html/highlight.rs +++ b/src/librustdoc/html/highlight.rs @@ -258,11 +258,7 @@ impl Iterator for PeekIter<'a> { type Item = (TokenKind, &'a str); fn next(&mut self) -> Option { self.peek_pos = 0; - if let Some(first) = self.stored.pop_front() { - Some(first) - } else { - self.iter.next() - } + if let Some(first) = self.stored.pop_front() { Some(first) } else { self.iter.next() } } } diff --git a/src/librustdoc/html/highlight/tests.rs b/src/librustdoc/html/highlight/tests.rs index 405bdf0d8108e..3fa386dded9b3 100644 --- a/src/librustdoc/html/highlight/tests.rs +++ b/src/librustdoc/html/highlight/tests.rs @@ -60,7 +60,7 @@ fn test_union_highlighting() { create_default_session_globals_then(|| { let src = include_str!("fixtures/union.rs"); let mut html = Buffer::new(); - write_code(&mut html, src, Edition::Edition2018, None); + write_code(&mut html, src, Edition::Edition2018, None, None); expect_file!["fixtures/union.html"].assert_eq(&html.into_inner()); }); } diff --git a/src/librustdoc/html/render/mod.rs b/src/librustdoc/html/render/mod.rs index cd5316240d00f..2454ae492b213 100644 --- a/src/librustdoc/html/render/mod.rs +++ b/src/librustdoc/html/render/mod.rs @@ -2465,7 +2465,7 @@ const MAX_FULL_EXAMPLES: usize = 5; /// Generates the HTML for example call locations generated via the --scrape-examples flag. fn render_call_locations(w: &mut Buffer, cx: &Context<'_>, def_id: DefId, item: &clean::Item) { let tcx = cx.tcx(); - let key = crate::scrape_examples::def_id_call_key(tcx, def_id); + let key = tcx.def_path_hash(def_id); let call_locations = match cx.shared.call_locations.get(&key) { Some(call_locations) => call_locations, _ => { @@ -2474,13 +2474,14 @@ fn render_call_locations(w: &mut Buffer, cx: &Context<'_>, def_id: DefId, item: }; // Generate a unique ID so users can link to this section for a given method + // FIXME: this should use init_id_map instead of derive let id = cx.id_map.borrow_mut().derive("scraped-examples"); write!( w, - r##"
        -

        - Examples found in repository -

        "##, + "
        \ +

        \ + Examples found in repository\ +

        ", id ); @@ -2533,11 +2534,11 @@ fn render_call_locations(w: &mut Buffer, cx: &Context<'_>, def_id: DefId, item: write!( w, - r#"
        -
        - {name} ({line_range}) -
        -
        "#, + "
        \ +
        \ + {name} ({line_range})\ +
        \ +
        ", root = cx.root_path(), url = call_data.url, name = call_data.display_name, @@ -2625,14 +2626,13 @@ fn render_call_locations(w: &mut Buffer, cx: &Context<'_>, def_id: DefId, item: if it.peek().is_some() { write!( w, - r#"
        - - More examples - -
        -
        -
        -"# + "
        \ + \ + More examples\ + \ +
        \ +
        \ +
        " ); // Only generate inline code for MAX_FULL_EXAMPLES number of examples. Otherwise we could @@ -2643,10 +2643,7 @@ fn render_call_locations(w: &mut Buffer, cx: &Context<'_>, def_id: DefId, item: // For the remaining examples, generate a
          containing links to the source files. if it.peek().is_some() { - write!( - w, - r#"