diff --git a/src/doc/rustdoc/src/unstable-features.md b/src/doc/rustdoc/src/unstable-features.md index 917959976411c..51f365be922fa 100644 --- a/src/doc/rustdoc/src/unstable-features.md +++ b/src/doc/rustdoc/src/unstable-features.md @@ -455,3 +455,27 @@ Calculating code examples follows these rules: * static * typedef 2. If one of the previously listed items has a code example, then it'll be counted. + +### `--with-examples`: include examples of uses of items as documentation + +This option, combined with `--scrape-examples-target-crate` and +`--scrape-examples-output-path`, is used to implement the functionality in [RFC +#3123](https://github.com/rust-lang/rfcs/pull/3123). Uses of an item (currently +functions / call-sites) are found in a crate and its reverse-dependencies, and +then the uses are included as documentation for that item. This feature is +intended to be used via `cargo doc --scrape-examples`, but the rustdoc-only +workflow looks like: + +```bash +$ rustdoc examples/ex.rs -Z unstable-options \ + --extern foobar=target/deps/libfoobar.rmeta \ + --scrape-examples-target-crate foobar \ + --scrape-examples-output-path output.calls +$ rustdoc src/lib.rs -Z unstable-options --with-examples output.calls +``` + +First, the library must be checked to generate an `rmeta`. Then a +reverse-dependency like `examples/ex.rs` is given to rustdoc with the target +crate being documented (`foobar`) and a path to output the calls +(`output.calls`). Then, the generated calls file can be passed via +`--with-examples` to the subsequent documentation of `foobar`. diff --git a/src/librustdoc/clean/mod.rs b/src/librustdoc/clean/mod.rs index 075efd29b5969..7a1c561c8e531 100644 --- a/src/librustdoc/clean/mod.rs +++ b/src/librustdoc/clean/mod.rs @@ -2062,7 +2062,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()); diff --git a/src/librustdoc/config.rs b/src/librustdoc/config.rs index ac440a395155c..7342478c3ec0d 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, ScrapeExamplesOptions}; use crate::theme; #[derive(Clone, Copy, PartialEq, Eq, Debug)] @@ -158,6 +159,10 @@ crate struct Options { crate json_unused_externs: bool, /// Whether to skip capturing stdout and stderr of tests. crate nocapture: bool, + + /// 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_options: Option, } impl fmt::Debug for Options { @@ -202,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_options", &self.scrape_examples_options) .finish() } } @@ -280,6 +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: AllCallLocations, } #[derive(Copy, Clone, Debug, PartialEq, Eq)] @@ -671,6 +678,10 @@ impl Options { return Err(1); } + 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)?; + let (lint_opts, describe_lints, lint_cap) = get_cmd_lint_options(matches, error_format); Ok(Options { @@ -737,10 +748,12 @@ impl Options { ), emit, generate_link_to_definition, + call_locations, }, crate_name, output_format, json_unused_externs, + scrape_examples_options, }) } diff --git a/src/librustdoc/html/highlight.rs b/src/librustdoc/html/highlight.rs index 8ed69962875a6..fa8ad2a37e726 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,10 @@ crate struct ContextInfo<'a, 'b, 'c> { crate root_path: &'c str, } +/// 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( src: &str, @@ -40,6 +45,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 +62,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 +95,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 +139,7 @@ enum Class { PreludeTy, PreludeVal, QuestionMark, + Decoration(&'static str), } impl Class { @@ -150,6 +163,7 @@ impl Class { Class::PreludeTy => "prelude-ty", Class::PreludeVal => "prelude-val", Class::QuestionMark => "question-mark", + Class::Decoration(kind) => kind, } } @@ -248,6 +262,24 @@ impl Iterator for PeekIter<'a> { } } +/// 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 + .0 + .into_iter() + .map(|(kind, ranges)| ranges.into_iter().map(move |(lo, hi)| ((lo, kind), hi))) + .flatten() + .unzip(); + Decorations { starts, ends } + } +} + /// Processes program tokens, classifying strings of text by highlighting /// category (`Class`). struct Classifier<'a> { @@ -259,13 +291,20 @@ 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 }); + let decorations = decoration_info.map(Decorations::new); Classifier { tokens, in_attribute: false, @@ -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() @@ -657,7 +710,7 @@ fn string( // https://github.com/rust-lang/rust/blob/60f1a2fc4b535ead9c85ce085fdce49b1b097531/src/librustdoc/html/render/context.rs#L315-L338 match href { LinkFromSrc::Local(span) => context - .href_from_span(*span) + .href_from_span(*span, true) .map(|s| format!("{}{}", context_info.root_path, s)), LinkFromSrc::External(def_id) => { format::href_with_root_path(*def_id, context, Some(context_info.root_path)) diff --git a/src/librustdoc/html/highlight/fixtures/decorations.html b/src/librustdoc/html/highlight/fixtures/decorations.html new file mode 100644 index 0000000000000..45f567880c9d9 --- /dev/null +++ b/src/librustdoc/html/highlight/fixtures/decorations.html @@ -0,0 +1,2 @@ +let x = 1; +let y = 2; \ No newline at end of file diff --git a/src/librustdoc/html/highlight/tests.rs b/src/librustdoc/html/highlight/tests.rs index 450bbfea1ea86..1fea7e983b448 100644 --- a/src/librustdoc/html/highlight/tests.rs +++ b/src/librustdoc/html/highlight/tests.rs @@ -1,6 +1,7 @@ -use super::write_code; +use super::{write_code, DecorationInfo}; use crate::html::format::Buffer; use expect_test::expect_file; +use rustc_data_structures::fx::FxHashMap; use rustc_span::create_default_session_globals_then; use rustc_span::edition::Edition; @@ -22,7 +23,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 +37,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 +51,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()); }); } @@ -60,7 +61,21 @@ 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()); }); } + +#[test] +fn test_decorations() { + create_default_session_globals_then(|| { + let src = "let x = 1; +let y = 2;"; + let mut decorations = FxHashMap::default(); + decorations.insert("example", vec![(0, 10)]); + + let mut html = Buffer::new(); + write_code(&mut html, src, Edition::Edition2018, None, Some(DecorationInfo(decorations))); + expect_file!["fixtures/decorations.html"].assert_eq(&html.into_inner()); + }); +} diff --git a/src/librustdoc/html/layout.rs b/src/librustdoc/html/layout.rs index 6ed603c96bbf2..bd06f88cb3587 100644 --- a/src/librustdoc/html/layout.rs +++ b/src/librustdoc/html/layout.rs @@ -22,6 +22,8 @@ crate struct Layout { /// If false, the `select` element to have search filtering by crates on rendered docs /// won't be generated. crate generate_search_filter: bool, + /// If true, then scrape-examples.js will be included in the output HTML file + crate scrape_examples_extension: bool, } #[derive(Serialize)] diff --git a/src/librustdoc/html/markdown.rs b/src/librustdoc/html/markdown.rs index c46439b851050..bda0f0aa3f13e 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/context.rs b/src/librustdoc/html/render/context.rs index 011d3cfcf72d7..d7ef8513d6a89 100644 --- a/src/librustdoc/html/render/context.rs +++ b/src/librustdoc/html/render/context.rs @@ -34,6 +34,7 @@ use crate::html::escape::Escape; use crate::html::format::Buffer; use crate::html::markdown::{self, plain_text_summary, ErrorCodes, IdMap}; 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 @@ -123,6 +124,8 @@ crate struct SharedContext<'tcx> { crate span_correspondance_map: FxHashMap, /// The [`Cache`] used during rendering. crate cache: Cache, + + crate call_locations: AllCallLocations, } impl SharedContext<'_> { @@ -291,10 +294,10 @@ impl<'tcx> Context<'tcx> { /// may happen, for example, with externally inlined items where the source /// of their crate documentation isn't known. pub(super) fn src_href(&self, item: &clean::Item) -> Option { - self.href_from_span(item.span(self.tcx())) + self.href_from_span(item.span(self.tcx()), true) } - crate fn href_from_span(&self, span: clean::Span) -> Option { + crate fn href_from_span(&self, span: clean::Span, with_lines: bool) -> Option { if span.is_dummy() { return None; } @@ -341,16 +344,26 @@ impl<'tcx> Context<'tcx> { (&*symbol, &path) }; - let loline = span.lo(self.sess()).line; - let hiline = span.hi(self.sess()).line; - let lines = - if loline == hiline { loline.to_string() } else { format!("{}-{}", loline, hiline) }; + let anchor = if with_lines { + let loline = span.lo(self.sess()).line; + let hiline = span.hi(self.sess()).line; + format!( + "#{}", + if loline == hiline { + loline.to_string() + } else { + format!("{}-{}", loline, hiline) + } + ) + } else { + "".to_string() + }; Some(format!( - "{root}src/{krate}/{path}#{lines}", + "{root}src/{krate}/{path}{anchor}", root = Escape(&root), krate = krate, path = path, - lines = lines + anchor = anchor )) } } @@ -388,6 +401,7 @@ impl<'tcx> FormatRenderer<'tcx> for Context<'tcx> { generate_redirect_map, show_type_layout, generate_link_to_definition, + call_locations, .. } = options; @@ -412,6 +426,7 @@ impl<'tcx> FormatRenderer<'tcx> for Context<'tcx> { krate: krate.name.to_string(), css_file_extension: extension_css, generate_search_filter, + scrape_examples_extension: !call_locations.is_empty(), }; let mut issue_tracker_base_url = None; let mut include_sources = true; @@ -474,6 +489,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 69c5c2c4abc2a..bd6cb9c298842 100644 --- a/src/librustdoc/html/render/mod.rs +++ b/src/librustdoc/html/render/mod.rs @@ -40,20 +40,25 @@ 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::iter::Peekable; use std::path::PathBuf; use std::str; 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; 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}; @@ -68,7 +73,10 @@ use crate::html::format::{ href, print_abi_with_space, print_constness_with_space, print_default_space, print_generic_bounds, print_where_clause, Buffer, HrefError, PrintWithSpace, }; +use crate::html::highlight; use crate::html::markdown::{HeadingOffset, Markdown, MarkdownHtml, MarkdownSummaryLine}; +use crate::html::sources; +use crate::scrape_examples::CallData; /// A pair of name and its optional document. crate type NameDoc = (String, Option); @@ -585,6 +593,14 @@ fn document_full_inner( render_markdown(w, cx, &s, item.links(cx), heading_offset); } } + + let kind = match &*item.kind { + clean::ItemKind::StrippedItem(box kind) | kind => kind, + }; + + if let clean::ItemKind::FunctionItem(..) | clean::ItemKind::MethodItem(..) = kind { + render_call_locations(w, cx, item); + } } /// Add extra information about an item such as: @@ -2490,3 +2506,221 @@ fn collect_paths_for_type(first_ty: clean::Type, cache: &Cache) -> Vec { } out } + +const MAX_FULL_EXAMPLES: usize = 5; +const NUM_VISIBLE_LINES: usize = 10; + +/// Generates the HTML for example call locations generated via the --scrape-examples flag. +fn render_call_locations(w: &mut Buffer, cx: &Context<'_>, item: &clean::Item) { + let tcx = cx.tcx(); + let def_id = item.def_id.expect_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, + _ => { + 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, + "
\ + \ +
\ + Examples found in repository\ +
", + id = id + ); + + // Generate the HTML for a single example, being the title and code block + 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. + 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 byte_min = min_loc.enclosing_item.byte_span.0; + let line_min = min_loc.enclosing_item.line_span.0; + let max_loc = + call_data.locations.iter().max_by_key(|loc| loc.enclosing_item.byte_span.1).unwrap(); + let byte_max = max_loc.enclosing_item.byte_span.1; + let line_max = max_loc.enclosing_item.line_span.1; + + // The output code is limited to that byte range. + let contents_subset = &contents[(byte_min as usize)..(byte_max 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 `byte_min` since that's the new zero point. + let (mut 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; + let byte_range = (byte_lo - byte_min, byte_hi - byte_min); + let line_range = (line_lo - line_min, line_hi - line_min); + let (anchor, line_title) = if line_lo == line_hi { + (format!("{}", line_lo + 1), format!("line {}", line_lo + 1)) + } else { + ( + format!("{}-{}", line_lo + 1, line_hi + 1), + format!("lines {}-{}", line_lo + 1, line_hi + 1), + ) + }; + let line_url = format!("{}{}#{}", cx.root_path(), call_data.url, anchor); + + (byte_range, (line_range, line_url, line_title)) + }) + .unzip(); + + let (_, init_url, init_title) = &line_ranges[0]; + let needs_expansion = line_max - line_min > NUM_VISIBLE_LINES; + let locations_encoded = serde_json::to_string(&line_ranges).unwrap(); + + write!( + w, + "
\ +
\ + {name} ({title})\ +
\ +
", + expanded_cls = if needs_expansion { "" } else { "expanded" }, + name = call_data.display_name, + url = init_url, + title = init_title, + // The locations are encoded as a data attribute, so they can be read + // later by the JS for interactions. + locations = Escape(&locations_encoded) + ); + + if line_ranges.len() > 1 { + write!(w, r#" "#); + } + + if needs_expansion { + write!(w, r#""#); + } + + // 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(byte_min), + file.start_pos + BytePos(byte_max), + )) + })() + .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 focus", vec![byte_ranges.remove(0)]); + decoration_info.insert("highlight", byte_ranges); + + sources::print_src( + w, + contents_subset, + call_data.edition, + file_span, + cx, + &root_path, + Some(highlight::DecorationInfo(decoration_info)), + sources::SourceContext::Embedded { offset: line_min }, + ); + write!(w, "
"); + + true + }; + + // 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)| { + // 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 + }; + + let mut locs = call_locations.into_iter().collect::>(); + locs.sort_by_key(sort_criterion); + locs + }; + + 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) { + break; + } + } + }; + + // 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. + if it.peek().is_some() { + write!( + w, + "
\ + \ + More examples\ + \ +
\ +
\ +
" + ); + + // Only generate inline code for MAX_FULL_EXAMPLES number of examples. Otherwise we could + // make the page arbitrarily huge! + 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() { + write!(w, r#""); + } + + write!(w, "
"); + } + + write!(w, "
"); +} diff --git a/src/librustdoc/html/render/print_item.rs b/src/librustdoc/html/render/print_item.rs index 58cd1018c316f..f452836962227 100644 --- a/src/librustdoc/html/render/print_item.rs +++ b/src/librustdoc/html/render/print_item.rs @@ -1159,6 +1159,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/render/write_shared.rs b/src/librustdoc/html/render/write_shared.rs index e4c2556118aeb..31aaf46d7d595 100644 --- a/src/librustdoc/html/render/write_shared.rs +++ b/src/librustdoc/html/render/write_shared.rs @@ -304,6 +304,15 @@ pub(super) fn write_shared( )?; } + if cx.shared.layout.scrape_examples_extension { + cx.write_minify( + SharedResource::InvocationSpecific { basename: "scrape-examples.js" }, + static_files::SCRAPE_EXAMPLES_JS, + options.enable_minification, + &options.emit, + )?; + } + if let Some(ref css) = cx.shared.layout.css_file_extension { let buffer = try_err!(fs::read_to_string(css), css); // This varies based on the invocation, so it can't go through the write_minify wrapper. diff --git a/src/librustdoc/html/sources.rs b/src/librustdoc/html/sources.rs index 71c64231a210e..ffefc5450cd73 100644 --- a/src/librustdoc/html/sources.rs +++ b/src/librustdoc/html/sources.rs @@ -204,7 +204,16 @@ 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, + SourceContext::Standalone, + ) }, &self.cx.shared.style_files, ); @@ -241,15 +250,22 @@ where } } +crate enum SourceContext { + Standalone, + Embedded { offset: usize }, +} + /// 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, file_span: rustc_span::Span, context: &Context<'_>, root_path: &str, + decoration_info: Option, + source_context: SourceContext, ) { let lines = s.lines().count(); let mut line_numbers = Buffer::empty_from(buf); @@ -261,7 +277,14 @@ fn print_src( } line_numbers.write_str("
");
     for i in 1..=lines {
-        writeln!(line_numbers, "{0:1$}", i, cols);
+        match source_context {
+            SourceContext::Standalone => {
+                writeln!(line_numbers, "{0:1$}", i, cols)
+            }
+            SourceContext::Embedded { offset } => {
+                writeln!(line_numbers, "{0:1$}", i + offset, cols)
+            }
+        }
     }
     line_numbers.write_str("
"); highlight::render_with_highlighting( @@ -273,5 +296,6 @@ 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 e41c993a5285d..8139f115cbb39 100644 --- a/src/librustdoc/html/static/css/rustdoc.css +++ b/src/librustdoc/html/static/css/rustdoc.css @@ -467,6 +467,11 @@ nav.sub { overflow-x: auto; } +.rustdoc:not(.source) .example-wrap > pre.line-numbers { + width: auto; + overflow-x: visible; +} + .rustdoc .example-wrap > pre { margin: 0; } @@ -1980,3 +1985,166 @@ details.undocumented[open] > summary::before { overflow-wrap: anywhere; } } + + +/* Begin: styles for --scrape-examples feature */ + +.scraped-example-title { + font-family: 'Fira Sans'; +} + +.scraped-example:not(.expanded) .code-wrapper pre.line-numbers { + overflow: hidden; + max-height: 240px; +} + +.scraped-example:not(.expanded) .code-wrapper .example-wrap pre.rust { + overflow-y: hidden; + max-height: 240px; + padding-bottom: 0; +} + +.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: 5px; + 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: 5px; + 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; + max-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 .example-wrap .rust span.highlight { + background: #fcffd6; +} + +.scraped-example .example-wrap .rust span.highlight.focus { + background: #f6fdb0; +} + +.more-examples-toggle { + margin-top: 10px; +} + +.more-examples-toggle summary { + color: #999; + font-family: 'Fira Sans'; +} + +.more-scraped-examples { + 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 { + 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%; +} + +.more-scraped-examples .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'; +} + +.example-links ul { + margin-bottom: 0; +} + +/* End: styles for --scrape-examples feature */ diff --git a/src/librustdoc/html/static/css/themes/ayu.css b/src/librustdoc/html/static/css/themes/ayu.css index ccb1a707032bb..f9c84dc3e318d 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 93801af46ecc5..9a38277d55905 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; +} diff --git a/src/librustdoc/html/static/js/scrape-examples.js b/src/librustdoc/html/static/js/scrape-examples.js new file mode 100644 index 0000000000000..664b187e33e9f --- /dev/null +++ b/src/librustdoc/html/static/js/scrape-examples.js @@ -0,0 +1,86 @@ +/* global addClass, hasClass, removeClass, onEach */ + +(function () { + // 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[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 locs = JSON.parse(example.attributes.getNamedItem("data-locs").textContent); + var locIndex = 0; + var highlights = example.querySelectorAll('.highlight'); + var link = example.querySelector('.scraped-example-title a'); + + if (locs.length > 1) { + // Toggle through list of examples in a given file + var onChangeLoc = function(changeIndex) { + removeClass(highlights[locIndex], 'focus'); + changeIndex(); + scrollToLoc(example, locs[locIndex][0]); + addClass(highlights[locIndex], 'focus'); + + var url = locs[locIndex][1]; + var title = locs[locIndex][2]; + + link.href = url; + link.innerHTML = title; + }; + + 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; + }); + }); + } + + var expandButton = example.querySelector('.expand'); + if (expandButton) { + expandButton.addEventListener('click', function () { + if (hasClass(example, "expanded")) { + removeClass(example, "expanded"); + scrollToLoc(example, locs[0][0]); + } else { + addClass(example, "expanded"); + } + }); + } + + // Start with the first example in view + scrollToLoc(example, locs[0][0]); + } + + 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; + }); + + 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}); + }); +})(); diff --git a/src/librustdoc/html/static_files.rs b/src/librustdoc/html/static_files.rs index 924e3f1d29dc9..9029933ad100e 100644 --- a/src/librustdoc/html/static_files.rs +++ b/src/librustdoc/html/static_files.rs @@ -35,6 +35,10 @@ crate static SETTINGS_JS: &str = include_str!("static/js/settings.js"); /// Storage, used to store documentation settings. crate static STORAGE_JS: &str = include_str!("static/js/storage.js"); +/// The file contents of `scraped-examples.js`, which contains functionality related to the +/// --scrape-examples flag that inserts automatically-found examples of usages of items. +crate static SCRAPE_EXAMPLES_JS: &str = include_str!("static/js/scrape-examples.js"); + /// The file contents of `brush.svg`, the icon used for the theme-switch button. crate static BRUSH_SVG: &[u8] = include_bytes!("static/images/brush.svg"); diff --git a/src/librustdoc/html/templates/page.html b/src/librustdoc/html/templates/page.html index 38dc3b30e72ac..b0174d59a7be2 100644 --- a/src/librustdoc/html/templates/page.html +++ b/src/librustdoc/html/templates/page.html @@ -109,6 +109,9 @@ data-search-js="{{static_root_path | safe}}search{{page.resource_suffix}}.js"> {#- -#} {#- -#} + {%- if layout.scrape_examples_extension -%} + {#- -#} + {%- endif -%} {%- for script in page.static_extra_scripts -%} {#- -#} {% endfor %} diff --git a/src/librustdoc/lib.rs b/src/librustdoc/lib.rs index ff0a6ef6cb74f..17e00e4b66271 100644 --- a/src/librustdoc/lib.rs +++ b/src/librustdoc/lib.rs @@ -48,11 +48,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; @@ -120,6 +122,7 @@ mod json; crate mod lint; mod markdown; mod passes; +mod scrape_examples; mod theme; mod visit_ast; mod visit_lib; @@ -619,6 +622,30 @@ fn opts() -> Vec { "Make the identifiers in the HTML source code pages navigable", ) }), + unstable("scrape-examples-output-path", |o| { + o.optopt( + "", + "scrape-examples-output-path", + "", + "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| { + o.optmulti( + "", + "with-examples", + "", + "path to function call information (for displaying examples in the documentation)", + ) + }), ] } @@ -732,6 +759,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 = options.scrape_examples_options.clone(); let config = core::create_config(options); interface::create_compiler_and_run(config, |compiler| { @@ -768,6 +796,10 @@ fn main_options(options: config::Options) -> MainResult { }); info!("finished with rustc"); + if let Some(options) = scrape_examples_options { + return scrape_examples::run(krate, render_opts, cache, tcx, options); + } + cache.crate_version = crate_version; if show_coverage { diff --git a/src/librustdoc/scrape_examples.rs b/src/librustdoc/scrape_examples.rs new file mode 100644 index 0000000000000..fc54e55b87655 --- /dev/null +++ b/src/librustdoc/scrape_examples.rs @@ -0,0 +1,266 @@ +//! This module analyzes crates to find call sites that can serve as examples in the documentation. + +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, + intravisit::{self, Visitor}, + HirId, +}; +use rustc_interface::interface; +use rustc_macros::{Decodable, Encodable}; +use rustc_middle::hir::map::Map; +use rustc_middle::ty::{self, TyCtxt}; +use rustc_serialize::{ + opaque::{Decoder, FileEncoder}, + Decodable, Encodable, +}; +use rustc_session::getopts; +use rustc_span::{ + def_id::{CrateNum, DefPathHash, LOCAL_CRATE}, + 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), + 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(Encodable, Decodable, Debug, Clone)] +crate struct CallLocation { + crate call_expr: SyntaxRange, + crate enclosing_item: SyntaxRange, +} + +impl CallLocation { + fn new( + tcx: TyCtxt<'_>, + expr_span: rustc_span::Span, + expr_id: HirId, + source_file: &SourceFile, + ) -> Self { + 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 { + call_expr: SyntaxRange::new(expr_span, source_file), + enclosing_item: SyntaxRange::new(enclosing_item_span, source_file), + } + } +} + +#[derive(Encodable, Decodable, Debug, Clone)] +crate struct CallData { + crate locations: Vec, + crate url: String, + crate display_name: String, + crate edition: Edition, +} + +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>, + cx: Context<'tcx>, + target_crates: Vec, + 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 tcx = self.tcx; + let (ty, span) = match ex.kind { + hir::ExprKind::Call(f, _) => { + let types = tcx.typeck(ex.hir_id.owner); + (types.node_type(f.hir_id), ex.span) + } + hir::ExprKind::MethodCall(_, _, _, span) => { + let types = tcx.typeck(ex.hir_id.owner); + let def_id = types.type_dependent_def_id(ex.hir_id).unwrap(); + (tcx.type_of(def_id), span) + } + _ => { + return; + } + }; + + // If this span comes from a macro expansion, then the source code may not actually show + // a use of the given item, so it would be a poor example. Hence, we skip all uses in macros. + if span.from_expansion() { + return; + } + + // 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(), + _ => None, + }; + + if let Some(file_path) = file_path { + let abs_path = fs::canonicalize(file_path.clone()).unwrap(); + let cx = &self.cx; + let mk_call_data = || { + let clean_span = crate::clean::types::Span::new(span); + let url = cx.href_from_span(clean_span, false).unwrap(); + let display_name = file_path.display().to_string(); + let edition = span.edition(); + CallData { locations: Vec::new(), url, display_name, edition } + }; + + let fn_key = tcx.def_path_hash(*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); + } + } + } +} + +crate fn run( + krate: clean::Crate, + renderopts: config::RenderOptions, + cache: formats::cache::Cache, + tcx: TyCtxt<'_>, + 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 all_crates = tcx + .crates(()) + .iter() + .chain([&LOCAL_CRATE]) + .map(|crate_num| (crate_num, tcx.crate_name(*crate_num))) + .collect::>(); + let target_crates = options + .target_crates + .into_iter() + .map(|target| all_crates.iter().filter(move |(_, name)| name.as_str() == target)) + .flatten() + .map(|(crate_num, _)| **crate_num) + .collect::>(); + + debug!("All crates in TyCtxt: {:?}", all_crates); + debug!("Scrape examples target_crates: {:?}", target_crates); + + // Run call-finder on all items + let mut calls = FxHashMap::default(); + let mut finder = FindCalls { calls: &mut calls, tcx, map: tcx.hir(), cx, target_crates }; + tcx.hir().visit_all_item_likes(&mut finder.as_deep_visitor()); + + // Save output to provided path + 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())?; + + Ok(()) + }; + + if let Err(e) = inner() { + tcx.sess.fatal(&e); + } + + Ok(()) +} + +// Note: the Handler must be passed in explicitly because sess isn't available while parsing options +crate fn load_call_locations( + with_examples: Vec, + diag: &rustc_errors::Handler, +) -> 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 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()); + } + } + + Ok(all_calls) + }; + + inner().map_err(|e: String| { + diag.err(&format!("failed to load examples: {}", e)); + 1 + }) +} diff --git a/src/test/run-make/rustdoc-scrape-examples-multiple/Makefile b/src/test/run-make/rustdoc-scrape-examples-multiple/Makefile new file mode 100644 index 0000000000000..897805e4405b9 --- /dev/null +++ b/src/test/run-make/rustdoc-scrape-examples-multiple/Makefile @@ -0,0 +1,5 @@ +deps := ex ex2 + +-include ./scrape.mk + +all: scrape diff --git a/src/test/run-make/rustdoc-scrape-examples-multiple/examples/ex.rs b/src/test/run-make/rustdoc-scrape-examples-multiple/examples/ex.rs new file mode 100644 index 0000000000000..01b730c6149ce --- /dev/null +++ b/src/test/run-make/rustdoc-scrape-examples-multiple/examples/ex.rs @@ -0,0 +1,4 @@ +fn main() { + foobar::ok(); + foobar::ok(); +} diff --git a/src/test/run-make/rustdoc-scrape-examples-multiple/examples/ex2.rs b/src/test/run-make/rustdoc-scrape-examples-multiple/examples/ex2.rs new file mode 100644 index 0000000000000..f83cf2f270914 --- /dev/null +++ b/src/test/run-make/rustdoc-scrape-examples-multiple/examples/ex2.rs @@ -0,0 +1,3 @@ +fn main() { + foobar::ok(); +} diff --git a/src/test/run-make/rustdoc-scrape-examples-multiple/scrape.mk b/src/test/run-make/rustdoc-scrape-examples-multiple/scrape.mk new file mode 100644 index 0000000000000..1fa1fae1a0b71 --- /dev/null +++ b/src/test/run-make/rustdoc-scrape-examples-multiple/scrape.mk @@ -0,0 +1,20 @@ +-include ../../run-make-fulldeps/tools.mk + +OUTPUT_DIR := "$(TMPDIR)/rustdoc" + +$(TMPDIR)/%.calls: $(TMPDIR)/libfoobar.rmeta + $(RUSTDOC) examples/$*.rs --crate-name $* --crate-type bin --output $(OUTPUT_DIR) \ + --extern foobar=$(TMPDIR)/libfoobar.rmeta \ + -Z unstable-options \ + --scrape-examples-output-path $@ \ + --scrape-examples-target-crate foobar + +$(TMPDIR)/lib%.rmeta: src/lib.rs + $(RUSTC) src/lib.rs --crate-name $* --crate-type lib --emit=metadata + +scrape: $(foreach d,$(deps),$(TMPDIR)/$(d).calls) + $(RUSTDOC) src/lib.rs --crate-name foobar --crate-type lib --output $(OUTPUT_DIR) \ + -Z unstable-options \ + $(foreach d,$(deps),--with-examples $(TMPDIR)/$(d).calls) + + $(HTMLDOCCK) $(OUTPUT_DIR) src/lib.rs diff --git a/src/test/run-make/rustdoc-scrape-examples-multiple/src/lib.rs b/src/test/run-make/rustdoc-scrape-examples-multiple/src/lib.rs new file mode 100644 index 0000000000000..bd59584bbbf4f --- /dev/null +++ b/src/test/run-make/rustdoc-scrape-examples-multiple/src/lib.rs @@ -0,0 +1,4 @@ +// @has foobar/fn.ok.html '//*[@class="docblock scraped-example-list"]//*[@class="prev"]' '' +// @has foobar/fn.ok.html '//*[@class="more-scraped-examples"]' '' + +pub fn ok() {} diff --git a/src/test/run-make/rustdoc-scrape-examples-ordering/Makefile b/src/test/run-make/rustdoc-scrape-examples-ordering/Makefile new file mode 100644 index 0000000000000..339d539bfd57d --- /dev/null +++ b/src/test/run-make/rustdoc-scrape-examples-ordering/Makefile @@ -0,0 +1,5 @@ +deps := ex1 ex2 + +-include ../rustdoc-scrape-examples-multiple/scrape.mk + +all: scrape diff --git a/src/test/run-make/rustdoc-scrape-examples-ordering/examples/ex1.rs b/src/test/run-make/rustdoc-scrape-examples-ordering/examples/ex1.rs new file mode 100644 index 0000000000000..d6d5982087658 --- /dev/null +++ b/src/test/run-make/rustdoc-scrape-examples-ordering/examples/ex1.rs @@ -0,0 +1,9 @@ +fn main() { + foobar::ok(); + + // this is a + + // BIG + + // item +} diff --git a/src/test/run-make/rustdoc-scrape-examples-ordering/examples/ex2.rs b/src/test/run-make/rustdoc-scrape-examples-ordering/examples/ex2.rs new file mode 100644 index 0000000000000..a1133117f861e --- /dev/null +++ b/src/test/run-make/rustdoc-scrape-examples-ordering/examples/ex2.rs @@ -0,0 +1,4 @@ +fn main() { + foobar::ok(); + // small item +} diff --git a/src/test/run-make/rustdoc-scrape-examples-ordering/src/lib.rs b/src/test/run-make/rustdoc-scrape-examples-ordering/src/lib.rs new file mode 100644 index 0000000000000..f1b7686d36800 --- /dev/null +++ b/src/test/run-make/rustdoc-scrape-examples-ordering/src/lib.rs @@ -0,0 +1,4 @@ +// @has foobar/fn.ok.html '//*[@class="docblock scraped-example-list"]' 'ex2' +// @has foobar/fn.ok.html '//*[@class="more-scraped-examples"]' 'ex1' + +pub fn ok() {} 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..dce8b83eefe4e --- /dev/null +++ b/src/test/run-make/rustdoc-scrape-examples-remap/Makefile @@ -0,0 +1,5 @@ +deps := ex + +-include ../rustdoc-scrape-examples-multiple/scrape.mk + +all: scrape 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..f525a4270dde1 --- /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 expanded"]' 'ex.rs' +// @has foobar/c/fn.foo.html '//*[@class="scraped-example expanded"]' 'ex.rs' + +#[path = "a.rs"] +pub mod b; + +#[path = "a.rs"] +pub mod c; diff --git a/src/test/rustdoc-ui/scrape-examples-wrong-options-1.rs b/src/test/rustdoc-ui/scrape-examples-wrong-options-1.rs new file mode 100644 index 0000000000000..a1f005c32ee0f --- /dev/null +++ b/src/test/rustdoc-ui/scrape-examples-wrong-options-1.rs @@ -0,0 +1 @@ +// compile-flags: -Z unstable-options --scrape-examples-target-crate foobar diff --git a/src/test/rustdoc-ui/scrape-examples-wrong-options-1.stderr b/src/test/rustdoc-ui/scrape-examples-wrong-options-1.stderr new file mode 100644 index 0000000000000..eb8e9f799681f --- /dev/null +++ b/src/test/rustdoc-ui/scrape-examples-wrong-options-1.stderr @@ -0,0 +1,2 @@ +error: must use --scrape-examples-output-path and --scrape-examples-target-crate together + diff --git a/src/test/rustdoc-ui/scrape-examples-wrong-options-2.rs b/src/test/rustdoc-ui/scrape-examples-wrong-options-2.rs new file mode 100644 index 0000000000000..4aacec7f09493 --- /dev/null +++ b/src/test/rustdoc-ui/scrape-examples-wrong-options-2.rs @@ -0,0 +1 @@ +// compile-flags: -Z unstable-options --scrape-examples-output-path ex.calls diff --git a/src/test/rustdoc-ui/scrape-examples-wrong-options-2.stderr b/src/test/rustdoc-ui/scrape-examples-wrong-options-2.stderr new file mode 100644 index 0000000000000..eb8e9f799681f --- /dev/null +++ b/src/test/rustdoc-ui/scrape-examples-wrong-options-2.stderr @@ -0,0 +1,2 @@ +error: must use --scrape-examples-output-path and --scrape-examples-target-crate together +