]> git.lizzy.rs Git - rust.git/blob - src/librustdoc/html/sources.rs
Rollup merge of #98713 - nikomatsakis:issue-98693, r=jackh726
[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::edition::Edition;
15 use rustc_span::source_map::FileName;
16
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         // skip all synthetic "files"
58         if !is_real_and_local(span, sess) {
59             return;
60         }
61         let filename = span.filename(sess);
62         let p = if let FileName::Real(file) = filename {
63             match file.into_local_path() {
64                 Some(p) => p,
65                 None => return,
66             }
67         } else {
68             return;
69         };
70         if self.local_sources.contains_key(&*p) {
71             // We've already emitted this source
72             return;
73         }
74
75         let mut href = String::new();
76         clean_path(self.src_root, &p, false, |component| {
77             href.push_str(&component.to_string_lossy());
78             href.push('/');
79         });
80
81         let mut src_fname = p.file_name().expect("source has no filename").to_os_string();
82         src_fname.push(".html");
83         href.push_str(&src_fname.to_string_lossy());
84         self.local_sources.insert(p, href);
85     }
86 }
87
88 impl DocVisitor for LocalSourcesCollector<'_, '_> {
89     fn visit_item(&mut self, item: &clean::Item) {
90         self.add_local_source(item);
91
92         self.visit_item_recur(item)
93     }
94 }
95
96 /// Helper struct to render all source code to HTML pages
97 struct SourceCollector<'a, 'tcx> {
98     cx: &'a mut Context<'tcx>,
99
100     /// Root destination to place all HTML output into
101     dst: PathBuf,
102     emitted_local_sources: FxHashSet<PathBuf>,
103 }
104
105 impl DocVisitor for SourceCollector<'_, '_> {
106     fn visit_item(&mut self, item: &clean::Item) {
107         if !self.cx.include_sources {
108             return;
109         }
110
111         let tcx = self.cx.tcx();
112         let span = item.span(tcx);
113         let sess = tcx.sess;
114
115         // If we're not rendering sources, there's nothing to do.
116         // If we're including source files, and we haven't seen this file yet,
117         // then we need to render it out to the filesystem.
118         if is_real_and_local(span, sess) {
119             let filename = span.filename(sess);
120             let span = span.inner();
121             let pos = sess.source_map().lookup_source_file(span.lo());
122             let file_span = span.with_lo(pos.start_pos).with_hi(pos.end_pos);
123             // If it turns out that we couldn't read this file, then we probably
124             // can't read any of the files (generating html output from json or
125             // something like that), so just don't include sources for the
126             // entire crate. The other option is maintaining this mapping on a
127             // per-file basis, but that's probably not worth it...
128             self.cx.include_sources = match self.emit_source(&filename, file_span) {
129                 Ok(()) => true,
130                 Err(e) => {
131                     self.cx.shared.tcx.sess.span_err(
132                         span,
133                         &format!(
134                             "failed to render source code for `{}`: {}",
135                             filename.prefer_local(),
136                             e,
137                         ),
138                     );
139                     false
140                 }
141             };
142         }
143
144         self.visit_item_recur(item)
145     }
146 }
147
148 impl SourceCollector<'_, '_> {
149     /// Renders the given filename into its corresponding HTML source file.
150     fn emit_source(
151         &mut self,
152         filename: &FileName,
153         file_span: rustc_span::Span,
154     ) -> Result<(), Error> {
155         let p = match *filename {
156             FileName::Real(ref file) => {
157                 if let Some(local_path) = file.local_path() {
158                     local_path.to_path_buf()
159                 } else {
160                     unreachable!("only the current crate should have sources emitted");
161                 }
162             }
163             _ => return Ok(()),
164         };
165         if self.emitted_local_sources.contains(&*p) {
166             // We've already emitted this source
167             return Ok(());
168         }
169
170         let contents = match fs::read_to_string(&p) {
171             Ok(contents) => contents,
172             Err(e) => {
173                 return Err(Error::new(e, &p));
174             }
175         };
176
177         // Remove the utf-8 BOM if any
178         let contents = contents.strip_prefix('\u{feff}').unwrap_or(&contents);
179
180         let shared = Rc::clone(&self.cx.shared);
181         // Create the intermediate directories
182         let mut cur = self.dst.clone();
183         let mut root_path = String::from("../../");
184         clean_path(&shared.src_root, &p, false, |component| {
185             cur.push(component);
186             root_path.push_str("../");
187         });
188
189         shared.ensure_dir(&cur)?;
190
191         let src_fname = p.file_name().expect("source has no filename").to_os_string();
192         let mut fname = src_fname.clone();
193         fname.push(".html");
194         cur.push(&fname);
195
196         let title = format!("{} - source", src_fname.to_string_lossy());
197         let desc = format!("Source of the Rust file `{}`.", filename.prefer_remapped());
198         let page = layout::Page {
199             title: &title,
200             css_class: "source",
201             root_path: &root_path,
202             static_root_path: shared.static_root_path.as_deref(),
203             description: &desc,
204             keywords: BASIC_KEYWORDS,
205             resource_suffix: &shared.resource_suffix,
206         };
207         let v = layout::render(
208             &shared.layout,
209             &page,
210             "",
211             |buf: &mut _| {
212                 let cx = &mut self.cx;
213                 print_src(
214                     buf,
215                     contents,
216                     cx.shared.edition(),
217                     file_span,
218                     cx,
219                     &root_path,
220                     None,
221                     SourceContext::Standalone,
222                 )
223             },
224             &shared.style_files,
225         );
226         shared.fs.write(cur, v)?;
227         self.emitted_local_sources.insert(p);
228         Ok(())
229     }
230 }
231
232 /// Takes a path to a source file and cleans the path to it. This canonicalizes
233 /// things like ".." to components which preserve the "top down" hierarchy of a
234 /// static HTML tree. Each component in the cleaned path will be passed as an
235 /// argument to `f`. The very last component of the path (ie the file name) will
236 /// be passed to `f` if `keep_filename` is true, and ignored otherwise.
237 pub(crate) fn clean_path<F>(src_root: &Path, p: &Path, keep_filename: bool, mut f: F)
238 where
239     F: FnMut(&OsStr),
240 {
241     // make it relative, if possible
242     let p = p.strip_prefix(src_root).unwrap_or(p);
243
244     let mut iter = p.components().peekable();
245
246     while let Some(c) = iter.next() {
247         if !keep_filename && iter.peek().is_none() {
248             break;
249         }
250
251         match c {
252             Component::ParentDir => f("up".as_ref()),
253             Component::Normal(c) => f(c),
254             _ => continue,
255         }
256     }
257 }
258
259 pub(crate) enum SourceContext {
260     Standalone,
261     Embedded { offset: usize },
262 }
263
264 /// Wrapper struct to render the source code of a file. This will do things like
265 /// adding line numbers to the left-hand side.
266 pub(crate) fn print_src(
267     buf: &mut Buffer,
268     s: &str,
269     edition: Edition,
270     file_span: rustc_span::Span,
271     context: &Context<'_>,
272     root_path: &str,
273     decoration_info: Option<highlight::DecorationInfo>,
274     source_context: SourceContext,
275 ) {
276     let lines = s.lines().count();
277     let mut line_numbers = Buffer::empty_from(buf);
278     line_numbers.write_str("<pre class=\"line-numbers\">");
279     match source_context {
280         SourceContext::Standalone => {
281             for line in 1..=lines {
282                 writeln!(line_numbers, "<span id=\"{0}\">{0}</span>", line)
283             }
284         }
285         SourceContext::Embedded { offset } => {
286             for line in 1..=lines {
287                 writeln!(line_numbers, "<span>{0}</span>", line + offset)
288             }
289         }
290     }
291     line_numbers.write_str("</pre>");
292     highlight::render_with_highlighting(
293         s,
294         buf,
295         None,
296         None,
297         None,
298         edition,
299         Some(line_numbers),
300         Some(highlight::HrefContext { context, file_span, root_path }),
301         decoration_info,
302     );
303 }