]> git.lizzy.rs Git - rust.git/blob - src/librustdoc/html/render/write_shared.rs
Rollup merge of #104024 - noeddl:unused-must-use, r=compiler-errors
[rust.git] / src / librustdoc / html / render / write_shared.rs
1 use std::fs::{self, File};
2 use std::io::prelude::*;
3 use std::io::{self, BufReader};
4 use std::path::{Component, Path};
5 use std::rc::Rc;
6
7 use itertools::Itertools;
8 use rustc_data_structures::flock;
9 use rustc_data_structures::fx::{FxHashMap, FxHashSet};
10 use serde::ser::SerializeSeq;
11 use serde::{Serialize, Serializer};
12
13 use super::{collect_paths_for_type, ensure_trailing_slash, Context, BASIC_KEYWORDS};
14 use crate::clean::Crate;
15 use crate::config::{EmitType, RenderOptions};
16 use crate::docfs::PathError;
17 use crate::error::Error;
18 use crate::html::{layout, static_files};
19 use crate::{try_err, try_none};
20
21 /// Rustdoc writes out two kinds of shared files:
22 ///  - Static files, which are embedded in the rustdoc binary and are written with a
23 ///    filename that includes a hash of their contents. These will always have a new
24 ///    URL if the contents change, so they are safe to cache with the
25 ///    `Cache-Control: immutable` directive. They are written under the static.files/
26 ///    directory and are written when --emit-type is empty (default) or contains
27 ///    "toolchain-specific". If using the --static-root-path flag, it should point
28 ///    to a URL path prefix where each of these filenames can be fetched.
29 ///  - Invocation specific files. These are generated based on the crate(s) being
30 ///    documented. Their filenames need to be predictable without knowing their
31 ///    contents, so they do not include a hash in their filename and are not safe to
32 ///    cache with `Cache-Control: immutable`. They include the contents of the
33 ///    --resource-suffix flag and are emitted when --emit-type is empty (default)
34 ///    or contains "invocation-specific".
35 pub(super) fn write_shared(
36     cx: &mut Context<'_>,
37     krate: &Crate,
38     search_index: String,
39     options: &RenderOptions,
40 ) -> Result<(), Error> {
41     // Write out the shared files. Note that these are shared among all rustdoc
42     // docs placed in the output directory, so this needs to be a synchronized
43     // operation with respect to all other rustdocs running around.
44     let lock_file = cx.dst.join(".lock");
45     let _lock = try_err!(flock::Lock::new(&lock_file, true, true, true), &lock_file);
46
47     // InvocationSpecific resources should always be dynamic.
48     let write_invocation_specific = |p: &str, make_content: &dyn Fn() -> Result<Vec<u8>, Error>| {
49         let content = make_content()?;
50         if options.emit.is_empty() || options.emit.contains(&EmitType::InvocationSpecific) {
51             let output_filename = static_files::suffix_path(p, &cx.shared.resource_suffix);
52             cx.shared.fs.write(cx.dst.join(output_filename), content)
53         } else {
54             Ok(())
55         }
56     };
57
58     cx.shared
59         .fs
60         .create_dir_all(cx.dst.join("static.files"))
61         .map_err(|e| PathError::new(e, "static.files"))?;
62
63     // Handle added third-party themes
64     for entry in &cx.shared.style_files {
65         let theme = entry.basename()?;
66         let extension =
67             try_none!(try_none!(entry.path.extension(), &entry.path).to_str(), &entry.path);
68
69         // Skip the official themes. They are written below as part of STATIC_FILES_LIST.
70         if matches!(theme.as_str(), "light" | "dark" | "ayu") {
71             continue;
72         }
73
74         let bytes = try_err!(fs::read(&entry.path), &entry.path);
75         let filename = format!("{}{}.{}", theme, cx.shared.resource_suffix, extension);
76         cx.shared.fs.write(cx.dst.join(filename), bytes)?;
77     }
78
79     // When the user adds their own CSS files with --extend-css, we write that as an
80     // invocation-specific file (that is, with a resource suffix).
81     if let Some(ref css) = cx.shared.layout.css_file_extension {
82         let buffer = try_err!(fs::read_to_string(css), css);
83         let path = static_files::suffix_path("theme.css", &cx.shared.resource_suffix);
84         cx.shared.fs.write(cx.dst.join(path), buffer)?;
85     }
86
87     if options.emit.is_empty() || options.emit.contains(&EmitType::Toolchain) {
88         let static_dir = cx.dst.join(Path::new("static.files"));
89         static_files::for_each(|f: &static_files::StaticFile| {
90             let filename = static_dir.join(f.output_filename());
91             cx.shared.fs.write(filename, f.minified())
92         })?;
93     }
94
95     /// Read a file and return all lines that match the `"{crate}":{data},` format,
96     /// and return a tuple `(Vec<DataString>, Vec<CrateNameString>)`.
97     ///
98     /// This forms the payload of files that look like this:
99     ///
100     /// ```javascript
101     /// var data = {
102     /// "{crate1}":{data},
103     /// "{crate2}":{data}
104     /// };
105     /// use_data(data);
106     /// ```
107     ///
108     /// The file needs to be formatted so that *only crate data lines start with `"`*.
109     fn collect(path: &Path, krate: &str) -> io::Result<(Vec<String>, Vec<String>)> {
110         let mut ret = Vec::new();
111         let mut krates = Vec::new();
112
113         if path.exists() {
114             let prefix = format!("\"{}\"", krate);
115             for line in BufReader::new(File::open(path)?).lines() {
116                 let line = line?;
117                 if !line.starts_with('"') {
118                     continue;
119                 }
120                 if line.starts_with(&prefix) {
121                     continue;
122                 }
123                 if line.ends_with(',') {
124                     ret.push(line[..line.len() - 1].to_string());
125                 } else {
126                     // No comma (it's the case for the last added crate line)
127                     ret.push(line.to_string());
128                 }
129                 krates.push(
130                     line.split('"')
131                         .find(|s| !s.is_empty())
132                         .map(|s| s.to_owned())
133                         .unwrap_or_else(String::new),
134                 );
135             }
136         }
137         Ok((ret, krates))
138     }
139
140     /// Read a file and return all lines that match the <code>"{crate}":{data},\ </code> format,
141     /// and return a tuple `(Vec<DataString>, Vec<CrateNameString>)`.
142     ///
143     /// This forms the payload of files that look like this:
144     ///
145     /// ```javascript
146     /// var data = JSON.parse('{\
147     /// "{crate1}":{data},\
148     /// "{crate2}":{data}\
149     /// }');
150     /// use_data(data);
151     /// ```
152     ///
153     /// The file needs to be formatted so that *only crate data lines start with `"`*.
154     fn collect_json(path: &Path, krate: &str) -> io::Result<(Vec<String>, Vec<String>)> {
155         let mut ret = Vec::new();
156         let mut krates = Vec::new();
157
158         if path.exists() {
159             let prefix = format!("\"{}\"", krate);
160             for line in BufReader::new(File::open(path)?).lines() {
161                 let line = line?;
162                 if !line.starts_with('"') {
163                     continue;
164                 }
165                 if line.starts_with(&prefix) {
166                     continue;
167                 }
168                 if line.ends_with(",\\") {
169                     ret.push(line[..line.len() - 2].to_string());
170                 } else {
171                     // Ends with "\\" (it's the case for the last added crate line)
172                     ret.push(line[..line.len() - 1].to_string());
173                 }
174                 krates.push(
175                     line.split('"')
176                         .find(|s| !s.is_empty())
177                         .map(|s| s.to_owned())
178                         .unwrap_or_else(String::new),
179                 );
180             }
181         }
182         Ok((ret, krates))
183     }
184
185     use std::ffi::OsString;
186
187     #[derive(Debug)]
188     struct Hierarchy {
189         elem: OsString,
190         children: FxHashMap<OsString, Hierarchy>,
191         elems: FxHashSet<OsString>,
192     }
193
194     impl Hierarchy {
195         fn new(elem: OsString) -> Hierarchy {
196             Hierarchy { elem, children: FxHashMap::default(), elems: FxHashSet::default() }
197         }
198
199         fn to_json_string(&self) -> String {
200             let mut subs: Vec<&Hierarchy> = self.children.values().collect();
201             subs.sort_unstable_by(|a, b| a.elem.cmp(&b.elem));
202             let mut files = self
203                 .elems
204                 .iter()
205                 .map(|s| format!("\"{}\"", s.to_str().expect("invalid osstring conversion")))
206                 .collect::<Vec<_>>();
207             files.sort_unstable();
208             let subs = subs.iter().map(|s| s.to_json_string()).collect::<Vec<_>>().join(",");
209             let dirs = if subs.is_empty() && files.is_empty() {
210                 String::new()
211             } else {
212                 format!(",[{}]", subs)
213             };
214             let files = files.join(",");
215             let files = if files.is_empty() { String::new() } else { format!(",[{}]", files) };
216             format!(
217                 "[\"{name}\"{dirs}{files}]",
218                 name = self.elem.to_str().expect("invalid osstring conversion"),
219                 dirs = dirs,
220                 files = files
221             )
222         }
223     }
224
225     if cx.include_sources {
226         let mut hierarchy = Hierarchy::new(OsString::new());
227         for source in cx
228             .shared
229             .local_sources
230             .iter()
231             .filter_map(|p| p.0.strip_prefix(&cx.shared.src_root).ok())
232         {
233             let mut h = &mut hierarchy;
234             let mut elems = source
235                 .components()
236                 .filter_map(|s| match s {
237                     Component::Normal(s) => Some(s.to_owned()),
238                     _ => None,
239                 })
240                 .peekable();
241             loop {
242                 let cur_elem = elems.next().expect("empty file path");
243                 if elems.peek().is_none() {
244                     h.elems.insert(cur_elem);
245                     break;
246                 } else {
247                     let e = cur_elem.clone();
248                     h = h.children.entry(cur_elem.clone()).or_insert_with(|| Hierarchy::new(e));
249                 }
250             }
251         }
252
253         let dst = cx.dst.join(&format!("source-files{}.js", cx.shared.resource_suffix));
254         let make_sources = || {
255             let (mut all_sources, _krates) =
256                 try_err!(collect_json(&dst, krate.name(cx.tcx()).as_str()), &dst);
257             all_sources.push(format!(
258                 r#""{}":{}"#,
259                 &krate.name(cx.tcx()),
260                 hierarchy
261                     .to_json_string()
262                     // All these `replace` calls are because we have to go through JS string for JSON content.
263                     .replace('\\', r"\\")
264                     .replace('\'', r"\'")
265                     // We need to escape double quotes for the JSON.
266                     .replace("\\\"", "\\\\\"")
267             ));
268             all_sources.sort();
269             let mut v = String::from("var sourcesIndex = JSON.parse('{\\\n");
270             v.push_str(&all_sources.join(",\\\n"));
271             v.push_str("\\\n}');\ncreateSourceSidebar();\n");
272             Ok(v.into_bytes())
273         };
274         write_invocation_specific("source-files.js", &make_sources)?;
275     }
276
277     // Update the search index and crate list.
278     let dst = cx.dst.join(&format!("search-index{}.js", cx.shared.resource_suffix));
279     let (mut all_indexes, mut krates) =
280         try_err!(collect_json(&dst, krate.name(cx.tcx()).as_str()), &dst);
281     all_indexes.push(search_index);
282     krates.push(krate.name(cx.tcx()).to_string());
283     krates.sort();
284
285     // Sort the indexes by crate so the file will be generated identically even
286     // with rustdoc running in parallel.
287     all_indexes.sort();
288     write_invocation_specific("search-index.js", &|| {
289         let mut v = String::from("var searchIndex = JSON.parse('{\\\n");
290         v.push_str(&all_indexes.join(",\\\n"));
291         v.push_str(
292             r#"\
293 }');
294 if (typeof window !== 'undefined' && window.initSearch) {window.initSearch(searchIndex)};
295 if (typeof exports !== 'undefined') {exports.searchIndex = searchIndex};
296 "#,
297         );
298         Ok(v.into_bytes())
299     })?;
300
301     write_invocation_specific("crates.js", &|| {
302         let krates = krates.iter().map(|k| format!("\"{}\"", k)).join(",");
303         Ok(format!("window.ALL_CRATES = [{}];", krates).into_bytes())
304     })?;
305
306     if options.enable_index_page {
307         if let Some(index_page) = options.index_page.clone() {
308             let mut md_opts = options.clone();
309             md_opts.output = cx.dst.clone();
310             md_opts.external_html = (*cx.shared).layout.external_html.clone();
311
312             crate::markdown::render(&index_page, md_opts, cx.shared.edition())
313                 .map_err(|e| Error::new(e, &index_page))?;
314         } else {
315             let shared = Rc::clone(&cx.shared);
316             let dst = cx.dst.join("index.html");
317             let page = layout::Page {
318                 title: "Index of crates",
319                 css_class: "mod",
320                 root_path: "./",
321                 static_root_path: shared.static_root_path.as_deref(),
322                 description: "List of crates",
323                 keywords: BASIC_KEYWORDS,
324                 resource_suffix: &shared.resource_suffix,
325             };
326
327             let content = format!(
328                 "<h1 class=\"fqn\">List of all crates</h1><ul class=\"all-items\">{}</ul>",
329                 krates
330                     .iter()
331                     .map(|s| {
332                         format!(
333                             "<li><a href=\"{}index.html\">{}</a></li>",
334                             ensure_trailing_slash(s),
335                             s
336                         )
337                     })
338                     .collect::<String>()
339             );
340             let v = layout::render(&shared.layout, &page, "", content, &shared.style_files);
341             shared.fs.write(dst, v)?;
342         }
343     }
344
345     // Update the list of all implementors for traits
346     let dst = cx.dst.join("implementors");
347     let cache = cx.cache();
348     for (&did, imps) in &cache.implementors {
349         // Private modules can leak through to this phase of rustdoc, which
350         // could contain implementations for otherwise private types. In some
351         // rare cases we could find an implementation for an item which wasn't
352         // indexed, so we just skip this step in that case.
353         //
354         // FIXME: this is a vague explanation for why this can't be a `get`, in
355         //        theory it should be...
356         let (remote_path, remote_item_type) = match cache.exact_paths.get(&did) {
357             Some(p) => match cache.paths.get(&did).or_else(|| cache.external_paths.get(&did)) {
358                 Some((_, t)) => (p, t),
359                 None => continue,
360             },
361             None => match cache.external_paths.get(&did) {
362                 Some((p, t)) => (p, t),
363                 None => continue,
364             },
365         };
366
367         struct Implementor {
368             text: String,
369             synthetic: bool,
370             types: Vec<String>,
371         }
372
373         impl Serialize for Implementor {
374             fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
375             where
376                 S: Serializer,
377             {
378                 let mut seq = serializer.serialize_seq(None)?;
379                 seq.serialize_element(&self.text)?;
380                 if self.synthetic {
381                     seq.serialize_element(&1)?;
382                     seq.serialize_element(&self.types)?;
383                 }
384                 seq.end()
385             }
386         }
387
388         let implementors = imps
389             .iter()
390             .filter_map(|imp| {
391                 // If the trait and implementation are in the same crate, then
392                 // there's no need to emit information about it (there's inlining
393                 // going on). If they're in different crates then the crate defining
394                 // the trait will be interested in our implementation.
395                 //
396                 // If the implementation is from another crate then that crate
397                 // should add it.
398                 if imp.impl_item.item_id.krate() == did.krate || !imp.impl_item.item_id.is_local() {
399                     None
400                 } else {
401                     Some(Implementor {
402                         text: imp.inner_impl().print(false, cx).to_string(),
403                         synthetic: imp.inner_impl().kind.is_auto(),
404                         types: collect_paths_for_type(imp.inner_impl().for_.clone(), cache),
405                     })
406                 }
407             })
408             .collect::<Vec<_>>();
409
410         // Only create a js file if we have impls to add to it. If the trait is
411         // documented locally though we always create the file to avoid dead
412         // links.
413         if implementors.is_empty() && !cache.paths.contains_key(&did) {
414             continue;
415         }
416
417         let implementors = format!(
418             r#""{}":{}"#,
419             krate.name(cx.tcx()),
420             serde_json::to_string(&implementors).expect("failed serde conversion"),
421         );
422
423         let mut mydst = dst.clone();
424         for part in &remote_path[..remote_path.len() - 1] {
425             mydst.push(part.to_string());
426         }
427         cx.shared.ensure_dir(&mydst)?;
428         mydst.push(&format!("{}.{}.js", remote_item_type, remote_path[remote_path.len() - 1]));
429
430         let (mut all_implementors, _) =
431             try_err!(collect(&mydst, krate.name(cx.tcx()).as_str()), &mydst);
432         all_implementors.push(implementors);
433         // Sort the implementors by crate so the file will be generated
434         // identically even with rustdoc running in parallel.
435         all_implementors.sort();
436
437         let mut v = String::from("(function() {var implementors = {\n");
438         v.push_str(&all_implementors.join(",\n"));
439         v.push_str("\n};");
440         v.push_str(
441             "if (window.register_implementors) {\
442                  window.register_implementors(implementors);\
443              } else {\
444                  window.pending_implementors = implementors;\
445              }",
446         );
447         v.push_str("})()");
448         cx.shared.fs.write(mydst, v)?;
449     }
450     Ok(())
451 }