mirror of
https://github.com/rust-lang/rust.git
synced 2024-11-22 23:04:33 +00:00
Rollup merge of #85833 - willcrichton:example-analyzer, r=jyn514
Scrape code examples from examples/ directory for Rustdoc Adds support for the functionality described in https://github.com/rust-lang/rfcs/pull/3123 Matching changes to Cargo are here: https://github.com/rust-lang/cargo/pull/9525 Live demo here: https://willcrichton.net/example-analyzer/warp/trait.Filter.html#method.and
This commit is contained in:
commit
dcf9242795
@ -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`.
|
||||
|
@ -2062,7 +2062,8 @@ fn clean_use_statement(
|
||||
impl Clean<Item> for (&hir::ForeignItem<'_>, Option<Symbol>) {
|
||||
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());
|
||||
|
@ -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<ScrapeExamplesOptions>,
|
||||
}
|
||||
|
||||
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<EmitType>,
|
||||
/// 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,
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -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<Buffer>,
|
||||
context_info: Option<ContextInfo<'_, '_, '_>>,
|
||||
decoration_info: Option<DecorationInfo>,
|
||||
) {
|
||||
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<ContextInfo<'_, '_, '_>>,
|
||||
decoration_info: Option<DecorationInfo>,
|
||||
) {
|
||||
// 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<u32>,
|
||||
}
|
||||
|
||||
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<Decorations>,
|
||||
}
|
||||
|
||||
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<DecorationInfo>,
|
||||
) -> 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<T: Display>(
|
||||
// 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))
|
||||
|
2
src/librustdoc/html/highlight/fixtures/decorations.html
Normal file
2
src/librustdoc/html/highlight/fixtures/decorations.html
Normal file
@ -0,0 +1,2 @@
|
||||
<span class="example"><span class="kw">let</span> <span class="ident">x</span> <span class="op">=</span> <span class="number">1</span>;</span>
|
||||
<span class="kw">let</span> <span class="ident">y</span> <span class="op">=</span> <span class="number">2</span>;
|
@ -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!("{}<pre><code>{}</code></pre>\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());
|
||||
});
|
||||
}
|
||||
|
@ -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)]
|
||||
|
@ -360,6 +360,7 @@ impl<'a, I: Iterator<Item = Event<'a>>> Iterator for CodeBlocks<'_, 'a, I> {
|
||||
edition,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
Some(Event::Html(s.into_inner().into()))
|
||||
}
|
||||
|
@ -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<rustc_span::Span, LinkFromSrc>,
|
||||
/// 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<String> {
|
||||
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<String> {
|
||||
crate fn href_from_span(&self, span: clean::Span, with_lines: bool) -> Option<String> {
|
||||
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
|
||||
|
@ -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<String>);
|
||||
@ -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<String> {
|
||||
}
|
||||
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,
|
||||
"<div class=\"docblock scraped-example-list\">\
|
||||
<span></span>\
|
||||
<h5 id=\"{id}\" class=\"section-header\">\
|
||||
<a href=\"#{id}\">Examples found in repository</a>\
|
||||
</h5>",
|
||||
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,
|
||||
"<div class=\"scraped-example {expanded_cls}\" data-locs=\"{locations}\">\
|
||||
<div class=\"scraped-example-title\">\
|
||||
{name} (<a href=\"{url}\">{title}</a>)\
|
||||
</div>\
|
||||
<div class=\"code-wrapper\">",
|
||||
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#"<span class="prev">≺</span> <span class="next">≻</span>"#);
|
||||
}
|
||||
|
||||
if needs_expansion {
|
||||
write!(w, r#"<span class="expand">↕</span>"#);
|
||||
}
|
||||
|
||||
// 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, "</div></div>");
|
||||
|
||||
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::<Vec<_>>();
|
||||
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,
|
||||
"<details class=\"rustdoc-toggle more-examples-toggle\">\
|
||||
<summary class=\"hideme\">\
|
||||
<span>More examples</span>\
|
||||
</summary>\
|
||||
<div class=\"more-scraped-examples\">\
|
||||
<div class=\"toggle-line\"><div class=\"toggle-line-inner\"></div></div>\
|
||||
<div class=\"more-scraped-examples-inner\">"
|
||||
);
|
||||
|
||||
// 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 <ul> containing links to the source files.
|
||||
if it.peek().is_some() {
|
||||
write!(w, r#"<div class="example-links">Additional examples can be found in:<br><ul>"#);
|
||||
it.for_each(|(_, call_data)| {
|
||||
write!(
|
||||
w,
|
||||
r#"<li><a href="{root}{url}">{name}</a></li>"#,
|
||||
root = cx.root_path(),
|
||||
url = call_data.url,
|
||||
name = call_data.display_name
|
||||
);
|
||||
});
|
||||
write!(w, "</ul></div>");
|
||||
}
|
||||
|
||||
write!(w, "</div></div></details>");
|
||||
}
|
||||
|
||||
write!(w, "</div>");
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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.
|
||||
|
@ -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<highlight::DecorationInfo>,
|
||||
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("<pre class=\"line-numbers\">");
|
||||
for i in 1..=lines {
|
||||
writeln!(line_numbers, "<span id=\"{0}\">{0:1$}</span>", i, cols);
|
||||
match source_context {
|
||||
SourceContext::Standalone => {
|
||||
writeln!(line_numbers, "<span id=\"{0}\">{0:1$}</span>", i, cols)
|
||||
}
|
||||
SourceContext::Embedded { offset } => {
|
||||
writeln!(line_numbers, "<span>{0:1$}</span>", i + offset, cols)
|
||||
}
|
||||
}
|
||||
}
|
||||
line_numbers.write_str("</pre>");
|
||||
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,
|
||||
);
|
||||
}
|
||||
|
@ -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 */
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
86
src/librustdoc/html/static/js/scrape-examples.js
Normal file
86
src/librustdoc/html/static/js/scrape-examples.js
Normal file
@ -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 <details> 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});
|
||||
});
|
||||
})();
|
@ -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");
|
||||
|
||||
|
@ -109,6 +109,9 @@
|
||||
data-search-js="{{static_root_path | safe}}search{{page.resource_suffix}}.js"> {#- -#}
|
||||
</div>
|
||||
<script src="{{static_root_path | safe}}main{{page.resource_suffix}}.js"></script> {#- -#}
|
||||
{%- if layout.scrape_examples_extension -%}
|
||||
<script src="{{static_root_path | safe}}scrape-examples{{page.resource_suffix}}.js"></script> {#- -#}
|
||||
{%- endif -%}
|
||||
{%- for script in page.static_extra_scripts -%}
|
||||
<script src="{{static_root_path | safe}}{{script}}.js"></script> {#- -#}
|
||||
{% endfor %}
|
||||
|
@ -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<RustcOptGroup> {
|
||||
"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 {
|
||||
|
266
src/librustdoc/scrape_examples.rs
Normal file
266
src/librustdoc/scrape_examples.rs
Normal file
@ -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<String>,
|
||||
}
|
||||
|
||||
impl ScrapeExamplesOptions {
|
||||
crate fn new(
|
||||
matches: &getopts::Matches,
|
||||
diag: &rustc_errors::Handler,
|
||||
) -> Result<Option<Self>, 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<CallLocation>,
|
||||
crate url: String,
|
||||
crate display_name: String,
|
||||
crate edition: Edition,
|
||||
}
|
||||
|
||||
crate type FnCallLocations = FxHashMap<PathBuf, CallData>;
|
||||
crate type AllCallLocations = FxHashMap<DefPathHash, FnCallLocations>;
|
||||
|
||||
/// 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<CrateNum>,
|
||||
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<Self::Map> {
|
||||
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::<Vec<_>>();
|
||||
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::<Vec<_>>();
|
||||
|
||||
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<String>,
|
||||
diag: &rustc_errors::Handler,
|
||||
) -> Result<AllCallLocations, i32> {
|
||||
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
|
||||
})
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
deps := ex ex2
|
||||
|
||||
-include ./scrape.mk
|
||||
|
||||
all: scrape
|
@ -0,0 +1,4 @@
|
||||
fn main() {
|
||||
foobar::ok();
|
||||
foobar::ok();
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
fn main() {
|
||||
foobar::ok();
|
||||
}
|
20
src/test/run-make/rustdoc-scrape-examples-multiple/scrape.mk
Normal file
20
src/test/run-make/rustdoc-scrape-examples-multiple/scrape.mk
Normal file
@ -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
|
@ -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() {}
|
@ -0,0 +1,5 @@
|
||||
deps := ex1 ex2
|
||||
|
||||
-include ../rustdoc-scrape-examples-multiple/scrape.mk
|
||||
|
||||
all: scrape
|
@ -0,0 +1,9 @@
|
||||
fn main() {
|
||||
foobar::ok();
|
||||
|
||||
// this is a
|
||||
|
||||
// BIG
|
||||
|
||||
// item
|
||||
}
|
@ -0,0 +1,4 @@
|
||||
fn main() {
|
||||
foobar::ok();
|
||||
// small item
|
||||
}
|
@ -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() {}
|
5
src/test/run-make/rustdoc-scrape-examples-remap/Makefile
Normal file
5
src/test/run-make/rustdoc-scrape-examples-remap/Makefile
Normal file
@ -0,0 +1,5 @@
|
||||
deps := ex
|
||||
|
||||
-include ../rustdoc-scrape-examples-multiple/scrape.mk
|
||||
|
||||
all: scrape
|
@ -0,0 +1,4 @@
|
||||
fn main() {
|
||||
foobar::b::foo();
|
||||
foobar::c::foo();
|
||||
}
|
1
src/test/run-make/rustdoc-scrape-examples-remap/src/a.rs
Normal file
1
src/test/run-make/rustdoc-scrape-examples-remap/src/a.rs
Normal file
@ -0,0 +1 @@
|
||||
pub fn foo() {}
|
@ -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;
|
1
src/test/rustdoc-ui/scrape-examples-wrong-options-1.rs
Normal file
1
src/test/rustdoc-ui/scrape-examples-wrong-options-1.rs
Normal file
@ -0,0 +1 @@
|
||||
// compile-flags: -Z unstable-options --scrape-examples-target-crate foobar
|
@ -0,0 +1,2 @@
|
||||
error: must use --scrape-examples-output-path and --scrape-examples-target-crate together
|
||||
|
1
src/test/rustdoc-ui/scrape-examples-wrong-options-2.rs
Normal file
1
src/test/rustdoc-ui/scrape-examples-wrong-options-2.rs
Normal file
@ -0,0 +1 @@
|
||||
// compile-flags: -Z unstable-options --scrape-examples-output-path ex.calls
|
@ -0,0 +1,2 @@
|
||||
error: must use --scrape-examples-output-path and --scrape-examples-target-crate together
|
||||
|
Loading…
Reference in New Issue
Block a user