]> git.lizzy.rs Git - rust.git/blob - src/librustdoc/html/sources.rs
incremental: migrate diagnostics
[rust.git] / src / librustdoc / html / sources.rs
1 use crate::clean;
2 use crate::docfs::PathError;
3 use crate::error::Error;
4 use crate::html::format::Buffer;
5 use crate::html::highlight;
6 use crate::html::layout;
7 use crate::html::render::{Context, BASIC_KEYWORDS};
8 use crate::visit::DocVisitor;
9
10 use rustc_data_structures::fx::{FxHashMap, FxHashSet};
11 use rustc_hir::def_id::LOCAL_CRATE;
12 use rustc_middle::ty::TyCtxt;
13 use rustc_session::Session;
14 use rustc_span::source_map::FileName;
15
16 use std::cell::RefCell;
17 use std::ffi::OsStr;
18 use std::fs;
19 use std::path::{Component, Path, PathBuf};
20 use std::rc::Rc;
21
22 pub(crate) fn render(cx: &mut Context<'_>, krate: &clean::Crate) -> Result<(), Error> {
23     info!("emitting source files");
24
25     let dst = cx.dst.join("src").join(krate.name(cx.tcx()).as_str());
26     cx.shared.ensure_dir(&dst)?;
27
28     let mut collector = SourceCollector { dst, cx, emitted_local_sources: FxHashSet::default() };
29     collector.visit_crate(krate);
30     Ok(())
31 }
32
33 pub(crate) fn collect_local_sources<'tcx>(
34     tcx: TyCtxt<'tcx>,
35     src_root: &Path,
36     krate: &clean::Crate,
37 ) -> FxHashMap<PathBuf, String> {
38     let mut lsc = LocalSourcesCollector { tcx, local_sources: FxHashMap::default(), src_root };
39     lsc.visit_crate(krate);
40     lsc.local_sources
41 }
42
43 struct LocalSourcesCollector<'a, 'tcx> {
44     tcx: TyCtxt<'tcx>,
45     local_sources: FxHashMap<PathBuf, String>,
46     src_root: &'a Path,
47 }
48
49 fn is_real_and_local(span: clean::Span, sess: &Session) -> bool {
50     span.cnum(sess) == LOCAL_CRATE && span.filename(sess).is_real()
51 }
52
53 impl LocalSourcesCollector<'_, '_> {
54     fn add_local_source(&mut self, item: &clean::Item) {
55         let sess = self.tcx.sess;
56         let span = item.span(self.tcx);
57         let Some(span) = span else { return };
58         // skip all synthetic "files"
59         if !is_real_and_local(span, sess) {
60             return;
61         }
62         let filename = span.filename(sess);
63         let p = if let FileName::Real(file) = filename {
64             match file.into_local_path() {
65                 Some(p) => p,
66                 None => return,
67             }
68         } else {
69             return;
70         };
71         if self.local_sources.contains_key(&*p) {
72             // We've already emitted this source
73             return;
74         }
75
76         let href = RefCell::new(PathBuf::new());
77         clean_path(
78             &self.src_root,
79             &p,
80             |component| {
81                 href.borrow_mut().push(component);
82             },
83             || {
84                 href.borrow_mut().pop();
85             },
86         );
87
88         let mut href = href.into_inner().to_string_lossy().to_string();
89         if let Some(c) = href.as_bytes().last() && *c != b'/' {
90             href.push('/');
91         }
92         let mut src_fname = p.file_name().expect("source has no filename").to_os_string();
93         src_fname.push(".html");
94         href.push_str(&src_fname.to_string_lossy());
95         self.local_sources.insert(p, href);
96     }
97 }
98
99 impl DocVisitor for LocalSourcesCollector<'_, '_> {
100     fn visit_item(&mut self, item: &clean::Item) {
101         self.add_local_source(item);
102
103         self.visit_item_recur(item)
104     }
105 }
106
107 /// Helper struct to render all source code to HTML pages
108 struct SourceCollector<'a, 'tcx> {
109     cx: &'a mut Context<'tcx>,
110
111     /// Root destination to place all HTML output into
112     dst: PathBuf,
113     emitted_local_sources: FxHashSet<PathBuf>,
114 }
115
116 impl DocVisitor for SourceCollector<'_, '_> {
117     fn visit_item(&mut self, item: &clean::Item) {
118         if !self.cx.include_sources {
119             return;
120         }
121
122         let tcx = self.cx.tcx();
123         let span = item.span(tcx);
124         let Some(span) = span else { return };
125         let sess = tcx.sess;
126
127         // If we're not rendering sources, there's nothing to do.
128         // If we're including source files, and we haven't seen this file yet,
129         // then we need to render it out to the filesystem.
130         if is_real_and_local(span, sess) {
131             let filename = span.filename(sess);
132             let span = span.inner();
133             let pos = sess.source_map().lookup_source_file(span.lo());
134             let file_span = span.with_lo(pos.start_pos).with_hi(pos.end_pos);
135             // If it turns out that we couldn't read this file, then we probably
136             // can't read any of the files (generating html output from json or
137             // something like that), so just don't include sources for the
138             // entire crate. The other option is maintaining this mapping on a
139             // per-file basis, but that's probably not worth it...
140             self.cx.include_sources = match self.emit_source(&filename, file_span) {
141                 Ok(()) => true,
142                 Err(e) => {
143                     self.cx.shared.tcx.sess.span_err(
144                         span,
145                         &format!(
146                             "failed to render source code for `{}`: {}",
147                             filename.prefer_local(),
148                             e,
149                         ),
150                     );
151                     false
152                 }
153             };
154         }
155
156         self.visit_item_recur(item)
157     }
158 }
159
160 impl SourceCollector<'_, '_> {
161     /// Renders the given filename into its corresponding HTML source file.
162     fn emit_source(
163         &mut self,
164         filename: &FileName,
165         file_span: rustc_span::Span,
166     ) -> Result<(), Error> {
167         let p = match *filename {
168             FileName::Real(ref file) => {
169                 if let Some(local_path) = file.local_path() {
170                     local_path.to_path_buf()
171                 } else {
172                     unreachable!("only the current crate should have sources emitted");
173                 }
174             }
175             _ => return Ok(()),
176         };
177         if self.emitted_local_sources.contains(&*p) {
178             // We've already emitted this source
179             return Ok(());
180         }
181
182         let contents = match fs::read_to_string(&p) {
183             Ok(contents) => contents,
184             Err(e) => {
185                 return Err(Error::new(e, &p));
186             }
187         };
188
189         // Remove the utf-8 BOM if any
190         let contents = contents.strip_prefix('\u{feff}').unwrap_or(&contents);
191
192         let shared = Rc::clone(&self.cx.shared);
193         // Create the intermediate directories
194         let cur = RefCell::new(PathBuf::new());
195         let root_path = RefCell::new(PathBuf::new());
196
197         clean_path(
198             &shared.src_root,
199             &p,
200             |component| {
201                 cur.borrow_mut().push(component);
202                 root_path.borrow_mut().push("..");
203             },
204             || {
205                 cur.borrow_mut().pop();
206                 root_path.borrow_mut().pop();
207             },
208         );
209
210         let root_path = PathBuf::from("../../").join(root_path.into_inner());
211         let mut root_path = root_path.to_string_lossy();
212         if let Some(c) = root_path.as_bytes().last() && *c != b'/' {
213             root_path += "/";
214         }
215         let mut cur = self.dst.join(cur.into_inner());
216         shared.ensure_dir(&cur)?;
217
218         let src_fname = p.file_name().expect("source has no filename").to_os_string();
219         let mut fname = src_fname.clone();
220         fname.push(".html");
221         cur.push(&fname);
222
223         let title = format!("{} - source", src_fname.to_string_lossy());
224         let desc = format!("Source of the Rust file `{}`.", filename.prefer_remapped());
225         let page = layout::Page {
226             title: &title,
227             css_class: "source",
228             root_path: &root_path,
229             static_root_path: shared.static_root_path.as_deref(),
230             description: &desc,
231             keywords: BASIC_KEYWORDS,
232             resource_suffix: &shared.resource_suffix,
233         };
234         let v = layout::render(
235             &shared.layout,
236             &page,
237             "",
238             |buf: &mut _| {
239                 let cx = &mut self.cx;
240                 print_src(
241                     buf,
242                     contents,
243                     file_span,
244                     cx,
245                     &root_path,
246                     highlight::DecorationInfo::default(),
247                     SourceContext::Standalone,
248                 )
249             },
250             &shared.style_files,
251         );
252         shared.fs.write(cur, v)?;
253         self.emitted_local_sources.insert(p);
254         Ok(())
255     }
256 }
257
258 /// Takes a path to a source file and cleans the path to it. This canonicalizes
259 /// things like ".." to components which preserve the "top down" hierarchy of a
260 /// static HTML tree. Each component in the cleaned path will be passed as an
261 /// argument to `f`. The very last component of the path (ie the file name) is ignored.
262 /// If a `..` is encountered, the `parent` closure will be called to allow the callee to
263 /// handle it.
264 pub(crate) fn clean_path<F, P>(src_root: &Path, p: &Path, mut f: F, mut parent: P)
265 where
266     F: FnMut(&OsStr),
267     P: FnMut(),
268 {
269     // make it relative, if possible
270     let p = p.strip_prefix(src_root).unwrap_or(p);
271
272     let mut iter = p.components().peekable();
273
274     while let Some(c) = iter.next() {
275         if iter.peek().is_none() {
276             break;
277         }
278
279         match c {
280             Component::ParentDir => parent(),
281             Component::Normal(c) => f(c),
282             _ => continue,
283         }
284     }
285 }
286
287 pub(crate) enum SourceContext {
288     Standalone,
289     Embedded { offset: usize, needs_expansion: bool },
290 }
291
292 /// Wrapper struct to render the source code of a file. This will do things like
293 /// adding line numbers to the left-hand side.
294 pub(crate) fn print_src(
295     buf: &mut Buffer,
296     s: &str,
297     file_span: rustc_span::Span,
298     context: &Context<'_>,
299     root_path: &str,
300     decoration_info: highlight::DecorationInfo,
301     source_context: SourceContext,
302 ) {
303     let lines = s.lines().count();
304     let mut line_numbers = Buffer::empty_from(buf);
305     let extra;
306     line_numbers.write_str("<pre class=\"src-line-numbers\">");
307     let current_href = context
308         .href_from_span(clean::Span::new(file_span), false)
309         .expect("only local crates should have sources emitted");
310     match source_context {
311         SourceContext::Standalone => {
312             extra = None;
313             for line in 1..=lines {
314                 writeln!(line_numbers, "<a href=\"#{line}\" id=\"{line}\">{line}</a>")
315             }
316         }
317         SourceContext::Embedded { offset, needs_expansion } => {
318             extra = if needs_expansion {
319                 Some(r#"<button class="expand">&varr;</button>"#)
320             } else {
321                 None
322             };
323             for line_number in 1..=lines {
324                 let line = line_number + offset;
325                 writeln!(line_numbers, "<span>{line}</span>")
326             }
327         }
328     }
329     line_numbers.write_str("</pre>");
330     highlight::render_source_with_highlighting(
331         s,
332         buf,
333         line_numbers,
334         highlight::HrefContext { context, file_span, root_path, current_href },
335         decoration_info,
336         extra,
337     );
338 }