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