]> git.lizzy.rs Git - rust.git/blob - src/librustdoc/scrape_examples.rs
Rollup merge of #91192 - r00ster91:futuredocs, r=GuillaumeGomez
[rust.git] / src / librustdoc / scrape_examples.rs
1 //! This module analyzes crates to find call sites that can serve as examples in the documentation.
2
3 use crate::clean;
4 use crate::config;
5 use crate::formats;
6 use crate::formats::renderer::FormatRenderer;
7 use crate::html::render::Context;
8
9 use rustc_data_structures::fx::FxHashMap;
10 use rustc_hir::{
11     self as hir,
12     intravisit::{self, Visitor},
13 };
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::{Decoder, FileEncoder},
21     Decodable, Encodable,
22 };
23 use rustc_session::getopts;
24 use rustc_span::{
25     def_id::{CrateNum, DefPathHash, LOCAL_CRATE},
26     edition::Edition,
27     BytePos, FileName, SourceFile,
28 };
29
30 use std::fs;
31 use std::path::PathBuf;
32
33 #[derive(Debug, Clone)]
34 crate struct ScrapeExamplesOptions {
35     output_path: PathBuf,
36     target_crates: Vec<String>,
37     crate scrape_tests: bool,
38 }
39
40 impl ScrapeExamplesOptions {
41     crate fn new(
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),
51                 target_crates,
52                 scrape_tests,
53             })),
54             (Some(_), false, _) | (None, true, _) => {
55                 diag.err("must use --scrape-examples-output-path and --scrape-examples-target-crate together");
56                 Err(1)
57             }
58             (None, false, true) => {
59                 diag.err("must use --scrape-examples-output-path and --scrape-examples-target-crate with --scrape-tests");
60                 Err(1)
61             }
62             (None, false, false) => Ok(None),
63         }
64     }
65 }
66
67 #[derive(Encodable, Decodable, Debug, Clone)]
68 crate struct SyntaxRange {
69     crate byte_span: (u32, u32),
70     crate line_span: (usize, usize),
71 }
72
73 impl SyntaxRange {
74     fn new(span: rustc_span::Span, file: &SourceFile) -> Self {
75         let get_pos = |bytepos: BytePos| file.original_relative_byte_pos(bytepos).0;
76         let get_line = |bytepos: BytePos| file.lookup_line(bytepos).unwrap();
77
78         SyntaxRange {
79             byte_span: (get_pos(span.lo()), get_pos(span.hi())),
80             line_span: (get_line(span.lo()), get_line(span.hi())),
81         }
82     }
83 }
84
85 #[derive(Encodable, Decodable, Debug, Clone)]
86 crate struct CallLocation {
87     crate call_expr: SyntaxRange,
88     crate enclosing_item: SyntaxRange,
89 }
90
91 impl CallLocation {
92     fn new(
93         expr_span: rustc_span::Span,
94         enclosing_item_span: rustc_span::Span,
95         source_file: &SourceFile,
96     ) -> Self {
97         CallLocation {
98             call_expr: SyntaxRange::new(expr_span, source_file),
99             enclosing_item: SyntaxRange::new(enclosing_item_span, source_file),
100         }
101     }
102 }
103
104 #[derive(Encodable, Decodable, Debug, Clone)]
105 crate struct CallData {
106     crate locations: Vec<CallLocation>,
107     crate url: String,
108     crate display_name: String,
109     crate edition: Edition,
110 }
111
112 crate type FnCallLocations = FxHashMap<PathBuf, CallData>;
113 crate type AllCallLocations = FxHashMap<DefPathHash, FnCallLocations>;
114
115 /// Visitor for traversing a crate and finding instances of function calls.
116 struct FindCalls<'a, 'tcx> {
117     tcx: TyCtxt<'tcx>,
118     map: Map<'tcx>,
119     cx: Context<'tcx>,
120     target_crates: Vec<CrateNum>,
121     calls: &'a mut AllCallLocations,
122 }
123
124 impl<'a, 'tcx> Visitor<'tcx> for FindCalls<'a, 'tcx>
125 where
126     'tcx: 'a,
127 {
128     type NestedFilter = nested_filter::OnlyBodies;
129
130     fn nested_visit_map(&mut self) -> Self::Map {
131         self.map
132     }
133
134     fn visit_expr(&mut self, ex: &'tcx hir::Expr<'tcx>) {
135         intravisit::walk_expr(self, ex);
136
137         let tcx = self.tcx;
138
139         // If we visit an item that contains an expression outside a function body,
140         // then we need to exit before calling typeck (which will panic). See
141         // test/run-make/rustdoc-scrape-examples-invalid-expr for an example.
142         let hir = tcx.hir();
143         let owner = hir.local_def_id_to_hir_id(ex.hir_id.owner);
144         if hir.maybe_body_owned_by(owner).is_none() {
145             return;
146         }
147
148         // Get type of function if expression is a function call
149         let (ty, span) = match ex.kind {
150             hir::ExprKind::Call(f, _) => {
151                 let types = tcx.typeck(ex.hir_id.owner);
152
153                 if let Some(ty) = types.node_type_opt(f.hir_id) {
154                     (ty, ex.span)
155                 } else {
156                     trace!("node_type_opt({}) = None", f.hir_id);
157                     return;
158                 }
159             }
160             hir::ExprKind::MethodCall(_, _, span) => {
161                 let types = tcx.typeck(ex.hir_id.owner);
162                 let Some(def_id) = types.type_dependent_def_id(ex.hir_id) else {
163                     trace!("type_dependent_def_id({}) = None", ex.hir_id);
164                     return;
165                 };
166                 (tcx.type_of(def_id), span)
167             }
168             _ => {
169                 return;
170             }
171         };
172
173         // If this span comes from a macro expansion, then the source code may not actually show
174         // a use of the given item, so it would be a poor example. Hence, we skip all uses in macros.
175         if span.from_expansion() {
176             trace!("Rejecting expr from macro: {:?}", span);
177             return;
178         }
179
180         // If the enclosing item has a span coming from a proc macro, then we also don't want to include
181         // the example.
182         let enclosing_item_span = tcx
183             .hir()
184             .span_with_body(tcx.hir().local_def_id_to_hir_id(tcx.hir().get_parent_item(ex.hir_id)));
185         if enclosing_item_span.from_expansion() {
186             trace!("Rejecting expr ({:?}) from macro item: {:?}", span, enclosing_item_span);
187             return;
188         }
189
190         assert!(
191             enclosing_item_span.contains(span),
192             "Attempted to scrape call at [{:?}] whose enclosing item [{:?}] doesn't contain the span of the call.",
193             span,
194             enclosing_item_span
195         );
196
197         // Save call site if the function resolves to a concrete definition
198         if let ty::FnDef(def_id, _) = ty.kind() {
199             if self.target_crates.iter().all(|krate| *krate != def_id.krate) {
200                 trace!("Rejecting expr from crate not being documented: {:?}", span);
201                 return;
202             }
203
204             let source_map = tcx.sess.source_map();
205             let file = source_map.lookup_char_pos(span.lo()).file;
206             let file_path = match file.name.clone() {
207                 FileName::Real(real_filename) => real_filename.into_local_path(),
208                 _ => None,
209             };
210
211             if let Some(file_path) = file_path {
212                 let abs_path = fs::canonicalize(file_path.clone()).unwrap();
213                 let cx = &self.cx;
214                 let mk_call_data = || {
215                     let clean_span = crate::clean::types::Span::new(span);
216                     let url = cx.href_from_span(clean_span, false).unwrap();
217                     let display_name = file_path.display().to_string();
218                     let edition = span.edition();
219                     CallData { locations: Vec::new(), url, display_name, edition }
220                 };
221
222                 let fn_key = tcx.def_path_hash(*def_id);
223                 let fn_entries = self.calls.entry(fn_key).or_default();
224
225                 trace!("Including expr: {:?}", span);
226                 let enclosing_item_span =
227                     source_map.span_extend_to_prev_char(enclosing_item_span, '\n', false);
228                 let location = CallLocation::new(span, enclosing_item_span, &file);
229                 fn_entries.entry(abs_path).or_insert_with(mk_call_data).locations.push(location);
230             }
231         }
232     }
233 }
234
235 crate fn run(
236     krate: clean::Crate,
237     mut renderopts: config::RenderOptions,
238     cache: formats::cache::Cache,
239     tcx: TyCtxt<'_>,
240     options: ScrapeExamplesOptions,
241 ) -> interface::Result<()> {
242     let inner = move || -> Result<(), String> {
243         // Generates source files for examples
244         renderopts.no_emit_shared = true;
245         let (cx, _) = Context::init(krate, renderopts, cache, tcx).map_err(|e| e.to_string())?;
246
247         // Collect CrateIds corresponding to provided target crates
248         // If two different versions of the crate in the dependency tree, then examples will be collcted from both.
249         let all_crates = tcx
250             .crates(())
251             .iter()
252             .chain([&LOCAL_CRATE])
253             .map(|crate_num| (crate_num, tcx.crate_name(*crate_num)))
254             .collect::<Vec<_>>();
255         let target_crates = options
256             .target_crates
257             .into_iter()
258             .flat_map(|target| all_crates.iter().filter(move |(_, name)| name.as_str() == target))
259             .map(|(crate_num, _)| **crate_num)
260             .collect::<Vec<_>>();
261
262         debug!("All crates in TyCtxt: {:?}", all_crates);
263         debug!("Scrape examples target_crates: {:?}", target_crates);
264
265         // Run call-finder on all items
266         let mut calls = FxHashMap::default();
267         let mut finder = FindCalls { calls: &mut calls, tcx, map: tcx.hir(), cx, target_crates };
268         tcx.hir().visit_all_item_likes(&mut finder.as_deep_visitor());
269
270         // Sort call locations within a given file in document order
271         for fn_calls in calls.values_mut() {
272             for file_calls in fn_calls.values_mut() {
273                 file_calls.locations.sort_by_key(|loc| loc.call_expr.byte_span.0);
274             }
275         }
276
277         // Save output to provided path
278         let mut encoder = FileEncoder::new(options.output_path).map_err(|e| e.to_string())?;
279         calls.encode(&mut encoder).map_err(|e| e.to_string())?;
280         encoder.flush().map_err(|e| e.to_string())?;
281
282         Ok(())
283     };
284
285     if let Err(e) = inner() {
286         tcx.sess.fatal(&e);
287     }
288
289     Ok(())
290 }
291
292 // Note: the Handler must be passed in explicitly because sess isn't available while parsing options
293 crate fn load_call_locations(
294     with_examples: Vec<String>,
295     diag: &rustc_errors::Handler,
296 ) -> Result<AllCallLocations, i32> {
297     let inner = || {
298         let mut all_calls: AllCallLocations = FxHashMap::default();
299         for path in with_examples {
300             let bytes = fs::read(&path).map_err(|e| format!("{} (for path {})", e, path))?;
301             let mut decoder = Decoder::new(&bytes, 0);
302             let calls = AllCallLocations::decode(&mut decoder);
303
304             for (function, fn_calls) in calls.into_iter() {
305                 all_calls.entry(function).or_default().extend(fn_calls.into_iter());
306             }
307         }
308
309         Ok(all_calls)
310     };
311
312     inner().map_err(|e: String| {
313         diag.err(&format!("failed to load examples: {}", e));
314         1
315     })
316 }