1 //! This module analyzes crates to find call sites that can serve as examples in the documentation.
6 use crate::formats
::renderer
::FormatRenderer
;
7 use crate::html
::render
::Context
;
9 use rustc_data_structures
::fx
::FxHashMap
;
12 intravisit
::{self, Visitor}
,
14 use rustc_interface
::interface
;
15 use rustc_macros
::{Decodable, Encodable}
;
16 use rustc_middle
::hir
::map
::Map
;
17 use rustc_middle
::hir
::nested_filter
;
18 use rustc_middle
::ty
::{self, TyCtxt}
;
19 use rustc_serialize
::{
20 opaque
::{FileEncoder, MemDecoder}
,
23 use rustc_session
::getopts
;
25 def_id
::{CrateNum, DefPathHash, LOCAL_CRATE}
,
27 BytePos
, FileName
, SourceFile
,
31 use std
::path
::PathBuf
;
33 #[derive(Debug, Clone)]
34 pub(crate) struct ScrapeExamplesOptions
{
36 target_crates
: Vec
<String
>,
37 pub(crate) scrape_tests
: bool
,
40 impl ScrapeExamplesOptions
{
42 matches
: &getopts
::Matches
,
43 diag
: &rustc_errors
::Handler
,
44 ) -> Result
<Option
<Self>, i32> {
45 let output_path
= matches
.opt_str("scrape-examples-output-path");
46 let target_crates
= matches
.opt_strs("scrape-examples-target-crate");
47 let scrape_tests
= matches
.opt_present("scrape-tests");
48 match (output_path
, !target_crates
.is_empty(), scrape_tests
) {
49 (Some(output_path
), true, _
) => Ok(Some(ScrapeExamplesOptions
{
50 output_path
: PathBuf
::from(output_path
),
54 (Some(_
), false, _
) | (None
, true, _
) => {
55 diag
.err("must use --scrape-examples-output-path and --scrape-examples-target-crate together");
58 (None
, false, true) => {
59 diag
.err("must use --scrape-examples-output-path and --scrape-examples-target-crate with --scrape-tests");
62 (None
, false, false) => Ok(None
),
67 #[derive(Encodable, Decodable, Debug, Clone)]
68 pub(crate) struct SyntaxRange
{
69 pub(crate) byte_span
: (u32, u32),
70 pub(crate) line_span
: (usize, usize),
74 fn new(span
: rustc_span
::Span
, file
: &SourceFile
) -> Option
<Self> {
75 let get_pos
= |bytepos
: BytePos
| file
.original_relative_byte_pos(bytepos
).0;
76 let get_line
= |bytepos
: BytePos
| file
.lookup_line(bytepos
);
79 byte_span
: (get_pos(span
.lo()), get_pos(span
.hi())),
80 line_span
: (get_line(span
.lo())?
, get_line(span
.hi())?
),
85 #[derive(Encodable, Decodable, Debug, Clone)]
86 pub(crate) struct CallLocation
{
87 pub(crate) call_expr
: SyntaxRange
,
88 pub(crate) call_ident
: SyntaxRange
,
89 pub(crate) enclosing_item
: SyntaxRange
,
94 expr_span
: rustc_span
::Span
,
95 ident_span
: rustc_span
::Span
,
96 enclosing_item_span
: rustc_span
::Span
,
97 source_file
: &SourceFile
,
100 call_expr
: SyntaxRange
::new(expr_span
, source_file
)?
,
101 call_ident
: SyntaxRange
::new(ident_span
, source_file
)?
,
102 enclosing_item
: SyntaxRange
::new(enclosing_item_span
, source_file
)?
,
107 #[derive(Encodable, Decodable, Debug, Clone)]
108 pub(crate) struct CallData
{
109 pub(crate) locations
: Vec
<CallLocation
>,
110 pub(crate) url
: String
,
111 pub(crate) display_name
: String
,
112 pub(crate) edition
: Edition
,
115 pub(crate) type FnCallLocations
= FxHashMap
<PathBuf
, CallData
>;
116 pub(crate) type AllCallLocations
= FxHashMap
<DefPathHash
, FnCallLocations
>;
118 /// Visitor for traversing a crate and finding instances of function calls.
119 struct FindCalls
<'a
, 'tcx
> {
123 target_crates
: Vec
<CrateNum
>,
124 calls
: &'a
mut AllCallLocations
,
127 impl<'a
, 'tcx
> Visitor
<'tcx
> for FindCalls
<'a
, 'tcx
>
131 type NestedFilter
= nested_filter
::OnlyBodies
;
133 fn nested_visit_map(&mut self) -> Self::Map
{
137 fn visit_expr(&mut self, ex
: &'tcx hir
::Expr
<'tcx
>) {
138 intravisit
::walk_expr(self, ex
);
142 // If we visit an item that contains an expression outside a function body,
143 // then we need to exit before calling typeck (which will panic). See
144 // test/run-make/rustdoc-scrape-examples-invalid-expr for an example.
146 if hir
.maybe_body_owned_by(ex
.hir_id
.owner
).is_none() {
150 // Get type of function if expression is a function call
151 let (ty
, call_span
, ident_span
) = match ex
.kind
{
152 hir
::ExprKind
::Call(f
, _
) => {
153 let types
= tcx
.typeck(ex
.hir_id
.owner
);
155 if let Some(ty
) = types
.node_type_opt(f
.hir_id
) {
156 (ty
, ex
.span
, f
.span
)
158 trace
!("node_type_opt({}) = None", f
.hir_id
);
162 hir
::ExprKind
::MethodCall(path
, _
, call_span
) => {
163 let types
= tcx
.typeck(ex
.hir_id
.owner
);
164 let Some(def_id
) = types
.type_dependent_def_id(ex
.hir_id
) else {
165 trace
!("type_dependent_def_id({}) = None", ex
.hir_id
);
169 let ident_span
= path
.ident
.span
;
170 (tcx
.type_of(def_id
), call_span
, ident_span
)
177 // If this span comes from a macro expansion, then the source code may not actually show
178 // a use of the given item, so it would be a poor example. Hence, we skip all uses in macros.
179 if call_span
.from_expansion() {
180 trace
!("Rejecting expr from macro: {call_span:?}");
184 // If the enclosing item has a span coming from a proc macro, then we also don't want to include
186 let enclosing_item_span
= tcx
188 .span_with_body(tcx
.hir().local_def_id_to_hir_id(tcx
.hir().get_parent_item(ex
.hir_id
)));
189 if enclosing_item_span
.from_expansion() {
190 trace
!("Rejecting expr ({call_span:?}) from macro item: {enclosing_item_span:?}");
194 // If the enclosing item doesn't actually enclose the call, this means we probably have a weird
195 // macro issue even though the spans aren't tagged as being from an expansion.
196 if !enclosing_item_span
.contains(call_span
) {
198 "Attempted to scrape call at [{call_span:?}] whose enclosing item [{enclosing_item_span:?}] doesn't contain the span of the call."
203 // Similarly for the call w/ the function ident.
204 if !call_span
.contains(ident_span
) {
206 "Attempted to scrape call at [{call_span:?}] whose identifier [{ident_span:?}] was not contained in the span of the call."
211 // Save call site if the function resolves to a concrete definition
212 if let ty
::FnDef(def_id
, _
) = ty
.kind() {
213 if self.target_crates
.iter().all(|krate
| *krate
!= def_id
.krate
) {
214 trace
!("Rejecting expr from crate not being documented: {call_span:?}");
218 let source_map
= tcx
.sess
.source_map();
219 let file
= source_map
.lookup_char_pos(call_span
.lo()).file
;
220 let file_path
= match file
.name
.clone() {
221 FileName
::Real(real_filename
) => real_filename
.into_local_path(),
225 if let Some(file_path
) = file_path
{
226 let abs_path
= match fs
::canonicalize(file_path
.clone()) {
227 Ok(abs_path
) => abs_path
,
229 trace
!("Could not canonicalize file path: {}", file_path
.display());
235 let clean_span
= crate::clean
::types
::Span
::new(call_span
);
236 let url
= match cx
.href_from_span(clean_span
, false) {
240 "Rejecting expr ({call_span:?}) whose clean span ({clean_span:?}) cannot be turned into a link"
246 let mk_call_data
= || {
247 let display_name
= file_path
.display().to_string();
248 let edition
= call_span
.edition();
249 CallData { locations: Vec::new(), url, display_name, edition }
252 let fn_key
= tcx
.def_path_hash(*def_id
);
253 let fn_entries
= self.calls
.entry(fn_key
).or_default();
255 trace
!("Including expr: {:?}", call_span
);
256 let enclosing_item_span
=
257 source_map
.span_extend_to_prev_char(enclosing_item_span
, '
\n'
, false);
259 match CallLocation
::new(call_span
, ident_span
, enclosing_item_span
, &file
) {
260 Some(location
) => location
,
262 trace
!("Could not get serializable call location for {call_span:?}");
266 fn_entries
.entry(abs_path
).or_insert_with(mk_call_data
).locations
.push(location
);
274 mut renderopts
: config
::RenderOptions
,
275 cache
: formats
::cache
::Cache
,
277 options
: ScrapeExamplesOptions
,
278 ) -> interface
::Result
<()> {
279 let inner
= move || -> Result
<(), String
> {
280 // Generates source files for examples
281 renderopts
.no_emit_shared
= true;
282 let (cx
, _
) = Context
::init(krate
, renderopts
, cache
, tcx
).map_err(|e
| e
.to_string())?
;
284 // Collect CrateIds corresponding to provided target crates
285 // If two different versions of the crate in the dependency tree, then examples will be collcted from both.
289 .chain([&LOCAL_CRATE
])
290 .map(|crate_num
| (crate_num
, tcx
.crate_name(*crate_num
)))
291 .collect
::<Vec
<_
>>();
292 let target_crates
= options
295 .flat_map(|target
| all_crates
.iter().filter(move |(_
, name
)| name
.as_str() == target
))
296 .map(|(crate_num
, _
)| **crate_num
)
297 .collect
::<Vec
<_
>>();
299 debug
!("All crates in TyCtxt: {all_crates:?}");
300 debug
!("Scrape examples target_crates: {target_crates:?}");
302 // Run call-finder on all items
303 let mut calls
= FxHashMap
::default();
304 let mut finder
= FindCalls { calls: &mut calls, tcx, map: tcx.hir(), cx, target_crates }
;
305 tcx
.hir().visit_all_item_likes_in_crate(&mut finder
);
307 // The visitor might have found a type error, which we need to
308 // promote to a fatal error
309 if tcx
.sess
.diagnostic().has_errors_or_lint_errors().is_some() {
310 return Err(String
::from("Compilation failed, aborting rustdoc"));
313 // Sort call locations within a given file in document order
314 for fn_calls
in calls
.values_mut() {
315 for file_calls
in fn_calls
.values_mut() {
316 file_calls
.locations
.sort_by_key(|loc
| loc
.call_expr
.byte_span
.0);
320 // Save output to provided path
321 let mut encoder
= FileEncoder
::new(options
.output_path
).map_err(|e
| e
.to_string())?
;
322 calls
.encode(&mut encoder
);
323 encoder
.finish().map_err(|e
| e
.to_string())?
;
328 if let Err(e
) = inner() {
335 // Note: the Handler must be passed in explicitly because sess isn't available while parsing options
336 pub(crate) fn load_call_locations(
337 with_examples
: Vec
<String
>,
338 diag
: &rustc_errors
::Handler
,
339 ) -> Result
<AllCallLocations
, i32> {
341 let mut all_calls
: AllCallLocations
= FxHashMap
::default();
342 for path
in with_examples
{
343 let bytes
= fs
::read(&path
).map_err(|e
| format
!("{} (for path {})", e
, path
))?
;
344 let mut decoder
= MemDecoder
::new(&bytes
, 0);
345 let calls
= AllCallLocations
::decode(&mut decoder
);
347 for (function
, fn_calls
) in calls
.into_iter() {
348 all_calls
.entry(function
).or_default().extend(fn_calls
.into_iter());
355 inner().map_err(|e
: String
| {
356 diag
.err(&format
!("failed to load examples: {}", e
));